From 34ef0df5ca9f8dab1a681e28919c313c1923b1f4 Mon Sep 17 00:00:00 2001 From: Chris Walters Date: Sun, 12 Oct 2025 22:11:10 +0100 Subject: [PATCH] will return name if configured in id token --- internal/handlers/token.go | 10 ++- internal/jwt/jwt.go | 8 ++- internal/jwt/jwt_test.go | 13 +++- pkg/oauth/google.go | 26 +++++++- pkg/oauth/google_test.go | 130 +++++++++++++++++++++++++++++++++++++ 5 files changed, 177 insertions(+), 10 deletions(-) diff --git a/internal/handlers/token.go b/internal/handlers/token.go index 09444a3..6473fdd 100644 --- a/internal/handlers/token.go +++ b/internal/handlers/token.go @@ -142,18 +142,22 @@ func generateRefreshToken(clientID string) string { func (h *TokenHandler) generateIDToken(issuerURL, clientID string) (string, error) { // Generate a subject ID based on client ID sub := "user-" + clientID - + // Check if there's a configured email in the token config var email string + var name string tokenConfig := h.store.GetTokenConfig() if tokenConfig != nil { if userInfoConfig, ok := tokenConfig["user_info"].(map[string]interface{}); ok { if configuredEmail, ok := userInfoConfig["email"].(string); ok { email = configuredEmail } + if configuredName, ok := userInfoConfig["name"].(string); ok { + name = configuredName + } } } - + // If no email is configured, pass empty string (don't default to generated email) - return jwt.GenerateIDToken(issuerURL, clientID, sub, email) + return jwt.GenerateIDToken(issuerURL, clientID, sub, email, name) } diff --git a/internal/jwt/jwt.go b/internal/jwt/jwt.go index 2908c65..def180d 100644 --- a/internal/jwt/jwt.go +++ b/internal/jwt/jwt.go @@ -37,7 +37,7 @@ func InitKeys() error { } // GenerateIDToken creates a signed JWT ID token -func GenerateIDToken(issuer, clientID, sub, email string) (string, error) { +func GenerateIDToken(issuer, clientID, sub, email, name string) (string, error) { if privateKey == nil { if err := InitKeys(); err != nil { return "", err @@ -53,12 +53,16 @@ func GenerateIDToken(issuer, clientID, sub, email string) (string, error) { "iat": now.Unix(), "nonce": generateNonce(), } - + // Only include email claim if an email is provided if email != "" { claims["email"] = email } + if name != "" { + claims["name"] = name + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) token.Header["kid"] = keyID diff --git a/internal/jwt/jwt_test.go b/internal/jwt/jwt_test.go index 742d2c7..fe646a1 100644 --- a/internal/jwt/jwt_test.go +++ b/internal/jwt/jwt_test.go @@ -35,8 +35,9 @@ func TestGenerateIDToken(t *testing.T) { clientID := "test-client" sub := "user-123" email := "user-123@example.com" + name := "User 123" - tokenString, err := GenerateIDToken(issuer, clientID, sub, email) + tokenString, err := GenerateIDToken(issuer, clientID, sub, email, name) if err != nil { t.Fatalf("Failed to generate ID token: %v", err) } @@ -67,6 +68,10 @@ func TestGenerateIDToken(t *testing.T) { if claims["email"] != email { t.Errorf("Expected email %s, got %v", email, claims["email"]) } + + if claims["name"] != name { + t.Errorf("Expected name %s, got %v", name, claims["name"]) + } } func TestGenerateAccessToken(t *testing.T) { @@ -159,8 +164,9 @@ func TestVerifyToken(t *testing.T) { clientID := "test-client" sub := "user-123" email := "user-123@example.com" + name := "User 123" - tokenString, err := GenerateIDToken(issuer, clientID, sub, email) + tokenString, err := GenerateIDToken(issuer, clientID, sub, email, name) if err != nil { t.Fatalf("Failed to generate ID token: %v", err) } @@ -193,8 +199,9 @@ func TestTokenFormat(t *testing.T) { clientID := "test-client" sub := "user-123" email := "user-123@example.com" + name := "User 123" - tokenString, err := GenerateIDToken(issuer, clientID, sub, email) + tokenString, err := GenerateIDToken(issuer, clientID, sub, email, name) if err != nil { t.Fatalf("Failed to generate ID token: %v", err) } diff --git a/pkg/oauth/google.go b/pkg/oauth/google.go index 4dd5da6..22c52dc 100644 --- a/pkg/oauth/google.go +++ b/pkg/oauth/google.go @@ -50,15 +50,37 @@ func (p *GoogleProvider) ExchangeCodeForToken(code string) (map[string]interface // Generate proper JWT tokens sub := "user-" + authRequest.ClientID - email := authRequest.ClientID + "@example.com" scopes := []string{"openid", "email", "profile"} + // Pull email and name from token config if available + var email string + var name string + tokenConfig := p.Store.GetTokenConfig() + if tokenConfig != nil { + if userInfoConfig, ok := tokenConfig["user_info"].(map[string]interface{}); ok { + if configuredEmail, ok := userInfoConfig["email"].(string); ok { + email = configuredEmail + } + if configuredName, ok := userInfoConfig["name"].(string); ok { + name = configuredName + } + } + } + + // Fall back to default values if not configured + if email == "" { + email = authRequest.ClientID + "@example.com" + } + if name == "" { + name = "User " + authRequest.ClientID + } + accessToken, err := jwt.GenerateAccessToken(p.IssuerURL, authRequest.ClientID, sub, scopes) if err != nil { return nil, &Error{Code: "server_error", Description: "Failed to generate access token"} } - idToken, err := jwt.GenerateIDToken(p.IssuerURL, authRequest.ClientID, sub, email) + idToken, err := jwt.GenerateIDToken(p.IssuerURL, authRequest.ClientID, sub, email, name) if err != nil { return nil, &Error{Code: "server_error", Description: "Failed to generate ID token"} } diff --git a/pkg/oauth/google_test.go b/pkg/oauth/google_test.go index 8bb6e5f..4de8bda 100644 --- a/pkg/oauth/google_test.go +++ b/pkg/oauth/google_test.go @@ -130,6 +130,136 @@ func TestGoogleProvider_ExchangeCodeForToken(t *testing.T) { } } +func TestGoogleProvider_ExchangeCodeForToken_WithConfiguration(t *testing.T) { + tests := []struct { + name string + tokenConfig map[string]interface{} + expectedEmail string + expectedName string + }{ + { + name: "No configuration - uses defaults", + tokenConfig: nil, + expectedEmail: "test-client@example.com", + expectedName: "User test-client", + }, + { + name: "Full configuration - uses configured values", + tokenConfig: map[string]interface{}{ + "user_info": map[string]interface{}{ + "email": "custom@example.com", + "name": "Custom User", + }, + }, + expectedEmail: "custom@example.com", + expectedName: "Custom User", + }, + { + name: "Partial configuration - email only", + tokenConfig: map[string]interface{}{ + "user_info": map[string]interface{}{ + "email": "partial@example.com", + }, + }, + expectedEmail: "partial@example.com", + expectedName: "User test-client", + }, + { + name: "Partial configuration - name only", + tokenConfig: map[string]interface{}{ + "user_info": map[string]interface{}{ + "name": "Partial User", + }, + }, + expectedEmail: "test-client@example.com", + expectedName: "Partial User", + }, + { + name: "Empty configuration object", + tokenConfig: map[string]interface{}{ + "user_info": map[string]interface{}{}, + }, + expectedEmail: "test-client@example.com", + expectedName: "User test-client", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + store := store.NewMemoryStore() + provider := NewGoogleProvider(store) + + // Set up token configuration if provided + if tt.tokenConfig != nil { + store.StoreTokenConfig(tt.tokenConfig) + } + + // Add a valid authorization code + code := "valid-code" + authRequest := &models.AuthRequest{ + ClientID: "test-client", + Expiration: time.Now().Add(10 * time.Minute), + } + store.StoreAuthCode(code, authRequest) + + // Exchange the code for a token + result, err := provider.ExchangeCodeForToken(code) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Get the ID token + idToken, ok := result["id_token"].(string) + if !ok || idToken == "" { + t.Fatal("id_token should be a non-empty string") + } + + // Parse the ID token to verify claims + parser := jwtlib.NewParser() + token, _, err := parser.ParseUnverified(idToken, jwtlib.MapClaims{}) + if err != nil { + t.Fatalf("failed to parse ID token: %v", err) + } + + claims, ok := token.Claims.(jwtlib.MapClaims) + if !ok { + t.Fatal("failed to get claims from token") + } + + // Verify email claim + email, hasEmail := claims["email"].(string) + if tt.expectedEmail != "" { + if !hasEmail { + t.Errorf("expected email claim to be present") + } else if email != tt.expectedEmail { + t.Errorf("expected email=%s, got %s", tt.expectedEmail, email) + } + } + + // Verify name claim + name, hasName := claims["name"].(string) + if tt.expectedName != "" { + if !hasName { + t.Errorf("expected name claim to be present") + } else if name != tt.expectedName { + t.Errorf("expected name=%s, got %s", tt.expectedName, name) + } + } + + // Verify other expected claims are present + if claims["sub"] == "" { + t.Error("expected sub claim to be present") + } + if claims["aud"] != "test-client" { + t.Errorf("expected aud=test-client, got %v", claims["aud"]) + } + if claims["iss"] == "" { + t.Error("expected iss claim to be present") + } + }) + } +} + func TestGoogleProvider_GetUserInfo(t *testing.T) { store := store.NewMemoryStore() provider := NewGoogleProvider(store)