@@ -2,8 +2,10 @@ package providers
22
33import (
44 "context"
5+ "fmt"
56
67 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
8+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
79)
810
911const keycloakOIDCProviderName = "Keycloak OIDC"
@@ -25,6 +27,15 @@ func NewKeycloakOIDCProvider(p *ProviderData) *KeycloakOIDCProvider {
2527
2628var _ Provider = (*KeycloakOIDCProvider)(nil)
2729
30+// AddAllowedRoles sets Keycloak roles that are authorized.
31+// Assumes `SetAllowedGroups` is already called on groups and appends to that
32+// with `role:` prefixed roles.
33+func (p *KeycloakOIDCProvider) AddAllowedRoles(roles []string) {
34+ for _, role := range roles {
35+ p.AllowedGroups[formatRole(role)] = struct{}{}
36+ }
37+}
38+
2839// EnrichSession is called after Redeem to allow providers to enrich session fields
2940// such as User, Email, Groups with provider specific API calls.
3041func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
@@ -36,6 +47,83 @@ func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.Se
3647}
3748
3849func (p *KeycloakOIDCProvider) extractRoles(ctx context.Context, s *sessions.SessionState) error {
39− // TODO: Implement me with Access Token Role claim extraction logic
40− return ErrNotImplemented
50+ claims, err := p.getAccessClaims(ctx, s)
51+ if err != nil {
52+ return err
53+ }
54+
55+ var roles []string
56+ roles = append(roles, claims.RealmAccess.Roles...)
57+ roles = append(roles, getClientRoles(claims)...)
58+
59+ // Add to groups list with `role:` prefix to distinguish from groups
60+ for _, role := range roles {
61+ s.Groups = append(s.Groups, formatRole(role))
62+ }
63+ return nil
64+}
65+
66+type realmAccess struct {
67+ Roles []string `json:"roles"`
68+}
69+
70+type accessClaims struct {
71+ RealmAccess realmAccess `json:"realm_access"`
72+ ResourceAccess map[string]interface{} `json:"resource_access"`
73+}
74+
75+func (p *KeycloakOIDCProvider) getAccessClaims(ctx context.Context, s *sessions.SessionState) (*accessClaims, error) {
76+ // HACK: This isn't an ID Token, but has similar structure & signing
77+ token, err := p.Verifier.Verify(ctx, s.AccessToken)
78+ if err != nil {
79+ return nil, err
80+ }
81+
82+ var claims *accessClaims
83+ if err = token.Claims(&claims); err != nil {
84+ return nil, err
85+ }
86+ return claims, nil
87+}
88+
89+// getClientRoles extracts client roles from the `resource_access` claim with
90+// the format `client:role`.
91+//
92+// ResourceAccess format:
93+// "resource_access": {
94+// "clientA": {
95+// "roles": [
96+// "roleA"
97+// ]
98+// },
99+// "clientB": {
100+// "roles": [
101+// "roleA",
102+// "roleB",
103+// "roleC"
104+// ]
105+// }
106+// }
107+func getClientRoles(claims *accessClaims) []string {
108+ var clientRoles []string
109+ for clientName, access := range claims.ResourceAccess {
110+ accessMap, ok := access.(map[string]interface{})
111+ if !ok {
112+ logger.Errorf("Unable to parse client roles from claims for client: %v", clientName)
113+ continue
114+ }
115+
116+ var roles interface{}
117+ if roles, ok = accessMap["roles"]; !ok {
118+ continue
119+ }
120+ for _, role := range roles.([]interface{}) {
121+ clientRoles = append(clientRoles, fmt.Sprintf("%s:%s", clientName, role))
122+ }
123+ }
124+ return clientRoles
125+}
126+
127+func formatRole(role string) string {
128+ return fmt.Sprintf("role:%s", role)
41129}