Enhance user authentication with Remember Me functionality
- Added Remember Me flag to LoginContext and AuthorizedInfo structures to support extended session management. - Updated GetAuthorizedInfo function to retrieve Remember Me state from the context. - Modified token issuance logic to accommodate Remember Me settings, adjusting token expiration durations accordingly. - Preserved Remember Me state during login and team selection processes, improving user experience and session persistence.
This commit is contained in:
parent
9a2a636314
commit
5d1b665457
6 changed files with 154 additions and 28 deletions
|
|
@ -66,6 +66,12 @@ func GetAuthorizedInfo(c *gin.Context) *types.AuthorizedInfo {
|
|||
info.TenantID = tenantID.(string)
|
||||
}
|
||||
|
||||
if rememberMe, ok := c.Get("__remember_me"); ok {
|
||||
if rmBool, ok := rememberMe.(bool); ok {
|
||||
info.RememberMe = rmBool
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ import (
|
|||
|
||||
// LoginContext represents the context information for login
|
||||
type LoginContext struct {
|
||||
IP string `json:"ip,omitempty"` // Client IP address
|
||||
UserAgent string `json:"user_agent,omitempty"` // Client user agent
|
||||
Device string `json:"device,omitempty"` // Device type (e.g., "mobile", "desktop", "tablet")
|
||||
Platform string `json:"platform,omitempty"` // Platform (e.g., "ios", "android", "web")
|
||||
Location string `json:"location,omitempty"` // Geographic location (optional)
|
||||
IP string `json:"ip,omitempty"` // Client IP address
|
||||
UserAgent string `json:"user_agent,omitempty"` // Client user agent
|
||||
Device string `json:"device,omitempty"` // Device type (e.g., "mobile", "desktop", "tablet")
|
||||
Platform string `json:"platform,omitempty"` // Platform (e.g., "ios", "android", "web")
|
||||
Location string `json:"location,omitempty"` // Geographic location (optional)
|
||||
RememberMe bool `json:"remember_me,omitempty"` // Remember Me flag for extended session
|
||||
}
|
||||
|
||||
// MFAOptions contains configuration for MFA operations
|
||||
|
|
@ -599,8 +600,9 @@ type AuthorizedInfo struct {
|
|||
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
|
||||
TeamID string `json:"team_id,omitempty"` // Team identifier
|
||||
TenantID string `json:"tenant_id,omitempty"` // Tenant identifier
|
||||
RememberMe bool `json:"remember_me,omitempty"` // Remember Me flag preserved from login
|
||||
}
|
||||
|
||||
// JWTClaims represents JWT-specific claims structure
|
||||
|
|
|
|||
|
|
@ -466,8 +466,10 @@ func createPublicEntryConfig(config *EntryConfig) *EntryConfig {
|
|||
// Deep copy Token config
|
||||
if config.Token != nil {
|
||||
publicConfig.Token = &TokenConfig{
|
||||
ExpiresIn: config.Token.ExpiresIn,
|
||||
RememberMeExpiresIn: config.Token.RememberMeExpiresIn,
|
||||
ExpiresIn: config.Token.ExpiresIn,
|
||||
RefreshTokenExpiresIn: config.Token.RefreshTokenExpiresIn,
|
||||
RememberMeExpiresIn: config.Token.RememberMeExpiresIn,
|
||||
RememberMeRefreshTokenExpiresIn: config.Token.RememberMeRefreshTokenExpiresIn,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1015,6 +1017,7 @@ func GinEntryLogin(c *gin.Context) {
|
|||
|
||||
// Login using LoginByUserID (all status checks are handled inside)
|
||||
loginCtx := makeLoginContext(c)
|
||||
loginCtx.RememberMe = req.RememberMe // Set Remember Me from request
|
||||
loginResponse, err := LoginByUserID(userID, loginCtx)
|
||||
if err != nil {
|
||||
log.Error("Failed to login user %s: %v", userID, err)
|
||||
|
|
@ -1142,6 +1145,9 @@ func GinVerifyInvite(c *gin.Context) {
|
|||
// Generate login context
|
||||
loginCtx := makeLoginContext(c)
|
||||
|
||||
// Preserve Remember Me state from temporary token (authInfo is already available from above)
|
||||
loginCtx.RememberMe = authInfo.RememberMe
|
||||
|
||||
// Generate full login token
|
||||
loginResponse, err := LoginByUserID(userID, loginCtx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -162,7 +162,14 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
|
|||
case "pending_invite":
|
||||
// User needs to verify invitation code, generate temporary token
|
||||
var inviteExpire int = 10 * 60 // 10 minutes
|
||||
accessToken, err := oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, ScopeInviteVerification, subject, inviteExpire)
|
||||
|
||||
// Prepare extra claims to preserve Remember Me state
|
||||
extraClaims := make(map[string]interface{})
|
||||
if loginCtx != nil && loginCtx.RememberMe {
|
||||
extraClaims["remember_me"] = true
|
||||
}
|
||||
|
||||
accessToken, err := oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, ScopeInviteVerification, subject, inviteExpire, extraClaims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -188,7 +195,14 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
|
|||
if mfaEnabled {
|
||||
// Sign temporary access token for MFA
|
||||
var mfaExpire int = 10 * 60 // 10 minutes
|
||||
accessToken, err := oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, ScopeMFAVerification, subject, mfaExpire)
|
||||
|
||||
// Prepare extra claims to preserve Remember Me state
|
||||
extraClaims := make(map[string]interface{})
|
||||
if loginCtx != nil && loginCtx.RememberMe {
|
||||
extraClaims["remember_me"] = true
|
||||
}
|
||||
|
||||
accessToken, err := oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, ScopeMFAVerification, subject, mfaExpire, extraClaims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -222,7 +236,14 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
|
|||
if numTeams > 0 {
|
||||
// Sign temporary access token for Team Selection
|
||||
var teamSelectionExpire int = 10 * 60 // 10 minutes
|
||||
accessToken, err := oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, ScopeTeamSelection, subject, teamSelectionExpire)
|
||||
|
||||
// Prepare extra claims to preserve Remember Me state
|
||||
extraClaims := make(map[string]interface{})
|
||||
if loginCtx != nil && loginCtx.RememberMe {
|
||||
extraClaims["remember_me"] = true
|
||||
}
|
||||
|
||||
accessToken, err := oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, ScopeTeamSelection, subject, teamSelectionExpire, extraClaims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -240,7 +261,7 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
|
|||
}
|
||||
|
||||
// Issue tokens without team context
|
||||
return issueTokens(ctx, userid, "", nil, user, subject, scopes)
|
||||
return issueTokens(ctx, userid, "", nil, user, subject, scopes, loginCtx)
|
||||
}
|
||||
|
||||
// LoginByTeamID is the handler for login by team ID (after team selection)
|
||||
|
|
@ -274,7 +295,7 @@ func LoginByTeamID(userid string, teamID string, loginCtx *LoginContext) (*Login
|
|||
|
||||
// Handle personal account (no team)
|
||||
if teamID == "" || teamID == "personal" {
|
||||
return issueTokens(ctx, userid, "", nil, user, subject, scopes)
|
||||
return issueTokens(ctx, userid, "", nil, user, subject, scopes, loginCtx)
|
||||
}
|
||||
|
||||
// Verify user is a member of the team and get team details
|
||||
|
|
@ -292,13 +313,98 @@ func LoginByTeamID(userid string, teamID string, loginCtx *LoginContext) (*Login
|
|||
}
|
||||
|
||||
// Issue tokens with team context
|
||||
return issueTokens(ctx, userid, teamID, team, user, subject, scopes)
|
||||
return issueTokens(ctx, userid, teamID, team, user, subject, scopes, loginCtx)
|
||||
}
|
||||
|
||||
// issueTokens is the core function that issues all necessary tokens (ID token, access token, refresh token)
|
||||
func issueTokens(ctx context.Context, userid string, teamID string, team map[string]interface{}, user map[string]interface{}, subject string, scopes []string) (*LoginResponse, error) {
|
||||
func issueTokens(ctx context.Context, userid string, teamID string, team map[string]interface{}, user map[string]interface{}, subject string, scopes []string, loginCtx *LoginContext) (*LoginResponse, error) {
|
||||
yaoClientConfig := GetYaoClientConfig()
|
||||
|
||||
// Determine token expiration times based on Remember Me setting
|
||||
var expiresIn, refreshTokenExpiresIn int
|
||||
|
||||
// Try to get token config from entry config first
|
||||
locale := ""
|
||||
entryConfig := GetEntryConfig(locale)
|
||||
|
||||
if loginCtx != nil && loginCtx.RememberMe {
|
||||
// Remember Me mode: use extended token durations
|
||||
if entryConfig != nil && entryConfig.Token != nil {
|
||||
// Parse Remember Me access token expires_in
|
||||
if entryConfig.Token.RememberMeExpiresIn != "" {
|
||||
normalized, err := normalizeDuration(entryConfig.Token.RememberMeExpiresIn)
|
||||
if err != nil {
|
||||
log.Warn("Failed to parse remember_me_expires_in: %s, using default", err.Error())
|
||||
} else {
|
||||
duration, err := time.ParseDuration(normalized)
|
||||
if err == nil {
|
||||
expiresIn = int(duration.Seconds())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Remember Me refresh token expires_in
|
||||
if entryConfig.Token.RememberMeRefreshTokenExpiresIn != "" {
|
||||
normalized, err := normalizeDuration(entryConfig.Token.RememberMeRefreshTokenExpiresIn)
|
||||
if err != nil {
|
||||
log.Warn("Failed to parse remember_me_refresh_token_expires_in: %s, using default", err.Error())
|
||||
} else {
|
||||
duration, err := time.ParseDuration(normalized)
|
||||
if err == nil {
|
||||
refreshTokenExpiresIn = int(duration.Seconds())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If refresh token not configured, default to 2x the access token duration
|
||||
if refreshTokenExpiresIn == 0 && expiresIn > 0 {
|
||||
refreshTokenExpiresIn = expiresIn * 2
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal login: use standard token durations from entry config
|
||||
if entryConfig != nil && entryConfig.Token != nil {
|
||||
// Parse access token expires_in
|
||||
if entryConfig.Token.ExpiresIn != "" {
|
||||
normalized, err := normalizeDuration(entryConfig.Token.ExpiresIn)
|
||||
if err != nil {
|
||||
log.Warn("Failed to parse expires_in: %s, using default", err.Error())
|
||||
} else {
|
||||
duration, err := time.ParseDuration(normalized)
|
||||
if err == nil {
|
||||
expiresIn = int(duration.Seconds())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse refresh token expires_in
|
||||
if entryConfig.Token.RefreshTokenExpiresIn != "" {
|
||||
normalized, err := normalizeDuration(entryConfig.Token.RefreshTokenExpiresIn)
|
||||
if err != nil {
|
||||
log.Warn("Failed to parse refresh_token_expires_in: %s, using default", err.Error())
|
||||
} else {
|
||||
duration, err := time.ParseDuration(normalized)
|
||||
if err == nil {
|
||||
refreshTokenExpiresIn = int(duration.Seconds())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If refresh token not configured, default to 24x the access token duration
|
||||
if refreshTokenExpiresIn == 0 && expiresIn > 0 {
|
||||
refreshTokenExpiresIn = expiresIn * 24
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to YaoClientConfig defaults if not set from entry config
|
||||
if expiresIn == 0 {
|
||||
expiresIn = yaoClientConfig.ExpiresIn
|
||||
}
|
||||
if refreshTokenExpiresIn == 0 {
|
||||
refreshTokenExpiresIn = yaoClientConfig.RefreshTokenExpiresIn
|
||||
}
|
||||
|
||||
// Prepare OIDC user info
|
||||
oidcUserInfo := oauthtypes.MakeOIDCUserInfo(user)
|
||||
oidcUserInfo.Sub = subject
|
||||
|
|
@ -388,9 +494,9 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str
|
|||
var oidcToken string
|
||||
var err error
|
||||
if len(extraClaims) > 0 {
|
||||
oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), yaoClientConfig.ExpiresIn, oidcUserInfo, extraClaims)
|
||||
oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), expiresIn, oidcUserInfo, extraClaims)
|
||||
} else {
|
||||
oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), yaoClientConfig.ExpiresIn, oidcUserInfo)
|
||||
oidcToken, err = oauth.OAuth.SignIDToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), expiresIn, oidcUserInfo)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign OIDC token: %w", err)
|
||||
|
|
@ -399,9 +505,9 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str
|
|||
// Sign Access Token
|
||||
var accessToken string
|
||||
if len(extraClaims) > 0 {
|
||||
accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.ExpiresIn, extraClaims)
|
||||
accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, expiresIn, extraClaims)
|
||||
} else {
|
||||
accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.ExpiresIn)
|
||||
accessToken, err = oauth.OAuth.MakeAccessToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, expiresIn)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign access token: %w", err)
|
||||
|
|
@ -410,9 +516,9 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str
|
|||
// Sign Refresh Token
|
||||
var refreshToken string
|
||||
if len(extraClaims) > 0 {
|
||||
refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.RefreshTokenExpiresIn, extraClaims)
|
||||
refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, refreshTokenExpiresIn, extraClaims)
|
||||
} else {
|
||||
refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, yaoClientConfig.RefreshTokenExpiresIn)
|
||||
refreshToken, err = oauth.OAuth.MakeRefreshToken(yaoClientConfig.ClientID, strings.Join(scopes, " "), subject, refreshTokenExpiresIn)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign refresh token: %w", err)
|
||||
|
|
@ -424,8 +530,8 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str
|
|||
AccessToken: accessToken,
|
||||
IDToken: oidcToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresIn: yaoClientConfig.ExpiresIn,
|
||||
RefreshTokenExpiresIn: yaoClientConfig.RefreshTokenExpiresIn,
|
||||
ExpiresIn: expiresIn,
|
||||
RefreshTokenExpiresIn: refreshTokenExpiresIn,
|
||||
TokenType: "Bearer",
|
||||
MFAEnabled: toBool(user["mfa_enabled"]),
|
||||
Scope: strings.Join(scopes, " "),
|
||||
|
|
|
|||
|
|
@ -347,6 +347,9 @@ func GinTeamSelection(c *gin.Context) {
|
|||
// Prepare login context with full device/platform information
|
||||
loginCtx := makeLoginContext(c)
|
||||
|
||||
// Preserve Remember Me state from temporary token
|
||||
loginCtx.RememberMe = authInfo.RememberMe
|
||||
|
||||
// Login with selected team
|
||||
loginResponse, err := LoginByTeamID(authInfo.UserID, req.TeamID, loginCtx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -76,8 +76,10 @@ type CaptchaConfig struct {
|
|||
|
||||
// TokenConfig represents the token configuration
|
||||
type TokenConfig struct {
|
||||
ExpiresIn string `json:"expires_in,omitempty"`
|
||||
RememberMeExpiresIn string `json:"remember_me_expires_in,omitempty"`
|
||||
ExpiresIn string `json:"expires_in,omitempty"`
|
||||
RefreshTokenExpiresIn string `json:"refresh_token_expires_in,omitempty"`
|
||||
RememberMeExpiresIn string `json:"remember_me_expires_in,omitempty"`
|
||||
RememberMeRefreshTokenExpiresIn string `json:"remember_me_refresh_token_expires_in,omitempty"`
|
||||
}
|
||||
|
||||
// ThirdParty represents the third party login configuration
|
||||
|
|
@ -297,8 +299,9 @@ type EntryRegisterRequest struct {
|
|||
|
||||
// EntryLoginRequest represents the request to login with username and password
|
||||
type EntryLoginRequest struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
RememberMe bool `json:"remember_me,omitempty"`
|
||||
Locale string `json:"locale,omitempty"`
|
||||
}
|
||||
|
||||
// EntrySendOTPResponse represents the response for sending OTP verification code
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue