git.iko.soy/typeable/oauth2-proxy
on master help sign in
171 lines 4.7 kB history raw
1package providers
3import (
4 "context"
5 "encoding/base64"
6 "encoding/json"
7 "fmt"
8 "strings"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
14const keycloakOIDCProviderName = "Keycloak OIDC"
16// KeycloakOIDCProvider creates a Keycloak provider based on OIDCProvider
17type KeycloakOIDCProvider struct {
18 *OIDCProvider
21// NewKeycloakOIDCProvider makes a KeycloakOIDCProvider using the ProviderData
22func NewKeycloakOIDCProvider(p *ProviderData, opts options.Provider) *KeycloakOIDCProvider {
23 p.setProviderDefaults(providerDefaults{
24 name: keycloakOIDCProviderName,
25 })
27 provider := &KeycloakOIDCProvider{
28 OIDCProvider: NewOIDCProvider(p, opts.OIDCConfig),
29 }
31 provider.addAllowedRoles(opts.KeycloakConfig.Roles)
32 return provider
35var _ Provider = (*KeycloakOIDCProvider)(nil)
37// addAllowedRoles sets Keycloak roles that are authorized.
38// Assumes `SetAllowedGroups` is already called on groups and appends to that
39// with `role:` prefixed roles.
40func (p *KeycloakOIDCProvider) addAllowedRoles(roles []string) {
41 if p.AllowedGroups == nil {
42 p.AllowedGroups = make(map[string]struct{})
43 }
44 for _, role := range roles {
45 p.AllowedGroups[formatRole(role)] = struct{}{}
46 }
49// CreateSessionFromToken converts Bearer IDTokens into sessions
50func (p *KeycloakOIDCProvider) CreateSessionFromToken(ctx context.Context, token string) (*sessions.SessionState, error) {
51 ss, err := p.OIDCProvider.CreateSessionFromToken(ctx, token)
52 if err != nil {
53 return nil, fmt.Errorf("could not create session from token: %v", err)
54 }
56 // Extract custom keycloak roles and enrich session
57 if err := p.extractRoles(ss); err != nil {
58 return nil, err
59 }
61 return ss, nil
64// EnrichSession is called after Redeem to allow providers to enrich session fields
65// such as User, Email, Groups with provider specific API calls.
66func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
67 err := p.OIDCProvider.EnrichSession(ctx, s)
68 if err != nil {
69 return fmt.Errorf("could not enrich oidc session: %v", err)
70 }
71 return p.extractRoles(s)
74// RefreshSession adds role extraction logic to the refresh flow
75func (p *KeycloakOIDCProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
76 refreshed, err := p.OIDCProvider.RefreshSession(ctx, s)
78 // Refresh could have failed or there was not session to refresh (with no error raised)
79 if err != nil || !refreshed {
80 return refreshed, err
81 }
83 return true, p.extractRoles(s)
86func (p *KeycloakOIDCProvider) extractRoles(s *sessions.SessionState) error {
87 claims, err := p.getAccessClaims(s)
88 if err != nil {
89 return err
90 }
92 //nolint:prealloc
93 var roles []string
94 roles = append(roles, claims.RealmAccess.Roles...)
95 roles = append(roles, getClientRoles(claims)...)
97 // Add to groups list with `role:` prefix to distinguish from groups
98 for _, role := range roles {
99 s.Groups = append(s.Groups, formatRole(role))
100 }
101 return nil
104type realmAccess struct {
105 Roles []string `json:"roles"`
108type accessClaims struct {
109 RealmAccess realmAccess `json:"realm_access"`
110 ResourceAccess map[string]interface{} `json:"resource_access"`
113func (p *KeycloakOIDCProvider) getAccessClaims(s *sessions.SessionState) (*accessClaims, error) {
114 parts := strings.Split(s.AccessToken, ".")
115 if len(parts) < 2 {
116 return nil, fmt.Errorf("malformed access token, expected 3 parts got %d", len(parts))
117 }
119 payload, err := base64.RawURLEncoding.DecodeString(parts[1])
120 if err != nil {
121 return nil, fmt.Errorf("malformed access token, couldn't extract jwt payload: %v", err)
122 }
124 var claims accessClaims
125 if err := json.Unmarshal(payload, &claims); err != nil {
126 return nil, err
127 }
128 return &claims, nil
131// getClientRoles extracts client roles from the `resource_access` claim with
132// the format `client:role`.
133//
134// ResourceAccess format:
135//
136// "resource_access": {
137// "clientA": {
138// "roles": [
139// "roleA"
140// ]
141// },
142// "clientB": {
143// "roles": [
144// "roleA",
145// "roleB",
146// "roleC"
147// ]
148// }
149// }
150func getClientRoles(claims *accessClaims) []string {
151 var clientRoles []string
152 for clientName, access := range claims.ResourceAccess {
153 accessMap, ok := access.(map[string]interface{})
154 if !ok {
155 continue
156 }
158 var roles interface{}
159 if roles, ok = accessMap["roles"]; !ok {
160 continue
161 }
162 for _, role := range roles.([]interface{}) {
163 clientRoles = append(clientRoles, fmt.Sprintf("%s:%s", clientName, role))
164 }
165 }
166 return clientRoles
169func formatRole(role string) string {
170 return fmt.Sprintf("role:%s", role)