mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
Resolve issue #85 by adding ability to set custom claims in JWT tokens
This commit is contained in:
@@ -74,6 +74,10 @@ testData:
|
|||||||
- admin
|
- admin
|
||||||
- developer
|
- developer
|
||||||
|
|
||||||
|
# Custom claim names for Auth0 and other providers with namespaced claims
|
||||||
|
roleClaimName: roles # JWT claim name for extracting user roles (default: "roles")
|
||||||
|
groupClaimName: groups # JWT claim name for extracting user groups (default: "groups")
|
||||||
|
|
||||||
# ⚠️ CRITICAL for TLS termination scenarios (AWS ALB, Cloud Load Balancers, etc.)
|
# ⚠️ CRITICAL for TLS termination scenarios (AWS ALB, Cloud Load Balancers, etc.)
|
||||||
# When NOT specified in config: defaults to FALSE (Go zero value)
|
# When NOT specified in config: defaults to FALSE (Go zero value)
|
||||||
# When running behind load balancer that terminates TLS: MUST set to TRUE
|
# When running behind load balancer that terminates TLS: MUST set to TRUE
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ The middleware supports the following configuration options:
|
|||||||
| `allowedUserDomains` | Restricts access to specific email domains | none | `["company.com", "subsidiary.com"]` |
|
| `allowedUserDomains` | Restricts access to specific email domains | none | `["company.com", "subsidiary.com"]` |
|
||||||
| `allowedUsers` | A list of specific email addresses that are allowed access | none | `["[email protected]", "[email protected]"]` |
|
| `allowedUsers` | A list of specific email addresses that are allowed access | none | `["[email protected]", "[email protected]"]` |
|
||||||
| `allowedRolesAndGroups` | Restricts access to users with specific roles or groups | none | `["admin", "developer"]` |
|
| `allowedRolesAndGroups` | Restricts access to users with specific roles or groups | none | `["admin", "developer"]` |
|
||||||
|
| `roleClaimName` | JWT claim name for extracting user roles (supports namespaced claims for Auth0) | `"roles"` | `"https://myapp.com/roles"`, `"user_roles"` |
|
||||||
|
| `groupClaimName` | JWT claim name for extracting user groups (supports namespaced claims for Auth0) | `"groups"` | `"https://myapp.com/groups"`, `"user_groups"` |
|
||||||
| `revocationURL` | The endpoint for revoking tokens | auto-discovered | `https://accounts.google.com/revoke` |
|
| `revocationURL` | The endpoint for revoking tokens | auto-discovered | `https://accounts.google.com/revoke` |
|
||||||
| `oidcEndSessionURL` | The provider's end session endpoint | auto-discovered | `https://accounts.google.com/logout` |
|
| `oidcEndSessionURL` | The provider's end session endpoint | auto-discovered | `https://accounts.google.com/logout` |
|
||||||
| `enablePKCE` | Enables PKCE (Proof Key for Code Exchange) for authorization code flow | `false` | `true`, `false` |
|
| `enablePKCE` | Enables PKCE (Proof Key for Code Exchange) for authorization code flow | `false` | `true`, `false` |
|
||||||
@@ -1062,8 +1064,13 @@ spec:
|
|||||||
|
|
||||||
scopes:
|
scopes:
|
||||||
- read:custom_data # Custom scopes as needed
|
- read:custom_data # Custom scopes as needed
|
||||||
|
|
||||||
|
# Custom claim names for Auth0 namespaced claims
|
||||||
|
roleClaimName: "https://your-app.com/roles" # Auth0 requires namespaced custom claims
|
||||||
|
groupClaimName: "https://your-app.com/groups" # Must match claims added in Auth0 Actions
|
||||||
|
|
||||||
allowedRolesAndGroups:
|
allowedRolesAndGroups:
|
||||||
- "https://your-app.com/roles:admin" # Namespaced claims from Actions
|
- admin # Will match "admin" in https://your-app.com/roles claim
|
||||||
- editor
|
- editor
|
||||||
postLogoutRedirectURI: /logged-out-page # Must be in Auth0 Allowed Logout URLs
|
postLogoutRedirectURI: /logged-out-page # Must be in Auth0 Allowed Logout URLs
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
//go:build !yaegi
|
||||||
|
|
||||||
|
package traefikoidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCustomClaimNames_DefaultBehavior tests backward compatibility with default claim names
|
||||||
|
func TestCustomClaimNames_DefaultBehavior(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Explicitly set defaults to test backward compatibility
|
||||||
|
ts.tOidc.roleClaimName = "roles"
|
||||||
|
ts.tOidc.groupClaimName = "groups"
|
||||||
|
|
||||||
|
// Test that when no custom claim names are configured, it uses defaults "roles" and "groups"
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"groups": []interface{}{"admin", "users"},
|
||||||
|
"roles": []interface{}{"editor", "viewer"},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, roles, err := ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceEqual(groups, []string{"admin", "users"}) {
|
||||||
|
t.Errorf("Expected groups [admin users], got %v", groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceEqual(roles, []string{"editor", "viewer"}) {
|
||||||
|
t.Errorf("Expected roles [editor viewer], got %v", roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomClaimNames_Auth0Namespaced tests Auth0-style namespaced claims
|
||||||
|
func TestCustomClaimNames_Auth0Namespaced(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure custom claim names for Auth0
|
||||||
|
ts.tOidc.roleClaimName = "https://myapp.com/roles"
|
||||||
|
ts.tOidc.groupClaimName = "https://myapp.com/groups"
|
||||||
|
|
||||||
|
// Create token with Auth0-style namespaced claims
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"https://myapp.com/groups": []interface{}{"admin", "users"},
|
||||||
|
"https://myapp.com/roles": []interface{}{"editor", "viewer"},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, roles, err := ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceEqual(groups, []string{"admin", "users"}) {
|
||||||
|
t.Errorf("Expected groups [admin users], got %v", groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceEqual(roles, []string{"editor", "viewer"}) {
|
||||||
|
t.Errorf("Expected roles [editor viewer], got %v", roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomClaimNames_CustomSimpleNames tests custom simple claim names
|
||||||
|
func TestCustomClaimNames_CustomSimpleNames(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure custom simple claim names
|
||||||
|
ts.tOidc.roleClaimName = "user_roles"
|
||||||
|
ts.tOidc.groupClaimName = "user_groups"
|
||||||
|
|
||||||
|
// Create token with custom claim names
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"user_groups": []interface{}{"engineering", "product"},
|
||||||
|
"user_roles": []interface{}{"developer", "manager"},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, roles, err := ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceEqual(groups, []string{"engineering", "product"}) {
|
||||||
|
t.Errorf("Expected groups [engineering product], got %v", groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceEqual(roles, []string{"developer", "manager"}) {
|
||||||
|
t.Errorf("Expected roles [developer manager], got %v", roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomClaimNames_MissingClaims tests behavior when custom claims are missing
|
||||||
|
func TestCustomClaimNames_MissingClaims(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure custom claim names
|
||||||
|
ts.tOidc.roleClaimName = "custom_roles"
|
||||||
|
ts.tOidc.groupClaimName = "custom_groups"
|
||||||
|
|
||||||
|
// Create token WITHOUT the custom claims
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"sub": "user123",
|
||||||
|
"email": "[email protected]",
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, roles, err := ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should return empty slices, not error
|
||||||
|
if len(groups) != 0 {
|
||||||
|
t.Errorf("Expected empty groups, got %v", groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(roles) != 0 {
|
||||||
|
t.Errorf("Expected empty roles, got %v", roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomClaimNames_MalformedClaims tests error handling for malformed claims
|
||||||
|
func TestCustomClaimNames_MalformedRoleClaim(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure custom claim names
|
||||||
|
ts.tOidc.roleClaimName = "custom_roles"
|
||||||
|
|
||||||
|
// Create token with malformed role claim (not an array)
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"custom_roles": "this-should-be-an-array",
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for malformed role claim, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check error message contains the custom claim name
|
||||||
|
expectedError := "custom_roles claim is not an array"
|
||||||
|
if err.Error() != expectedError {
|
||||||
|
t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomClaimNames_MalformedGroupClaim tests error handling for malformed group claims
|
||||||
|
func TestCustomClaimNames_MalformedGroupClaim(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure custom claim names
|
||||||
|
ts.tOidc.groupClaimName = "custom_groups"
|
||||||
|
|
||||||
|
// Create token with malformed group claim (not an array)
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"custom_groups": 12345, // Not an array
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for malformed group claim, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check error message contains the custom claim name
|
||||||
|
expectedError := "custom_groups claim is not an array"
|
||||||
|
if err.Error() != expectedError {
|
||||||
|
t.Errorf("Expected error '%s', got '%s'", expectedError, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomClaimNames_PartialConfiguration tests when only one claim name is customized
|
||||||
|
func TestCustomClaimNames_OnlyRoleCustomized(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure only role claim name (group uses default)
|
||||||
|
ts.tOidc.roleClaimName = "https://myapp.com/roles"
|
||||||
|
ts.tOidc.groupClaimName = "groups" // default
|
||||||
|
|
||||||
|
// Create token with mixed claim names
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"groups": []interface{}{"admin"},
|
||||||
|
"https://myapp.com/roles": []interface{}{"editor"},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, roles, err := ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceEqual(groups, []string{"admin"}) {
|
||||||
|
t.Errorf("Expected groups [admin], got %v", groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceEqual(roles, []string{"editor"}) {
|
||||||
|
t.Errorf("Expected roles [editor], got %v", roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomClaimNames_OnlyGroupCustomized tests when only group claim name is customized
|
||||||
|
func TestCustomClaimNames_OnlyGroupCustomized(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure only group claim name (role uses default)
|
||||||
|
ts.tOidc.roleClaimName = "roles" // default
|
||||||
|
ts.tOidc.groupClaimName = "https://myapp.com/groups"
|
||||||
|
|
||||||
|
// Create token with mixed claim names
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"roles": []interface{}{"viewer"},
|
||||||
|
"https://myapp.com/groups": []interface{}{"users"},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, roles, err := ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceEqual(groups, []string{"users"}) {
|
||||||
|
t.Errorf("Expected groups [users], got %v", groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stringSliceEqual(roles, []string{"viewer"}) {
|
||||||
|
t.Errorf("Expected roles [viewer], got %v", roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomClaimNames_EmptyArrays tests extraction with empty claim arrays
|
||||||
|
func TestCustomClaimNames_EmptyArrays(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure custom claim names
|
||||||
|
ts.tOidc.roleClaimName = "https://myapp.com/roles"
|
||||||
|
ts.tOidc.groupClaimName = "https://myapp.com/groups"
|
||||||
|
|
||||||
|
// Create token with empty arrays
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"https://myapp.com/groups": []interface{}{},
|
||||||
|
"https://myapp.com/roles": []interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, roles, err := ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(groups) != 0 {
|
||||||
|
t.Errorf("Expected empty groups, got %v", groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(roles) != 0 {
|
||||||
|
t.Errorf("Expected empty roles, got %v", roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomClaimNames_NonStringElements tests handling of non-string elements in claim arrays
|
||||||
|
func TestCustomClaimNames_NonStringInRoleArray(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure custom claim names
|
||||||
|
ts.tOidc.roleClaimName = "custom_roles"
|
||||||
|
|
||||||
|
// Create token with mixed-type array (should skip non-string elements)
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"custom_roles": []interface{}{"role1", 12345, "role2", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, roles, err := ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should only extract string elements
|
||||||
|
if !stringSliceEqual(roles, []string{"role1", "role2"}) {
|
||||||
|
t.Errorf("Expected roles [role1 role2], got %v", roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCustomClaimNames_NonStringInGroupArray tests handling of non-string elements in group arrays
|
||||||
|
func TestCustomClaimNames_NonStringInGroupArray(t *testing.T) {
|
||||||
|
ts := NewTestSuite(t)
|
||||||
|
ts.Setup()
|
||||||
|
|
||||||
|
// Configure custom claim names
|
||||||
|
ts.tOidc.groupClaimName = "custom_groups"
|
||||||
|
|
||||||
|
// Create token with mixed-type array (should skip non-string elements)
|
||||||
|
claims := map[string]interface{}{
|
||||||
|
"custom_groups": []interface{}{"group1", nil, "group2", 3.14},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, _, err := ts.tOidc.extractGroupsAndRoles(token)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should only extract string elements
|
||||||
|
if !stringSliceEqual(groups, []string{"group1", "group2"}) {
|
||||||
|
t.Errorf("Expected groups [group1 group2], got %v", groups)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,6 +165,18 @@ func NewWithContext(ctx context.Context, config *Config, next http.Handler, name
|
|||||||
}
|
}
|
||||||
return config.ClientID
|
return config.ClientID
|
||||||
}(),
|
}(),
|
||||||
|
roleClaimName: func() string {
|
||||||
|
if config.RoleClaimName != "" {
|
||||||
|
return config.RoleClaimName
|
||||||
|
}
|
||||||
|
return "roles" // Backward compatible default
|
||||||
|
}(),
|
||||||
|
groupClaimName: func() string {
|
||||||
|
if config.GroupClaimName != "" {
|
||||||
|
return config.GroupClaimName
|
||||||
|
}
|
||||||
|
return "groups" // Backward compatible default
|
||||||
|
}(),
|
||||||
forceHTTPS: config.ForceHTTPS,
|
forceHTTPS: config.ForceHTTPS,
|
||||||
enablePKCE: config.EnablePKCE,
|
enablePKCE: config.EnablePKCE,
|
||||||
overrideScopes: config.OverrideScopes,
|
overrideScopes: config.OverrideScopes,
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ func (ts *TestSuite) Setup() {
|
|||||||
clientID: "test-client-id",
|
clientID: "test-client-id",
|
||||||
audience: "test-client-id",
|
audience: "test-client-id",
|
||||||
clientSecret: "test-client-secret",
|
clientSecret: "test-client-secret",
|
||||||
|
roleClaimName: "roles", // Set default for backward compatibility
|
||||||
|
groupClaimName: "groups", // Set default for backward compatibility
|
||||||
jwkCache: ts.mockJWKCache,
|
jwkCache: ts.mockJWKCache,
|
||||||
jwksURL: "https://test-jwks-url.com",
|
jwksURL: "https://test-jwks-url.com",
|
||||||
revocationURL: "https://revocation-endpoint.com",
|
revocationURL: "https://revocation-endpoint.com",
|
||||||
|
|||||||
+30
@@ -93,6 +93,36 @@ type Config struct {
|
|||||||
// When enabled, provides cache sharing across multiple Traefik replicas.
|
// When enabled, provides cache sharing across multiple Traefik replicas.
|
||||||
// Default: nil (disabled - uses in-memory caching)
|
// Default: nil (disabled - uses in-memory caching)
|
||||||
Redis *RedisConfig `json:"redis,omitempty"`
|
Redis *RedisConfig `json:"redis,omitempty"`
|
||||||
|
|
||||||
|
// RoleClaimName specifies the JWT claim name to extract user roles from.
|
||||||
|
// This allows compatibility with different OIDC providers that use different claim names.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - Default (backward compatible): "roles"
|
||||||
|
// - Auth0 namespaced: "https://myapp.com/roles"
|
||||||
|
// - Keycloak realm roles: "realm_access.roles"
|
||||||
|
// - Custom claim: "user_roles"
|
||||||
|
//
|
||||||
|
// If not specified, defaults to "roles" for backward compatibility.
|
||||||
|
// Supports both simple names and namespaced URIs per OIDC specification.
|
||||||
|
//
|
||||||
|
// Default: "roles"
|
||||||
|
RoleClaimName string `json:"roleClaimName,omitempty"`
|
||||||
|
|
||||||
|
// GroupClaimName specifies the JWT claim name to extract user groups from.
|
||||||
|
// This allows compatibility with different OIDC providers that use different claim names.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - Default (backward compatible): "groups"
|
||||||
|
// - Auth0 namespaced: "https://myapp.com/groups"
|
||||||
|
// - Azure AD groups: "groups"
|
||||||
|
// - Custom claim: "user_groups"
|
||||||
|
//
|
||||||
|
// If not specified, defaults to "groups" for backward compatibility.
|
||||||
|
// Supports both simple names and namespaced URIs per OIDC specification.
|
||||||
|
//
|
||||||
|
// Default: "groups"
|
||||||
|
GroupClaimName string `json:"groupClaimName,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedisConfig configures Redis cache backend settings for distributed caching.
|
// RedisConfig configures Redis cache backend settings for distributed caching.
|
||||||
|
|||||||
+10
-8
@@ -1207,32 +1207,34 @@ func (t *TraefikOidc) extractGroupsAndRoles(idToken string) ([]string, []string,
|
|||||||
var groups []string
|
var groups []string
|
||||||
var roles []string
|
var roles []string
|
||||||
|
|
||||||
if groupsClaim, exists := claims["groups"]; exists {
|
// Extract groups using configurable claim name (defaults to "groups")
|
||||||
|
if groupsClaim, exists := claims[t.groupClaimName]; exists {
|
||||||
groupsSlice, ok := groupsClaim.([]interface{})
|
groupsSlice, ok := groupsClaim.([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil, fmt.Errorf("groups claim is not an array")
|
return nil, nil, fmt.Errorf("%s claim is not an array", t.groupClaimName)
|
||||||
}
|
}
|
||||||
for _, group := range groupsSlice {
|
for _, group := range groupsSlice {
|
||||||
if groupStr, ok := group.(string); ok {
|
if groupStr, ok := group.(string); ok {
|
||||||
t.logger.Debugf("Found group: %s", groupStr)
|
t.logger.Debugf("Found group from %s claim: %s", t.groupClaimName, groupStr)
|
||||||
groups = append(groups, groupStr)
|
groups = append(groups, groupStr)
|
||||||
} else {
|
} else {
|
||||||
t.logger.Errorf("Non-string value found in groups claim array: %v", group)
|
t.logger.Errorf("Non-string value found in %s claim array: %v", t.groupClaimName, group)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if rolesClaim, exists := claims["roles"]; exists {
|
// Extract roles using configurable claim name (defaults to "roles")
|
||||||
|
if rolesClaim, exists := claims[t.roleClaimName]; exists {
|
||||||
rolesSlice, ok := rolesClaim.([]interface{})
|
rolesSlice, ok := rolesClaim.([]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, nil, fmt.Errorf("roles claim is not an array")
|
return nil, nil, fmt.Errorf("%s claim is not an array", t.roleClaimName)
|
||||||
}
|
}
|
||||||
for _, role := range rolesSlice {
|
for _, role := range rolesSlice {
|
||||||
if roleStr, ok := role.(string); ok {
|
if roleStr, ok := role.(string); ok {
|
||||||
t.logger.Debugf("Found role: %s", roleStr)
|
t.logger.Debugf("Found role from %s claim: %s", t.roleClaimName, roleStr)
|
||||||
roles = append(roles, roleStr)
|
roles = append(roles, roleStr)
|
||||||
} else {
|
} else {
|
||||||
t.logger.Errorf("Non-string value found in roles claim array: %v", role)
|
t.logger.Errorf("Non-string value found in %s claim array: %v", t.roleClaimName, role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ type TraefikOidc struct {
|
|||||||
clientSecret string
|
clientSecret string
|
||||||
clientID string
|
clientID string
|
||||||
audience string // Expected JWT audience, defaults to clientID
|
audience string // Expected JWT audience, defaults to clientID
|
||||||
|
roleClaimName string // JWT claim name for extracting roles, defaults to "roles"
|
||||||
|
groupClaimName string // JWT claim name for extracting groups, defaults to "groups"
|
||||||
name string
|
name string
|
||||||
redirURLPath string
|
redirURLPath string
|
||||||
logoutURLPath string
|
logoutURLPath string
|
||||||
|
|||||||
Reference in New Issue
Block a user