mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
Add ability to overwrite the default scopes in the settings file
This commit is contained in:
@@ -67,7 +67,8 @@ The middleware supports the following configuration options:
|
|||||||
|-----------|-------------|---------|---------|
|
|-----------|-------------|---------|---------|
|
||||||
| `logoutURL` | The path for handling logout requests | `callbackURL + "/logout"` | `/oauth2/logout` |
|
| `logoutURL` | The path for handling logout requests | `callbackURL + "/logout"` | `/oauth2/logout` |
|
||||||
| `postLogoutRedirectURI` | The URL to redirect to after logout | `/` | `/logged-out-page` |
|
| `postLogoutRedirectURI` | The URL to redirect to after logout | `/` | `/logged-out-page` |
|
||||||
| `scopes` | Additional OAuth 2.0 scopes to append to defaults | `["openid", "profile", "email"]` (always included) | `["roles", "custom_scope"]` (appended to defaults) |
|
| `scopes` | OAuth 2.0 scopes to use for authentication | `["openid", "profile", "email"]` (always included by default) | `["roles", "custom_scope"]` (appended to defaults) |
|
||||||
|
| `overrideScopes` | When true, replaces default scopes with provided scopes instead of appending | `false` | `true` (use only the scopes explicitly provided) |
|
||||||
| `logLevel` | Sets the logging verbosity | `info` | `debug`, `info`, `error` |
|
| `logLevel` | Sets the logging verbosity | `info` | `debug`, `info`, `error` |
|
||||||
| `forceHTTPS` | Forces the use of HTTPS for all URLs | `true` | `true`, `false` |
|
| `forceHTTPS` | Forces the use of HTTPS for all URLs | `true` | `true`, `false` |
|
||||||
| `rateLimit` | Sets the maximum number of requests per second | `100` | `500` |
|
| `rateLimit` | Sets the maximum number of requests per second | `100` | `500` |
|
||||||
@@ -83,23 +84,35 @@ The middleware supports the following configuration options:
|
|||||||
|
|
||||||
## Scope Configuration
|
## Scope Configuration
|
||||||
|
|
||||||
### Scope Append Behavior
|
### Scope Behavior
|
||||||
|
|
||||||
The middleware uses an **append** behavior for OAuth 2.0 scopes, not replacement. This means:
|
The middleware supports two modes for handling OAuth 2.0 scopes, controlled by the `overrideScopes` parameter:
|
||||||
|
|
||||||
|
#### Default Append Mode (`overrideScopes: false`)
|
||||||
|
|
||||||
|
By default, the middleware uses an **append** behavior for OAuth 2.0 scopes:
|
||||||
|
|
||||||
- **Default scopes** are always included: `["openid", "profile", "email"]`
|
- **Default scopes** are always included: `["openid", "profile", "email"]`
|
||||||
- **User-provided scopes** are appended to the defaults with automatic deduplication
|
- **User-provided scopes** are appended to the defaults with automatic deduplication
|
||||||
- The final scope list maintains the order: defaults first, then user scopes
|
- The final scope list maintains the order: defaults first, then user scopes
|
||||||
|
|
||||||
#### Examples:
|
#### Override Mode (`overrideScopes: true`)
|
||||||
|
|
||||||
**No custom scopes specified:**
|
When `overrideScopes` is set to `true`, the middleware uses **replacement** behavior:
|
||||||
|
|
||||||
|
- Default scopes are **not** automatically included
|
||||||
|
- Only the scopes explicitly provided in the `scopes` field are used
|
||||||
|
- You must include all required scopes explicitly, including `openid` if needed
|
||||||
|
|
||||||
|
### Examples:
|
||||||
|
|
||||||
|
**Default behavior (no custom scopes):**
|
||||||
```yaml
|
```yaml
|
||||||
# No scopes field specified
|
# No scopes field specified
|
||||||
# Result: ["openid", "profile", "email"]
|
# Result: ["openid", "profile", "email"]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Custom scopes appended:**
|
**Default append behavior:**
|
||||||
```yaml
|
```yaml
|
||||||
scopes:
|
scopes:
|
||||||
- roles
|
- roles
|
||||||
@@ -107,7 +120,7 @@ scopes:
|
|||||||
# Result: ["openid", "profile", "email", "roles", "custom_scope"]
|
# Result: ["openid", "profile", "email", "roles", "custom_scope"]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Overlapping scopes (automatic deduplication):**
|
**Overlapping scopes with append (automatic deduplication):**
|
||||||
```yaml
|
```yaml
|
||||||
scopes:
|
scopes:
|
||||||
- openid # Duplicate - will be deduplicated
|
- openid # Duplicate - will be deduplicated
|
||||||
@@ -117,13 +130,30 @@ scopes:
|
|||||||
# Result: ["openid", "profile", "email", "roles", "permissions"]
|
# Result: ["openid", "profile", "email", "roles", "permissions"]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Empty scopes list:**
|
**Using override mode:**
|
||||||
|
```yaml
|
||||||
|
overrideScopes: true
|
||||||
|
scopes:
|
||||||
|
- openid
|
||||||
|
- profile
|
||||||
|
- custom_scope
|
||||||
|
# Result: ["openid", "profile", "custom_scope"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Empty scopes list with default behavior:**
|
||||||
```yaml
|
```yaml
|
||||||
scopes: []
|
scopes: []
|
||||||
# Result: ["openid", "profile", "email"]
|
# Result: ["openid", "profile", "email"]
|
||||||
```
|
```
|
||||||
|
|
||||||
This append behavior ensures that essential OIDC scopes are always present while allowing you to add provider-specific or application-specific scopes as needed.
|
**Empty scopes list with override mode:**
|
||||||
|
```yaml
|
||||||
|
overrideScopes: true
|
||||||
|
scopes: []
|
||||||
|
# Result: [] (Warning: empty scopes may cause authentication to fail)
|
||||||
|
```
|
||||||
|
|
||||||
|
The default append behavior ensures essential OIDC scopes are always present, while the override mode gives you complete control over the exact scopes requested from the provider.
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ type TraefikOidc struct {
|
|||||||
shutdownOnce sync.Once
|
shutdownOnce sync.Once
|
||||||
forceHTTPS bool
|
forceHTTPS bool
|
||||||
enablePKCE bool
|
enablePKCE bool
|
||||||
|
overrideScopes bool
|
||||||
suppressDiagnosticLogs bool
|
suppressDiagnosticLogs bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,14 +653,20 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h
|
|||||||
}
|
}
|
||||||
return config.PostLogoutRedirectURI
|
return config.PostLogoutRedirectURI
|
||||||
}(),
|
}(),
|
||||||
tokenBlacklist: cacheManager.GetSharedTokenBlacklist(),
|
tokenBlacklist: cacheManager.GetSharedTokenBlacklist(),
|
||||||
jwkCache: cacheManager.GetSharedJWKCache(),
|
jwkCache: cacheManager.GetSharedJWKCache(),
|
||||||
metadataCache: cacheManager.GetSharedMetadataCache(),
|
metadataCache: cacheManager.GetSharedMetadataCache(),
|
||||||
clientID: config.ClientID,
|
clientID: config.ClientID,
|
||||||
clientSecret: config.ClientSecret,
|
clientSecret: config.ClientSecret,
|
||||||
forceHTTPS: config.ForceHTTPS,
|
forceHTTPS: config.ForceHTTPS,
|
||||||
enablePKCE: config.EnablePKCE,
|
enablePKCE: config.EnablePKCE,
|
||||||
scopes: mergeScopes([]string{"openid", "profile", "email"}, config.Scopes),
|
overrideScopes: config.OverrideScopes,
|
||||||
|
scopes: func() []string {
|
||||||
|
if config.OverrideScopes {
|
||||||
|
return append([]string(nil), config.Scopes...)
|
||||||
|
}
|
||||||
|
return mergeScopes([]string{"openid", "profile", "email"}, config.Scopes)
|
||||||
|
}(),
|
||||||
limiter: rate.NewLimiter(rate.Every(time.Second), config.RateLimit),
|
limiter: rate.NewLimiter(rate.Every(time.Second), config.RateLimit),
|
||||||
tokenCache: cacheManager.GetSharedTokenCache(),
|
tokenCache: cacheManager.GetSharedTokenCache(),
|
||||||
httpClient: httpClient,
|
httpClient: httpClient,
|
||||||
|
|||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
package traefikoidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMergeScopes(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
defaultScopes []string
|
||||||
|
userScopes []string
|
||||||
|
expectedScopes []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Empty user scopes",
|
||||||
|
defaultScopes: []string{"openid", "profile", "email"},
|
||||||
|
userScopes: []string{},
|
||||||
|
expectedScopes: []string{"openid", "profile", "email"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-overlapping scopes",
|
||||||
|
defaultScopes: []string{"openid", "profile", "email"},
|
||||||
|
userScopes: []string{"roles", "custom_scope"},
|
||||||
|
expectedScopes: []string{"openid", "profile", "email", "roles", "custom_scope"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Overlapping scopes",
|
||||||
|
defaultScopes: []string{"openid", "profile", "email"},
|
||||||
|
userScopes: []string{"openid", "roles", "profile", "permissions"},
|
||||||
|
expectedScopes: []string{"openid", "profile", "email", "roles", "permissions"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nil user scopes",
|
||||||
|
defaultScopes: []string{"openid", "profile", "email"},
|
||||||
|
userScopes: nil,
|
||||||
|
expectedScopes: []string{"openid", "profile", "email"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nil default scopes",
|
||||||
|
defaultScopes: nil,
|
||||||
|
userScopes: []string{"roles", "custom_scope"},
|
||||||
|
expectedScopes: []string{"roles", "custom_scope"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := mergeScopes(tc.defaultScopes, tc.userScopes)
|
||||||
|
if !reflect.DeepEqual(result, tc.expectedScopes) {
|
||||||
|
t.Errorf("Expected %v, got %v", tc.expectedScopes, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestScopesConfiguration(t *testing.T) {
|
||||||
|
defaultScopes := []string{"openid", "profile", "email"}
|
||||||
|
userScopes := []string{"roles", "custom_scope"}
|
||||||
|
|
||||||
|
t.Run("Default Append Behavior", func(t *testing.T) {
|
||||||
|
// Create config with user scopes but overrideScopes=false
|
||||||
|
config := &Config{
|
||||||
|
Scopes: userScopes,
|
||||||
|
OverrideScopes: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate middleware initialization
|
||||||
|
var result []string
|
||||||
|
if config.OverrideScopes {
|
||||||
|
result = append([]string(nil), config.Scopes...)
|
||||||
|
} else {
|
||||||
|
result = mergeScopes(defaultScopes, config.Scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect defaultScopes + userScopes with deduplication
|
||||||
|
expectedScopes := []string{"openid", "profile", "email", "roles", "custom_scope"}
|
||||||
|
if !reflect.DeepEqual(result, expectedScopes) {
|
||||||
|
t.Errorf("Expected %v, got %v", expectedScopes, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Override Behavior", func(t *testing.T) {
|
||||||
|
// Create config with user scopes and overrideScopes=true
|
||||||
|
config := &Config{
|
||||||
|
Scopes: userScopes,
|
||||||
|
OverrideScopes: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate middleware initialization
|
||||||
|
var result []string
|
||||||
|
if config.OverrideScopes {
|
||||||
|
result = append([]string(nil), config.Scopes...)
|
||||||
|
} else {
|
||||||
|
result = mergeScopes(defaultScopes, config.Scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect only userScopes
|
||||||
|
if !reflect.DeepEqual(result, userScopes) {
|
||||||
|
t.Errorf("Expected %v, got %v", userScopes, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Empty Scopes with Override", func(t *testing.T) {
|
||||||
|
// Create config with empty scopes and overrideScopes=true
|
||||||
|
config := &Config{
|
||||||
|
Scopes: []string{},
|
||||||
|
OverrideScopes: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate middleware initialization
|
||||||
|
var result []string
|
||||||
|
if config.OverrideScopes {
|
||||||
|
result = append([]string(nil), config.Scopes...)
|
||||||
|
} else {
|
||||||
|
result = mergeScopes(defaultScopes, config.Scopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect empty scopes - check length instead of DeepEqual
|
||||||
|
if len(result) != 0 {
|
||||||
|
t.Errorf("Expected empty slice, got %v with length %d", result, len(result))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ type Config struct {
|
|||||||
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
RefreshGracePeriodSeconds int `json:"refreshGracePeriodSeconds"`
|
||||||
ForceHTTPS bool `json:"forceHTTPS"`
|
ForceHTTPS bool `json:"forceHTTPS"`
|
||||||
EnablePKCE bool `json:"enablePKCE"`
|
EnablePKCE bool `json:"enablePKCE"`
|
||||||
|
OverrideScopes bool `json:"overrideScopes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -87,6 +88,7 @@ func CreateConfig() *Config {
|
|||||||
RateLimit: DefaultRateLimit,
|
RateLimit: DefaultRateLimit,
|
||||||
ForceHTTPS: true, // Secure by default
|
ForceHTTPS: true, // Secure by default
|
||||||
EnablePKCE: false, // PKCE is opt-in
|
EnablePKCE: false, // PKCE is opt-in
|
||||||
|
OverrideScopes: false, // Default to appending scopes, not overriding
|
||||||
RefreshGracePeriodSeconds: 60, // Default grace period of 60 seconds
|
RefreshGracePeriodSeconds: 60, // Default grace period of 60 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ func TestCreateConfig(t *testing.T) {
|
|||||||
if !config.ForceHTTPS {
|
if !config.ForceHTTPS {
|
||||||
t.Error("Expected ForceHTTPS to be true by default")
|
t.Error("Expected ForceHTTPS to be true by default")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check OverrideScopes default
|
||||||
|
if config.OverrideScopes {
|
||||||
|
t.Error("Expected OverrideScopes to be false by default")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Config Can Hold Custom Values", func(t *testing.T) {
|
t.Run("Config Can Hold Custom Values", func(t *testing.T) {
|
||||||
@@ -57,6 +62,7 @@ func TestCreateConfig(t *testing.T) {
|
|||||||
config.LogLevel = "debug"
|
config.LogLevel = "debug"
|
||||||
config.RateLimit = 50
|
config.RateLimit = 50
|
||||||
config.ForceHTTPS = false
|
config.ForceHTTPS = false
|
||||||
|
config.OverrideScopes = true
|
||||||
|
|
||||||
// Verify config struct can hold custom values
|
// Verify config struct can hold custom values
|
||||||
if len(config.Scopes) != 1 || config.Scopes[0] != "custom_scope" {
|
if len(config.Scopes) != 1 || config.Scopes[0] != "custom_scope" {
|
||||||
@@ -71,6 +77,9 @@ func TestCreateConfig(t *testing.T) {
|
|||||||
if config.ForceHTTPS {
|
if config.ForceHTTPS {
|
||||||
t.Error("Config struct cannot hold custom ForceHTTPS value")
|
t.Error("Config struct cannot hold custom ForceHTTPS value")
|
||||||
}
|
}
|
||||||
|
if !config.OverrideScopes {
|
||||||
|
t.Error("Config struct cannot hold custom OverrideScopes value")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user