Merge pull request #1210 from trheyi/main
Enhance user authentication with Remember Me functionality
This commit is contained in:
commit
08744728c6
6 changed files with 154 additions and 28 deletions
|
|
@ -66,6 +66,12 @@ func GetAuthorizedInfo(c *gin.Context) *types.AuthorizedInfo {
|
||||||
info.TenantID = tenantID.(string)
|
info.TenantID = tenantID.(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rememberMe, ok := c.Get("__remember_me"); ok {
|
||||||
|
if rmBool, ok := rememberMe.(bool); ok {
|
||||||
|
info.RememberMe = rmBool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type LoginContext struct {
|
||||||
Device string `json:"device,omitempty"` // Device type (e.g., "mobile", "desktop", "tablet")
|
Device string `json:"device,omitempty"` // Device type (e.g., "mobile", "desktop", "tablet")
|
||||||
Platform string `json:"platform,omitempty"` // Platform (e.g., "ios", "android", "web")
|
Platform string `json:"platform,omitempty"` // Platform (e.g., "ios", "android", "web")
|
||||||
Location string `json:"location,omitempty"` // Geographic location (optional)
|
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
|
// MFAOptions contains configuration for MFA operations
|
||||||
|
|
@ -601,6 +602,7 @@ type AuthorizedInfo struct {
|
||||||
// Extended fields for multi-tenancy and team support
|
// Extended fields for multi-tenancy and team support
|
||||||
TeamID string `json:"team_id,omitempty"` // Team identifier
|
TeamID string `json:"team_id,omitempty"` // Team identifier
|
||||||
TenantID string `json:"tenant_id,omitempty"` // Tenant 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
|
// JWTClaims represents JWT-specific claims structure
|
||||||
|
|
|
||||||
|
|
@ -467,7 +467,9 @@ func createPublicEntryConfig(config *EntryConfig) *EntryConfig {
|
||||||
if config.Token != nil {
|
if config.Token != nil {
|
||||||
publicConfig.Token = &TokenConfig{
|
publicConfig.Token = &TokenConfig{
|
||||||
ExpiresIn: config.Token.ExpiresIn,
|
ExpiresIn: config.Token.ExpiresIn,
|
||||||
|
RefreshTokenExpiresIn: config.Token.RefreshTokenExpiresIn,
|
||||||
RememberMeExpiresIn: config.Token.RememberMeExpiresIn,
|
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)
|
// Login using LoginByUserID (all status checks are handled inside)
|
||||||
loginCtx := makeLoginContext(c)
|
loginCtx := makeLoginContext(c)
|
||||||
|
loginCtx.RememberMe = req.RememberMe // Set Remember Me from request
|
||||||
loginResponse, err := LoginByUserID(userID, loginCtx)
|
loginResponse, err := LoginByUserID(userID, loginCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to login user %s: %v", userID, err)
|
log.Error("Failed to login user %s: %v", userID, err)
|
||||||
|
|
@ -1142,6 +1145,9 @@ func GinVerifyInvite(c *gin.Context) {
|
||||||
// Generate login context
|
// Generate login context
|
||||||
loginCtx := makeLoginContext(c)
|
loginCtx := makeLoginContext(c)
|
||||||
|
|
||||||
|
// Preserve Remember Me state from temporary token (authInfo is already available from above)
|
||||||
|
loginCtx.RememberMe = authInfo.RememberMe
|
||||||
|
|
||||||
// Generate full login token
|
// Generate full login token
|
||||||
loginResponse, err := LoginByUserID(userID, loginCtx)
|
loginResponse, err := LoginByUserID(userID, loginCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,14 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
|
||||||
case "pending_invite":
|
case "pending_invite":
|
||||||
// User needs to verify invitation code, generate temporary token
|
// User needs to verify invitation code, generate temporary token
|
||||||
var inviteExpire int = 10 * 60 // 10 minutes
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +195,14 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
|
||||||
if mfaEnabled {
|
if mfaEnabled {
|
||||||
// Sign temporary access token for MFA
|
// Sign temporary access token for MFA
|
||||||
var mfaExpire int = 10 * 60 // 10 minutes
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -222,7 +236,14 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
|
||||||
if numTeams > 0 {
|
if numTeams > 0 {
|
||||||
// Sign temporary access token for Team Selection
|
// Sign temporary access token for Team Selection
|
||||||
var teamSelectionExpire int = 10 * 60 // 10 minutes
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +261,7 @@ func LoginByUserID(userid string, loginCtx *LoginContext) (*LoginResponse, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Issue tokens without team context
|
// 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)
|
// 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)
|
// Handle personal account (no team)
|
||||||
if teamID == "" || teamID == "personal" {
|
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
|
// 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
|
// 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)
|
// 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()
|
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
|
// Prepare OIDC user info
|
||||||
oidcUserInfo := oauthtypes.MakeOIDCUserInfo(user)
|
oidcUserInfo := oauthtypes.MakeOIDCUserInfo(user)
|
||||||
oidcUserInfo.Sub = subject
|
oidcUserInfo.Sub = subject
|
||||||
|
|
@ -388,9 +494,9 @@ func issueTokens(ctx context.Context, userid string, teamID string, team map[str
|
||||||
var oidcToken string
|
var oidcToken string
|
||||||
var err error
|
var err error
|
||||||
if len(extraClaims) > 0 {
|
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 {
|
} 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to sign OIDC token: %w", err)
|
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
|
// Sign Access Token
|
||||||
var accessToken string
|
var accessToken string
|
||||||
if len(extraClaims) > 0 {
|
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 {
|
} 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to sign access token: %w", err)
|
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
|
// Sign Refresh Token
|
||||||
var refreshToken string
|
var refreshToken string
|
||||||
if len(extraClaims) > 0 {
|
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 {
|
} 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to sign refresh token: %w", err)
|
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,
|
AccessToken: accessToken,
|
||||||
IDToken: oidcToken,
|
IDToken: oidcToken,
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
ExpiresIn: yaoClientConfig.ExpiresIn,
|
ExpiresIn: expiresIn,
|
||||||
RefreshTokenExpiresIn: yaoClientConfig.RefreshTokenExpiresIn,
|
RefreshTokenExpiresIn: refreshTokenExpiresIn,
|
||||||
TokenType: "Bearer",
|
TokenType: "Bearer",
|
||||||
MFAEnabled: toBool(user["mfa_enabled"]),
|
MFAEnabled: toBool(user["mfa_enabled"]),
|
||||||
Scope: strings.Join(scopes, " "),
|
Scope: strings.Join(scopes, " "),
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,9 @@ func GinTeamSelection(c *gin.Context) {
|
||||||
// Prepare login context with full device/platform information
|
// Prepare login context with full device/platform information
|
||||||
loginCtx := makeLoginContext(c)
|
loginCtx := makeLoginContext(c)
|
||||||
|
|
||||||
|
// Preserve Remember Me state from temporary token
|
||||||
|
loginCtx.RememberMe = authInfo.RememberMe
|
||||||
|
|
||||||
// Login with selected team
|
// Login with selected team
|
||||||
loginResponse, err := LoginByTeamID(authInfo.UserID, req.TeamID, loginCtx)
|
loginResponse, err := LoginByTeamID(authInfo.UserID, req.TeamID, loginCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,9 @@ type CaptchaConfig struct {
|
||||||
// TokenConfig represents the token configuration
|
// TokenConfig represents the token configuration
|
||||||
type TokenConfig struct {
|
type TokenConfig struct {
|
||||||
ExpiresIn string `json:"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"`
|
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
|
// ThirdParty represents the third party login configuration
|
||||||
|
|
@ -298,6 +300,7 @@ type EntryRegisterRequest struct {
|
||||||
// EntryLoginRequest represents the request to login with username and password
|
// EntryLoginRequest represents the request to login with username and password
|
||||||
type EntryLoginRequest struct {
|
type EntryLoginRequest struct {
|
||||||
Password string `json:"password" binding:"required"`
|
Password string `json:"password" binding:"required"`
|
||||||
|
RememberMe bool `json:"remember_me,omitempty"`
|
||||||
Locale string `json:"locale,omitempty"`
|
Locale string `json:"locale,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue