mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-18 04:47:28 +00:00
Additional micro fixes and cleanups.
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
docker/
|
||||||
|
.claude/
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
### TODO / wishlist
|
|
||||||
|
|
||||||
- [] Improve test coverage
|
|
||||||
- [x] Improve caching mechanism
|
|
||||||
- [x] Add automatic release and semver generation
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Package providers implements a universal OIDC provider abstraction system.
|
||||||
|
// It provides a clean interface for different OIDC providers (Google, Azure, Generic)
|
||||||
|
// with provider-specific logic encapsulated in separate implementations.
|
||||||
package providers
|
package providers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ type JWKCacheInterface interface {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - A pointer to the JWKSet containing the keys.
|
// - A pointer to the JWKSet containing the keys.
|
||||||
// - An error if fetching fails or the response cannot be decoded.
|
// - An error if fetching fails or the response cannot be decoded.
|
||||||
|
|
||||||
|
// NewJWKCache creates a new JWK cache with default configuration.
|
||||||
|
// It initializes a cache with a 1-hour lifetime and maximum size of 100 entries.
|
||||||
func NewJWKCache() *JWKCache {
|
func NewJWKCache() *JWKCache {
|
||||||
cache := &JWKCache{
|
cache := &JWKCache{
|
||||||
CacheLifetime: 1 * time.Hour,
|
CacheLifetime: 1 * time.Hour,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func startReplayCacheCleanup(ctx context.Context, logger *Logger) {
|
|||||||
|
|
||||||
replayCacheMu.RLock()
|
replayCacheMu.RLock()
|
||||||
if replayCache != nil {
|
if replayCache != nil {
|
||||||
|
replayCache.Cleanup()
|
||||||
}
|
}
|
||||||
replayCacheMu.RUnlock()
|
replayCacheMu.RUnlock()
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Package traefikoidc provides OIDC authentication middleware for Traefik.
|
||||||
|
// It supports multiple OIDC providers including Google, Azure AD, and generic OIDC providers
|
||||||
|
// with features like token refresh, session management, and provider-specific optimizations.
|
||||||
package traefikoidc
|
package traefikoidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -1217,7 +1220,7 @@ func (t *TraefikOidc) processAuthorizedRequest(rw http.ResponseWriter, req *http
|
|||||||
// Fields must be exported (uppercase) to be accessible in templates
|
// Fields must be exported (uppercase) to be accessible in templates
|
||||||
templateData := map[string]interface{}{
|
templateData := map[string]interface{}{
|
||||||
"AccessToken": session.GetAccessToken(),
|
"AccessToken": session.GetAccessToken(),
|
||||||
"IdToken": session.GetIDToken(),
|
"IDToken": session.GetIDToken(),
|
||||||
"RefreshToken": session.GetRefreshToken(),
|
"RefreshToken": session.GetRefreshToken(),
|
||||||
"Claims": claims,
|
"Claims": claims,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ func decompressTokenInternal(compressed string) string {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if closeErr := gz.Close(); closeErr != nil {
|
if closeErr := gz.Close(); closeErr != nil {
|
||||||
// Log error but don't fail the operation
|
// Log error but don't fail the operation
|
||||||
|
_ = closeErr // Explicitly ignore the error
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -83,9 +83,9 @@ func TestTemplateExecution(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ID Token",
|
name: "ID Token",
|
||||||
templateText: "{{.IdToken}}",
|
templateText: "{{.IDToken}}",
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"IdToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Et9HFtf9R3GEMA0IICOfFMVXY7kkTX1wr4qCyhIf58U",
|
"IDToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Et9HFtf9R3GEMA0IICOfFMVXY7kkTX1wr4qCyhIf58U",
|
||||||
},
|
},
|
||||||
expectedValue: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Et9HFtf9R3GEMA0IICOfFMVXY7kkTX1wr4qCyhIf58U",
|
expectedValue: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Et9HFtf9R3GEMA0IICOfFMVXY7kkTX1wr4qCyhIf58U",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
@@ -238,10 +238,10 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Access and ID token distinction with map",
|
name: "Access and ID token distinction with map",
|
||||||
templateText: "Access: {{.AccessToken}} ID: {{.IdToken}}",
|
templateText: "Access: {{.AccessToken}} ID: {{.IDToken}}",
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"AccessToken": "access-token-value",
|
"AccessToken": "access-token-value",
|
||||||
"IdToken": "id-token-value",
|
"IDToken": "id-token-value",
|
||||||
"Claims": map[string]interface{}{},
|
"Claims": map[string]interface{}{},
|
||||||
"RefreshToken": "refresh-token-value",
|
"RefreshToken": "refresh-token-value",
|
||||||
},
|
},
|
||||||
@@ -252,7 +252,7 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
templateText: "User: {{.Claims.sub}} Token: {{.AccessToken}}",
|
templateText: "User: {{.Claims.sub}} Token: {{.AccessToken}}",
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"AccessToken": "access-token",
|
"AccessToken": "access-token",
|
||||||
"IdToken": "id-token",
|
"IDToken": "id-token",
|
||||||
"Claims": map[string]interface{}{
|
"Claims": map[string]interface{}{
|
||||||
"sub": "user123",
|
"sub": "user123",
|
||||||
},
|
},
|
||||||
@@ -265,7 +265,7 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
templateText: "Bearer {{.AccessToken}}",
|
templateText: "Bearer {{.AccessToken}}",
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"AccessToken": "jwt-access-token",
|
"AccessToken": "jwt-access-token",
|
||||||
"IdToken": "id-token",
|
"IDToken": "id-token",
|
||||||
"Claims": map[string]interface{}{},
|
"Claims": map[string]interface{}{},
|
||||||
},
|
},
|
||||||
expectedValue: "Bearer jwt-access-token",
|
expectedValue: "Bearer jwt-access-token",
|
||||||
@@ -283,7 +283,7 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
templateText: "X-User-Role: {{.Claims.role}}, X-User-Permissions: {{.Claims.permissions}}",
|
templateText: "X-User-Role: {{.Claims.role}}, X-User-Permissions: {{.Claims.permissions}}",
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"AccessToken": "access-token-value",
|
"AccessToken": "access-token-value",
|
||||||
"IdToken": "id-token-value",
|
"IDToken": "id-token-value",
|
||||||
"Claims": map[string]interface{}{
|
"Claims": map[string]interface{}{
|
||||||
"email": "[email protected]",
|
"email": "[email protected]",
|
||||||
"role": "admin",
|
"role": "admin",
|
||||||
@@ -314,7 +314,7 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
templateText: "X-User-Email: {{.Claims.email}}, X-Email-Verified: {{.Claims.email_verified}}",
|
templateText: "X-User-Email: {{.Claims.email}}, X-Email-Verified: {{.Claims.email_verified}}",
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"AccessToken": "access-token-value",
|
"AccessToken": "access-token-value",
|
||||||
"IdToken": "id-token-value",
|
"IDToken": "id-token-value",
|
||||||
"Claims": map[string]interface{}{
|
"Claims": map[string]interface{}{
|
||||||
"email": "[email protected]",
|
"email": "[email protected]",
|
||||||
"email_verified": true,
|
"email_verified": true,
|
||||||
@@ -327,7 +327,7 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
templateText: "X-User-ID: {{.Claims.sub}}, X-User-Name: {{.Claims.name}}, X-Username: {{.Claims.preferred_username}}",
|
templateText: "X-User-ID: {{.Claims.sub}}, X-User-Name: {{.Claims.name}}, X-Username: {{.Claims.preferred_username}}",
|
||||||
data: map[string]interface{}{
|
data: map[string]interface{}{
|
||||||
"AccessToken": "access-token-value",
|
"AccessToken": "access-token-value",
|
||||||
"IdToken": "id-token-value",
|
"IDToken": "id-token-value",
|
||||||
"Claims": map[string]interface{}{
|
"Claims": map[string]interface{}{
|
||||||
"sub": "user123456",
|
"sub": "user123456",
|
||||||
"name": "Jane Doe",
|
"name": "Jane Doe",
|
||||||
@@ -363,7 +363,7 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
type templateData struct {
|
type templateData struct {
|
||||||
Claims map[string]interface{}
|
Claims map[string]interface{}
|
||||||
AccessToken string
|
AccessToken string
|
||||||
IdToken string
|
IDToken string
|
||||||
RefreshToken string
|
RefreshToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,10 +376,10 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Access and ID token distinction with struct",
|
name: "Access and ID token distinction with struct",
|
||||||
templateText: "Access: {{.AccessToken}} ID: {{.IdToken}}",
|
templateText: "Access: {{.AccessToken}} ID: {{.IDToken}}",
|
||||||
data: templateData{
|
data: templateData{
|
||||||
AccessToken: "access-token-value",
|
AccessToken: "access-token-value",
|
||||||
IdToken: "id-token-value", // Now these should be distinct values
|
IDToken: "id-token-value", // Now these should be distinct values
|
||||||
Claims: map[string]interface{}{},
|
Claims: map[string]interface{}{},
|
||||||
},
|
},
|
||||||
expectedValue: "Access: access-token-value ID: id-token-value",
|
expectedValue: "Access: access-token-value ID: id-token-value",
|
||||||
@@ -389,7 +389,7 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
templateText: "User: {{.Claims.sub}} Token: {{.AccessToken}}",
|
templateText: "User: {{.Claims.sub}} Token: {{.AccessToken}}",
|
||||||
data: templateData{
|
data: templateData{
|
||||||
AccessToken: "access-token",
|
AccessToken: "access-token",
|
||||||
IdToken: "access-token",
|
IDToken: "access-token",
|
||||||
Claims: map[string]interface{}{
|
Claims: map[string]interface{}{
|
||||||
"sub": "user123",
|
"sub": "user123",
|
||||||
},
|
},
|
||||||
@@ -401,7 +401,7 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
templateText: "X-Custom: {{.Claims.custom_field}}, X-Group: {{.Claims.group}}",
|
templateText: "X-Custom: {{.Claims.custom_field}}, X-Group: {{.Claims.group}}",
|
||||||
data: templateData{
|
data: templateData{
|
||||||
AccessToken: "access-token",
|
AccessToken: "access-token",
|
||||||
IdToken: "id-token",
|
IDToken: "id-token",
|
||||||
Claims: map[string]interface{}{
|
Claims: map[string]interface{}{
|
||||||
"sub": "user123",
|
"sub": "user123",
|
||||||
"custom_field": "custom-value",
|
"custom_field": "custom-value",
|
||||||
@@ -415,7 +415,7 @@ func TestTemplateExecutionContext(t *testing.T) {
|
|||||||
templateText: "X-Email: {{.Claims.email}}, X-Name: {{.Claims.name}}",
|
templateText: "X-Email: {{.Claims.email}}, X-Name: {{.Claims.name}}",
|
||||||
data: templateData{
|
data: templateData{
|
||||||
AccessToken: "access-token",
|
AccessToken: "access-token",
|
||||||
IdToken: "id-token",
|
IDToken: "id-token",
|
||||||
Claims: map[string]interface{}{
|
Claims: map[string]interface{}{
|
||||||
"email": "[email protected]",
|
"email": "[email protected]",
|
||||||
"name": "John Smith",
|
"name": "John Smith",
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func TestTemplatedHeadersIntegration(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "ID Token Header",
|
name: "ID Token Header",
|
||||||
headers: []TemplatedHeader{
|
headers: []TemplatedHeader{
|
||||||
{Name: "X-ID-Token", Value: "{{.IdToken}}"},
|
{Name: "X-ID-Token", Value: "{{.IDToken}}"},
|
||||||
},
|
},
|
||||||
expectedHeaders: map[string]string{
|
expectedHeaders: map[string]string{
|
||||||
// We'll update this dynamically after generating the token
|
// We'll update this dynamically after generating the token
|
||||||
@@ -81,7 +81,7 @@ func TestTemplatedHeadersIntegration(t *testing.T) {
|
|||||||
name: "Both Token Types",
|
name: "Both Token Types",
|
||||||
headers: []TemplatedHeader{
|
headers: []TemplatedHeader{
|
||||||
{Name: "X-Access-Token", Value: "{{.AccessToken}}"},
|
{Name: "X-Access-Token", Value: "{{.AccessToken}}"},
|
||||||
{Name: "X-ID-Token", Value: "{{.IdToken}}"},
|
{Name: "X-ID-Token", Value: "{{.IDToken}}"},
|
||||||
},
|
},
|
||||||
expectedHeaders: map[string]string{
|
expectedHeaders: map[string]string{
|
||||||
// We'll update these dynamically after generating the tokens
|
// We'll update these dynamically after generating the tokens
|
||||||
@@ -389,6 +389,7 @@ func TestTemplatedHeadersIntegration(t *testing.T) {
|
|||||||
// The current test expects the literal string "<no value>".
|
// The current test expects the literal string "<no value>".
|
||||||
// Let's assume for now that if it's missing, it's an error unless specifically handled.
|
// Let's assume for now that if it's missing, it's an error unless specifically handled.
|
||||||
// The test as written expects "<no value>" to be present.
|
// The test as written expects "<no value>" to be present.
|
||||||
|
t.Logf("Header %s not set, but expected '<no value>' for missing claim", name)
|
||||||
}
|
}
|
||||||
t.Errorf("Expected header %s was not set", name)
|
t.Errorf("Expected header %s was not set", name)
|
||||||
|
|
||||||
|
|||||||
@@ -15,19 +15,19 @@ import (
|
|||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestTokenTypeDistinction tests that AccessToken and IdToken are correctly distinguished in templates
|
// TestTokenTypeDistinction tests that AccessToken and IDToken are correctly distinguished in templates
|
||||||
func TestTokenTypeDistinction(t *testing.T) {
|
func TestTokenTypeDistinction(t *testing.T) {
|
||||||
// Define test data where AccessToken and IdToken are deliberately different
|
// Define test data where AccessToken and IDToken are deliberately different
|
||||||
type templateData struct {
|
type templateData struct {
|
||||||
Claims map[string]interface{}
|
Claims map[string]interface{}
|
||||||
AccessToken string
|
AccessToken string
|
||||||
IdToken string
|
IDToken string
|
||||||
RefreshToken string
|
RefreshToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
testData := templateData{
|
testData := templateData{
|
||||||
AccessToken: "test-access-token-abc123",
|
AccessToken: "test-access-token-abc123",
|
||||||
IdToken: "test-id-token-xyz789",
|
IDToken: "test-id-token-xyz789",
|
||||||
RefreshToken: "test-refresh-token",
|
RefreshToken: "test-refresh-token",
|
||||||
Claims: map[string]interface{}{
|
Claims: map[string]interface{}{
|
||||||
"sub": "test-subject",
|
"sub": "test-subject",
|
||||||
@@ -48,17 +48,17 @@ func TestTokenTypeDistinction(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ID Token Only",
|
name: "ID Token Only",
|
||||||
templateText: "ID: {{.IdToken}}",
|
templateText: "ID: {{.IDToken}}",
|
||||||
expectedValue: "ID: test-id-token-xyz789",
|
expectedValue: "ID: test-id-token-xyz789",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Both Tokens",
|
name: "Both Tokens",
|
||||||
templateText: "Access: {{.AccessToken}} ID: {{.IdToken}}",
|
templateText: "Access: {{.AccessToken}} ID: {{.IDToken}}",
|
||||||
expectedValue: "Access: test-access-token-abc123 ID: test-id-token-xyz789",
|
expectedValue: "Access: test-access-token-abc123 ID: test-id-token-xyz789",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Both Tokens in Authorization Format",
|
name: "Both Tokens in Authorization Format",
|
||||||
templateText: "Bearer {{.AccessToken}} and Bearer {{.IdToken}}",
|
templateText: "Bearer {{.AccessToken}} and Bearer {{.IDToken}}",
|
||||||
expectedValue: "Bearer test-access-token-abc123 and Bearer test-id-token-xyz789",
|
expectedValue: "Bearer test-access-token-abc123 and Bearer test-id-token-xyz789",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ func TestTokenTypeIntegration(t *testing.T) {
|
|||||||
|
|
||||||
// Define test headers that use both token types
|
// Define test headers that use both token types
|
||||||
headers := []TemplatedHeader{
|
headers := []TemplatedHeader{
|
||||||
{Name: "X-ID-Token", Value: "{{.IdToken}}"},
|
{Name: "X-ID-Token", Value: "{{.IDToken}}"},
|
||||||
{Name: "X-Access-Token", Value: "{{.AccessToken}}"},
|
{Name: "X-Access-Token", Value: "{{.AccessToken}}"},
|
||||||
{Name: "Authorization", Value: "Bearer {{.AccessToken}}"},
|
{Name: "Authorization", Value: "Bearer {{.AccessToken}}"},
|
||||||
{Name: "X-Email-From-Claims", Value: "{{.Claims.email}}"},
|
{Name: "X-Email-From-Claims", Value: "{{.Claims.email}}"},
|
||||||
|
|||||||
Reference in New Issue
Block a user