You've already forked oauth2-proxy
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:
committed by
GitHub
parent
367183d7b8
commit
7b41c8e987
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
Reference in New Issue
Block a user