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

Extract roles from Keycloak Access Tokens

This commit is contained in:
Nick Meves 2021-03-14 18:32:24 -07:00
parent 07eb0efa6e
commit 3bda10f005
No known key found for this signature in database
GPG Key ID: 93BA8A3CEDCDD1CF
3 changed files with 94 additions and 3 deletions

View File

@ -33,6 +33,7 @@ type Options struct {
AuthenticatedEmailsFile string `flag:"authenticated-emails-file" cfg:"authenticated_emails_file"`
KeycloakGroups []string `flag:"keycloak-group" cfg:"keycloak_groups"`
KeycloakRoles []string `flag:"keycloak-role" cfg:"keycloak_roles"`
AzureTenant string `flag:"azure-tenant" cfg:"azure_tenant"`
BitbucketTeam string `flag:"bitbucket-team" cfg:"bitbucket_team"`
BitbucketRepository string `flag:"bitbucket-repository" cfg:"bitbucket_repository"`
@ -173,6 +174,7 @@ func NewFlagSet() *pflag.FlagSet {
flagSet.StringSlice("email-domain", []string{}, "authenticate emails with the specified domain (may be given multiple times). Use * to authenticate any email")
flagSet.StringSlice("whitelist-domain", []string{}, "allowed domains for redirection after authentication. Prefix domain with a . to allow subdomains (eg .example.com)")
flagSet.StringSlice("keycloak-group", []string{}, "restrict logins to members of these groups (may be given multiple times)")
flagSet.StringSlice("keycloak-role", []string{}, "restrict logins to members of these roles (may be given multiple times)")
flagSet.String("azure-tenant", "common", "go to a tenant-specific or common (tenant-independent) endpoint.")
flagSet.String("bitbucket-team", "", "restrict logins to members of this team")
flagSet.String("bitbucket-repository", "", "restrict logins to user with access to this repository")

View File

@ -275,12 +275,13 @@ func parseProviderInfo(o *options.Options, msgs []string) []string {
// Backwards compatibility with `--keycloak-group` option
if len(o.KeycloakGroups) > 0 {
// Maybe already added with proper `--allowed-group` flag
// Maybe already added with `--allowed-group` flag
if !strings.Contains(o.Scope, " groups") {
o.Scope += " groups"
}
p.SetAllowedGroups(o.KeycloakGroups)
}
p.AddAllowedRoles(o.KeycloakRoles)
case *providers.GoogleProvider:
if o.GoogleServiceAccountJSON != "" {
file, err := os.Open(o.GoogleServiceAccountJSON)

View File

@ -2,8 +2,10 @@ package providers
import (
"context"
"fmt"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
)
const keycloakOIDCProviderName = "Keycloak OIDC"
@ -25,6 +27,15 @@ func NewKeycloakOIDCProvider(p *ProviderData) *KeycloakOIDCProvider {
var _ Provider = (*KeycloakOIDCProvider)(nil)
// AddAllowedRoles sets Keycloak roles that are authorized.
// Assumes `SetAllowedGroups` is already called on groups and appends to that
// with `role:` prefixed roles.
func (p *KeycloakOIDCProvider) AddAllowedRoles(roles []string) {
for _, role := range roles {
p.AllowedGroups[formatRole(role)] = struct{}{}
}
}
// EnrichSession is called after Redeem to allow providers to enrich session fields
// such as User, Email, Groups with provider specific API calls.
func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
@ -36,6 +47,83 @@ func (p *KeycloakOIDCProvider) EnrichSession(ctx context.Context, s *sessions.Se
}
func (p *KeycloakOIDCProvider) extractRoles(ctx context.Context, s *sessions.SessionState) error {
// TODO: Implement me with Access Token Role claim extraction logic
return ErrNotImplemented
claims, err := p.getAccessClaims(ctx, s)
if err != nil {
return err
}
var roles []string
roles = append(roles, claims.RealmAccess.Roles...)
roles = append(roles, getClientRoles(claims)...)
// Add to groups list with `role:` prefix to distinguish from groups
for _, role := range roles {
s.Groups = append(s.Groups, formatRole(role))
}
return nil
}
type realmAccess struct {
Roles []string `json:"roles"`
}
type accessClaims struct {
RealmAccess realmAccess `json:"realm_access"`
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
}
var claims *accessClaims
if err = token.Claims(&claims); err != nil {
return nil, err
}
return claims, nil
}
// getClientRoles extracts client roles from the `resource_access` claim with
// the format `client:role`.
//
// ResourceAccess format:
// "resource_access": {
// "clientA": {
// "roles": [
// "roleA"
// ]
// },
// "clientB": {
// "roles": [
// "roleA",
// "roleB",
// "roleC"
// ]
// }
// }
func getClientRoles(claims *accessClaims) []string {
var clientRoles []string
for clientName, access := range claims.ResourceAccess {
accessMap, ok := access.(map[string]interface{})
if !ok {
logger.Errorf("Unable to parse client roles from claims for client: %v", clientName)
continue
}
var roles interface{}
if roles, ok = accessMap["roles"]; !ok {
continue
}
for _, role := range roles.([]interface{}) {
clientRoles = append(clientRoles, fmt.Sprintf("%s:%s", clientName, role))
}
}
return clientRoles
}
func formatRole(role string) string {
return fmt.Sprintf("role:%s", role)
}