Refactor token handling to support optional extra claims in access token generation

- Updated storeAccessToken and related methods to accept an optional extraClaims parameter for additional flexibility.
- Modified MakeAccessToken and generateAccessTokenWithScope functions to incorporate extra claims, enhancing token customization.
- Adjusted tests to utilize the new extraClaims parameter, ensuring comprehensive coverage of the updated functionality.
This commit is contained in:
Max 2025-10-11 18:59:45 +08:00
parent 60c830099e
commit 07cc11b363
7 changed files with 169 additions and 70 deletions

View file

@ -240,7 +240,7 @@ func (s *Service) RefreshToken(ctx context.Context, refreshToken string, scope .
// Generate new access token with final scope // Generate new access token with final scope
expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds()) expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds())
newAccessToken, err := s.generateAccessTokenWithScope(clientID, finalScope, originalSubject, expiresIn) newAccessToken, err := s.generateAccessTokenWithScope(clientID, finalScope, originalSubject, expiresIn, nil)
if err != nil { if err != nil {
return nil, &types.ErrorResponse{ return nil, &types.ErrorResponse{
Code: types.ErrorServerError, Code: types.ErrorServerError,
@ -339,7 +339,7 @@ func (s *Service) RotateRefreshToken(ctx context.Context, oldToken string, reque
// Generate new tokens with final scope and original subject // Generate new tokens with final scope and original subject
expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds()) expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds())
newAccessToken, err := s.generateAccessTokenWithScope(clientID, finalScope, originalSubject, expiresIn) newAccessToken, err := s.generateAccessTokenWithScope(clientID, finalScope, originalSubject, expiresIn, nil)
if err != nil { if err != nil {
return nil, &types.ErrorResponse{ return nil, &types.ErrorResponse{
Code: types.ErrorServerError, Code: types.ErrorServerError,
@ -428,7 +428,7 @@ func (s *Service) handleAuthorizationCodeGrant(ctx context.Context, client *type
// Generate and store access token with proper scope and subject // Generate and store access token with proper scope and subject
expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds()) expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds())
accessToken, err := s.generateAccessTokenWithScope(client.ClientID, scope, subject, expiresIn) accessToken, err := s.generateAccessTokenWithScope(client.ClientID, scope, subject, expiresIn, nil)
if err != nil { if err != nil {
return nil, &types.ErrorResponse{ return nil, &types.ErrorResponse{
Code: types.ErrorServerError, Code: types.ErrorServerError,
@ -464,7 +464,7 @@ func (s *Service) handleClientCredentialsGrant(ctx context.Context, client *type
// Generate and store access token with client's scope (no user subject for client credentials) // Generate and store access token with client's scope (no user subject for client credentials)
expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds()) expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds())
accessToken, err := s.generateAccessTokenWithScope(client.ClientID, scope, "", expiresIn) accessToken, err := s.generateAccessTokenWithScope(client.ClientID, scope, "", expiresIn, nil)
if err != nil { if err != nil {
return nil, &types.ErrorResponse{ return nil, &types.ErrorResponse{
Code: types.ErrorServerError, Code: types.ErrorServerError,
@ -507,7 +507,7 @@ func (s *Service) handleRefreshTokenGrant(ctx context.Context, client *types.Cli
// Generate and store new access token with proper scope and subject // Generate and store new access token with proper scope and subject
expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds()) expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds())
accessToken, err := s.generateAccessTokenWithScope(client.ClientID, scope, subject, expiresIn) accessToken, err := s.generateAccessTokenWithScope(client.ClientID, scope, subject, expiresIn, nil)
if err != nil { if err != nil {
return nil, &types.ErrorResponse{ return nil, &types.ErrorResponse{
Code: types.ErrorServerError, Code: types.ErrorServerError,

View file

@ -315,7 +315,7 @@ func TestRevoke(t *testing.T) {
clientID := GetActualClientID(testClients[0].ClientID) clientID := GetActualClientID(testClients[0].ClientID)
// Store token using the new method // Store token using the new method
err := service.storeAccessToken(token, clientID, "", "", 3600) err := service.storeAccessToken(token, clientID, "", "", 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
err = service.Revoke(ctx, token, "access_token") err = service.Revoke(ctx, token, "access_token")

View file

@ -128,14 +128,14 @@ var (
// DefaultTeamFields contains basic team fields // DefaultTeamFields contains basic team fields
DefaultTeamFields = []interface{}{ DefaultTeamFields = []interface{}{
"id", "team_id", "name", "display_name", "description", "website", "logo", "team_id", "name", "display_name", "description", "website", "logo",
"owner_id", "status", "type_id", "type", "is_verified", "verified_at", "owner_id", "status", "type_id", "type", "is_verified", "verified_at",
"created_at", "updated_at", "created_at", "updated_at",
} }
// DefaultTeamDetailFields contains all team fields including contact info and metadata // DefaultTeamDetailFields contains all team fields including contact info and metadata
DefaultTeamDetailFields = []interface{}{ DefaultTeamDetailFields = []interface{}{
"id", "team_id", "name", "display_name", "description", "website", "logo", "team_id", "name", "display_name", "description", "website", "logo",
"owner_id", "contact_email", "contact_phone", "is_verified", "verified_at", "verified_by", "owner_id", "contact_email", "contact_phone", "is_verified", "verified_at", "verified_by",
"team_code", "team_code_type", "status", "type_id", "type", "address", "street_address", "team_code", "team_code_type", "status", "type_id", "type", "address", "street_address",
"city", "state_province", "postal_code", "country", "country_name", "region", "zoneinfo", "city", "state_province", "postal_code", "country", "country_name", "region", "zoneinfo",
@ -144,14 +144,14 @@ var (
// DefaultMemberFields contains basic member fields // DefaultMemberFields contains basic member fields
DefaultMemberFields = []interface{}{ DefaultMemberFields = []interface{}{
"id", "team_id", "user_id", "member_type", "role_id", "status", "team_id", "user_id", "member_type", "role_id", "status",
"invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token", "invitation_expires_at", "invitation_id", "invited_by", "invited_at", "joined_at", "invitation_token", "invitation_expires_at",
"last_active_at", "login_count", "message", "created_at", "updated_at", "last_active_at", "login_count", "message", "created_at", "updated_at",
} }
// DefaultMemberDetailFields contains all member fields including robot config and permissions // DefaultMemberDetailFields contains all member fields including robot config and permissions
DefaultMemberDetailFields = []interface{}{ DefaultMemberDetailFields = []interface{}{
"id", "team_id", "user_id", "member_type", "role_id", "status", "team_id", "user_id", "member_type", "role_id", "status",
"robot_name", "robot_description", "robot_avatar", "robot_config", "agents", "tools", "robot_name", "robot_description", "robot_avatar", "robot_config", "agents", "tools",
"mcp_servers", "data_access_permissions", "system_prompt", "is_active_robot", "mcp_servers", "data_access_permissions", "system_prompt", "is_active_robot",
"schedule_config", "random_activity", "activity_frequency", "last_robot_activity", "schedule_config", "random_activity", "activity_frequency", "last_robot_activity",

View file

@ -434,15 +434,21 @@ func (s *Service) GetKeyID() string {
} }
// SignToken signs a token based on the configured format (jwt or opaque) // SignToken signs a token based on the configured format (jwt or opaque)
func (s *Service) SignToken(tokenType, clientID, scope, subject string, expiresIn int) (string, error) { // extraClaims: optional extra claims to add to the token (e.g., team_id, tenant_id)
func (s *Service) SignToken(tokenType, clientID, scope, subject string, expiresIn int, extraClaims ...map[string]interface{}) (string, error) {
var claims map[string]interface{}
if len(extraClaims) > 0 {
claims = extraClaims[0]
}
switch s.config.Token.AccessTokenFormat { switch s.config.Token.AccessTokenFormat {
case "jwt": case "jwt":
return s.signJWTToken(tokenType, clientID, scope, subject, expiresIn) return s.signJWTToken(tokenType, clientID, scope, subject, expiresIn, claims)
case "opaque": case "opaque":
return s.signOpaqueToken(tokenType, clientID, scope, subject) return s.signOpaqueToken(tokenType, clientID, scope, subject)
default: default:
// Default to JWT if format is not specified or unknown // Default to JWT if format is not specified or unknown
return s.signJWTToken(tokenType, clientID, scope, subject, expiresIn) return s.signJWTToken(tokenType, clientID, scope, subject, expiresIn, claims)
} }
} }
@ -577,25 +583,32 @@ func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *t
} }
// signJWTToken signs a JWT token using the configured signing algorithm // signJWTToken signs a JWT token using the configured signing algorithm
func (s *Service) signJWTToken(tokenType, clientID, scope, subject string, expiresIn int) (string, error) { func (s *Service) signJWTToken(tokenType, clientID, scope, subject string, expiresIn int, extraClaims map[string]interface{}) (string, error) {
if s.signingCerts == nil || s.signingCerts.SigningKey == nil { if s.signingCerts == nil || s.signingCerts.SigningKey == nil {
return "", fmt.Errorf("signing certificates not initialized") return "", fmt.Errorf("signing certificates not initialized")
} }
now := time.Now() now := time.Now()
claims := &types.JWTClaims{
StandardClaims: jwt.StandardClaims{ // Use MapClaims to support extra claims
Issuer: s.config.IssuerURL, claims := jwt.MapClaims{
Subject: subject, // Standard JWT claims
Audience: clientID, "iss": s.config.IssuerURL,
ExpiresAt: now.Add(time.Duration(expiresIn) * time.Second).Unix(), "sub": subject,
NotBefore: now.Unix(), "aud": clientID,
IssuedAt: now.Unix(), "exp": now.Add(time.Duration(expiresIn) * time.Second).Unix(),
Id: generateJTI(), "nbf": now.Unix(),
}, "iat": now.Unix(),
ClientID: clientID, "jti": generateJTI(),
Scope: scope, // Custom OAuth claims
TokenType: tokenType, "client_id": clientID,
"scope": scope,
"token_type": tokenType,
}
// Add extra claims if provided (e.g., team_id, tenant_id)
for key, value := range extraClaims {
claims[key] = value
} }
// Create token with claims // Create token with claims
@ -614,8 +627,8 @@ func (s *Service) verifyJWTToken(tokenString string) (*types.TokenClaims, error)
return nil, fmt.Errorf("signing certificates not initialized") return nil, fmt.Errorf("signing certificates not initialized")
} }
// Parse token with claims // Parse token with MapClaims to support extra claims
token, err := jwt.ParseWithClaims(tokenString, &types.JWTClaims{}, func(token *jwt.Token) (interface{}, error) { token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
// Validate signing method // Validate signing method
expectedMethod := getSigningMethod(s.config.Token.AccessTokenSigningAlg) expectedMethod := getSigningMethod(s.config.Token.AccessTokenSigningAlg)
if token.Method != expectedMethod { if token.Method != expectedMethod {
@ -635,22 +648,75 @@ func (s *Service) verifyJWTToken(tokenString string) (*types.TokenClaims, error)
} }
// Extract claims // Extract claims
jwtClaims, ok := token.Claims.(*types.JWTClaims) mapClaims, ok := token.Claims.(jwt.MapClaims)
if !ok { if !ok {
return nil, fmt.Errorf("invalid JWT claims type") return nil, fmt.Errorf("invalid JWT claims type")
} }
// Convert to TokenClaims // Convert to TokenClaims
tokenClaims := &types.TokenClaims{ tokenClaims := &types.TokenClaims{
Subject: jwtClaims.Subject, Extra: make(map[string]interface{}),
ClientID: jwtClaims.ClientID, }
Scope: jwtClaims.Scope,
TokenType: jwtClaims.TokenType, // Extract standard claims
ExpiresAt: time.Unix(jwtClaims.ExpiresAt, 0), if sub, ok := mapClaims["sub"].(string); ok {
IssuedAt: time.Unix(jwtClaims.IssuedAt, 0), tokenClaims.Subject = sub
Issuer: jwtClaims.Issuer, }
Audience: []string{jwtClaims.Audience}, if clientID, ok := mapClaims["client_id"].(string); ok {
JTI: jwtClaims.Id, tokenClaims.ClientID = clientID
}
if scope, ok := mapClaims["scope"].(string); ok {
tokenClaims.Scope = scope
}
if tokenType, ok := mapClaims["token_type"].(string); ok {
tokenClaims.TokenType = tokenType
}
if iss, ok := mapClaims["iss"].(string); ok {
tokenClaims.Issuer = iss
}
if jti, ok := mapClaims["jti"].(string); ok {
tokenClaims.JTI = jti
}
// Extract time claims
if exp, ok := mapClaims["exp"].(float64); ok {
tokenClaims.ExpiresAt = time.Unix(int64(exp), 0)
}
if iat, ok := mapClaims["iat"].(float64); ok {
tokenClaims.IssuedAt = time.Unix(int64(iat), 0)
}
// Extract audience
if aud, ok := mapClaims["aud"].(string); ok {
tokenClaims.Audience = []string{aud}
} else if audArray, ok := mapClaims["aud"].([]interface{}); ok {
audience := make([]string, 0, len(audArray))
for _, a := range audArray {
if audStr, ok := a.(string); ok {
audience = append(audience, audStr)
}
}
tokenClaims.Audience = audience
}
// Extract extended claims for multi-tenancy and team support
if teamID, ok := mapClaims["team_id"].(string); ok {
tokenClaims.TeamID = teamID
}
if tenantID, ok := mapClaims["tenant_id"].(string); ok {
tokenClaims.TenantID = tenantID
}
// Store all extra claims for flexibility
standardClaims := map[string]bool{
"sub": true, "client_id": true, "scope": true, "token_type": true,
"exp": true, "iat": true, "nbf": true, "iss": true, "aud": true, "jti": true,
"team_id": true, "tenant_id": true,
}
for key, value := range mapClaims {
if !standardClaims[key] {
tokenClaims.Extra[key] = value
}
} }
return tokenClaims, nil return tokenClaims, nil

View file

@ -248,8 +248,13 @@ func (s *Service) ValidateTokenBinding(ctx context.Context, token string, bindin
// ============================================================================ // ============================================================================
// MakeAccessToken generates a new access token with specific parameters and stores it // MakeAccessToken generates a new access token with specific parameters and stores it
func (s *Service) MakeAccessToken(clientID, scope, subject string, expiresIn int) (string, error) { // extraClaims: optional extra claims to add to the token (e.g., team_id, tenant_id)
return s.generateAccessTokenWithScope(clientID, scope, subject, expiresIn) func (s *Service) MakeAccessToken(clientID, scope, subject string, expiresIn int, extraClaims ...map[string]interface{}) (string, error) {
var claims map[string]interface{}
if len(extraClaims) > 0 {
claims = extraClaims[0]
}
return s.generateAccessTokenWithScope(clientID, scope, subject, expiresIn, claims)
} }
// MakeRefreshToken generates a new refresh token with specific parameters and stores it // MakeRefreshToken generates a new refresh token with specific parameters and stores it
@ -338,19 +343,27 @@ func (s *Service) validateAudience(audience string) error {
// generateAccessToken generates a new access token // generateAccessToken generates a new access token
func (s *Service) generateAccessToken(clientID string) (string, error) { func (s *Service) generateAccessToken(clientID string) (string, error) {
expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds()) expiresIn := int(s.config.Token.AccessTokenLifetime.Seconds())
return s.generateAccessTokenWithScope(clientID, "", "", expiresIn) return s.generateAccessTokenWithScope(clientID, "", "", expiresIn, nil)
} }
// generateAccessTokenWithScope generates a new access token with specific parameters and stores it // generateAccessTokenWithScope generates a new access token with specific parameters and stores it
func (s *Service) generateAccessTokenWithScope(clientID, scope, subject string, expiresIn int) (string, error) { func (s *Service) generateAccessTokenWithScope(clientID, scope, subject string, expiresIn int, extraClaims map[string]interface{}) (string, error) {
// Use the new signing mechanism based on configuration // Use the new signing mechanism based on configuration
accessToken, err := s.SignToken("access_token", clientID, scope, subject, expiresIn) var accessToken string
var err error
if extraClaims != nil {
accessToken, err = s.SignToken("access_token", clientID, scope, subject, expiresIn, extraClaims)
} else {
accessToken, err = s.SignToken("access_token", clientID, scope, subject, expiresIn)
}
if err != nil { if err != nil {
return "", err return "", err
} }
// Store access token with metadata // Store access token with metadata (including extra claims)
err = s.storeAccessToken(accessToken, clientID, scope, subject, expiresIn) err = s.storeAccessToken(accessToken, clientID, scope, subject, expiresIn, extraClaims)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -359,7 +372,7 @@ func (s *Service) generateAccessTokenWithScope(clientID, scope, subject string,
} }
// storeAccessToken stores access token with metadata and specified expiration // storeAccessToken stores access token with metadata and specified expiration
func (s *Service) storeAccessToken(accessToken, clientID string, scope string, subject string, expiresIn int) error { func (s *Service) storeAccessToken(accessToken, clientID string, scope string, subject string, expiresIn int, extraClaims map[string]interface{}) error {
now := time.Now() now := time.Now()
expiresAt := now.Add(time.Duration(expiresIn) * time.Second).Unix() expiresAt := now.Add(time.Duration(expiresIn) * time.Second).Unix()
@ -373,6 +386,11 @@ func (s *Service) storeAccessToken(accessToken, clientID string, scope string, s
"expires_at": expiresAt, "expires_at": expiresAt,
} }
// Add extra claims if provided (e.g., team_id, tenant_id)
for key, value := range extraClaims {
tokenData[key] = value
}
ttl := time.Duration(expiresIn) * time.Second ttl := time.Duration(expiresIn) * time.Second
return s.store.Set(s.accessTokenKey(accessToken), tokenData, ttl) return s.store.Set(s.accessTokenKey(accessToken), tokenData, ttl)
} }

View file

@ -26,7 +26,7 @@ func TestIntrospect(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store token using the updated method with expiresIn parameter // Store token using the updated method with expiresIn parameter
err := service.storeAccessToken(token, clientID, scope, subject, 3600) err := service.storeAccessToken(token, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
response, err := service.Introspect(ctx, token) response, err := service.Introspect(ctx, token)
@ -49,7 +49,7 @@ func TestIntrospect(t *testing.T) {
// Store expired token with negative expiresIn (already expired) // Store expired token with negative expiresIn (already expired)
expiresIn := -3600 // Expired 1 hour ago expiresIn := -3600 // Expired 1 hour ago
err := service.storeAccessToken(token, clientID, scope, subject, expiresIn) err := service.storeAccessToken(token, clientID, scope, subject, expiresIn, nil)
assert.NoError(t, err) assert.NoError(t, err)
response, err := service.Introspect(ctx, token) response, err := service.Introspect(ctx, token)
@ -72,7 +72,7 @@ func TestIntrospect(t *testing.T) {
clientID := GetActualClientID(testClients[0].ClientID) clientID := GetActualClientID(testClients[0].ClientID)
// Store minimal token data with expiresIn parameter // Store minimal token data with expiresIn parameter
err := service.storeAccessToken(token, clientID, "", "", 3600) err := service.storeAccessToken(token, clientID, "", "", 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
response, err := service.Introspect(ctx, token) response, err := service.Introspect(ctx, token)
@ -91,7 +91,7 @@ func TestIntrospect(t *testing.T) {
scope := "openid profile" scope := "openid profile"
// Store token with expiration based on config // Store token with expiration based on config
err := service.storeAccessToken(token, clientID, scope, "", 3600) err := service.storeAccessToken(token, clientID, scope, "", 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
response, err := service.Introspect(ctx, token) response, err := service.Introspect(ctx, token)
@ -119,7 +119,7 @@ func TestTokenExchange(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store subject token with expiresIn parameter // Store subject token with expiresIn parameter
err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600) err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
// Test token exchange // Test token exchange
@ -174,7 +174,7 @@ func TestTokenExchange(t *testing.T) {
// Store expired token with negative expiresIn // Store expired token with negative expiresIn
expiresIn := -3600 // Expired 1 hour ago expiresIn := -3600 // Expired 1 hour ago
err := service.storeAccessToken(subjectToken, clientID, scope, subject, expiresIn) err := service.storeAccessToken(subjectToken, clientID, scope, subject, expiresIn, nil)
assert.NoError(t, err) assert.NoError(t, err)
response, err := service.TokenExchange(ctx, subjectToken, "urn:ietf:params:oauth:token-type:access_token", "https://api.example.com", "openid profile") response, err := service.TokenExchange(ctx, subjectToken, "urn:ietf:params:oauth:token-type:access_token", "https://api.example.com", "openid profile")
@ -194,7 +194,7 @@ func TestTokenExchange(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store subject token with expiresIn parameter // Store subject token with expiresIn parameter
err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600) err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
// Test with valid audience (should succeed since audience validation is not enforced) // Test with valid audience (should succeed since audience validation is not enforced)
@ -212,7 +212,7 @@ func TestTokenExchange(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store subject token with expiresIn parameter // Store subject token with expiresIn parameter
err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600) err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
// Test with empty audience // Test with empty audience
@ -230,7 +230,7 @@ func TestTokenExchange(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store subject token with expiresIn parameter // Store subject token with expiresIn parameter
err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600) err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
// Test with invalid scope (should succeed since scope validation is basic) // Test with invalid scope (should succeed since scope validation is basic)
@ -247,7 +247,7 @@ func TestTokenExchange(t *testing.T) {
// Store expired subject token with negative expiresIn // Store expired subject token with negative expiresIn
expiresIn := -3600 // Expired 1 hour ago expiresIn := -3600 // Expired 1 hour ago
err := service.storeAccessToken(subjectToken, clientID, scope, subject, expiresIn) err := service.storeAccessToken(subjectToken, clientID, scope, subject, expiresIn, nil)
assert.NoError(t, err) assert.NoError(t, err)
response, err := service.TokenExchange(ctx, subjectToken, "urn:ietf:params:oauth:token-type:access_token", "https://api.example.com", "openid profile") response, err := service.TokenExchange(ctx, subjectToken, "urn:ietf:params:oauth:token-type:access_token", "https://api.example.com", "openid profile")
@ -266,7 +266,7 @@ func TestTokenExchange(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store subject token with expiresIn parameter // Store subject token with expiresIn parameter
err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600) err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
// Test without audience and scope // Test without audience and scope
@ -299,7 +299,7 @@ func TestValidateTokenAudience(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store token with expiresIn parameter // Store token with expiresIn parameter
err := service.storeAccessToken(token, clientID, scope, subject, 3600) err := service.storeAccessToken(token, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
result, err := service.ValidateTokenAudience(ctx, token, expectedAudience) result, err := service.ValidateTokenAudience(ctx, token, expectedAudience)
@ -317,7 +317,7 @@ func TestValidateTokenAudience(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store token with expiresIn parameter // Store token with expiresIn parameter
err := service.storeAccessToken(token, clientID, scope, subject, 3600) err := service.storeAccessToken(token, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
result, err := service.ValidateTokenAudience(ctx, token, expectedAudience) result, err := service.ValidateTokenAudience(ctx, token, expectedAudience)
@ -335,7 +335,7 @@ func TestValidateTokenAudience(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store token with expiresIn parameter // Store token with expiresIn parameter
err := service.storeAccessToken(token, clientID, scope, subject, 3600) err := service.storeAccessToken(token, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
result, err := service.ValidateTokenAudience(ctx, token, expectedAudience) result, err := service.ValidateTokenAudience(ctx, token, expectedAudience)
@ -354,7 +354,7 @@ func TestValidateTokenAudience(t *testing.T) {
// Store expired token with negative expiresIn // Store expired token with negative expiresIn
expiresIn := -3600 // Expired 1 hour ago expiresIn := -3600 // Expired 1 hour ago
err := service.storeAccessToken(token, clientID, scope, subject, expiresIn) err := service.storeAccessToken(token, clientID, scope, subject, expiresIn, nil)
assert.NoError(t, err) assert.NoError(t, err)
result, err := service.ValidateTokenAudience(ctx, token, expectedAudience) result, err := service.ValidateTokenAudience(ctx, token, expectedAudience)
@ -413,7 +413,7 @@ func TestValidateTokenBinding(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store token with expiresIn parameter // Store token with expiresIn parameter
err := service.storeAccessToken(token, clientID, scope, subject, 3600) err := service.storeAccessToken(token, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
binding := &types.TokenBinding{ binding := &types.TokenBinding{
@ -434,7 +434,7 @@ func TestValidateTokenBinding(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store token with expiresIn parameter // Store token with expiresIn parameter
err := service.storeAccessToken(token, clientID, scope, subject, 3600) err := service.storeAccessToken(token, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
binding := &types.TokenBinding{ binding := &types.TokenBinding{
@ -455,7 +455,7 @@ func TestValidateTokenBinding(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store token with expiresIn parameter // Store token with expiresIn parameter
err := service.storeAccessToken(token, clientID, scope, subject, 3600) err := service.storeAccessToken(token, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
binding := &types.TokenBinding{ binding := &types.TokenBinding{
@ -476,7 +476,7 @@ func TestValidateTokenBinding(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store token with expiresIn parameter // Store token with expiresIn parameter
err := service.storeAccessToken(token, clientID, scope, subject, 3600) err := service.storeAccessToken(token, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
binding := &types.TokenBinding{ binding := &types.TokenBinding{
@ -498,7 +498,7 @@ func TestValidateTokenBinding(t *testing.T) {
// Store expired token with negative expiresIn // Store expired token with negative expiresIn
expiresIn := -3600 // Expired 1 hour ago expiresIn := -3600 // Expired 1 hour ago
err := service.storeAccessToken(token, clientID, scope, subject, expiresIn) err := service.storeAccessToken(token, clientID, scope, subject, expiresIn, nil)
assert.NoError(t, err) assert.NoError(t, err)
binding := &types.TokenBinding{ binding := &types.TokenBinding{
@ -670,7 +670,7 @@ func TestTokenIntegration(t *testing.T) {
// Step 2: Store token data with expiresIn parameter // Step 2: Store token data with expiresIn parameter
scope := "openid profile email" scope := "openid profile email"
subject := testUsers[0].UserID subject := testUsers[0].UserID
err = service.storeAccessToken(accessToken, clientID, scope, subject, 3600) err = service.storeAccessToken(accessToken, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
// Step 3: Introspect token // Step 3: Introspect token
@ -756,7 +756,7 @@ func TestTokenEdgeCases(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store token with expiresIn parameter (it will handle data types correctly) // Store token with expiresIn parameter (it will handle data types correctly)
err := service.storeAccessToken(token, clientID, scope, subject, 3600) err := service.storeAccessToken(token, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
// Should handle gracefully // Should handle gracefully
@ -774,7 +774,7 @@ func TestTokenEdgeCases(t *testing.T) {
subject := testUsers[0].UserID subject := testUsers[0].UserID
// Store subject token with expiresIn parameter // Store subject token with expiresIn parameter
err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600) err := service.storeAccessToken(subjectToken, clientID, scope, subject, 3600, nil)
assert.NoError(t, err) assert.NoError(t, err)
// Very long audience // Very long audience

View file

@ -572,6 +572,13 @@ type TokenClaims struct {
Issuer string `json:"iss,omitempty"` // Token issuer Issuer string `json:"iss,omitempty"` // Token issuer
Audience []string `json:"aud,omitempty"` // Token audience Audience []string `json:"aud,omitempty"` // Token audience
JTI string `json:"jti,omitempty"` // JWT ID (for JWT tokens) JTI string `json:"jti,omitempty"` // JWT ID (for JWT tokens)
// Extended claims for multi-tenancy and team support
TeamID string `json:"team_id,omitempty"` // Team identifier
TenantID string `json:"tenant_id,omitempty"` // Tenant identifier
// Extra claims for flexibility
Extra map[string]interface{} `json:"-"` // Additional custom claims (not serialized directly)
} }
// AuthorizedInfo represents authorized information // AuthorizedInfo represents authorized information
@ -581,6 +588,10 @@ type AuthorizedInfo struct {
Scope string `json:"scope,omitempty"` // Access scope Scope string `json:"scope,omitempty"` // Access scope
SessionID string `json:"session_id,omitempty"` // Session ID SessionID string `json:"session_id,omitempty"` // Session ID
UserID string `json:"user_id,omitempty"` // User ID UserID string `json:"user_id,omitempty"` // User ID
// Extended fields for multi-tenancy and team support
TeamID string `json:"team_id,omitempty"` // Team identifier
TenantID string `json:"tenant_id,omitempty"` // Tenant identifier
} }
// JWTClaims represents JWT-specific claims structure // JWTClaims represents JWT-specific claims structure
@ -589,6 +600,10 @@ type JWTClaims struct {
ClientID string `json:"client_id"` // OAuth client ID ClientID string `json:"client_id"` // OAuth client ID
Scope string `json:"scope,omitempty"` // Access scope Scope string `json:"scope,omitempty"` // Access scope
TokenType string `json:"token_type"` // Token type TokenType string `json:"token_type"` // Token type
// Extended claims for multi-tenancy and team support
TeamID string `json:"team_id,omitempty"` // Team identifier
TenantID string `json:"tenant_id,omitempty"` // Tenant identifier
} }
// ClientConfig represents default client configuration // ClientConfig represents default client configuration