Extract roles from Keycloak Access Tokens
This commit is contained in:
		
							parent
							
								
									07eb0efa6e
								
							
						
					
					
						commit
						3bda10f005
					
				|  | @ -33,6 +33,7 @@ type Options struct { | |||
| 
 | ||||
| 	AuthenticatedEmailsFile  string   `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` | ||||
| 	KeycloakGroups           []string `flag:"keycloak-group" cfg:"keycloak_groups"` | ||||
| 	KeycloakRoles            []string `flag:"keycloak-role" cfg:"keycloak_roles"` | ||||
| 	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant"` | ||||
| 	BitbucketTeam            string   `flag:"bitbucket-team" cfg:"bitbucket_team"` | ||||
| 	BitbucketRepository      string   `flag:"bitbucket-repository" cfg:"bitbucket_repository"` | ||||
|  | @ -173,6 +174,7 @@ func NewFlagSet() *pflag.FlagSet { | |||
| 	flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email") | ||||
| 	flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)") | ||||
| 	flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)") | ||||
| 	flagSet.StringSlice("keycloak-role", []string{}, "restrict logins to members of these roles (may be given multiple times)") | ||||
| 	flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.") | ||||
| 	flagSet.String("bitbucket-team", "", "restrict logins to members of this team") | ||||
| 	flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository") | ||||
|  |  | |||
|  | @ -275,12 +275,13 @@ func parseProviderInfo(o *options.Options, msgs []string) []string { | |||
| 
 | ||||
| 		// Backwards compatibility with `--keycloak-group` option
 | ||||
| 		if len(o.KeycloakGroups) > 0 { | ||||
| 			// Maybe already added with proper `--allowed-group` flag
 | ||||
| 			// Maybe already added with `--allowed-group` flag
 | ||||
| 			if !strings.Contains(o.Scope, " groups") { | ||||
| 				o.Scope += " groups" | ||||
| 			} | ||||
| 			p.SetAllowedGroups(o.KeycloakGroups) | ||||
| 		} | ||||
| 		p.AddAllowedRoles(o.KeycloakRoles) | ||||
| 	case *providers.GoogleProvider: | ||||
| 		if o.GoogleServiceAccountJSON != "" { | ||||
| 			file, err := os.Open(o.GoogleServiceAccountJSON) | ||||
|  |  | |||
|  | @ -2,8 +2,10 @@ package providers | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions" | ||||
| 	"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger" | ||||
| ) | ||||
| 
 | ||||
| const keycloakOIDCProviderName = "Keycloak OIDC" | ||||
|  | @ -25,6 +27,15 @@ func NewKeycloakOIDCProvider(p *ProviderData) *KeycloakOIDCProvider { | |||
| 
 | ||||
| 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
 | ||||
| // such as User, Email, Groups with provider specific API calls.
 | ||||
| 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 { | ||||
| 	// TODO: Implement me with Access Token Role claim extraction logic
 | ||||
| 	return ErrNotImplemented | ||||
| 	claims, err := p.getAccessClaims(ctx, s) | ||||
| 	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