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"` | 	AuthenticatedEmailsFile  string   `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"` | ||||||
| 	KeycloakGroups           []string `flag:"keycloak-group" cfg:"keycloak_groups"` | 	KeycloakGroups           []string `flag:"keycloak-group" cfg:"keycloak_groups"` | ||||||
|  | 	KeycloakRoles            []string `flag:"keycloak-role" cfg:"keycloak_roles"` | ||||||
| 	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant"` | 	AzureTenant              string   `flag:"azure-tenant" cfg:"azure_tenant"` | ||||||
| 	BitbucketTeam            string   `flag:"bitbucket-team" cfg:"bitbucket_team"` | 	BitbucketTeam            string   `flag:"bitbucket-team" cfg:"bitbucket_team"` | ||||||
| 	BitbucketRepository      string   `flag:"bitbucket-repository" cfg:"bitbucket_repository"` | 	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("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("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-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("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-team", "", "restrict logins to members of this team") | ||||||
| 	flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository") | 	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
 | 		// Backwards compatibility with `--keycloak-group` option
 | ||||||
| 		if len(o.KeycloakGroups) > 0 { | 		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") { | 			if !strings.Contains(o.Scope, " groups") { | ||||||
| 				o.Scope += " groups" | 				o.Scope += " groups" | ||||||
| 			} | 			} | ||||||
| 			p.SetAllowedGroups(o.KeycloakGroups) | 			p.SetAllowedGroups(o.KeycloakGroups) | ||||||
| 		} | 		} | ||||||
|  | 		p.AddAllowedRoles(o.KeycloakRoles) | ||||||
| 	case *providers.GoogleProvider: | 	case *providers.GoogleProvider: | ||||||
| 		if o.GoogleServiceAccountJSON != "" { | 		if o.GoogleServiceAccountJSON != "" { | ||||||
| 			file, err := os.Open(o.GoogleServiceAccountJSON) | 			file, err := os.Open(o.GoogleServiceAccountJSON) | ||||||
|  |  | ||||||
|  | @ -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