Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
7c6491c2
Commit
7c6491c2
authored
Apr 21, 2026
by
IanShaw027
Browse files
fix auth pending session hardening
parent
1d8432b8
Changes
12
Hide whitespace changes
Inline
Side-by-side
backend/internal/handler/auth_handler.go
View file @
7c6491c2
package
handler
package
handler
import
(
import
(
"context"
"log/slog"
"log/slog"
"strings"
"strings"
...
@@ -105,6 +106,34 @@ func (h *AuthHandler) respondWithTokenPair(c *gin.Context, user *service.User) {
...
@@ -105,6 +106,34 @@ func (h *AuthHandler) respondWithTokenPair(c *gin.Context, user *service.User) {
})
})
}
}
func
(
h
*
AuthHandler
)
ensureBackendModeAllowsUser
(
ctx
context
.
Context
,
user
*
service
.
User
)
error
{
if
user
==
nil
{
return
infraerrors
.
Unauthorized
(
"INVALID_USER"
,
"user not found"
)
}
if
h
==
nil
||
!
h
.
isBackendModeEnabled
(
ctx
)
||
user
.
IsAdmin
()
{
return
nil
}
return
infraerrors
.
Forbidden
(
"BACKEND_MODE_ADMIN_ONLY"
,
"Backend mode is active. Only admin login is allowed."
)
}
func
(
h
*
AuthHandler
)
ensureBackendModeAllowsNewUserLogin
(
ctx
context
.
Context
)
error
{
if
h
==
nil
||
!
h
.
isBackendModeEnabled
(
ctx
)
{
return
nil
}
return
infraerrors
.
Forbidden
(
"BACKEND_MODE_ADMIN_ONLY"
,
"Backend mode is active. Only admin login is allowed."
)
}
func
(
h
*
AuthHandler
)
isBackendModeEnabled
(
ctx
context
.
Context
)
bool
{
if
h
==
nil
||
h
.
settingSvc
==
nil
{
return
false
}
settings
,
err
:=
h
.
settingSvc
.
GetPublicSettings
(
ctx
)
if
err
==
nil
&&
settings
!=
nil
{
return
settings
.
BackendModeEnabled
}
return
h
.
settingSvc
.
IsBackendModeEnabled
(
ctx
)
}
// Register handles user registration
// Register handles user registration
// POST /api/v1/auth/register
// POST /api/v1/auth/register
func
(
h
*
AuthHandler
)
Register
(
c
*
gin
.
Context
)
{
func
(
h
*
AuthHandler
)
Register
(
c
*
gin
.
Context
)
{
...
@@ -178,6 +207,11 @@ func (h *AuthHandler) Login(c *gin.Context) {
...
@@ -178,6 +207,11 @@ func (h *AuthHandler) Login(c *gin.Context) {
}
}
_
=
token
// token 由 authService.Login 返回但此处由 respondWithTokenPair 重新生成
_
=
token
// token 由 authService.Login 返回但此处由 respondWithTokenPair 重新生成
if
err
:=
h
.
ensureBackendModeAllowsUser
(
c
.
Request
.
Context
(),
user
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
// Check if TOTP 2FA is enabled for this user
// Check if TOTP 2FA is enabled for this user
if
h
.
totpService
!=
nil
&&
h
.
settingSvc
.
IsTotpEnabled
(
c
.
Request
.
Context
())
&&
user
.
TotpEnabled
{
if
h
.
totpService
!=
nil
&&
h
.
settingSvc
.
IsTotpEnabled
(
c
.
Request
.
Context
())
&&
user
.
TotpEnabled
{
// Create a temporary login session for 2FA
// Create a temporary login session for 2FA
...
@@ -195,11 +229,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
...
@@ -195,11 +229,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
return
return
}
}
// Backend mode: only admin can login
h
.
authService
.
RecordSuccessfulLogin
(
c
.
Request
.
Context
(),
user
.
ID
)
if
h
.
settingSvc
.
IsBackendModeEnabled
(
c
.
Request
.
Context
())
&&
!
user
.
IsAdmin
()
{
response
.
Forbidden
(
c
,
"Backend mode is active. Only admin login is allowed."
)
return
}
h
.
respondWithTokenPair
(
c
,
user
)
h
.
respondWithTokenPair
(
c
,
user
)
}
}
...
@@ -264,9 +294,8 @@ func (h *AuthHandler) Login2FA(c *gin.Context) {
...
@@ -264,9 +294,8 @@ func (h *AuthHandler) Login2FA(c *gin.Context) {
return
return
}
}
// Backend mode: only admin can login (check BEFORE deleting session)
if
err
:=
h
.
ensureBackendModeAllowsUser
(
c
.
Request
.
Context
(),
user
);
err
!=
nil
{
if
h
.
settingSvc
.
IsBackendModeEnabled
(
c
.
Request
.
Context
())
&&
!
user
.
IsAdmin
()
{
response
.
ErrorFrom
(
c
,
err
)
response
.
Forbidden
(
c
,
"Backend mode is active. Only admin login is allowed."
)
return
return
}
}
...
@@ -330,6 +359,10 @@ func (h *AuthHandler) Login2FA(c *gin.Context) {
...
@@ -330,6 +359,10 @@ func (h *AuthHandler) Login2FA(c *gin.Context) {
// Delete the login session (only after all checks pass)
// Delete the login session (only after all checks pass)
_
=
h
.
totpService
.
DeleteLoginSession
(
c
.
Request
.
Context
(),
req
.
TempToken
)
_
=
h
.
totpService
.
DeleteLoginSession
(
c
.
Request
.
Context
(),
req
.
TempToken
)
if
session
.
PendingOAuthBind
==
nil
{
h
.
authService
.
RecordSuccessfulLogin
(
c
.
Request
.
Context
(),
user
.
ID
)
}
h
.
respondWithTokenPair
(
c
,
user
)
h
.
respondWithTokenPair
(
c
,
user
)
}
}
...
...
backend/internal/handler/auth_linuxdo_oauth.go
View file @
7c6491c2
...
@@ -474,6 +474,14 @@ func (h *AuthHandler) CompleteLinuxDoOAuthRegistration(c *gin.Context) {
...
@@ -474,6 +474,14 @@ func (h *AuthHandler) CompleteLinuxDoOAuthRegistration(c *gin.Context) {
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
}
}
if
err
:=
ensurePendingOAuthCompleteRegistrationSession
(
session
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
if
err
:=
h
.
ensureBackendModeAllowsNewUserLogin
(
c
.
Request
.
Context
());
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
email
:=
strings
.
TrimSpace
(
session
.
ResolvedEmail
)
email
:=
strings
.
TrimSpace
(
session
.
ResolvedEmail
)
username
:=
pendingSessionStringValue
(
session
.
UpstreamIdentityClaims
,
"username"
)
username
:=
pendingSessionStringValue
(
session
.
UpstreamIdentityClaims
,
"username"
)
...
@@ -499,6 +507,7 @@ func (h *AuthHandler) CompleteLinuxDoOAuthRegistration(c *gin.Context) {
...
@@ -499,6 +507,7 @@ func (h *AuthHandler) CompleteLinuxDoOAuthRegistration(c *gin.Context) {
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_ADOPTION_APPLY_FAILED"
,
"failed to apply oauth profile adoption"
)
.
WithCause
(
err
))
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_ADOPTION_APPLY_FAILED"
,
"failed to apply oauth profile adoption"
)
.
WithCause
(
err
))
return
return
}
}
h
.
authService
.
RecordSuccessfulLogin
(
c
.
Request
.
Context
(),
user
.
ID
)
if
_
,
err
:=
pendingSvc
.
ConsumeBrowserSession
(
c
.
Request
.
Context
(),
sessionToken
,
browserSessionKey
);
err
!=
nil
{
if
_
,
err
:=
pendingSvc
.
ConsumeBrowserSession
(
c
.
Request
.
Context
(),
sessionToken
,
browserSessionKey
);
err
!=
nil
{
clearOAuthPendingSessionCookie
(
c
,
secureCookie
)
clearOAuthPendingSessionCookie
(
c
,
secureCookie
)
clearOAuthPendingBrowserCookie
(
c
,
secureCookie
)
clearOAuthPendingBrowserCookie
(
c
,
secureCookie
)
...
...
backend/internal/handler/auth_linuxdo_oauth_test.go
View file @
7c6491c2
...
@@ -591,6 +591,58 @@ func TestCompleteLinuxDoOAuthRegistrationAppliesPendingAdoptionDecision(t *testi
...
@@ -591,6 +591,58 @@ func TestCompleteLinuxDoOAuthRegistrationAppliesPendingAdoptionDecision(t *testi
require
.
NotNil
(
t
,
consumed
.
ConsumedAt
)
require
.
NotNil
(
t
,
consumed
.
ConsumedAt
)
}
}
func
TestCompleteLinuxDoOAuthRegistrationRejectsAdoptExistingUserSession
(
t
*
testing
.
T
)
{
handler
,
client
:=
newOAuthPendingFlowTestHandler
(
t
,
false
)
ctx
:=
context
.
Background
()
existingUser
,
err
:=
client
.
User
.
Create
()
.
SetEmail
(
"owner@example.com"
)
.
SetUsername
(
"owner-user"
)
.
SetPasswordHash
(
"hash"
)
.
SetRole
(
service
.
RoleUser
)
.
SetStatus
(
service
.
StatusActive
)
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
session
,
err
:=
client
.
PendingAuthSession
.
Create
()
.
SetSessionToken
(
"linuxdo-complete-invalid-session"
)
.
SetIntent
(
"adopt_existing_user_by_email"
)
.
SetProviderType
(
"linuxdo"
)
.
SetProviderKey
(
"linuxdo"
)
.
SetProviderSubject
(
"linuxdo-invalid-subject-1"
)
.
SetTargetUserID
(
existingUser
.
ID
)
.
SetResolvedEmail
(
existingUser
.
Email
)
.
SetBrowserSessionKey
(
"linuxdo-invalid-browser"
)
.
SetUpstreamIdentityClaims
(
map
[
string
]
any
{
"username"
:
"linuxdo_user"
,
})
.
SetLocalFlowState
(
map
[
string
]
any
{
oauthCompletionResponseKey
:
map
[
string
]
any
{
"step"
:
"bind_login_required"
,
},
})
.
SetExpiresAt
(
time
.
Now
()
.
UTC
()
.
Add
(
10
*
time
.
Minute
))
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
body
:=
bytes
.
NewBufferString
(
`{"invitation_code":"invite-1"}`
)
recorder
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
recorder
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/auth/oauth/linuxdo/complete-registration"
,
body
)
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingSessionCookieName
,
Value
:
encodeCookieValue
(
session
.
SessionToken
)})
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingBrowserCookieName
,
Value
:
encodeCookieValue
(
"linuxdo-invalid-browser"
)})
c
.
Request
=
req
handler
.
CompleteLinuxDoOAuthRegistration
(
c
)
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
recorder
.
Code
)
storedSession
,
err
:=
client
.
PendingAuthSession
.
Get
(
ctx
,
session
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
storedSession
.
ConsumedAt
)
}
func
newLinuxDoOAuthTestHandler
(
t
*
testing
.
T
,
invitationEnabled
bool
,
oauthCfg
config
.
LinuxDoConnectConfig
)
*
AuthHandler
{
func
newLinuxDoOAuthTestHandler
(
t
*
testing
.
T
,
invitationEnabled
bool
,
oauthCfg
config
.
LinuxDoConnectConfig
)
*
AuthHandler
{
t
.
Helper
()
t
.
Helper
()
handler
,
_
:=
newLinuxDoOAuthHandlerAndClient
(
t
,
invitationEnabled
,
oauthCfg
)
handler
,
_
:=
newLinuxDoOAuthHandlerAndClient
(
t
,
invitationEnabled
,
oauthCfg
)
...
...
backend/internal/handler/auth_oauth_pending_flow.go
View file @
7c6491c2
...
@@ -253,6 +253,35 @@ func pendingSessionWantsInvitation(payload map[string]any) bool {
...
@@ -253,6 +253,35 @@ func pendingSessionWantsInvitation(payload map[string]any) bool {
return
strings
.
EqualFold
(
strings
.
TrimSpace
(
pendingSessionStringValue
(
payload
,
"error"
)),
"invitation_required"
)
return
strings
.
EqualFold
(
strings
.
TrimSpace
(
pendingSessionStringValue
(
payload
,
"error"
)),
"invitation_required"
)
}
}
func
pendingOAuthCompletionIncludesTokenPayload
(
payload
map
[
string
]
any
)
bool
{
if
len
(
payload
)
==
0
{
return
false
}
for
_
,
key
:=
range
[]
string
{
"access_token"
,
"refresh_token"
}
{
if
value
:=
pendingSessionStringValue
(
payload
,
key
);
value
!=
""
{
return
true
}
}
return
false
}
func
ensurePendingOAuthCompleteRegistrationSession
(
session
*
dbent
.
PendingAuthSession
)
error
{
if
session
==
nil
{
return
infraerrors
.
BadRequest
(
"PENDING_AUTH_SESSION_INVALID"
,
"pending auth registration context is invalid"
)
}
if
strings
.
TrimSpace
(
session
.
Intent
)
!=
oauthIntentLogin
{
return
infraerrors
.
BadRequest
(
"PENDING_AUTH_SESSION_INVALID"
,
"pending auth registration context is invalid"
)
}
if
session
.
TargetUserID
!=
nil
&&
*
session
.
TargetUserID
>
0
{
return
infraerrors
.
BadRequest
(
"PENDING_AUTH_SESSION_INVALID"
,
"pending auth registration context is invalid"
)
}
payload
,
_
:=
readCompletionResponse
(
session
.
LocalFlowState
)
if
strings
.
EqualFold
(
strings
.
TrimSpace
(
pendingSessionStringValue
(
payload
,
"step"
)),
"bind_login_required"
)
{
return
infraerrors
.
BadRequest
(
"PENDING_AUTH_SESSION_INVALID"
,
"pending auth registration context is invalid"
)
}
return
nil
}
func
(
r
oauthAdoptionDecisionRequest
)
hasDecision
()
bool
{
func
(
r
oauthAdoptionDecisionRequest
)
hasDecision
()
bool
{
return
r
.
AdoptDisplayName
!=
nil
||
r
.
AdoptAvatar
!=
nil
return
r
.
AdoptDisplayName
!=
nil
||
r
.
AdoptAvatar
!=
nil
}
}
...
@@ -1090,6 +1119,10 @@ func (h *AuthHandler) bindPendingOAuthLogin(c *gin.Context, provider string) {
...
@@ -1090,6 +1119,10 @@ func (h *AuthHandler) bindPendingOAuthLogin(c *gin.Context, provider string) {
response
.
ErrorFrom
(
c
,
infraerrors
.
Conflict
(
"PENDING_AUTH_TARGET_USER_MISMATCH"
,
"pending oauth session must be completed by the targeted user"
))
response
.
ErrorFrom
(
c
,
infraerrors
.
Conflict
(
"PENDING_AUTH_TARGET_USER_MISMATCH"
,
"pending oauth session must be completed by the targeted user"
))
return
return
}
}
if
err
:=
h
.
ensureBackendModeAllowsUser
(
c
.
Request
.
Context
(),
user
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
decision
,
err
:=
h
.
ensurePendingOAuthAdoptionDecision
(
c
,
session
.
ID
,
req
.
adoptionDecision
())
decision
,
err
:=
h
.
ensurePendingOAuthAdoptionDecision
(
c
,
session
.
ID
,
req
.
adoptionDecision
())
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -1192,6 +1225,10 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
...
@@ -1192,6 +1225,10 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
c
.
JSON
(
http
.
StatusOK
,
buildPendingOAuthSessionStatusPayload
(
session
))
c
.
JSON
(
http
.
StatusOK
,
buildPendingOAuthSessionStatusPayload
(
session
))
return
return
}
}
if
err
:=
h
.
ensureBackendModeAllowsNewUserLogin
(
c
.
Request
.
Context
());
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
tokenPair
,
user
,
err
:=
h
.
authService
.
RegisterOAuthEmailAccount
(
tokenPair
,
user
,
err
:=
h
.
authService
.
RegisterOAuthEmailAccount
(
c
.
Request
.
Context
(),
c
.
Request
.
Context
(),
...
@@ -1215,6 +1252,7 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
...
@@ -1215,6 +1252,7 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_BIND_APPLY_FAILED"
,
"failed to bind pending oauth identity"
)
.
WithCause
(
err
))
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_BIND_APPLY_FAILED"
,
"failed to bind pending oauth identity"
)
.
WithCause
(
err
))
return
return
}
}
h
.
authService
.
RecordSuccessfulLogin
(
c
.
Request
.
Context
(),
user
.
ID
)
if
_
,
err
:=
pendingSvc
.
ConsumeBrowserSession
(
c
.
Request
.
Context
(),
session
.
SessionToken
,
session
.
BrowserSessionKey
);
err
!=
nil
{
if
_
,
err
:=
pendingSvc
.
ConsumeBrowserSession
(
c
.
Request
.
Context
(),
session
.
SessionToken
,
session
.
BrowserSessionKey
);
err
!=
nil
{
clearCookies
()
clearCookies
()
...
@@ -1279,6 +1317,25 @@ func (h *AuthHandler) ExchangePendingOAuthCompletion(c *gin.Context) {
...
@@ -1279,6 +1317,25 @@ func (h *AuthHandler) ExchangePendingOAuthCompletion(c *gin.Context) {
}
}
}
}
applySuggestedProfileToCompletionResponse
(
payload
,
session
.
UpstreamIdentityClaims
)
applySuggestedProfileToCompletionResponse
(
payload
,
session
.
UpstreamIdentityClaims
)
if
pendingOAuthCompletionIncludesTokenPayload
(
payload
)
{
if
session
.
TargetUserID
==
nil
||
*
session
.
TargetUserID
<=
0
{
clearCookies
()
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_COMPLETION_INVALID"
,
"pending auth completion payload is invalid"
))
return
}
user
,
err
:=
h
.
userService
.
GetByID
(
c
.
Request
.
Context
(),
*
session
.
TargetUserID
)
if
err
!=
nil
{
clearCookies
()
response
.
ErrorFrom
(
c
,
err
)
return
}
if
err
:=
h
.
ensureBackendModeAllowsUser
(
c
.
Request
.
Context
(),
user
);
err
!=
nil
{
clearCookies
()
response
.
ErrorFrom
(
c
,
err
)
return
}
h
.
authService
.
RecordSuccessfulLogin
(
c
.
Request
.
Context
(),
user
.
ID
)
}
if
pendingSessionWantsInvitation
(
payload
)
{
if
pendingSessionWantsInvitation
(
payload
)
{
if
adoptionDecision
.
hasDecision
()
{
if
adoptionDecision
.
hasDecision
()
{
...
...
backend/internal/handler/auth_oauth_pending_flow_test.go
View file @
7c6491c2
...
@@ -523,6 +523,60 @@ func TestExchangePendingOAuthCompletionLoginFalseFalseBindsIdentityWithoutAdopti
...
@@ -523,6 +523,60 @@ func TestExchangePendingOAuthCompletionLoginFalseFalseBindsIdentityWithoutAdopti
require
.
NotNil
(
t
,
storedSession
.
ConsumedAt
)
require
.
NotNil
(
t
,
storedSession
.
ConsumedAt
)
}
}
func
TestExchangePendingOAuthCompletionBlocksBackendModeBeforeReturningTokenPayload
(
t
*
testing
.
T
)
{
handler
,
client
:=
newOAuthPendingFlowTestHandlerWithDependencies
(
t
,
oauthPendingFlowTestHandlerOptions
{
settingValues
:
map
[
string
]
string
{
service
.
SettingKeyBackendModeEnabled
:
"true"
,
},
})
ctx
:=
context
.
Background
()
userEntity
,
err
:=
client
.
User
.
Create
()
.
SetEmail
(
"blocked@example.com"
)
.
SetUsername
(
"blocked-user"
)
.
SetPasswordHash
(
"hash"
)
.
SetRole
(
service
.
RoleUser
)
.
SetStatus
(
service
.
StatusActive
)
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
session
,
err
:=
client
.
PendingAuthSession
.
Create
()
.
SetSessionToken
(
"blocked-backend-mode-session-token"
)
.
SetIntent
(
"login"
)
.
SetProviderType
(
"linuxdo"
)
.
SetProviderKey
(
"linuxdo"
)
.
SetProviderSubject
(
"blocked-subject-123"
)
.
SetTargetUserID
(
userEntity
.
ID
)
.
SetResolvedEmail
(
userEntity
.
Email
)
.
SetBrowserSessionKey
(
"blocked-backend-mode-browser-session-key"
)
.
SetLocalFlowState
(
map
[
string
]
any
{
oauthCompletionResponseKey
:
map
[
string
]
any
{
"access_token"
:
"access-token"
,
"refresh_token"
:
"refresh-token"
,
"expires_in"
:
float64
(
3600
),
"token_type"
:
"Bearer"
,
},
})
.
SetExpiresAt
(
time
.
Now
()
.
UTC
()
.
Add
(
10
*
time
.
Minute
))
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
recorder
:=
httptest
.
NewRecorder
()
ginCtx
,
_
:=
gin
.
CreateTestContext
(
recorder
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/auth/oauth/pending/exchange"
,
nil
)
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingSessionCookieName
,
Value
:
encodeCookieValue
(
session
.
SessionToken
)})
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingBrowserCookieName
,
Value
:
encodeCookieValue
(
"blocked-backend-mode-browser-session-key"
)})
ginCtx
.
Request
=
req
handler
.
ExchangePendingOAuthCompletion
(
ginCtx
)
require
.
Equal
(
t
,
http
.
StatusForbidden
,
recorder
.
Code
)
storedSession
,
err
:=
client
.
PendingAuthSession
.
Get
(
ctx
,
session
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
storedSession
.
ConsumedAt
)
}
func
TestExchangePendingOAuthCompletionInvitationRequiredFalseFalsePersistsDecisionWithoutBinding
(
t
*
testing
.
T
)
{
func
TestExchangePendingOAuthCompletionInvitationRequiredFalseFalsePersistsDecisionWithoutBinding
(
t
*
testing
.
T
)
{
handler
,
client
:=
newOAuthPendingFlowTestHandler
(
t
,
true
)
handler
,
client
:=
newOAuthPendingFlowTestHandler
(
t
,
true
)
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
...
@@ -773,6 +827,60 @@ func TestCreateOIDCOAuthAccountExistingEmailNormalizesLegacySpacingAndCase(t *te
...
@@ -773,6 +827,60 @@ func TestCreateOIDCOAuthAccountExistingEmailNormalizesLegacySpacingAndCase(t *te
require
.
Equal
(
t
,
"owner@example.com"
,
storedSession
.
ResolvedEmail
)
require
.
Equal
(
t
,
"owner@example.com"
,
storedSession
.
ResolvedEmail
)
}
}
func
TestCreateOIDCOAuthAccountBlocksBackendModeBeforeCreatingUser
(
t
*
testing
.
T
)
{
handler
,
client
:=
newOAuthPendingFlowTestHandlerWithDependencies
(
t
,
oauthPendingFlowTestHandlerOptions
{
emailVerifyEnabled
:
true
,
emailCache
:
&
oauthPendingFlowEmailCacheStub
{
verificationCodes
:
map
[
string
]
*
service
.
VerificationCodeData
{
"fresh@example.com"
:
{
Code
:
"246810"
,
CreatedAt
:
time
.
Now
()
.
UTC
(),
ExpiresAt
:
time
.
Now
()
.
UTC
()
.
Add
(
15
*
time
.
Minute
),
},
},
},
settingValues
:
map
[
string
]
string
{
service
.
SettingKeyBackendModeEnabled
:
"true"
,
},
})
ctx
:=
context
.
Background
()
session
,
err
:=
client
.
PendingAuthSession
.
Create
()
.
SetSessionToken
(
"create-account-backend-mode-session-token"
)
.
SetIntent
(
"login"
)
.
SetProviderType
(
"oidc"
)
.
SetProviderKey
(
"https://issuer.example"
)
.
SetProviderSubject
(
"oidc-create-backend-mode-123"
)
.
SetBrowserSessionKey
(
"create-account-backend-mode-browser-session-key"
)
.
SetUpstreamIdentityClaims
(
map
[
string
]
any
{
"username"
:
"oidc_user"
,
})
.
SetExpiresAt
(
time
.
Now
()
.
UTC
()
.
Add
(
10
*
time
.
Minute
))
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
body
:=
bytes
.
NewBufferString
(
`{"email":"fresh@example.com","verify_code":"246810","password":"secret-123"}`
)
recorder
:=
httptest
.
NewRecorder
()
ginCtx
,
_
:=
gin
.
CreateTestContext
(
recorder
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/auth/oauth/oidc/create-account"
,
body
)
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingSessionCookieName
,
Value
:
encodeCookieValue
(
session
.
SessionToken
)})
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingBrowserCookieName
,
Value
:
encodeCookieValue
(
"create-account-backend-mode-browser-session-key"
)})
ginCtx
.
Request
=
req
handler
.
CreateOIDCOAuthAccount
(
ginCtx
)
require
.
Equal
(
t
,
http
.
StatusForbidden
,
recorder
.
Code
)
userCount
,
err
:=
client
.
User
.
Query
()
.
Where
(
dbuser
.
EmailEQ
(
"fresh@example.com"
))
.
Count
(
ctx
)
require
.
NoError
(
t
,
err
)
require
.
Zero
(
t
,
userCount
)
storedSession
,
err
:=
client
.
PendingAuthSession
.
Get
(
ctx
,
session
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
storedSession
.
ConsumedAt
)
}
func
TestBindOIDCOAuthLoginBindsExistingUserAndConsumesSession
(
t
*
testing
.
T
)
{
func
TestBindOIDCOAuthLoginBindsExistingUserAndConsumesSession
(
t
*
testing
.
T
)
{
handler
,
client
:=
newOAuthPendingFlowTestHandler
(
t
,
false
)
handler
,
client
:=
newOAuthPendingFlowTestHandler
(
t
,
false
)
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
...
@@ -842,6 +950,70 @@ func TestBindOIDCOAuthLoginBindsExistingUserAndConsumesSession(t *testing.T) {
...
@@ -842,6 +950,70 @@ func TestBindOIDCOAuthLoginBindsExistingUserAndConsumesSession(t *testing.T) {
require
.
NotNil
(
t
,
storedSession
.
ConsumedAt
)
require
.
NotNil
(
t
,
storedSession
.
ConsumedAt
)
}
}
func
TestBindOIDCOAuthLoginBlocksBackendModeBeforeTokenIssue
(
t
*
testing
.
T
)
{
handler
,
client
:=
newOAuthPendingFlowTestHandlerWithDependencies
(
t
,
oauthPendingFlowTestHandlerOptions
{
settingValues
:
map
[
string
]
string
{
service
.
SettingKeyBackendModeEnabled
:
"true"
,
},
})
ctx
:=
context
.
Background
()
passwordHash
,
err
:=
handler
.
authService
.
HashPassword
(
"secret-123"
)
require
.
NoError
(
t
,
err
)
existingUser
,
err
:=
client
.
User
.
Create
()
.
SetEmail
(
"owner@example.com"
)
.
SetUsername
(
"owner-user"
)
.
SetPasswordHash
(
passwordHash
)
.
SetRole
(
service
.
RoleUser
)
.
SetStatus
(
service
.
StatusActive
)
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
session
,
err
:=
client
.
PendingAuthSession
.
Create
()
.
SetSessionToken
(
"bind-login-backend-mode-session-token"
)
.
SetIntent
(
"adopt_existing_user_by_email"
)
.
SetProviderType
(
"oidc"
)
.
SetProviderKey
(
"https://issuer.example"
)
.
SetProviderSubject
(
"oidc-bind-backend-mode-123"
)
.
SetTargetUserID
(
existingUser
.
ID
)
.
SetResolvedEmail
(
existingUser
.
Email
)
.
SetBrowserSessionKey
(
"bind-login-backend-mode-browser-session-key"
)
.
SetUpstreamIdentityClaims
(
map
[
string
]
any
{
"username"
:
"oidc_user"
,
})
.
SetExpiresAt
(
time
.
Now
()
.
UTC
()
.
Add
(
10
*
time
.
Minute
))
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
body
:=
bytes
.
NewBufferString
(
`{"email":"owner@example.com","password":"secret-123"}`
)
recorder
:=
httptest
.
NewRecorder
()
ginCtx
,
_
:=
gin
.
CreateTestContext
(
recorder
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/auth/oauth/oidc/bind-login"
,
body
)
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingSessionCookieName
,
Value
:
encodeCookieValue
(
session
.
SessionToken
)})
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingBrowserCookieName
,
Value
:
encodeCookieValue
(
"bind-login-backend-mode-browser-session-key"
)})
ginCtx
.
Request
=
req
handler
.
BindOIDCOAuthLogin
(
ginCtx
)
require
.
Equal
(
t
,
http
.
StatusForbidden
,
recorder
.
Code
)
identityCount
,
err
:=
client
.
AuthIdentity
.
Query
()
.
Where
(
authidentity
.
ProviderTypeEQ
(
"oidc"
),
authidentity
.
ProviderKeyEQ
(
"https://issuer.example"
),
authidentity
.
ProviderSubjectEQ
(
"oidc-bind-backend-mode-123"
),
)
.
Count
(
ctx
)
require
.
NoError
(
t
,
err
)
require
.
Zero
(
t
,
identityCount
)
storedSession
,
err
:=
client
.
PendingAuthSession
.
Get
(
ctx
,
session
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
storedSession
.
ConsumedAt
)
}
func
TestBindOIDCOAuthLoginRejectsInvalidPasswordWithoutConsumingSession
(
t
*
testing
.
T
)
{
func
TestBindOIDCOAuthLoginRejectsInvalidPasswordWithoutConsumingSession
(
t
*
testing
.
T
)
{
handler
,
client
:=
newOAuthPendingFlowTestHandler
(
t
,
false
)
handler
,
client
:=
newOAuthPendingFlowTestHandler
(
t
,
false
)
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
...
...
backend/internal/handler/auth_oidc_oauth.go
View file @
7c6491c2
...
@@ -516,6 +516,14 @@ func (h *AuthHandler) CompleteOIDCOAuthRegistration(c *gin.Context) {
...
@@ -516,6 +516,14 @@ func (h *AuthHandler) CompleteOIDCOAuthRegistration(c *gin.Context) {
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
}
}
if
err
:=
ensurePendingOAuthCompleteRegistrationSession
(
session
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
if
err
:=
h
.
ensureBackendModeAllowsNewUserLogin
(
c
.
Request
.
Context
());
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
email
:=
strings
.
TrimSpace
(
session
.
ResolvedEmail
)
email
:=
strings
.
TrimSpace
(
session
.
ResolvedEmail
)
username
:=
pendingSessionStringValue
(
session
.
UpstreamIdentityClaims
,
"username"
)
username
:=
pendingSessionStringValue
(
session
.
UpstreamIdentityClaims
,
"username"
)
...
@@ -541,6 +549,7 @@ func (h *AuthHandler) CompleteOIDCOAuthRegistration(c *gin.Context) {
...
@@ -541,6 +549,7 @@ func (h *AuthHandler) CompleteOIDCOAuthRegistration(c *gin.Context) {
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_ADOPTION_APPLY_FAILED"
,
"failed to apply oauth profile adoption"
)
.
WithCause
(
err
))
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_ADOPTION_APPLY_FAILED"
,
"failed to apply oauth profile adoption"
)
.
WithCause
(
err
))
return
return
}
}
h
.
authService
.
RecordSuccessfulLogin
(
c
.
Request
.
Context
(),
user
.
ID
)
if
_
,
err
:=
pendingSvc
.
ConsumeBrowserSession
(
c
.
Request
.
Context
(),
sessionToken
,
browserSessionKey
);
err
!=
nil
{
if
_
,
err
:=
pendingSvc
.
ConsumeBrowserSession
(
c
.
Request
.
Context
(),
sessionToken
,
browserSessionKey
);
err
!=
nil
{
clearOAuthPendingSessionCookie
(
c
,
secureCookie
)
clearOAuthPendingSessionCookie
(
c
,
secureCookie
)
clearOAuthPendingBrowserCookie
(
c
,
secureCookie
)
clearOAuthPendingBrowserCookie
(
c
,
secureCookie
)
...
...
backend/internal/handler/auth_oidc_oauth_test.go
View file @
7c6491c2
...
@@ -431,6 +431,58 @@ func TestCompleteOIDCOAuthRegistrationAppliesPendingAdoptionDecision(t *testing.
...
@@ -431,6 +431,58 @@ func TestCompleteOIDCOAuthRegistrationAppliesPendingAdoptionDecision(t *testing.
require
.
NotNil
(
t
,
consumed
.
ConsumedAt
)
require
.
NotNil
(
t
,
consumed
.
ConsumedAt
)
}
}
func
TestCompleteOIDCOAuthRegistrationRejectsAdoptExistingUserSession
(
t
*
testing
.
T
)
{
handler
,
client
:=
newOAuthPendingFlowTestHandler
(
t
,
false
)
ctx
:=
context
.
Background
()
existingUser
,
err
:=
client
.
User
.
Create
()
.
SetEmail
(
"owner@example.com"
)
.
SetUsername
(
"owner-user"
)
.
SetPasswordHash
(
"hash"
)
.
SetRole
(
service
.
RoleUser
)
.
SetStatus
(
service
.
StatusActive
)
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
session
,
err
:=
client
.
PendingAuthSession
.
Create
()
.
SetSessionToken
(
"oidc-complete-invalid-session"
)
.
SetIntent
(
"adopt_existing_user_by_email"
)
.
SetProviderType
(
"oidc"
)
.
SetProviderKey
(
"https://issuer.example.com"
)
.
SetProviderSubject
(
"oidc-invalid-subject-1"
)
.
SetTargetUserID
(
existingUser
.
ID
)
.
SetResolvedEmail
(
existingUser
.
Email
)
.
SetBrowserSessionKey
(
"oidc-invalid-browser"
)
.
SetUpstreamIdentityClaims
(
map
[
string
]
any
{
"username"
:
"oidc_user"
,
})
.
SetLocalFlowState
(
map
[
string
]
any
{
oauthCompletionResponseKey
:
map
[
string
]
any
{
"step"
:
"bind_login_required"
,
},
})
.
SetExpiresAt
(
time
.
Now
()
.
UTC
()
.
Add
(
10
*
time
.
Minute
))
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
body
:=
bytes
.
NewBufferString
(
`{"invitation_code":"invite-1"}`
)
recorder
:=
httptest
.
NewRecorder
()
c
,
_
:=
gin
.
CreateTestContext
(
recorder
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/auth/oauth/oidc/complete-registration"
,
body
)
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingSessionCookieName
,
Value
:
encodeCookieValue
(
session
.
SessionToken
)})
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingBrowserCookieName
,
Value
:
encodeCookieValue
(
"oidc-invalid-browser"
)})
c
.
Request
=
req
handler
.
CompleteOIDCOAuthRegistration
(
c
)
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
recorder
.
Code
)
storedSession
,
err
:=
client
.
PendingAuthSession
.
Get
(
ctx
,
session
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
storedSession
.
ConsumedAt
)
}
type
oidcProviderFixture
struct
{
type
oidcProviderFixture
struct
{
Subject
string
Subject
string
PreferredUsername
string
PreferredUsername
string
...
...
backend/internal/handler/auth_wechat_oauth.go
View file @
7c6491c2
...
@@ -506,6 +506,14 @@ func (h *AuthHandler) CompleteWeChatOAuthRegistration(c *gin.Context) {
...
@@ -506,6 +506,14 @@ func (h *AuthHandler) CompleteWeChatOAuthRegistration(c *gin.Context) {
response
.
ErrorFrom
(
c
,
err
)
response
.
ErrorFrom
(
c
,
err
)
return
return
}
}
if
err
:=
ensurePendingOAuthCompleteRegistrationSession
(
session
);
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
if
err
:=
h
.
ensureBackendModeAllowsNewUserLogin
(
c
.
Request
.
Context
());
err
!=
nil
{
response
.
ErrorFrom
(
c
,
err
)
return
}
email
:=
strings
.
TrimSpace
(
session
.
ResolvedEmail
)
email
:=
strings
.
TrimSpace
(
session
.
ResolvedEmail
)
username
:=
pendingSessionStringValue
(
session
.
UpstreamIdentityClaims
,
"username"
)
username
:=
pendingSessionStringValue
(
session
.
UpstreamIdentityClaims
,
"username"
)
...
@@ -531,6 +539,7 @@ func (h *AuthHandler) CompleteWeChatOAuthRegistration(c *gin.Context) {
...
@@ -531,6 +539,7 @@ func (h *AuthHandler) CompleteWeChatOAuthRegistration(c *gin.Context) {
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_ADOPTION_APPLY_FAILED"
,
"failed to apply oauth profile adoption"
)
.
WithCause
(
err
))
response
.
ErrorFrom
(
c
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_ADOPTION_APPLY_FAILED"
,
"failed to apply oauth profile adoption"
)
.
WithCause
(
err
))
return
return
}
}
h
.
authService
.
RecordSuccessfulLogin
(
c
.
Request
.
Context
(),
user
.
ID
)
if
_
,
err
:=
pendingSvc
.
ConsumeBrowserSession
(
c
.
Request
.
Context
(),
sessionToken
,
browserSessionKey
);
err
!=
nil
{
if
_
,
err
:=
pendingSvc
.
ConsumeBrowserSession
(
c
.
Request
.
Context
(),
sessionToken
,
browserSessionKey
);
err
!=
nil
{
clearOAuthPendingSessionCookie
(
c
,
secureCookie
)
clearOAuthPendingSessionCookie
(
c
,
secureCookie
)
clearOAuthPendingBrowserCookie
(
c
,
secureCookie
)
clearOAuthPendingBrowserCookie
(
c
,
secureCookie
)
...
...
backend/internal/handler/auth_wechat_oauth_test.go
View file @
7c6491c2
...
@@ -846,6 +846,59 @@ func TestWeChatOAuthCallbackRepairsLegacyOpenIDOnlyIdentity(t *testing.T) {
...
@@ -846,6 +846,59 @@ func TestWeChatOAuthCallbackRepairsLegacyOpenIDOnlyIdentity(t *testing.T) {
require
.
Equal
(
t
,
repairedIdentity
.
ID
,
channel
.
IdentityID
)
require
.
Equal
(
t
,
repairedIdentity
.
ID
,
channel
.
IdentityID
)
}
}
func
TestCompleteWeChatOAuthRegistrationRejectsAdoptExistingUserSession
(
t
*
testing
.
T
)
{
handler
,
client
:=
newWeChatOAuthTestHandler
(
t
,
false
)
defer
client
.
Close
()
ctx
:=
context
.
Background
()
existingUser
,
err
:=
client
.
User
.
Create
()
.
SetEmail
(
"owner@example.com"
)
.
SetUsername
(
"owner-user"
)
.
SetPasswordHash
(
"hash"
)
.
SetRole
(
service
.
RoleUser
)
.
SetStatus
(
service
.
StatusActive
)
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
session
,
err
:=
client
.
PendingAuthSession
.
Create
()
.
SetSessionToken
(
"wechat-complete-invalid-session"
)
.
SetIntent
(
"adopt_existing_user_by_email"
)
.
SetProviderType
(
"wechat"
)
.
SetProviderKey
(
"wechat-main"
)
.
SetProviderSubject
(
"union-invalid-1"
)
.
SetTargetUserID
(
existingUser
.
ID
)
.
SetResolvedEmail
(
existingUser
.
Email
)
.
SetBrowserSessionKey
(
"wechat-invalid-browser"
)
.
SetUpstreamIdentityClaims
(
map
[
string
]
any
{
"username"
:
"wechat_user"
,
})
.
SetLocalFlowState
(
map
[
string
]
any
{
oauthCompletionResponseKey
:
map
[
string
]
any
{
"step"
:
"bind_login_required"
,
},
})
.
SetExpiresAt
(
time
.
Now
()
.
UTC
()
.
Add
(
10
*
time
.
Minute
))
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
body
:=
bytes
.
NewBufferString
(
`{"invitation_code":"invite-1"}`
)
recorder
:=
httptest
.
NewRecorder
()
completeCtx
,
_
:=
gin
.
CreateTestContext
(
recorder
)
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/api/v1/auth/oauth/wechat/complete-registration"
,
body
)
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingSessionCookieName
,
Value
:
encodeCookieValue
(
session
.
SessionToken
)})
req
.
AddCookie
(
&
http
.
Cookie
{
Name
:
oauthPendingBrowserCookieName
,
Value
:
encodeCookieValue
(
"wechat-invalid-browser"
)})
completeCtx
.
Request
=
req
handler
.
CompleteWeChatOAuthRegistration
(
completeCtx
)
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
recorder
.
Code
)
storedSession
,
err
:=
client
.
PendingAuthSession
.
Get
(
ctx
,
session
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
storedSession
.
ConsumedAt
)
}
func
TestWeChatOAuthCallbackRepairsLegacyProviderKeyCanonicalIdentity
(
t
*
testing
.
T
)
{
func
TestWeChatOAuthCallbackRepairsLegacyProviderKeyCanonicalIdentity
(
t
*
testing
.
T
)
{
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_ID"
,
"wx-open-app"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
t
.
Setenv
(
"WECHAT_OAUTH_OPEN_APP_SECRET"
,
"wx-open-secret"
)
...
...
backend/internal/service/auth_oauth_email_flow.go
View file @
7c6491c2
...
@@ -104,7 +104,7 @@ func (s *AuthService) RegisterOAuthEmailAccount(
...
@@ -104,7 +104,7 @@ func (s *AuthService) RegisterOAuthEmailAccount(
return
nil
,
nil
,
ErrServiceUnavailable
return
nil
,
nil
,
ErrServiceUnavailable
}
}
s
.
postAuthUserBootstrap
(
ctx
,
user
,
signupSource
,
tru
e
)
s
.
postAuthUserBootstrap
(
ctx
,
user
,
signupSource
,
fals
e
)
s
.
assignSubscriptions
(
ctx
,
user
.
ID
,
grantPlan
.
Subscriptions
,
"auto assigned by signup defaults"
)
s
.
assignSubscriptions
(
ctx
,
user
.
ID
,
grantPlan
.
Subscriptions
,
"auto assigned by signup defaults"
)
if
invitationRedeemCode
!=
nil
{
if
invitationRedeemCode
!=
nil
{
...
...
backend/internal/service/auth_service.go
View file @
7c6491c2
...
@@ -430,8 +430,6 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (string
...
@@ -430,8 +430,6 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (string
if
!
user
.
IsActive
()
{
if
!
user
.
IsActive
()
{
return
""
,
nil
,
ErrUserNotActive
return
""
,
nil
,
ErrUserNotActive
}
}
s
.
backfillEmailIdentityOnSuccessfulLogin
(
ctx
,
user
)
s
.
touchUserLogin
(
ctx
,
user
.
ID
)
// 生成JWT token
// 生成JWT token
token
,
err
:=
s
.
GenerateToken
(
user
)
token
,
err
:=
s
.
GenerateToken
(
user
)
...
@@ -507,7 +505,7 @@ func (s *AuthService) LoginOrRegisterOAuth(ctx context.Context, email, username
...
@@ -507,7 +505,7 @@ func (s *AuthService) LoginOrRegisterOAuth(ctx context.Context, email, username
}
}
}
else
{
}
else
{
user
=
newUser
user
=
newUser
s
.
postAuthUserBootstrap
(
ctx
,
user
,
signupSource
,
tru
e
)
s
.
postAuthUserBootstrap
(
ctx
,
user
,
signupSource
,
fals
e
)
s
.
assignSubscriptions
(
ctx
,
user
.
ID
,
grantPlan
.
Subscriptions
,
"auto assigned by signup defaults"
)
s
.
assignSubscriptions
(
ctx
,
user
.
ID
,
grantPlan
.
Subscriptions
,
"auto assigned by signup defaults"
)
}
}
}
else
{
}
else
{
...
@@ -527,8 +525,6 @@ func (s *AuthService) LoginOrRegisterOAuth(ctx context.Context, email, username
...
@@ -527,8 +525,6 @@ func (s *AuthService) LoginOrRegisterOAuth(ctx context.Context, email, username
logger
.
LegacyPrintf
(
"service.auth"
,
"[Auth] Failed to update username after oauth login: %v"
,
err
)
logger
.
LegacyPrintf
(
"service.auth"
,
"[Auth] Failed to update username after oauth login: %v"
,
err
)
}
}
}
}
s
.
touchUserLogin
(
ctx
,
user
.
ID
)
token
,
err
:=
s
.
GenerateToken
(
user
)
token
,
err
:=
s
.
GenerateToken
(
user
)
if
err
!=
nil
{
if
err
!=
nil
{
return
""
,
nil
,
fmt
.
Errorf
(
"generate token: %w"
,
err
)
return
""
,
nil
,
fmt
.
Errorf
(
"generate token: %w"
,
err
)
...
@@ -634,7 +630,7 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
...
@@ -634,7 +630,7 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
return
nil
,
nil
,
ErrServiceUnavailable
return
nil
,
nil
,
ErrServiceUnavailable
}
}
user
=
newUser
user
=
newUser
s
.
postAuthUserBootstrap
(
ctx
,
user
,
signupSource
,
tru
e
)
s
.
postAuthUserBootstrap
(
ctx
,
user
,
signupSource
,
fals
e
)
s
.
assignSubscriptions
(
ctx
,
user
.
ID
,
grantPlan
.
Subscriptions
,
"auto assigned by signup defaults"
)
s
.
assignSubscriptions
(
ctx
,
user
.
ID
,
grantPlan
.
Subscriptions
,
"auto assigned by signup defaults"
)
}
}
}
else
{
}
else
{
...
@@ -651,7 +647,7 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
...
@@ -651,7 +647,7 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
}
}
}
else
{
}
else
{
user
=
newUser
user
=
newUser
s
.
postAuthUserBootstrap
(
ctx
,
user
,
signupSource
,
tru
e
)
s
.
postAuthUserBootstrap
(
ctx
,
user
,
signupSource
,
fals
e
)
s
.
assignSubscriptions
(
ctx
,
user
.
ID
,
grantPlan
.
Subscriptions
,
"auto assigned by signup defaults"
)
s
.
assignSubscriptions
(
ctx
,
user
.
ID
,
grantPlan
.
Subscriptions
,
"auto assigned by signup defaults"
)
if
invitationRedeemCode
!=
nil
{
if
invitationRedeemCode
!=
nil
{
if
err
:=
s
.
redeemRepo
.
Use
(
ctx
,
invitationRedeemCode
.
ID
,
user
.
ID
);
err
!=
nil
{
if
err
:=
s
.
redeemRepo
.
Use
(
ctx
,
invitationRedeemCode
.
ID
,
user
.
ID
);
err
!=
nil
{
...
@@ -676,8 +672,6 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
...
@@ -676,8 +672,6 @@ func (s *AuthService) LoginOrRegisterOAuthWithTokenPair(ctx context.Context, ema
logger
.
LegacyPrintf
(
"service.auth"
,
"[Auth] Failed to update username after oauth login: %v"
,
err
)
logger
.
LegacyPrintf
(
"service.auth"
,
"[Auth] Failed to update username after oauth login: %v"
,
err
)
}
}
}
}
s
.
touchUserLogin
(
ctx
,
user
.
ID
)
tokenPair
,
err
:=
s
.
GenerateTokenPair
(
ctx
,
user
,
""
)
tokenPair
,
err
:=
s
.
GenerateTokenPair
(
ctx
,
user
,
""
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
nil
,
fmt
.
Errorf
(
"generate token pair: %w"
,
err
)
return
nil
,
nil
,
fmt
.
Errorf
(
"generate token pair: %w"
,
err
)
...
...
backend/internal/service/auth_service_identity_sync_test.go
View file @
7c6491c2
...
@@ -170,24 +170,26 @@ func TestAuthServiceRegisterDualWritesEmailIdentity(t *testing.T) {
...
@@ -170,24 +170,26 @@ func TestAuthServiceRegisterDualWritesEmailIdentity(t *testing.T) {
require
.
NotNil
(
t
,
identity
.
VerifiedAt
)
require
.
NotNil
(
t
,
identity
.
VerifiedAt
)
}
}
func
TestAuthServiceLogin
TouchesLast
Login
At
(
t
*
testing
.
T
)
{
func
TestAuthServiceLogin
DefersLastLoginTouchUntilRecordSuccessful
Login
(
t
*
testing
.
T
)
{
svc
,
repo
,
client
:=
newAuthServiceWithEnt
(
t
,
map
[
string
]
string
{
svc
,
_
,
client
:=
newAuthServiceWithEnt
(
t
,
map
[
string
]
string
{
service
.
SettingKeyRegistrationEnabled
:
"true"
,
service
.
SettingKeyRegistrationEnabled
:
"true"
,
},
nil
)
},
nil
)
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
user
:=
&
service
.
User
{
passwordHash
,
err
:=
svc
.
HashPassword
(
"password"
)
Email
:
"login@example.com"
,
require
.
NoError
(
t
,
err
)
Role
:
service
.
RoleUser
,
user
,
err
:=
client
.
User
.
Create
()
.
Status
:
service
.
StatusActive
,
SetEmail
(
"login@example.com"
)
.
Balance
:
1
,
SetPasswordHash
(
passwordHash
)
.
Concurrency
:
1
,
SetRole
(
service
.
RoleUser
)
.
}
SetStatus
(
service
.
StatusActive
)
.
require
.
NoError
(
t
,
user
.
SetPassword
(
"password"
))
SetBalance
(
1
)
.
require
.
NoError
(
t
,
repo
.
Create
(
ctx
,
user
))
SetConcurrency
(
1
)
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
old
:=
time
.
Now
()
.
Add
(
-
2
*
time
.
Hour
)
.
UTC
()
.
Round
(
time
.
Second
)
old
:=
time
.
Now
()
.
Add
(
-
2
*
time
.
Hour
)
.
UTC
()
.
Round
(
time
.
Second
)
_
,
err
:
=
client
.
User
.
UpdateOneID
(
user
.
ID
)
.
_
,
err
=
client
.
User
.
UpdateOneID
(
user
.
ID
)
.
SetLastLoginAt
(
old
)
.
SetLastLoginAt
(
old
)
.
SetLastActiveAt
(
old
)
.
SetLastActiveAt
(
old
)
.
Save
(
ctx
)
Save
(
ctx
)
...
@@ -202,8 +204,20 @@ func TestAuthServiceLoginTouchesLastLoginAt(t *testing.T) {
...
@@ -202,8 +204,20 @@ func TestAuthServiceLoginTouchesLastLoginAt(t *testing.T) {
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
storedUser
.
LastLoginAt
)
require
.
NotNil
(
t
,
storedUser
.
LastLoginAt
)
require
.
NotNil
(
t
,
storedUser
.
LastActiveAt
)
require
.
NotNil
(
t
,
storedUser
.
LastActiveAt
)
require
.
True
(
t
,
storedUser
.
LastLoginAt
.
After
(
old
))
require
.
True
(
t
,
storedUser
.
LastLoginAt
.
Equal
(
old
))
require
.
True
(
t
,
storedUser
.
LastActiveAt
.
After
(
old
))
require
.
True
(
t
,
storedUser
.
LastActiveAt
.
Equal
(
old
))
identityCount
,
err
:=
client
.
AuthIdentity
.
Query
()
.
Where
(
authidentity
.
ProviderTypeEQ
(
"email"
),
authidentity
.
ProviderKeyEQ
(
"email"
),
authidentity
.
ProviderSubjectEQ
(
"login@example.com"
),
)
.
Count
(
ctx
)
require
.
NoError
(
t
,
err
)
require
.
Zero
(
t
,
identityCount
)
svc
.
RecordSuccessfulLogin
(
ctx
,
user
.
ID
)
identity
,
err
:=
client
.
AuthIdentity
.
Query
()
.
identity
,
err
:=
client
.
AuthIdentity
.
Query
()
.
Where
(
Where
(
...
@@ -273,6 +287,7 @@ func TestAuthServiceLogin_AppliesEmailFirstBindDefaultsOnlyWhenEmailIdentityIsNe
...
@@ -273,6 +287,7 @@ func TestAuthServiceLogin_AppliesEmailFirstBindDefaultsOnlyWhenEmailIdentityIsNe
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
NotEmpty
(
t
,
token
)
require
.
NotEmpty
(
t
,
token
)
require
.
NotNil
(
t
,
gotUser
)
require
.
NotNil
(
t
,
gotUser
)
svc
.
RecordSuccessfulLogin
(
ctx
,
user
.
ID
)
storedUser
,
err
:=
client
.
User
.
Get
(
ctx
,
user
.
ID
)
storedUser
,
err
:=
client
.
User
.
Get
(
ctx
,
user
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
...
@@ -343,6 +358,7 @@ func TestAuthServiceLogin_DoesNotApplyEmailFirstBindDefaultsWhenIdentityAlreadyE
...
@@ -343,6 +358,7 @@ func TestAuthServiceLogin_DoesNotApplyEmailFirstBindDefaultsWhenIdentityAlreadyE
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
NotEmpty
(
t
,
token
)
require
.
NotEmpty
(
t
,
token
)
require
.
NotNil
(
t
,
gotUser
)
require
.
NotNil
(
t
,
gotUser
)
svc
.
RecordSuccessfulLogin
(
ctx
,
user
.
ID
)
storedUser
,
err
:=
client
.
User
.
Get
(
ctx
,
user
.
ID
)
storedUser
,
err
:=
client
.
User
.
Get
(
ctx
,
user
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
...
@@ -380,6 +396,7 @@ func TestAuthServiceLogin_RetriesEmailFirstBindDefaultsAfterPreviousFailure(t *t
...
@@ -380,6 +396,7 @@ func TestAuthServiceLogin_RetriesEmailFirstBindDefaultsAfterPreviousFailure(t *t
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
NotEmpty
(
t
,
token
)
require
.
NotEmpty
(
t
,
token
)
require
.
NotNil
(
t
,
gotUser
)
require
.
NotNil
(
t
,
gotUser
)
svc
.
RecordSuccessfulLogin
(
ctx
,
user
.
ID
)
storedUser
,
err
:=
client
.
User
.
Get
(
ctx
,
user
.
ID
)
storedUser
,
err
:=
client
.
User
.
Get
(
ctx
,
user
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
...
@@ -392,6 +409,7 @@ func TestAuthServiceLogin_RetriesEmailFirstBindDefaultsAfterPreviousFailure(t *t
...
@@ -392,6 +409,7 @@ func TestAuthServiceLogin_RetriesEmailFirstBindDefaultsAfterPreviousFailure(t *t
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
NotEmpty
(
t
,
token
)
require
.
NotEmpty
(
t
,
token
)
require
.
NotNil
(
t
,
gotUser
)
require
.
NotNil
(
t
,
gotUser
)
svc
.
RecordSuccessfulLogin
(
ctx
,
user
.
ID
)
storedUser
,
err
=
client
.
User
.
Get
(
ctx
,
user
.
ID
)
storedUser
,
err
=
client
.
User
.
Get
(
ctx
,
user
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment