Add support for roles and groups.

This commit is contained in:
2024-10-03 19:48:43 +01:00
parent 7a53da6080
commit 93b49b6d17
4 changed files with 187 additions and 16 deletions
+2
View File
@@ -19,6 +19,8 @@ testData:
- profile - profile
allowedUserDomains: # If not provided - will rely entirely on the OIDC yes/no allowedUserDomains: # If not provided - will rely entirely on the OIDC yes/no
- raczylo.com - raczylo.com
allowedRolesAndGroups:
- guest-endpoints
sessionEncryptionKey: potato-secret sessionEncryptionKey: potato-secret
forceHTTPS: false forceHTTPS: false
logLevel: debug # debug, info, warn, error logLevel: debug # debug, info, warn, error
+90
View File
@@ -17,6 +17,96 @@ Middleware currently supports following scenarios:
* Using excluded URLs which do **NOT** require the OIDC authentication * Using excluded URLs which do **NOT** require the OIDC authentication
* Rate limiting requests to prevent the bruteforce attacks * Rate limiting requests to prevent the bruteforce attacks
#### How to configure...
##### Excluded URLs with open access
```
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-with-open-urls
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: xxx
clientID: yyy
clientSecret: zzz
sessionEncryptionKey: vvv
callbackURL: /cool-oidc/callback
logoutURL: /cool-oidc/logout
scopes:
- openid
- email
- profile
excludedURLs: # Determines the list of URLs which are NOT a subject to authentication
- /login # covers /login, /login/me, /login/reminder etc.
- /my-public-data
```
##### Allowed email domains
Assuming that your OIDC provider allows anyone to log in, you may want to limit the access to people using emains in specific domain.
```
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-only-my-users
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: xxx
clientID: yyy
clientSecret: zzz
sessionEncryptionKey: vvv
callbackURL: /new-oidc/callback
logoutURL: /new-oidc/logout
scopes:
- openid
- email
- profile
allowedUserDomains:
- raczylo.com
```
##### Allowed groups and roles
In case of multiple roles / groups and access separation for various endpoints you will need to create multiple traefik middlewares.
Following example allows access for users who have additional role `guest-endpoints` assigned.
```
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oidc-guest-endpoints
namespace: traefik
spec:
plugin:
traefikoidc:
providerURL: xxx
clientID: yyy
clientSecret: zzz
sessionEncryptionKey: vvv
callbackURL: /my-oidc/callback
logoutURL: /my-oidc/logout
scopes:
- openid
- email
- profile
- roles # This line queries the OIDC provider for roles
forceHTTPS: true
allowedRolesAndGroups:
- guest-endpoints # This line specifies the roles or groups allowed to access content
allowedUserDomains:
- raczylo.com
```
#### Docker compose example #### Docker compose example
`docker-compose.yaml` `docker-compose.yaml`
+80 -2
View File
@@ -55,6 +55,7 @@ type TraefikOidc struct {
jwtVerifier JWTVerifier jwtVerifier JWTVerifier
excludedURLs map[string]struct{} excludedURLs map[string]struct{}
allowedUserDomains map[string]struct{} allowedUserDomains map[string]struct{}
allowedRolesAndGroups map[string]struct{}
initiateAuthenticationFunc func(rw http.ResponseWriter, req *http.Request, session *sessions.Session, redirectURL string) initiateAuthenticationFunc func(rw http.ResponseWriter, req *http.Request, session *sessions.Session, redirectURL string)
exchangeCodeForTokenFunc func(code string) (map[string]interface{}, error) exchangeCodeForTokenFunc func(code string) (map[string]interface{}, error)
extractClaimsFunc func(tokenString string) (map[string]interface{}, error) extractClaimsFunc func(tokenString string) (map[string]interface{}, error)
@@ -246,6 +247,13 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
} }
return m return m
}(), }(),
allowedRolesAndGroups: func() map[string]struct{} {
m := make(map[string]struct{})
for _, roleOrGroup := range config.AllowedRolesAndGroups {
m[roleOrGroup] = struct{}{}
}
return m
}(),
} }
t.initiateAuthenticationFunc = t.defaultInitiateAuthentication t.initiateAuthenticationFunc = t.defaultInitiateAuthentication
@@ -405,6 +413,34 @@ func (t *TraefikOidc) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return return
} }
groups, roles, err := t.extractGroupsAndRoles(idToken)
if err != nil {
t.logger.Errorf("Failed to extract groups and roles: %v", err)
} else {
// Set headers for groups and roles
if len(groups) > 0 {
req.Header.Set("X-User-Groups", strings.Join(groups, ","))
}
if len(roles) > 0 {
req.Header.Set("X-User-Roles", strings.Join(roles, ","))
}
}
if len(t.allowedRolesAndGroups) > 0 {
allowed := false
for _, roleOrGroup := range append(groups, roles...) {
if _, ok := t.allowedRolesAndGroups[roleOrGroup]; ok {
allowed = true
break
}
}
if !allowed {
t.logger.Infof("User with email %s does not have any allowed roles or groups", email)
http.Error(rw, fmt.Sprintf("Access denied: You do not have any allowed roles or groups. To log out, visit: %s", t.logoutURLPath), http.StatusForbidden)
return
}
}
req.Header.Set("X-Forwarded-User", email) req.Header.Set("X-Forwarded-User", email)
t.next.ServeHTTP(rw, req) t.next.ServeHTTP(rw, req)
@@ -525,8 +561,6 @@ func (t *TraefikOidc) verifyToken(token string) error {
return t.tokenVerifier.VerifyToken(token) return t.tokenVerifier.VerifyToken(token)
} }
var authURLBuilder strings.Builder
func (t *TraefikOidc) buildAuthURL(redirectURL, state, nonce string) string { func (t *TraefikOidc) buildAuthURL(redirectURL, state, nonce string) string {
params := url.Values{} params := url.Values{}
params.Set("client_id", t.clientID) params.Set("client_id", t.clientID)
@@ -639,3 +673,47 @@ func (t *TraefikOidc) isAllowedDomain(email string) bool {
_, ok := t.allowedUserDomains[domain] _, ok := t.allowedUserDomains[domain]
return ok return ok
} }
func (t *TraefikOidc) extractGroupsAndRoles(idToken string) ([]string, []string, error) {
claims, err := t.extractClaimsFunc(idToken)
if err != nil {
return nil, nil, fmt.Errorf("failed to extract claims: %w", err)
}
var groups []string
var roles []string
// Check for groups claim
if groupsClaim, ok := claims["groups"]; ok {
if groupsSlice, ok := groupsClaim.([]interface{}); ok {
for _, group := range groupsSlice {
if groupStr, ok := group.(string); ok {
t.logger.Debugf("Found group: %s", groupStr)
groups = append(groups, groupStr)
}
}
}
}
if len(groups) == 0 {
t.logger.Debug("No groups found in groups claim, checking roles claim")
}
// Check for roles claim
if rolesClaim, ok := claims["roles"]; ok {
if rolesSlice, ok := rolesClaim.([]interface{}); ok {
for _, role := range rolesSlice {
if roleStr, ok := role.(string); ok {
t.logger.Debug("Found role: %s", roleStr)
roles = append(roles, roleStr)
}
}
}
}
if len(roles) == 0 {
t.logger.Debug("No roles found in roles claim")
}
return groups, roles, nil
}
+1
View File
@@ -28,6 +28,7 @@ type Config struct {
RateLimit int `json:"rateLimit"` RateLimit int `json:"rateLimit"`
ExcludedURLs []string `json:"excludedURLs"` ExcludedURLs []string `json:"excludedURLs"`
AllowedUserDomains []string `json:"allowedUserDomains"` AllowedUserDomains []string `json:"allowedUserDomains"`
AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"`
HTTPClient *http.Client HTTPClient *http.Client
} }