Additional micro fixes and cleanups.

This commit is contained in:
2025-06-24 18:48:02 +01:00
parent dbe3455f49
commit c2a81bc2df
10 changed files with 40 additions and 31 deletions
+2
View File
@@ -0,0 +1,2 @@
docker/
.claude/
-5
View File
@@ -1,5 +0,0 @@
### TODO / wishlist
- [] Improve test coverage
- [x] Improve caching mechanism
- [x] Add automatic release and semver generation
+3
View File
@@ -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 (
+3
View File
@@ -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,
+1
View File
@@ -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()
+4 -1
View File
@@ -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,
} }
+1
View File
@@ -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
} }
}() }()
+15 -15
View File
@@ -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",
+3 -2
View File
@@ -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)
+8 -8
View File
@@ -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}}"},