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)
|
- [#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)
|
- [#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)
|
- [#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
|
# V7.8.2
|
||||||
|
|
||||||
|
@ -2,7 +2,10 @@ package providers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
|
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
|
||||||
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
|
"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
|
// 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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,7 +68,7 @@ func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.Se
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not enrich oidc session: %v", err)
|
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
|
// 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 refreshed, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, p.extractRoles(ctx, s)
|
return true, p.extractRoles(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *KeycloakOIDCProvider) extractRoles(ctx context.Context, s *sessions.SessionState) error {
|
func (p *KeycloakOIDCProvider) extractRoles(s *sessions.SessionState) error {
|
||||||
claims, err := p.getAccessClaims(ctx, s)
|
claims, err := p.getAccessClaims(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -106,18 +109,22 @@ type accessClaims struct {
|
|||||||
ResourceAccess map[string]interface{} `json:"resource_access"`
|
ResourceAccess map[string]interface{} `json:"resource_access"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *KeycloakOIDCProvider) getAccessClaims(ctx context.Context, s *sessions.SessionState) (*accessClaims, error) {
|
func (p *KeycloakOIDCProvider) getAccessClaims(s *sessions.SessionState) (*accessClaims, error) {
|
||||||
// HACK: This isn't an ID Token, but has similar structure & signing
|
parts := strings.Split(s.AccessToken, ".")
|
||||||
token, err := p.Verifier.Verify(ctx, s.AccessToken)
|
if len(parts) < 2 {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("malformed access token, expected 3 parts got %d", len(parts))
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims *accessClaims
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
if err = token.Claims(&claims); err != nil {
|
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 nil, err
|
||||||
}
|
}
|
||||||
return claims, nil
|
return &claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getClientRoles extracts client roles from the `resource_access` claim with
|
// getClientRoles extracts client roles from the `resource_access` claim with
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
|
||||||
@ -18,28 +19,40 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
idTokenHeader = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjV1IteTRzRVU1MjZVelk1SFd6UEZJbWdMMWRKUllfQ0gyY1FFRXh4UGN3In0"
|
||||||
|
idTokenSignature = "Rh0zQGhWAm-2hn5JTWB3Lzuk9Ahpzs7As7ks-1VInl4"
|
||||||
accessTokenHeader = "ewogICJhbGciOiAiUlMyNTYiLAogICJ0eXAiOiAiSldUIgp9"
|
accessTokenHeader = "ewogICJhbGciOiAiUlMyNTYiLAogICJ0eXAiOiAiSldUIgp9"
|
||||||
accessTokenSignature = "dyt0CoTl4WoVjAHI9Q_CwSKhl6d_9rhM3NrXuJttkao"
|
accessTokenSignature = "dyt0CoTl4WoVjAHI9Q_CwSKhl6d_9rhM3NrXuJttkao"
|
||||||
defaultAudienceClaim = "aud"
|
defaultAudienceClaim = "aud"
|
||||||
mockClientID = "cd6d4fae-f6a6-4a34-8454-2c6b598e9532"
|
mockClientID = "cd6d4fae-f6a6-4a34-8454-2c6b598e9532"
|
||||||
)
|
)
|
||||||
|
|
||||||
var accessTokenPayload = base64.StdEncoding.EncodeToString([]byte(
|
var (
|
||||||
|
accessTokenPayload = base64.RawURLEncoding.EncodeToString([]byte(
|
||||||
fmt.Sprintf(`{"%s": "%s", "realm_access": {"roles": ["write"]}, "resource_access": {"default": {"roles": ["read"]}}}`, defaultAudienceClaim, mockClientID)))
|
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{}
|
type DummyKeySet struct{}
|
||||||
|
|
||||||
func (DummyKeySet) VerifySignature(_ context.Context, _ string) (payload []byte, err error) {
|
func (DummyKeySet) VerifySignature(_ context.Context, jwt string) (payload []byte, err error) {
|
||||||
p, _ := base64.RawURLEncoding.DecodeString(accessTokenPayload)
|
parts := strings.Split(jwt, ".")
|
||||||
|
p, _ := base64.RawURLEncoding.DecodeString(parts[1])
|
||||||
return p, nil
|
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)
|
return fmt.Sprintf("%s.%s.%s", accessTokenHeader, accessTokenPayload, accessTokenSignature)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestKeycloakOIDCSetup() (*httptest.Server, *KeycloakOIDCProvider) {
|
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{})
|
provider := newKeycloakOIDCProvider(redeemURL, options.Provider{})
|
||||||
return server, provider
|
return server, provider
|
||||||
}
|
}
|
||||||
@ -134,16 +147,16 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
|
|||||||
User: "already",
|
User: "already",
|
||||||
Email: "a@b.com",
|
Email: "a@b.com",
|
||||||
Groups: nil,
|
Groups: nil,
|
||||||
IDToken: idToken,
|
IDToken: makeIDToken(),
|
||||||
AccessToken: getAccessToken(),
|
AccessToken: makeAccessToken(),
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
}
|
}
|
||||||
expectedSession := &sessions.SessionState{
|
expectedSession := &sessions.SessionState{
|
||||||
User: "already",
|
User: "already",
|
||||||
Email: "a@b.com",
|
Email: "a@b.com",
|
||||||
Groups: []string{"role:write", "role:default:read"},
|
Groups: []string{"role:write", "role:default:read"},
|
||||||
IDToken: idToken,
|
IDToken: makeIDToken(),
|
||||||
AccessToken: getAccessToken(),
|
AccessToken: makeAccessToken(),
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,16 +177,16 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
|
|||||||
User: "already",
|
User: "already",
|
||||||
Email: "a@b.com",
|
Email: "a@b.com",
|
||||||
Groups: []string{"existing", "group"},
|
Groups: []string{"existing", "group"},
|
||||||
IDToken: idToken,
|
IDToken: makeIDToken(),
|
||||||
AccessToken: getAccessToken(),
|
AccessToken: makeAccessToken(),
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
}
|
}
|
||||||
expectedSession := &sessions.SessionState{
|
expectedSession := &sessions.SessionState{
|
||||||
User: "already",
|
User: "already",
|
||||||
Email: "a@b.com",
|
Email: "a@b.com",
|
||||||
Groups: []string{"existing", "group", "role:write", "role:default:read"},
|
Groups: []string{"existing", "group", "role:write", "role:default:read"},
|
||||||
IDToken: idToken,
|
IDToken: makeIDToken(),
|
||||||
AccessToken: getAccessToken(),
|
AccessToken: makeAccessToken(),
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,8 +209,8 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
|
|||||||
User: "already",
|
User: "already",
|
||||||
Email: "a@b.com",
|
Email: "a@b.com",
|
||||||
Groups: nil,
|
Groups: nil,
|
||||||
IDToken: idToken,
|
IDToken: makeIDToken(),
|
||||||
AccessToken: getAccessToken(),
|
AccessToken: makeAccessToken(),
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,7 +232,7 @@ var _ = Describe("Keycloak OIDC Provider Tests", func() {
|
|||||||
|
|
||||||
provider.ProfileURL = url
|
provider.ProfileURL = url
|
||||||
|
|
||||||
session, err := provider.CreateSessionFromToken(context.Background(), getAccessToken())
|
session, err := provider.CreateSessionFromToken(context.Background(), makeAccessToken())
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
Expect(session.ExpiresOn).ToNot(BeNil())
|
Expect(session.ExpiresOn).ToNot(BeNil())
|
||||||
Expect(session.CreatedAt).ToNot(BeNil())
|
Expect(session.CreatedAt).ToNot(BeNil())
|
||||||
|
Reference in New Issue
Block a user