mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-05 22:44:17 +00:00
Add support for roles and groups.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
+15
-14
@@ -15,20 +15,21 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ProviderURL string `json:"providerURL"`
|
ProviderURL string `json:"providerURL"`
|
||||||
RevocationURL string `json:"revocationURL"`
|
RevocationURL string `json:"revocationURL"`
|
||||||
CallbackURL string `json:"callbackURL"`
|
CallbackURL string `json:"callbackURL"`
|
||||||
LogoutURL string `json:"logoutURL"`
|
LogoutURL string `json:"logoutURL"`
|
||||||
ClientID string `json:"clientID"`
|
ClientID string `json:"clientID"`
|
||||||
ClientSecret string `json:"clientSecret"`
|
ClientSecret string `json:"clientSecret"`
|
||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
LogLevel string `json:"logLevel"`
|
LogLevel string `json:"logLevel"`
|
||||||
SessionEncryptionKey string `json:"sessionEncryptionKey"`
|
SessionEncryptionKey string `json:"sessionEncryptionKey"`
|
||||||
ForceHTTPS bool `json:"forceHTTPS"`
|
ForceHTTPS bool `json:"forceHTTPS"`
|
||||||
RateLimit int `json:"rateLimit"`
|
RateLimit int `json:"rateLimit"`
|
||||||
ExcludedURLs []string `json:"excludedURLs"`
|
ExcludedURLs []string `json:"excludedURLs"`
|
||||||
AllowedUserDomains []string `json:"allowedUserDomains"`
|
AllowedUserDomains []string `json:"allowedUserDomains"`
|
||||||
HTTPClient *http.Client
|
AllowedRolesAndGroups []string `json:"allowedRolesAndGroups"`
|
||||||
|
HTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultSessionOptions = &sessions.Options{
|
var defaultSessionOptions = &sessions.Options{
|
||||||
|
|||||||
Reference in New Issue
Block a user