Merge pull request #1188 from trheyi/main

Refactor token handling to support optional extra claims in access to…
This commit is contained in:
Max 2025-10-11 19:00:30 +08:00 committed by GitHub
commit a13061a415
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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
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 {
return nil, &types.ErrorResponse{
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
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 {
return nil, &types.ErrorResponse{
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
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 {
return nil, &types.ErrorResponse{
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)
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 {
return nil, &types.ErrorResponse{
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
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 {
return nil, &types.ErrorResponse{
Code: types.ErrorServerError,

View file

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

View file

@ -128,14 +128,14 @@ var (
// DefaultTeamFields contains basic team fields
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",
"created_at", "updated_at",
}
// DefaultTeamDetailFields contains all team fields including contact info and metadata
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",
"team_code", "team_code_type", "status", "type_id", "type", "address", "street_address",
"city", "state_province", "postal_code", "country", "country_name", "region", "zoneinfo",
@ -144,14 +144,14 @@ var (
// DefaultMemberFields contains basic member fields
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",
"last_active_at", "login_count", "message", "created_at", "updated_at",
}
// DefaultMemberDetailFields contains all member fields including robot config and permissions
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",
"mcp_servers", "data_access_permissions", "system_prompt", "is_active_robot",
"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)
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 {
case "jwt":
return s.signJWTToken(tokenType, clientID, scope, subject, expiresIn)
return s.signJWTToken(tokenType, clientID, scope, subject, expiresIn, claims)
case "opaque":
return s.signOpaqueToken(tokenType, clientID, scope, subject)
default:
// 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
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 {
return "", fmt.Errorf("signing certificates not initialized")
}
now := time.Now()
claims := &types.JWTClaims{
StandardClaims: jwt.StandardClaims{
Issuer: s.config.IssuerURL,
Subject: subject,
Audience: clientID,
ExpiresAt: now.Add(time.Duration(expiresIn) * time.Second).Unix(),
NotBefore: now.Unix(),
IssuedAt: now.Unix(),
Id: generateJTI(),
},
ClientID: clientID,
Scope: scope,
TokenType: tokenType,
// Use MapClaims to support extra claims
claims := jwt.MapClaims{
// Standard JWT claims
"iss": s.config.IssuerURL,
"sub": subject,
"aud": clientID,
"exp": now.Add(time.Duration(expiresIn) * time.Second).Unix(),
"nbf": now.Unix(),
"iat": now.Unix(),
"jti": generateJTI(),
// Custom OAuth claims
"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
@ -614,8 +627,8 @@ func (s *Service) verifyJWTToken(tokenString string) (*types.TokenClaims, error)
return nil, fmt.Errorf("signing certificates not initialized")
}
// Parse token with claims
token, err := jwt.ParseWithClaims(tokenString, &types.JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
// Parse token with MapClaims to support extra claims
token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
// Validate signing method
expectedMethod := getSigningMethod(s.config.Token.AccessTokenSigningAlg)
if token.Method != expectedMethod {
@ -635,22 +648,75 @@ func (s *Service) verifyJWTToken(tokenString string) (*types.TokenClaims, error)
}
// Extract claims
jwtClaims, ok := token.Claims.(*types.JWTClaims)
mapClaims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid JWT claims type")
}
// Convert to TokenClaims
tokenClaims := &types.TokenClaims{
Subject: jwtClaims.Subject,
ClientID: jwtClaims.ClientID,
Scope: jwtClaims.Scope,
TokenType: jwtClaims.TokenType,
ExpiresAt: time.Unix(jwtClaims.ExpiresAt, 0),
IssuedAt: time.Unix(jwtClaims.IssuedAt, 0),
Issuer: jwtClaims.Issuer,
Audience: []string{jwtClaims.Audience},
JTI: jwtClaims.Id,
Extra: make(map[string]interface{}),
}
// Extract standard claims
if sub, ok := mapClaims["sub"].(string); ok {
tokenClaims.Subject = sub
}
if clientID, ok := mapClaims["client_id"].(string); ok {
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

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
func (s *Service) MakeAccessToken(clientID, scope, subject string, expiresIn int) (string, error) {
return s.generateAccessTokenWithScope(clientID, scope, subject, expiresIn)
// extraClaims: optional extra claims to add to the token (e.g., team_id, tenant_id)
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
@ -338,19 +343,27 @@ func (s *Service) validateAudience(audience string) error {
// generateAccessToken generates a new access token
func (s *Service) generateAccessToken(clientID string) (string, error) {
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
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
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 {
return "", err
}
// Store access token with metadata
err = s.storeAccessToken(accessToken, clientID, scope, subject, expiresIn)
// Store access token with metadata (including extra claims)
err = s.storeAccessToken(accessToken, clientID, scope, subject, expiresIn, extraClaims)
if err != nil {
return "", err
}
@ -359,7 +372,7 @@ func (s *Service) generateAccessTokenWithScope(clientID, scope, subject string,
}
// 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()
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,
}
// 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
return s.store.Set(s.accessTokenKey(accessToken), tokenData, ttl)
}

View file

@ -26,7 +26,7 @@ func TestIntrospect(t *testing.T) {
subject := testUsers[0].UserID
// 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)
response, err := service.Introspect(ctx, token)
@ -49,7 +49,7 @@ func TestIntrospect(t *testing.T) {
// Store expired token with negative expiresIn (already expired)
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)
response, err := service.Introspect(ctx, token)
@ -72,7 +72,7 @@ func TestIntrospect(t *testing.T) {
clientID := GetActualClientID(testClients[0].ClientID)
// Store minimal token data with expiresIn parameter
err := service.storeAccessToken(token, clientID, "", "", 3600)
err := service.storeAccessToken(token, clientID, "", "", 3600, nil)
assert.NoError(t, err)
response, err := service.Introspect(ctx, token)
@ -91,7 +91,7 @@ func TestIntrospect(t *testing.T) {
scope := "openid profile"
// 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)
response, err := service.Introspect(ctx, token)
@ -119,7 +119,7 @@ func TestTokenExchange(t *testing.T) {
subject := testUsers[0].UserID
// 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)
// Test token exchange
@ -174,7 +174,7 @@ func TestTokenExchange(t *testing.T) {
// Store expired token with negative expiresIn
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)
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
// 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)
// 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
// 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)
// Test with empty audience
@ -230,7 +230,7 @@ func TestTokenExchange(t *testing.T) {
subject := testUsers[0].UserID
// 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)
// 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
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)
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
// 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)
// Test without audience and scope
@ -299,7 +299,7 @@ func TestValidateTokenAudience(t *testing.T) {
subject := testUsers[0].UserID
// 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)
result, err := service.ValidateTokenAudience(ctx, token, expectedAudience)
@ -317,7 +317,7 @@ func TestValidateTokenAudience(t *testing.T) {
subject := testUsers[0].UserID
// 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)
result, err := service.ValidateTokenAudience(ctx, token, expectedAudience)
@ -335,7 +335,7 @@ func TestValidateTokenAudience(t *testing.T) {
subject := testUsers[0].UserID
// 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)
result, err := service.ValidateTokenAudience(ctx, token, expectedAudience)
@ -354,7 +354,7 @@ func TestValidateTokenAudience(t *testing.T) {
// Store expired token with negative expiresIn
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)
result, err := service.ValidateTokenAudience(ctx, token, expectedAudience)
@ -413,7 +413,7 @@ func TestValidateTokenBinding(t *testing.T) {
subject := testUsers[0].UserID
// 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)
binding := &types.TokenBinding{
@ -434,7 +434,7 @@ func TestValidateTokenBinding(t *testing.T) {
subject := testUsers[0].UserID
// 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)
binding := &types.TokenBinding{
@ -455,7 +455,7 @@ func TestValidateTokenBinding(t *testing.T) {
subject := testUsers[0].UserID
// 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)
binding := &types.TokenBinding{
@ -476,7 +476,7 @@ func TestValidateTokenBinding(t *testing.T) {
subject := testUsers[0].UserID
// 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)
binding := &types.TokenBinding{
@ -498,7 +498,7 @@ func TestValidateTokenBinding(t *testing.T) {
// Store expired token with negative expiresIn
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)
binding := &types.TokenBinding{
@ -670,7 +670,7 @@ func TestTokenIntegration(t *testing.T) {
// Step 2: Store token data with expiresIn parameter
scope := "openid profile email"
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)
// Step 3: Introspect token
@ -756,7 +756,7 @@ func TestTokenEdgeCases(t *testing.T) {
subject := testUsers[0].UserID
// 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)
// Should handle gracefully
@ -774,7 +774,7 @@ func TestTokenEdgeCases(t *testing.T) {
subject := testUsers[0].UserID
// 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)
// Very long audience

View file

@ -572,6 +572,13 @@ type TokenClaims struct {
Issuer string `json:"iss,omitempty"` // Token issuer
Audience []string `json:"aud,omitempty"` // Token audience
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
@ -581,6 +588,10 @@ type AuthorizedInfo struct {
Scope string `json:"scope,omitempty"` // Access scope
SessionID string `json:"session_id,omitempty"` // Session 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
@ -589,6 +600,10 @@ type JWTClaims struct {
ClientID string `json:"client_id"` // OAuth client ID
Scope string `json:"scope,omitempty"` // Access scope
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