Extract roles from Keycloak Access Tokens
This commit is contained in:
		
							parent
							
								
									4c0beb373f
								
							
						
					
					
						commit
						ab54de38cc
					
				|  | @ -508,6 +508,7 @@ type LegacyProvider struct { | ||||||
| 	ApprovalPrompt                     string   `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
 | 	ApprovalPrompt                     string   `flag:"approval-prompt" cfg:"approval_prompt"` // Deprecated by OIDC 1.0
 | ||||||
| 	UserIDClaim                        string   `flag:"user-id-claim" cfg:"user_id_claim"` | 	UserIDClaim                        string   `flag:"user-id-claim" cfg:"user_id_claim"` | ||||||
| 	AllowedGroups                      []string `flag:"allowed-group" cfg:"allowed_groups"` | 	AllowedGroups                      []string `flag:"allowed-group" cfg:"allowed_groups"` | ||||||
|  | 	AllowedRoles                       []string `flag:"allowed-role" cfg:"allowed_roles"` | ||||||
| 
 | 
 | ||||||
| 	AcrValues  string `flag:"acr-values" cfg:"acr_values"` | 	AcrValues  string `flag:"acr-values" cfg:"acr_values"` | ||||||
| 	JWTKey     string `flag:"jwt-key" cfg:"jwt_key"` | 	JWTKey     string `flag:"jwt-key" cfg:"jwt_key"` | ||||||
|  | @ -563,6 +564,7 @@ func legacyProviderFlagSet() *pflag.FlagSet { | ||||||
| 
 | 
 | ||||||
| 	flagSet.String("user-id-claim", providers.OIDCEmailClaim, "(DEPRECATED for `oidc-email-claim`) which claim contains the user ID") | 	flagSet.String("user-id-claim", providers.OIDCEmailClaim, "(DEPRECATED for `oidc-email-claim`) which claim contains the user ID") | ||||||
| 	flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)") | 	flagSet.StringSlice("allowed-group", []string{}, "restrict logins to members of this group (may be given multiple times)") | ||||||
|  | 	flagSet.StringSlice("allowed-role", []string{}, "(keycloak-oidc) restrict logins to members of these roles (may be given multiple times)") | ||||||
| 
 | 
 | ||||||
| 	return flagSet | 	return flagSet | ||||||
| } | } | ||||||
|  | @ -659,6 +661,7 @@ func (l *LegacyProvider) convert() (Providers, error) { | ||||||
| 	case "keycloak": | 	case "keycloak": | ||||||
| 		provider.KeycloakConfig = KeycloakOptions{ | 		provider.KeycloakConfig = KeycloakOptions{ | ||||||
| 			Groups: l.KeycloakGroups, | 			Groups: l.KeycloakGroups, | ||||||
|  | 			Roles:  l.AllowedRoles, | ||||||
| 		} | 		} | ||||||
| 	case "gitlab": | 	case "gitlab": | ||||||
| 		provider.GitLabConfig = GitLabOptions{ | 		provider.GitLabConfig = GitLabOptions{ | ||||||
|  |  | ||||||
|  | @ -78,6 +78,7 @@ type Provider struct { | ||||||
| type KeycloakOptions struct { | type KeycloakOptions struct { | ||||||
| 	// Group enables to restrict login to members of indicated group
 | 	// Group enables to restrict login to members of indicated group
 | ||||||
| 	Groups []string `json:"groups,omitempty"` | 	Groups []string `json:"groups,omitempty"` | ||||||
|  | 	Roles  []string `json:"roles,omitempty"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type AzureOptions struct { | type AzureOptions struct { | ||||||
|  |  | ||||||
|  | @ -251,6 +251,7 @@ func parseProviderInfo(o *options.Options, msgs []string) []string { | ||||||
| 		if p.Verifier == nil { | 		if p.Verifier == nil { | ||||||
| 			msgs = append(msgs, "keycloak-oidc provider requires an oidc issuer URL") | 			msgs = append(msgs, "keycloak-oidc provider requires an oidc issuer URL") | ||||||
| 		} | 		} | ||||||
|  | 		p.AddAllowedRoles(o.Providers[0].KeycloakConfig.Roles) | ||||||
| 	case *providers.GoogleProvider: | 	case *providers.GoogleProvider: | ||||||
| 		if o.Providers[0].GoogleConfig.ServiceAccountJSON != "" { | 		if o.Providers[0].GoogleConfig.ServiceAccountJSON != "" { | ||||||
| 			file, err := os.Open(o.Providers[0].GoogleConfig.ServiceAccountJSON) | 			file, err := os.Open(o.Providers[0].GoogleConfig.ServiceAccountJSON) | ||||||
|  |  | ||||||
|  | @ -2,8 +2,10 @@ package providers | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"fmt" | ||||||
| 
 | 
 | ||||||
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | ||||||
|  | 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const keycloakOIDCProviderName = "Keycloak OIDC" | const keycloakOIDCProviderName = "Keycloak OIDC" | ||||||
|  | @ -25,6 +27,15 @@ func NewKeycloakOIDCProvider(p *ProviderData) *KeycloakOIDCProvider { | ||||||
| 
 | 
 | ||||||
| var _ Provider = (*KeycloakOIDCProvider)(nil) | var _ Provider = (*KeycloakOIDCProvider)(nil) | ||||||
| 
 | 
 | ||||||
|  | // AddAllowedRoles sets Keycloak roles that are authorized.
 | ||||||
|  | // Assumes `SetAllowedGroups` is already called on groups and appends to that
 | ||||||
|  | // with `role:` prefixed roles.
 | ||||||
|  | func (p *KeycloakOIDCProvider) AddAllowedRoles(roles []string) { | ||||||
|  | 	for _, role := range roles { | ||||||
|  | 		p.AllowedGroups[formatRole(role)] = struct{}{} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // EnrichSession is called after Redeem to allow providers to enrich session fields
 | // EnrichSession is called after Redeem to allow providers to enrich session fields
 | ||||||
| // such as User, Email, Groups with provider specific API calls.
 | // such as User, Email, Groups with provider specific API calls.
 | ||||||
| func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error { | func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error { | ||||||
|  | @ -36,6 +47,83 @@ func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.Se | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p *KeycloakOIDCProvider) extractRoles(ctx context.Context, s *sessions.SessionState) error { | func (p *KeycloakOIDCProvider) extractRoles(ctx context.Context, s *sessions.SessionState) error { | ||||||
| 	// TODO: Implement me with Access Token Role claim extraction logic
 | 	claims, err := p.getAccessClaims(ctx, s) | ||||||
| 	return ErrNotImplemented | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var roles []string | ||||||
|  | 	roles = append(roles, claims.RealmAccess.Roles...) | ||||||
|  | 	roles = append(roles, getClientRoles(claims)...) | ||||||
|  | 
 | ||||||
|  | 	// Add to groups list with `role:` prefix to distinguish from groups
 | ||||||
|  | 	for _, role := range roles { | ||||||
|  | 		s.Groups = append(s.Groups, formatRole(role)) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type realmAccess struct { | ||||||
|  | 	Roles []string `json:"roles"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type accessClaims struct { | ||||||
|  | 	RealmAccess    realmAccess            `json:"realm_access"` | ||||||
|  | 	ResourceAccess map[string]interface{} `json:"resource_access"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (p *KeycloakOIDCProvider) getAccessClaims(ctx context.Context, s *sessions.SessionState) (*accessClaims, error) { | ||||||
|  | 	// HACK: This isn't an ID Token, but has similar structure & signing
 | ||||||
|  | 	token, err := p.Verifier.Verify(ctx, s.AccessToken) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	var claims *accessClaims | ||||||
|  | 	if err = token.Claims(&claims); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return claims, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // getClientRoles extracts client roles from the `resource_access` claim with
 | ||||||
|  | // the format `client:role`.
 | ||||||
|  | //
 | ||||||
|  | // ResourceAccess format:
 | ||||||
|  | // "resource_access": {
 | ||||||
|  | //   "clientA": {
 | ||||||
|  | //     "roles": [
 | ||||||
|  | //       "roleA"
 | ||||||
|  | //     ]
 | ||||||
|  | //   },
 | ||||||
|  | //   "clientB": {
 | ||||||
|  | //     "roles": [
 | ||||||
|  | //       "roleA",
 | ||||||
|  | //       "roleB",
 | ||||||
|  | //       "roleC"
 | ||||||
|  | //     ]
 | ||||||
|  | //   }
 | ||||||
|  | // }
 | ||||||
|  | func getClientRoles(claims *accessClaims) []string { | ||||||
|  | 	var clientRoles []string | ||||||
|  | 	for clientName, access := range claims.ResourceAccess { | ||||||
|  | 		accessMap, ok := access.(map[string]interface{}) | ||||||
|  | 		if !ok { | ||||||
|  | 			logger.Errorf("Unable to parse client roles from claims for client: %v", clientName) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var roles interface{} | ||||||
|  | 		if roles, ok = accessMap["roles"]; !ok { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		for _, role := range roles.([]interface{}) { | ||||||
|  | 			clientRoles = append(clientRoles, fmt.Sprintf("%s:%s", clientName, role)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return clientRoles | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func formatRole(role string) string { | ||||||
|  | 	return fmt.Sprintf("role:%s", role) | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue