git.iko.soy/typeable/oauth2-proxy
on master help sign in
fix: role extraction from access token in keycloak oidc (#1916)

* Fix wrong token used in Keycloak OIDC provider * Update CHANGELOG for PR #1916 * Update tests * fix: keycloak oidc role extraction --------- Co-authored-by: Jan Larwig <jan@larwig.com>

commit7b41c8e98761ab54f00d0228caadd43f519e6f7d authorGuillaume "Elektordi" Genty <github@elektordi.net> dateMon 28 Apr 2025, 11:23 (1 year ago) parent367183d
3 files changed+50−29
CHANGELOG.md+1 providers/keycloak_oidc.go+19−12 providers/keycloak_oidc_test.go+30−17
CHANGELOG.md+1
@@ -11,6 +11,7 @@
1111- [#3031](https://github.com/oauth2-proxy/oauth2-proxy/pull/3031) Fixes Refresh Token bug with Entra ID and Workload Identity (#3027)[https://github.com/oauth2-proxy/oauth2-proxy/issues/3028] by using client assertion when redeeming the token (@richard87)
1212- [#3001](https://github.com/oauth2-proxy/oauth2-proxy/pull/3001) Allow to set non-default authorization request response mode (@stieler-it)
1313- [#3041](https://github.com/oauth2-proxy/oauth2-proxy/pull/3041) chore(deps): upgrade to latest golang v1.23.x release (@TheImplementer)
14+- [#1916](https://github.com/oauth2-proxy/oauth2-proxy/pull/1916) fix: role extraction from access token in keycloak oidc (@Elektordi / @tuunit)
1415
1516# V7.8.2
1617
providers/keycloak_oidc.go+19−12
@@ -2,7 +2,10 @@ package providers
22
33import (
44 "context"
5+ "encoding/base64"
6+ "encoding/json"
57 "fmt"
8+ "strings"
69
710 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
811 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
@@ -51,7 +54,7 @@ func (p *KeycloakOIDCProvider) CreateSessionFromToken(ctx context.Context, token
5154 }
5255
5356 // Extract custom keycloak roles and enrich session
54− if err := p.extractRoles(ctx, ss); err != nil {
57+ if err := p.extractRoles(ss); err != nil {
5558 return nil, err
5659 }
5760
@@ -65,7 +68,7 @@ func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.Se
6568 if err != nil {
6669 return fmt.Errorf("could not enrich oidc session: %v", err)
6770 }
68− return p.extractRoles(ctx, s)
71+ return p.extractRoles(s)
6972}
7073
7174// RefreshSession adds role extraction logic to the refresh flow
@@ -77,11 +80,11 @@ func (p *KeycloakOIDCProvider) RefreshSession(ctx context.Context, s *sessions.S
7780 return refreshed, err
7881 }
7982
80− return true, p.extractRoles(ctx, s)
83+ return true, p.extractRoles(s)
8184}
8285
83−func (p *KeycloakOIDCProvider) extractRoles(ctx context.Context, s *sessions.SessionState) error {
84− claims, err := p.getAccessClaims(ctx, s)
86+func (p *KeycloakOIDCProvider) extractRoles(s *sessions.SessionState) error {
87+ claims, err := p.getAccessClaims(s)
8588 if err != nil {
8689 return err
8790 }
@@ -106,18 +109,22 @@ type accessClaims struct {
106109 ResourceAccess map[string]interface{} `json:"resource_access"`
107110}
108111
109−func (p *KeycloakOIDCProvider) getAccessClaims(ctx context.Context, s *sessions.SessionState) (*accessClaims, error) {
110− // HACK: This isn't an ID Token, but has similar structure & signing
111− token, err := p.Verifier.Verify(ctx, s.AccessToken)
112+func (p *KeycloakOIDCProvider) getAccessClaims(s *sessions.SessionState) (*accessClaims, error) {
113+ parts := strings.Split(s.AccessToken, ".")
114+ if len(parts) < 2 {
115+ return nil, fmt.Errorf("malformed access token, expected 3 parts got %d", len(parts))
116+ }
117+
118+ payload, err := base64.RawURLEncoding.DecodeString(parts[1])
112119 if err != nil {
113− return nil, err
120+ return nil, fmt.Errorf("malformed access token, couldn't extract jwt payload: %v", err)
114121 }
115122
116− var claims *accessClaims
117− if err = token.Claims(&claims); err != nil {
123+ var claims accessClaims
124+ if err := json.Unmarshal(payload, &claims); err != nil {
118125 return nil, err
119126 }
120− return claims, nil
127+ return &claims, nil
121128}
122129
123130// getClientRoles extracts client roles from the `resource_access` claim with
providers/keycloak_oidc_test.go+30−17
@@ -6,6 +6,7 @@ import (
66 "fmt"
77 "net/http/httptest"
88 "net/url"
9+ "strings"
910
1011 "github.com/coreos/go-oidc/v3/oidc"
1112
@@ -18,28 +19,40 @@ import (
1819)
1920
2021const (
22+ idTokenHeader = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjV1IteTRzRVU1MjZVelk1SFd6UEZJbWdMMWRKUllfQ0gyY1FFRXh4UGN3In0"
23+ idTokenSignature = "Rh0zQGhWAm-2hn5JTWB3Lzuk9Ahpzs7As7ks-1VInl4"
2124 accessTokenHeader = "ewogICJhbGciOiAiUlMyNTYiLAogICJ0eXAiOiAiSldUIgp9"
2225 accessTokenSignature = "dyt0CoTl4WoVjAHI9Q_CwSKhl6d_9rhM3NrXuJttkao"
2326 defaultAudienceClaim = "aud"
2427 mockClientID = "cd6d4fae-f6a6-4a34-8454-2c6b598e9532"
2528)
2629
27−var accessTokenPayload = base64.StdEncoding.EncodeToString([]byte(
28− fmt.Sprintf(`{"%s": "%s", "realm_access": {"roles": ["write"]}, "resource_access": {"default": {"roles": ["read"]}}}`, defaultAudienceClaim, mockClientID)))
30+var (
31+ accessTokenPayload = base64.RawURLEncoding.EncodeToString([]byte(
32+ fmt.Sprintf(`{"%s": "%s", "realm_access": {"roles": ["write"]}, "resource_access": {"default": {"roles": ["read"]}}}`, defaultAudienceClaim, mockClientID)))
33+
34+ idTokenPayload = base64.RawURLEncoding.EncodeToString([]byte(
35+ fmt.Sprintf(`{"%s": "%s"}`, defaultAudienceClaim, mockClientID)))
36+)
2937
3038type DummyKeySet struct{}
3139
32−func (DummyKeySet) VerifySignature(_ context.Context, _ string) (payload []byte, err error) {
33− p, _ := base64.RawURLEncoding.DecodeString(accessTokenPayload)
40+func (DummyKeySet) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
41+ parts := strings.Split(jwt, ".")
42+ p, _ := base64.RawURLEncoding.DecodeString(parts[1])
3443 return p, nil
3544}
3645
37−func getAccessToken() string {
46+func makeIDToken() string {
47+ return fmt.Sprintf("%s.%s.%s", idTokenHeader, idTokenPayload, idTokenSignature)
48+}
49+
50+func makeAccessToken() string {
3851 return fmt.Sprintf("%s.%s.%s", accessTokenHeader, accessTokenPayload, accessTokenSignature)
3952}
4053
4154func newTestKeycloakOIDCSetup() (*httptest.Server, *KeycloakOIDCProvider) {
42− redeemURL, server := newOIDCServer([]byte(fmt.Sprintf(`{"email": "new@thing.com", "expires_in": 300, "access_token": "%v"}`, getAccessToken())))
55+ redeemURL, server := newOIDCServer([]byte(fmt.Sprintf(`{"email": "new@thing.com", "expires_in": 300, "id_token": "%v", "access_token": "%v"}`, makeIDToken(), makeAccessToken())))
4356 provider := newKeycloakOIDCProvider(redeemURL, options.Provider{})
4457 return server, provider
4558}
@@ -134,16 +147,16 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
134147 User: "already",
135148 Email: "a@b.com",
136149 Groups: nil,
137− IDToken: idToken,
138− AccessToken: getAccessToken(),
150+ IDToken: makeIDToken(),
151+ AccessToken: makeAccessToken(),
139152 RefreshToken: refreshToken,
140153 }
141154 expectedSession := &sessions.SessionState{
142155 User: "already",
143156 Email: "a@b.com",
144157 Groups: []string{"role:write", "role:default:read"},
145− IDToken: idToken,
146− AccessToken: getAccessToken(),
158+ IDToken: makeIDToken(),
159+ AccessToken: makeAccessToken(),
147160 RefreshToken: refreshToken,
148161 }
149162
@@ -164,16 +177,16 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
164177 User: "already",
165178 Email: "a@b.com",
166179 Groups: []string{"existing", "group"},
167− IDToken: idToken,
168− AccessToken: getAccessToken(),
180+ IDToken: makeIDToken(),
181+ AccessToken: makeAccessToken(),
169182 RefreshToken: refreshToken,
170183 }
171184 expectedSession := &sessions.SessionState{
172185 User: "already",
173186 Email: "a@b.com",
174187 Groups: []string{"existing", "group", "role:write", "role:default:read"},
175− IDToken: idToken,
176− AccessToken: getAccessToken(),
188+ IDToken: makeIDToken(),
189+ AccessToken: makeAccessToken(),
177190 RefreshToken: refreshToken,
178191 }
179192
@@ -196,8 +209,8 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
196209 User: "already",
197210 Email: "a@b.com",
198211 Groups: nil,
199− IDToken: idToken,
200− AccessToken: getAccessToken(),
212+ IDToken: makeIDToken(),
213+ AccessToken: makeAccessToken(),
201214 RefreshToken: refreshToken,
202215 }
203216
@@ -219,7 +232,7 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
219232
220233 provider.ProfileURL = url
221234
222− session, err := provider.CreateSessionFromToken(context.Background(), getAccessToken())
235+ session, err := provider.CreateSessionFromToken(context.Background(), makeAccessToken())
223236 Expect(err).To(BeNil())
224237 Expect(session.ExpiresOn).ToNot(BeNil())
225238 Expect(session.CreatedAt).ToNot(BeNil())