Files
traefikoidc/templated_header_execution_test.go
T

607 lines
18 KiB
Go

package traefikoidc
import (
"bytes"
"testing"
"text/template"
)
// TestTemplateExecution tests that templates are executed correctly with different types of claims
func TestTemplateExecution(t *testing.T) {
tests := []struct {
name string
templateText string
data map[string]interface{}
expectedValue string
expectError bool
}{
{
name: "String Claim",
templateText: "{{.Claims.email}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"email": "[email protected]",
},
},
expectedValue: "[email protected]",
expectError: false,
},
{
name: "Number Claim",
templateText: "{{.Claims.age}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"age": 30,
},
},
expectedValue: "30",
expectError: false,
},
{
name: "Boolean Claim",
templateText: "{{.Claims.admin}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"admin": true,
},
},
expectedValue: "true",
expectError: false,
},
{
name: "Array Claim",
templateText: "{{index .Claims.roles 0}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"roles": []string{"admin", "user"},
},
},
expectedValue: "admin",
expectError: false,
},
{
name: "Nested Object Claim",
templateText: "{{.Claims.user.name}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"user": map[string]interface{}{
"name": "John Doe",
},
},
},
expectedValue: "John Doe",
expectError: false,
},
{
name: "Access Token",
templateText: "Bearer {{.AccessToken}}",
data: map[string]interface{}{
"AccessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Et9HFtf9R3GEMA0IICOfFMVXY7kkTX1wr4qCyhIf58U",
},
expectedValue: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Et9HFtf9R3GEMA0IICOfFMVXY7kkTX1wr4qCyhIf58U",
expectError: false,
},
{
name: "ID Token",
templateText: "{{.IDToken}}",
data: map[string]interface{}{
"IDToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Et9HFtf9R3GEMA0IICOfFMVXY7kkTX1wr4qCyhIf58U",
},
expectedValue: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Et9HFtf9R3GEMA0IICOfFMVXY7kkTX1wr4qCyhIf58U",
expectError: false,
},
{
name: "Refresh Token",
templateText: "{{.RefreshToken}}",
data: map[string]interface{}{
"RefreshToken": "refresh-token-value",
},
expectedValue: "refresh-token-value",
expectError: false,
},
{
name: "Conditional Template",
templateText: "{{if .Claims.admin}}Admin User{{else}}Regular User{{end}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"admin": true,
},
},
expectedValue: "Admin User",
expectError: false,
},
{
name: "Multiple Claims",
templateText: "{{.Claims.firstName}} {{.Claims.lastName}} <{{.Claims.email}}>",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
},
},
expectedValue: "John Doe <[email protected]>",
expectError: false,
},
{
name: "Missing Claim",
templateText: "{{.Claims.missing}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{},
},
expectedValue: "<no value>",
expectError: false, // Go templates don't error on missing values
},
{
name: "Invalid Template Syntax",
templateText: "{{.Claims.email",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"email": "[email protected]",
},
},
expectedValue: "",
expectError: true, // Parsing should fail
},
{
name: "Custom Claims",
templateText: "Role: {{.Claims.role}}, Department: {{.Claims.department}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"email": "[email protected]",
"role": "admin",
"department": "engineering",
},
},
expectedValue: "Role: admin, Department: engineering",
expectError: false,
},
{
name: "Nested Custom Claims",
templateText: "Org: {{.Claims.metadata.organization}}, Team: {{.Claims.metadata.team}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"email": "[email protected]",
"metadata": map[string]interface{}{
"organization": "company-name",
"team": "platform",
},
},
},
expectedValue: "Org: company-name, Team: platform",
expectError: false,
},
{
name: "Email Claims",
templateText: "Email: {{.Claims.email}}, Verified: {{.Claims.email_verified}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"email": "[email protected]",
"email_verified": true,
},
},
expectedValue: "Email: [email protected], Verified: true",
expectError: false,
},
{
name: "User Identity Claims",
templateText: "Name: {{.Claims.name}}, Subject: {{.Claims.sub}}, Username: {{.Claims.preferred_username}}",
data: map[string]interface{}{
"Claims": map[string]interface{}{
"name": "John Doe",
"sub": "user123",
"preferred_username": "johndoe",
},
},
expectedValue: "Name: John Doe, Subject: user123, Username: johndoe",
expectError: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tmpl, err := template.New("test").Parse(tc.templateText)
if tc.expectError {
if err == nil {
t.Fatal("Expected template parsing error, but got nil")
}
return
}
if err != nil {
t.Fatalf("Failed to parse template: %v", err)
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, tc.data)
if err != nil {
t.Fatalf("Failed to execute template: %v", err)
}
result := buf.String()
if result != tc.expectedValue {
t.Errorf("Expected template output %q, got %q", tc.expectedValue, result)
}
})
}
}
// TestTemplateExecutionContext tests the specific template data context used in processAuthorizedRequest
func TestTemplateExecutionContext(t *testing.T) {
// Test cases for map-based template data, matching the new implementation
mapTests := []struct {
name string
templateText string
data map[string]interface{}
expectedValue string
}{
{
name: "Access and ID token distinction with map",
templateText: "Access: {{.AccessToken}} ID: {{.IDToken}}",
data: map[string]interface{}{
"AccessToken": "access-token-value",
"IDToken": "id-token-value",
"Claims": map[string]interface{}{},
"RefreshToken": "refresh-token-value",
},
expectedValue: "Access: access-token-value ID: id-token-value",
},
{
name: "Combining tokens and claims with map",
templateText: "User: {{.Claims.sub}} Token: {{.AccessToken}}",
data: map[string]interface{}{
"AccessToken": "access-token",
"IDToken": "id-token",
"Claims": map[string]interface{}{
"sub": "user123",
},
"RefreshToken": "refresh-token",
},
expectedValue: "User: user123 Token: access-token",
},
{
name: "Authorization header with Bearer token",
templateText: "Bearer {{.AccessToken}}",
data: map[string]interface{}{
"AccessToken": "jwt-access-token",
"IDToken": "id-token",
"Claims": map[string]interface{}{},
},
expectedValue: "Bearer jwt-access-token",
},
{
name: "Boolean template data with AccessToken",
templateText: "Bearer {{.AccessToken}}",
data: map[string]interface{}{
"AccessToken": true, // Test boolean values to ensure they render correctly
},
expectedValue: "Bearer true",
},
{
name: "Custom non-standard claims in ID token",
templateText: "X-User-Role: {{.Claims.role}}, X-User-Permissions: {{.Claims.permissions}}",
data: map[string]interface{}{
"AccessToken": "access-token-value",
"IDToken": "id-token-value",
"Claims": map[string]interface{}{
"email": "[email protected]",
"role": "admin",
"permissions": "read:all,write:own",
},
},
expectedValue: "X-User-Role: admin, X-User-Permissions: read:all,write:own",
},
{
name: "Deeply nested custom claims",
templateText: "X-Organization: {{.Claims.app_metadata.organization.name}}, X-Team: {{.Claims.app_metadata.team}}",
data: map[string]interface{}{
"AccessToken": "access-token-value",
"Claims": map[string]interface{}{
"app_metadata": map[string]interface{}{
"organization": map[string]interface{}{
"name": "acme-corp",
"id": "org-123",
},
"team": "platform",
},
},
},
expectedValue: "X-Organization: acme-corp, X-Team: platform",
},
{
name: "Email in claims",
templateText: "X-User-Email: {{.Claims.email}}, X-Email-Verified: {{.Claims.email_verified}}",
data: map[string]interface{}{
"AccessToken": "access-token-value",
"IDToken": "id-token-value",
"Claims": map[string]interface{}{
"email": "[email protected]",
"email_verified": true,
},
},
expectedValue: "X-User-Email: [email protected], X-Email-Verified: true",
},
{
name: "User info from claims",
templateText: "X-User-ID: {{.Claims.sub}}, X-User-Name: {{.Claims.name}}, X-Username: {{.Claims.preferred_username}}",
data: map[string]interface{}{
"AccessToken": "access-token-value",
"IDToken": "id-token-value",
"Claims": map[string]interface{}{
"sub": "user123456",
"name": "Jane Doe",
"preferred_username": "jane.doe",
},
},
expectedValue: "X-User-ID: user123456, X-User-Name: Jane Doe, X-Username: jane.doe",
},
}
// Run map-based tests (matching the new implementation)
for _, tc := range mapTests {
t.Run(tc.name, func(t *testing.T) {
tmpl, err := template.New("test").Parse(tc.templateText)
if err != nil {
t.Fatalf("Failed to parse template: %v", err)
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, tc.data)
if err != nil {
t.Fatalf("Failed to execute template: %v", err)
}
result := buf.String()
if result != tc.expectedValue {
t.Errorf("Expected template output %q, got %q", tc.expectedValue, result)
}
})
}
// For backward compatibility, also test the original struct-based implementation
type templateData struct {
Claims map[string]interface{}
AccessToken string
IDToken string
RefreshToken string
}
// Test cases for struct-based template data (original implementation)
structTests := []struct {
name string
templateText string
data templateData
expectedValue string
}{
{
name: "Access and ID token distinction with struct",
templateText: "Access: {{.AccessToken}} ID: {{.IDToken}}",
data: templateData{
AccessToken: "access-token-value",
IDToken: "id-token-value", // Now these should be distinct values
Claims: map[string]interface{}{},
},
expectedValue: "Access: access-token-value ID: id-token-value",
},
{
name: "Combining tokens and claims with struct",
templateText: "User: {{.Claims.sub}} Token: {{.AccessToken}}",
data: templateData{
AccessToken: "access-token",
IDToken: "access-token",
Claims: map[string]interface{}{
"sub": "user123",
},
},
expectedValue: "User: user123 Token: access-token",
},
{
name: "Custom claims with struct",
templateText: "X-Custom: {{.Claims.custom_field}}, X-Group: {{.Claims.group}}",
data: templateData{
AccessToken: "access-token",
IDToken: "id-token",
Claims: map[string]interface{}{
"sub": "user123",
"custom_field": "custom-value",
"group": "admins",
},
},
expectedValue: "X-Custom: custom-value, X-Group: admins",
},
{
name: "Email claim in struct context",
templateText: "X-Email: {{.Claims.email}}, X-Name: {{.Claims.name}}",
data: templateData{
AccessToken: "access-token",
IDToken: "id-token",
Claims: map[string]interface{}{
"email": "[email protected]",
"name": "John Smith",
},
},
expectedValue: "X-Email: [email protected], X-Name: John Smith",
},
}
for _, tc := range structTests {
t.Run(tc.name, func(t *testing.T) {
tmpl, err := template.New("test").Parse(tc.templateText)
if err != nil {
t.Fatalf("Failed to parse template: %v", err)
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, tc.data)
if err != nil {
t.Fatalf("Failed to execute template: %v", err)
}
result := buf.String()
if result != tc.expectedValue {
t.Errorf("Expected template output %q, got %q", tc.expectedValue, result)
}
})
}
}
// TestRegressionBooleanAccessToken specifically tests the regression case where
// a boolean value was causing "can't evaluate field AccessToken in type bool" error
func TestRegressionBooleanAccessToken(t *testing.T) {
// Test the specific case where we execute a template referencing AccessToken
// using a boolean context value
testCases := []struct {
name string
templateText string
dataContext interface{}
expectedValue string
expectError bool // Added to skip the test that demonstrates the error
}{
{
name: "Map with boolean as root",
templateText: "{{.AccessToken}}",
dataContext: map[string]interface{}{"AccessToken": "token-value"},
expectedValue: "token-value",
expectError: false,
},
{
name: "Boolean as root context",
templateText: "{{.AccessToken}}",
dataContext: true,
expectedValue: "<no value>",
expectError: true, // Skip this test as it demonstrates the error we're fixing
},
{
name: "Bearer with map context",
templateText: "Bearer {{.AccessToken}}",
dataContext: map[string]interface{}{"AccessToken": "token-value"},
expectedValue: "Bearer token-value",
expectError: false,
},
{
name: "Complex nesting with authorization",
templateText: "Authorization: Bearer {{.AccessToken}}",
dataContext: map[string]interface{}{
"AccessToken": "jwt-token-123",
"something": true,
"anotherField": map[string]interface{}{
"nested": "value",
},
},
expectedValue: "Authorization: Bearer jwt-token-123",
expectError: false,
},
{
name: "Custom claims access",
templateText: "X-User-Role: {{.Claims.role}}, X-User-Groups: {{.Claims.groups}}",
dataContext: map[string]interface{}{
"AccessToken": "jwt-token-xyz",
"Claims": map[string]interface{}{
"email": "[email protected]",
"role": "admin",
"groups": "group1,group2,group3",
"custom_data": map[string]interface{}{
"organization": "company-name",
"department": "engineering",
},
},
},
expectedValue: "X-User-Role: admin, X-User-Groups: group1,group2,group3",
expectError: false,
},
{
name: "Nested custom claims access",
templateText: "X-Organization: {{.Claims.custom_data.organization}}, X-Department: {{.Claims.custom_data.department}}",
dataContext: map[string]interface{}{
"Claims": map[string]interface{}{
"custom_data": map[string]interface{}{
"organization": "company-name",
"department": "engineering",
},
},
},
expectedValue: "X-Organization: company-name, X-Department: engineering",
expectError: false,
},
{
name: "Azure AD specific claims",
templateText: "X-TenantID: {{.Claims.tid}}, X-Roles: {{.Claims.roles}}",
dataContext: map[string]interface{}{
"Claims": map[string]interface{}{
"tid": "tenant-id-12345",
"roles": "User,Admin,Developer",
},
},
expectedValue: "X-TenantID: tenant-id-12345, X-Roles: User,Admin,Developer",
expectError: false,
},
{
name: "Auth0 specific claims",
templateText: "X-Permissions: {{.Claims.permissions}}, X-AppMetadata: {{.Claims.app_metadata.plan}}",
dataContext: map[string]interface{}{
"Claims": map[string]interface{}{
"permissions": "read:products,write:orders",
"app_metadata": map[string]interface{}{
"plan": "premium",
"status": "active",
"trial_ended": false,
},
},
},
expectedValue: "X-Permissions: read:products,write:orders, X-AppMetadata: premium",
expectError: false,
},
{
name: "Standard claims with email",
templateText: "X-Email: {{.Claims.email}}, X-Name: {{.Claims.name}}, X-Subject: {{.Claims.sub}}",
dataContext: map[string]interface{}{
"Claims": map[string]interface{}{
"email": "[email protected]",
"name": "John Doe",
"sub": "auth0|12345",
},
},
expectedValue: "X-Email: [email protected], X-Name: John Doe, X-Subject: auth0|12345",
expectError: false,
},
{
name: "Verified email claim",
templateText: "X-Email: {{.Claims.email}}, X-Email-Verified: {{.Claims.email_verified}}",
dataContext: map[string]interface{}{
"Claims": map[string]interface{}{
"email": "[email protected]",
"email_verified": true,
},
},
expectedValue: "X-Email: [email protected], X-Email-Verified: true",
expectError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tmpl, err := template.New("test").Parse(tc.templateText)
if err != nil {
t.Fatalf("Failed to parse template: %v", err)
}
// Skip tests that demonstrate the error
if tc.expectError {
t.Skip("Skipping test that demonstrates the error we're fixing")
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, tc.dataContext)
if err != nil {
t.Fatalf("Failed to execute template: %v", err)
}
result := buf.String()
if result != tc.expectedValue {
t.Errorf("Expected template output %q, got %q", tc.expectedValue, result)
}
})
}
}