1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-08-06 22:42:56 +02:00

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>
This commit is contained in:
Guillaume "Elektordi" Genty
2025-04-28 11:23:19 +02:00
committed by GitHub
parent 367183d7b8
commit 7b41c8e987
3 changed files with 51 additions and 30 deletions

View File

@ -11,6 +11,7 @@
- [#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)
- [#3001](https://github.com/oauth2-proxy/oauth2-proxy/pull/3001) Allow to set non-default authorization request response mode (@stieler-it)
- [#3041](https://github.com/oauth2-proxy/oauth2-proxy/pull/3041) chore(deps): upgrade to latest golang v1.23.x release (@TheImplementer)
- [#1916](https://github.com/oauth2-proxy/oauth2-proxy/pull/1916) fix: role extraction from access token in keycloak oidc (@Elektordi / @tuunit)
# V7.8.2

View File

@ -2,7 +2,10 @@ package providers
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
@ -51,7 +54,7 @@ func (p *KeycloakOIDCProvider) CreateSessionFromToken(ctx context.Context, token
}
// Extract custom keycloak roles and enrich session
if err := p.extractRoles(ctx, ss); err != nil {
if err := p.extractRoles(ss); err != nil {
return nil, err
}
@ -65,7 +68,7 @@ func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.Se
if err != nil {
return fmt.Errorf("could not enrich oidc session: %v", err)
}
return p.extractRoles(ctx, s)
return p.extractRoles(s)
}
// RefreshSession adds role extraction logic to the refresh flow
@ -77,11 +80,11 @@ func (p *KeycloakOIDCProvider) RefreshSession(ctx context.Context, s *sessions.S
return refreshed, err
}
return true, p.extractRoles(ctx, s)
return true, p.extractRoles(s)
}
func (p *KeycloakOIDCProvider) extractRoles(ctx context.Context, s *sessions.SessionState) error {
claims, err := p.getAccessClaims(ctx, s)
func (p *KeycloakOIDCProvider) extractRoles(s *sessions.SessionState) error {
claims, err := p.getAccessClaims(s)
if err != nil {
return err
}
@ -106,18 +109,22 @@ type accessClaims struct {
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
func (p *KeycloakOIDCProvider) getAccessClaims(s *sessions.SessionState) (*accessClaims, error) {
parts := strings.Split(s.AccessToken, ".")
if len(parts) < 2 {
return nil, fmt.Errorf("malformed access token, expected 3 parts got %d", len(parts))
}
var claims *accessClaims
if err = token.Claims(&claims); err != nil {
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("malformed access token, couldn't extract jwt payload: %v", err)
}
var claims accessClaims
if err := json.Unmarshal(payload, &claims); err != nil {
return nil, err
}
return claims, nil
return &claims, nil
}
// getClientRoles extracts client roles from the `resource_access` claim with

View File

@ -6,6 +6,7 @@ import (
"fmt"
"net/http/httptest"
"net/url"
"strings"
"github.com/coreos/go-oidc/v3/oidc"
@ -18,28 +19,40 @@ import (
)
const (
idTokenHeader = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjV1IteTRzRVU1MjZVelk1SFd6UEZJbWdMMWRKUllfQ0gyY1FFRXh4UGN3In0"
idTokenSignature = "Rh0zQGhWAm-2hn5JTWB3Lzuk9Ahpzs7As7ks-1VInl4"
accessTokenHeader = "ewogICJhbGciOiAiUlMyNTYiLAogICJ0eXAiOiAiSldUIgp9"
accessTokenSignature = "dyt0CoTl4WoVjAHI9Q_CwSKhl6d_9rhM3NrXuJttkao"
defaultAudienceClaim = "aud"
mockClientID = "cd6d4fae-f6a6-4a34-8454-2c6b598e9532"
)
var accessTokenPayload = base64.StdEncoding.EncodeToString([]byte(
fmt.Sprintf(`{"%s": "%s", "realm_access": {"roles": ["write"]}, "resource_access": {"default": {"roles": ["read"]}}}`, defaultAudienceClaim, mockClientID)))
var (
accessTokenPayload = base64.RawURLEncoding.EncodeToString([]byte(
fmt.Sprintf(`{"%s": "%s", "realm_access": {"roles": ["write"]}, "resource_access": {"default": {"roles": ["read"]}}}`, defaultAudienceClaim, mockClientID)))
idTokenPayload = base64.RawURLEncoding.EncodeToString([]byte(
fmt.Sprintf(`{"%s": "%s"}`, defaultAudienceClaim, mockClientID)))
)
type DummyKeySet struct{}
func (DummyKeySet) VerifySignature(_ context.Context, _ string) (payload []byte, err error) {
p, _ := base64.RawURLEncoding.DecodeString(accessTokenPayload)
func (DummyKeySet) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
parts := strings.Split(jwt, ".")
p, _ := base64.RawURLEncoding.DecodeString(parts[1])
return p, nil
}
func getAccessToken() string {
func makeIDToken() string {
return fmt.Sprintf("%s.%s.%s", idTokenHeader, idTokenPayload, idTokenSignature)
}
func makeAccessToken() string {
return fmt.Sprintf("%s.%s.%s", accessTokenHeader, accessTokenPayload, accessTokenSignature)
}
func newTestKeycloakOIDCSetup() (*httptest.Server, *KeycloakOIDCProvider) {
redeemURL, server := newOIDCServer([]byte(fmt.Sprintf(`{"email": "new@thing.com", "expires_in": 300, "access_token": "%v"}`, getAccessToken())))
redeemURL, server := newOIDCServer([]byte(fmt.Sprintf(`{"email": "new@thing.com", "expires_in": 300, "id_token": "%v", "access_token": "%v"}`, makeIDToken(), makeAccessToken())))
provider := newKeycloakOIDCProvider(redeemURL, options.Provider{})
return server, provider
}
@ -134,16 +147,16 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
User: "already",
Email: "a@b.com",
Groups: nil,
IDToken: idToken,
AccessToken: getAccessToken(),
IDToken: makeIDToken(),
AccessToken: makeAccessToken(),
RefreshToken: refreshToken,
}
expectedSession := &sessions.SessionState{
User: "already",
Email: "a@b.com",
Groups: []string{"role:write", "role:default:read"},
IDToken: idToken,
AccessToken: getAccessToken(),
IDToken: makeIDToken(),
AccessToken: makeAccessToken(),
RefreshToken: refreshToken,
}
@ -164,16 +177,16 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
User: "already",
Email: "a@b.com",
Groups: []string{"existing", "group"},
IDToken: idToken,
AccessToken: getAccessToken(),
IDToken: makeIDToken(),
AccessToken: makeAccessToken(),
RefreshToken: refreshToken,
}
expectedSession := &sessions.SessionState{
User: "already",
Email: "a@b.com",
Groups: []string{"existing", "group", "role:write", "role:default:read"},
IDToken: idToken,
AccessToken: getAccessToken(),
IDToken: makeIDToken(),
AccessToken: makeAccessToken(),
RefreshToken: refreshToken,
}
@ -196,8 +209,8 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
User: "already",
Email: "a@b.com",
Groups: nil,
IDToken: idToken,
AccessToken: getAccessToken(),
IDToken: makeIDToken(),
AccessToken: makeAccessToken(),
RefreshToken: refreshToken,
}
@ -219,7 +232,7 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
provider.ProfileURL = url
session, err := provider.CreateSessionFromToken(context.Background(), getAccessToken())
session, err := provider.CreateSessionFromToken(context.Background(), makeAccessToken())
Expect(err).To(BeNil())
Expect(session.ExpiresOn).ToNot(BeNil())
Expect(session.CreatedAt).ToNot(BeNil())