Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions internal/handlers/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
8 changes: 6 additions & 2 deletions internal/jwt/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
13 changes: 10 additions & 3 deletions internal/jwt/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ func TestGenerateIDToken(t *testing.T) {
clientID := "test-client"
sub := "user-123"
email := "[email protected]"
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)
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -159,8 +164,9 @@ func TestVerifyToken(t *testing.T) {
clientID := "test-client"
sub := "user-123"
email := "[email protected]"
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)
}
Expand Down Expand Up @@ -193,8 +199,9 @@ func TestTokenFormat(t *testing.T) {
clientID := "test-client"
sub := "user-123"
email := "[email protected]"
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)
}
Expand Down
26 changes: 24 additions & 2 deletions pkg/oauth/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
}
Expand Down
130 changes: 130 additions & 0 deletions pkg/oauth/google_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: "[email protected]",
expectedName: "User test-client",
},
{
name: "Full configuration - uses configured values",
tokenConfig: map[string]interface{}{
"user_info": map[string]interface{}{
"email": "[email protected]",
"name": "Custom User",
},
},
expectedEmail: "[email protected]",
expectedName: "Custom User",
},
{
name: "Partial configuration - email only",
tokenConfig: map[string]interface{}{
"user_info": map[string]interface{}{
"email": "[email protected]",
},
},
expectedEmail: "[email protected]",
expectedName: "User test-client",
},
{
name: "Partial configuration - name only",
tokenConfig: map[string]interface{}{
"user_info": map[string]interface{}{
"name": "Partial User",
},
},
expectedEmail: "[email protected]",
expectedName: "Partial User",
},
{
name: "Empty configuration object",
tokenConfig: map[string]interface{}{
"user_info": map[string]interface{}{},
},
expectedEmail: "[email protected]",
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)
Expand Down
Loading