Merge pull request #1188 from trheyi/main
Refactor token handling to support optional extra claims in access to…
This commit is contained in:
commit
a13061a415
7 changed files with 169 additions and 70 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue