Enhance token generation and storage to support optional extra claims
- Updated methods related to refresh token generation and storage to accept optional extra claims, allowing for additional metadata such as team_id and tenant_id. - Refactored tests to utilize the new method signatures, ensuring comprehensive coverage of the updated functionality. - Introduced a new endpoint for team selection that issues tokens with team-specific claims, improving user experience in multi-team scenarios.
This commit is contained in:
parent
aae3447575
commit
d78ed77b9e
12 changed files with 327 additions and 25 deletions
|
|
@ -347,7 +347,7 @@ func (s *Service) RotateRefreshToken(ctx context.Context, oldToken string, reque
|
|||
}
|
||||
}
|
||||
|
||||
newRefreshToken, err := s.generateRefreshToken(clientID, finalScope, originalSubject)
|
||||
newRefreshToken, err := s.generateRefreshToken(clientID, finalScope, originalSubject, 0, nil)
|
||||
if err != nil {
|
||||
return nil, &types.ErrorResponse{
|
||||
Code: types.ErrorServerError,
|
||||
|
|
@ -444,7 +444,7 @@ func (s *Service) handleAuthorizationCodeGrant(ctx context.Context, client *type
|
|||
|
||||
// Generate refresh token if supported
|
||||
if types.Contains(client.GrantTypes, types.GrantTypeRefreshToken) {
|
||||
refreshToken, err := s.generateRefreshToken(client.ClientID, scope, subject)
|
||||
refreshToken, err := s.generateRefreshToken(client.ClientID, scope, subject, 0, nil)
|
||||
if err != nil {
|
||||
return nil, &types.ErrorResponse{
|
||||
Code: types.ErrorServerError,
|
||||
|
|
@ -523,7 +523,7 @@ func (s *Service) handleRefreshTokenGrant(ctx context.Context, client *types.Cli
|
|||
|
||||
// Include refresh token if rotation is enabled
|
||||
if s.config.Features.RefreshTokenRotationEnabled {
|
||||
newRefreshToken, err := s.generateRefreshToken(client.ClientID, scope, subject)
|
||||
newRefreshToken, err := s.generateRefreshToken(client.ClientID, scope, subject, 0, nil)
|
||||
if err != nil {
|
||||
return nil, &types.ErrorResponse{
|
||||
Code: types.ErrorServerError,
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@ func TestRefreshToken(t *testing.T) {
|
|||
subject := testUsers[0].UserID
|
||||
|
||||
// Store refresh token with scope using storeRefreshTokenWithScope
|
||||
err := service.storeRefreshTokenWithScope(refreshToken, clientID, originalScope, subject)
|
||||
err := service.storeRefreshTokenWithScope(refreshToken, clientID, originalScope, subject, 0, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
response, err := service.RefreshToken(ctx, refreshToken, "openid profile")
|
||||
|
|
@ -391,7 +391,7 @@ func TestRefreshToken(t *testing.T) {
|
|||
subject := testUsers[0].UserID
|
||||
|
||||
// Store refresh token with scope using storeRefreshTokenWithScope
|
||||
err := service.storeRefreshTokenWithScope(refreshToken, clientID, originalScope, subject)
|
||||
err := service.storeRefreshTokenWithScope(refreshToken, clientID, originalScope, subject, 0, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Ensure rotation is enabled
|
||||
|
|
@ -443,7 +443,7 @@ func TestRefreshToken(t *testing.T) {
|
|||
subject := testUsers[0].UserID
|
||||
|
||||
// Store refresh token with limited scope
|
||||
err := service.storeRefreshTokenWithScope(refreshToken, clientID, originalScope, subject)
|
||||
err := service.storeRefreshTokenWithScope(refreshToken, clientID, originalScope, subject, 0, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try to request scope that exceeds the original scope
|
||||
|
|
@ -490,7 +490,7 @@ func TestRotateRefreshToken(t *testing.T) {
|
|||
subject := testUsers[0].UserID
|
||||
|
||||
// Store old refresh token with scope using storeRefreshTokenWithScope
|
||||
err := service.storeRefreshTokenWithScope(oldToken, clientID, originalScope, subject)
|
||||
err := service.storeRefreshTokenWithScope(oldToken, clientID, originalScope, subject, 0, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Ensure rotation is enabled
|
||||
|
|
@ -651,7 +651,7 @@ func TestHandleRefreshTokenGrant(t *testing.T) {
|
|||
subject := testUsers[0].UserID
|
||||
|
||||
// Store refresh token with scope using storeRefreshTokenWithScope
|
||||
err := service.storeRefreshTokenWithScope(refreshToken, client.ClientID, originalScope, subject)
|
||||
err := service.storeRefreshTokenWithScope(refreshToken, client.ClientID, originalScope, subject, 0, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Ensure rotation is enabled
|
||||
|
|
@ -686,7 +686,7 @@ func TestHandleRefreshTokenGrant(t *testing.T) {
|
|||
subject := testUsers[0].UserID
|
||||
|
||||
// Store refresh token with scope using storeRefreshTokenWithScope
|
||||
err := service.storeRefreshTokenWithScope(refreshToken, client.ClientID, originalScope, subject)
|
||||
err := service.storeRefreshTokenWithScope(refreshToken, client.ClientID, originalScope, subject, 0, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
token, err := service.handleRefreshTokenGrant(ctx, client, refreshToken)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@ func GetAuthorizedInfo(c *gin.Context) *types.AuthorizedInfo {
|
|||
info.Scope = scope.(string)
|
||||
}
|
||||
|
||||
if teamID, ok := c.Get("__team_id"); ok {
|
||||
info.TeamID = teamID.(string)
|
||||
}
|
||||
|
||||
if tenantID, ok := c.Get("__tenant_id"); ok {
|
||||
info.TenantID = tenantID.(string)
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +88,14 @@ func (s *Service) setAuthorizedInfo(c *gin.Context, claims *types.TokenClaims) {
|
|||
c.Set("__subject", claims.Subject)
|
||||
c.Set("__scope", claims.Scope)
|
||||
c.Set("__client_id", claims.ClientID)
|
||||
|
||||
// Set team_id and tenant_id in context if available
|
||||
if claims.TeamID != "" {
|
||||
c.Set("__team_id", claims.TeamID)
|
||||
}
|
||||
if claims.TenantID != "" {
|
||||
c.Set("__tenant_id", claims.TenantID)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) tryAutoRefreshToken(c *gin.Context, _ *types.TokenClaims) {
|
||||
|
|
@ -113,6 +129,11 @@ func (s *Service) getAccessToken(c *gin.Context) string {
|
|||
return strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
|
||||
// GetAccessToken gets the access token from the request (public method)
|
||||
func (s *Service) GetAccessToken(c *gin.Context) string {
|
||||
return s.getAccessToken(c)
|
||||
}
|
||||
|
||||
func (s *Service) getRefreshToken(c *gin.Context) string {
|
||||
token := c.GetHeader("Authorization")
|
||||
if token == "" {
|
||||
|
|
|
|||
|
|
@ -316,6 +316,56 @@ func (u *DefaultUser) GetTeamsByMember(ctx context.Context, memberID string) ([]
|
|||
return teams, nil
|
||||
}
|
||||
|
||||
// GetTeamByMember retrieves a specific team by team_id and member_id, verifying membership
|
||||
// Returns the team with role information if the user is a member, or error if not
|
||||
func (u *DefaultUser) GetTeamByMember(ctx context.Context, teamID string, memberID string) (maps.MapStrAny, error) {
|
||||
// First, verify the user is a member of this team
|
||||
memberParam := model.QueryParam{
|
||||
Select: []interface{}{"team_id", "user_id", "member_type", "role_id"},
|
||||
Wheres: []model.QueryWhere{
|
||||
{Column: "team_id", Value: teamID},
|
||||
{Column: "user_id", Value: memberID},
|
||||
{Column: "member_type", Value: "user"},
|
||||
{Column: "status", Value: "active"},
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
memberModel := model.Select(u.memberModel)
|
||||
members, err := memberModel.Get(memberParam)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify team membership: %w", err)
|
||||
}
|
||||
|
||||
if len(members) == 0 {
|
||||
return nil, fmt.Errorf("user is not a member of the team")
|
||||
}
|
||||
|
||||
// Get role_id from member record
|
||||
roleID := ""
|
||||
if role, ok := members[0]["role_id"]; ok && role != nil {
|
||||
roleID = fmt.Sprintf("%v", role)
|
||||
}
|
||||
|
||||
// Get team details
|
||||
teamData, err := u.GetTeamDetail(ctx, teamID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get team details: %w", err)
|
||||
}
|
||||
|
||||
// Add role_id to team data
|
||||
teamData["role_id"] = roleID
|
||||
|
||||
// Check if user is the owner
|
||||
ownerID := ""
|
||||
if owner, ok := teamData["owner_id"]; ok && owner != nil {
|
||||
ownerID = fmt.Sprintf("%v", owner)
|
||||
}
|
||||
teamData["is_owner"] = (ownerID == memberID)
|
||||
|
||||
return teamData, nil
|
||||
}
|
||||
|
||||
// CountTeamsByMember returns total count of teams by member_id
|
||||
func (u *DefaultUser) CountTeamsByMember(ctx context.Context, memberID string) (int64, error) {
|
||||
|
||||
|
|
|
|||
|
|
@ -464,7 +464,7 @@ func (s *Service) VerifyToken(token string) (*types.TokenClaims, error) {
|
|||
}
|
||||
|
||||
// SignIDToken signs an ID token with specific parameters and stores it
|
||||
func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *types.OIDCUserInfo) (string, error) {
|
||||
func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *types.OIDCUserInfo, extraClaims ...map[string]interface{}) (string, error) {
|
||||
if s.signingCerts == nil || s.signingCerts.SigningKey == nil {
|
||||
return "", fmt.Errorf("signing certificates not initialized")
|
||||
}
|
||||
|
|
@ -499,6 +499,13 @@ func (s *Service) SignIDToken(clientID, scope string, expiresIn int, userdata *t
|
|||
"jti": generateJTI(),
|
||||
}
|
||||
|
||||
// Add extra claims if provided (e.g., team_id, tenant_id)
|
||||
if len(extraClaims) > 0 {
|
||||
for key, value := range extraClaims[0] {
|
||||
claims[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Add user information from userdata
|
||||
// Add standard OIDC user claims if they exist
|
||||
if userdata.Name != "" {
|
||||
|
|
|
|||
|
|
@ -258,8 +258,12 @@ func (s *Service) MakeAccessToken(clientID, scope, subject string, expiresIn int
|
|||
}
|
||||
|
||||
// MakeRefreshToken generates a new refresh token with specific parameters and stores it
|
||||
func (s *Service) MakeRefreshToken(clientID, scope, subject string, expiresIn ...int) (string, error) {
|
||||
return s.generateRefreshToken(clientID, scope, subject, expiresIn...)
|
||||
func (s *Service) MakeRefreshToken(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.generateRefreshToken(clientID, scope, subject, expiresIn, claims)
|
||||
}
|
||||
|
||||
// Subject converts a userID to a subject using NanoID fingerprint
|
||||
|
|
@ -433,14 +437,14 @@ func (s *Service) revokeAccessToken(accessToken string) error {
|
|||
}
|
||||
|
||||
// generateRefreshToken generates and stores a new refresh token with scope and subject
|
||||
func (s *Service) generateRefreshToken(clientID, scope, subject string, expiresIn ...int) (string, error) {
|
||||
func (s *Service) generateRefreshToken(clientID, scope, subject string, expiresIn int, extraClaims map[string]interface{}) (string, error) {
|
||||
refreshToken, err := s.generateToken("rfk", clientID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Store refresh token with metadata
|
||||
err = s.storeRefreshTokenWithScope(refreshToken, clientID, scope, subject, expiresIn...)
|
||||
err = s.storeRefreshTokenWithScope(refreshToken, clientID, scope, subject, expiresIn, extraClaims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -547,7 +551,7 @@ func (s *Service) storeRefreshToken(refreshToken, clientID string) error {
|
|||
}
|
||||
|
||||
// storeRefreshTokenWithScope stores refresh token with metadata including scope and subject
|
||||
func (s *Service) storeRefreshTokenWithScope(refreshToken, clientID, scope, subject string, expiresIn ...int) error {
|
||||
func (s *Service) storeRefreshTokenWithScope(refreshToken, clientID, scope, subject string, expiresIn int, extraClaims map[string]interface{}) error {
|
||||
tokenData := map[string]interface{}{
|
||||
"client_id": clientID,
|
||||
"scope": scope,
|
||||
|
|
@ -556,9 +560,14 @@ func (s *Service) storeRefreshTokenWithScope(refreshToken, clientID, scope, subj
|
|||
"issued_at": time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Add extra claims if provided (e.g., team_id, tenant_id)
|
||||
for key, value := range extraClaims {
|
||||
tokenData[key] = value
|
||||
}
|
||||
|
||||
expires := s.config.Token.RefreshTokenLifetime
|
||||
if len(expiresIn) > 0 && expiresIn[0] > 0 {
|
||||
expires = time.Duration(expiresIn[0]) * time.Second
|
||||
if expiresIn > 0 {
|
||||
expires = time.Duration(expiresIn) * time.Second
|
||||
}
|
||||
|
||||
return s.store.Set(s.refreshTokenKey(refreshToken), tokenData, expires)
|
||||
|
|
|
|||
|
|
@ -573,7 +573,7 @@ func TestTokenGeneration(t *testing.T) {
|
|||
|
||||
t.Run("generate refresh token", func(t *testing.T) {
|
||||
// Updated to use new generateRefreshToken signature with scope and subject
|
||||
token, err := service.generateRefreshToken(clientID, "openid profile", testUsers[0].UserID)
|
||||
token, err := service.generateRefreshToken(clientID, "openid profile", testUsers[0].UserID, 0, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
assert.True(t, strings.HasPrefix(token, "rfk_"))
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ type UserProvider interface {
|
|||
// Team Query Methods
|
||||
GetTeamsByOwner(ctx context.Context, ownerID string) ([]maps.MapStr, error)
|
||||
GetTeamsByMember(ctx context.Context, memberID string) ([]maps.MapStr, error)
|
||||
GetTeamByMember(ctx context.Context, teamID string, memberID string) (maps.MapStrAny, error)
|
||||
GetTeamsByStatus(ctx context.Context, status string) ([]maps.MapStr, error)
|
||||
CountTeamsByMember(ctx context.Context, memberID string) (int64, error)
|
||||
|
||||
|
|
|
|||
|
|
@ -265,15 +265,17 @@ func generateSessionID() string {
|
|||
}
|
||||
|
||||
// SendLoginCookies sends all necessary cookies for a successful login
|
||||
// This includes access token, refresh token, and session ID cookies with appropriate security settings
|
||||
// This includes access token, refresh token, and optionally session ID cookies with appropriate security settings
|
||||
func SendLoginCookies(c *gin.Context, loginResponse *LoginResponse, sessionID string) {
|
||||
|
||||
// Send session ID cookie
|
||||
expires := time.Now().Add(time.Duration(yaoClientConfig.ExpiresIn) * time.Second)
|
||||
options := response.NewSecureCookieOptions().
|
||||
WithExpires(expires).
|
||||
WithSameSite("Strict")
|
||||
response.SendSecureCookieWithOptions(c, "session_id", sessionID, options)
|
||||
// Send session ID cookie only if sessionID is provided
|
||||
if sessionID != "" {
|
||||
expires := time.Now().Add(time.Duration(yaoClientConfig.ExpiresIn) * time.Second)
|
||||
options := response.NewSecureCookieOptions().
|
||||
WithExpires(expires).
|
||||
WithSameSite("Strict")
|
||||
response.SendSecureCookieWithOptions(c, "session_id", sessionID, options)
|
||||
}
|
||||
|
||||
// MFA Temporary Access Token
|
||||
if loginResponse.Status == LoginStatusMFA {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/yaoapp/kun/maps"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
||||
oauthtypes "github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
|
|
@ -308,6 +309,209 @@ func GinTeamUpdate(c *gin.Context) {
|
|||
response.RespondWithSuccess(c, http.StatusOK, team)
|
||||
}
|
||||
|
||||
// GinTeamSelection handles POST /teams/select - Select a team and issue tokens with team_id
|
||||
func GinTeamSelection(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
authInfo := oauth.GetAuthorizedInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidClient.Code,
|
||||
ErrorDescription: "User not authenticated",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusUnauthorized, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the current token has team_selection scope
|
||||
if authInfo.Scope != ScopeTeamSelection {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrAccessDenied.Code,
|
||||
ErrorDescription: "Invalid scope: team_selection scope required",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req TeamSelectionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Invalid request body: " + err.Error(),
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// Handle personal account selection (no team_id or "personal")
|
||||
var extraClaims map[string]interface{}
|
||||
var selectedTeam map[string]interface{}
|
||||
|
||||
if req.TeamID == "" || req.TeamID == "personal" {
|
||||
// Personal account - no team_id in token
|
||||
extraClaims = nil
|
||||
} else {
|
||||
// Team account - verify membership and add team_id to token
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
log.Error("Failed to get user provider: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to initialize user provider",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify user is a member of the team and get team details in one call
|
||||
selectedTeam, err = provider.GetTeamByMember(ctx, req.TeamID, authInfo.UserID)
|
||||
if err != nil {
|
||||
log.Error("Failed to verify team membership: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrAccessDenied.Code,
|
||||
ErrorDescription: "Access denied: you are not a member of this team",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare extra claims with team_id and tenant_id (if available)
|
||||
extraClaims = map[string]interface{}{
|
||||
"team_id": req.TeamID,
|
||||
}
|
||||
|
||||
// Add tenant_id if available from the team
|
||||
if tenantID := toString(selectedTeam["tenant_id"]); tenantID != "" {
|
||||
extraClaims["tenant_id"] = tenantID
|
||||
}
|
||||
}
|
||||
|
||||
// Get user provider and user data
|
||||
provider, err := getUserProvider()
|
||||
if err != nil {
|
||||
log.Error("Failed to get user provider: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to initialize user provider",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user data with scopes
|
||||
user, err := provider.GetUserWithScopes(ctx, authInfo.UserID)
|
||||
if err != nil {
|
||||
log.Error("Failed to get user: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to retrieve user information",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Get Yao client config
|
||||
yaoClientConfig := GetYaoClientConfig()
|
||||
var scopes []string = yaoClientConfig.Scopes
|
||||
if v, ok := user["scopes"].([]string); ok {
|
||||
scopes = v
|
||||
}
|
||||
|
||||
// Get subject from auth info (it should be already stored)
|
||||
subject := authInfo.Subject
|
||||
if subject == "" {
|
||||
// Fallback: create new subject if not available
|
||||
subject, err = oauth.OAuth.Subject(yaoClientConfig.ClientID, authInfo.UserID)
|
||||
if err != nil {
|
||||
log.Warn("Failed to get subject: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Sign OIDC Token (with or without team_id based on extraClaims)
|
||||
oidcUserInfo := oauthtypes.MakeOIDCUserInfo(user)
|
||||
oidcUserInfo.Sub = subject
|
||||
var oidcToken string
|
||||
if extraClaims != nil {
|
||||
oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), yaoClientConfig.ExpiresIn, oidcUserInfo, extraClaims)
|
||||
} else {
|
||||
oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), yaoClientConfig.ExpiresIn, oidcUserInfo)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Failed to sign OIDC token: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to sign OIDC token",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Sign Access Token (with or without team_id based on extraClaims)
|
||||
var accessToken string
|
||||
if extraClaims != nil {
|
||||
accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.ExpiresIn, extraClaims)
|
||||
} else {
|
||||
accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.ExpiresIn)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Failed to sign access token: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to sign access token",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Sign Refresh Token (with or without team_id based on extraClaims)
|
||||
var refreshToken string
|
||||
if extraClaims != nil {
|
||||
refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.RefreshTokenExpiresIn, extraClaims)
|
||||
} else {
|
||||
refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.RefreshTokenExpiresIn)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Failed to sign refresh token: %v", err)
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrServerError.Code,
|
||||
ErrorDescription: "Failed to sign refresh token",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Revoke the current temporary token (read from header)
|
||||
currentToken := oauth.OAuth.GetAccessToken(c)
|
||||
if currentToken != "" {
|
||||
if err := oauth.OAuth.Revoke(ctx, currentToken, "access_token"); err != nil {
|
||||
// Log the error but don't fail the request
|
||||
log.Warn("Failed to revoke temporary token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare login response
|
||||
loginResponse := &LoginResponse{
|
||||
UserID: authInfo.UserID,
|
||||
Subject: subject,
|
||||
AccessToken: accessToken,
|
||||
IDToken: oidcToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: yaoClientConfig.ExpiresIn,
|
||||
RefreshTokenExpiresIn: yaoClientConfig.RefreshTokenExpiresIn,
|
||||
TokenType: "Bearer",
|
||||
Scope: strings.Join(scopes, " "),
|
||||
Status: LoginStatusSuccess,
|
||||
}
|
||||
|
||||
// Send secure cookies (access token, refresh token, and session ID)
|
||||
SendLoginCookies(c, loginResponse, "")
|
||||
|
||||
// Return the new tokens in response body
|
||||
response.RespondWithSuccess(c, http.StatusOK, loginResponse)
|
||||
}
|
||||
|
||||
// GinTeamDelete handles DELETE /teams/:id - Delete user team
|
||||
func GinTeamDelete(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
|
|
|
|||
|
|
@ -282,6 +282,11 @@ type UpdateTeamRequest struct {
|
|||
Settings *TeamSettings `json:"settings,omitempty"`
|
||||
}
|
||||
|
||||
// TeamSelectionRequest represents the request to select a team
|
||||
type TeamSelectionRequest struct {
|
||||
TeamID string `json:"team_id" binding:"required"`
|
||||
}
|
||||
|
||||
// ==== Member API Types ====
|
||||
|
||||
// MemberResponse represents a team member in API responses
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ func attachTeam(group *gin.RouterGroup, oauth types.OAuth) {
|
|||
// Team Configuration
|
||||
team.GET("/config", GinTeamConfig) // Get team configuration (requires authentication)
|
||||
|
||||
// Team Selection
|
||||
team.POST("/select", GinTeamSelection) // POST /teams/select - Select a team and issue tokens with team_id (requires authentication)
|
||||
|
||||
// Team CRUD - Standard REST endpoints
|
||||
team.GET("/", GinTeamList) // GET /teams - List user teams
|
||||
team.POST("/", GinTeamCreate) // POST /teams - Create new team
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue