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
aaf4946b
Commit
aaf4946b
authored
Apr 20, 2026
by
IanShaw027
Browse files
fix: normalize pending oauth email lookups
parent
31d0183d
Changes
2
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/auth_oauth_pending_flow.go
View file @
aaf4946b
...
@@ -3,6 +3,7 @@ package handler
...
@@ -3,6 +3,7 @@ package handler
import
(
import
(
"context"
"context"
"errors"
"errors"
"fmt"
"io"
"io"
"net/http"
"net/http"
"net/url"
"net/url"
...
@@ -12,12 +13,14 @@ import (
...
@@ -12,12 +13,14 @@ import (
"github.com/Wei-Shaw/sub2api/ent/authidentity"
"github.com/Wei-Shaw/sub2api/ent/authidentity"
"github.com/Wei-Shaw/sub2api/ent/authidentitychannel"
"github.com/Wei-Shaw/sub2api/ent/authidentitychannel"
"github.com/Wei-Shaw/sub2api/ent/identityadoptiondecision"
"github.com/Wei-Shaw/sub2api/ent/identityadoptiondecision"
"github.com/Wei-Shaw/sub2api/ent/predicate"
dbuser
"github.com/Wei-Shaw/sub2api/ent/user"
dbuser
"github.com/Wei-Shaw/sub2api/ent/user"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
infraerrors
"github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/Wei-Shaw/sub2api/internal/service"
entsql
"entgo.io/ent/dialect/sql"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
)
)
...
@@ -531,11 +534,9 @@ func resolvePendingOAuthTargetUserID(ctx context.Context, client *dbent.Client,
...
@@ -531,11 +534,9 @@ func resolvePendingOAuthTargetUserID(ctx context.Context, client *dbent.Client,
return
0
,
infraerrors
.
BadRequest
(
"PENDING_AUTH_TARGET_USER_MISSING"
,
"pending auth target user is missing"
)
return
0
,
infraerrors
.
BadRequest
(
"PENDING_AUTH_TARGET_USER_MISSING"
,
"pending auth target user is missing"
)
}
}
userEntity
,
err
:=
client
.
User
.
Query
()
.
userEntity
,
err
:=
findUserByNormalizedEmail
(
ctx
,
client
,
email
)
Where
(
dbuser
.
EmailEQ
(
email
))
.
Only
(
ctx
)
if
err
!=
nil
{
if
err
!=
nil
{
if
dbent
.
Is
NotFound
(
err
)
{
if
errors
.
Is
(
err
,
service
.
ErrUser
NotFound
)
{
return
0
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_TARGET_USER_NOT_FOUND"
,
"pending auth target user was not found"
)
return
0
,
infraerrors
.
InternalServer
(
"PENDING_AUTH_TARGET_USER_NOT_FOUND"
,
"pending auth target user was not found"
)
}
}
return
0
,
err
return
0
,
err
...
@@ -543,6 +544,40 @@ func resolvePendingOAuthTargetUserID(ctx context.Context, client *dbent.Client,
...
@@ -543,6 +544,40 @@ func resolvePendingOAuthTargetUserID(ctx context.Context, client *dbent.Client,
return
userEntity
.
ID
,
nil
return
userEntity
.
ID
,
nil
}
}
func
userNormalizedEmailPredicate
(
email
string
)
predicate
.
User
{
normalized
:=
strings
.
TrimSpace
(
email
)
if
normalized
==
""
{
return
dbuser
.
EmailEQ
(
email
)
}
return
predicate
.
User
(
func
(
s
*
entsql
.
Selector
)
{
s
.
Where
(
entsql
.
ExprP
(
fmt
.
Sprintf
(
"LOWER(TRIM(%s)) = LOWER(TRIM(?))"
,
s
.
C
(
dbuser
.
FieldEmail
)),
normalized
,
))
})
}
func
findUserByNormalizedEmail
(
ctx
context
.
Context
,
client
*
dbent
.
Client
,
email
string
)
(
*
dbent
.
User
,
error
)
{
if
client
==
nil
{
return
nil
,
infraerrors
.
ServiceUnavailable
(
"PENDING_AUTH_NOT_READY"
,
"pending auth service is not ready"
)
}
matches
,
err
:=
client
.
User
.
Query
()
.
Where
(
userNormalizedEmailPredicate
(
email
))
.
Order
(
dbent
.
Asc
(
dbuser
.
FieldID
))
.
All
(
ctx
)
if
err
!=
nil
{
return
nil
,
err
}
if
len
(
matches
)
==
0
{
return
nil
,
service
.
ErrUserNotFound
}
if
len
(
matches
)
>
1
{
return
nil
,
infraerrors
.
Conflict
(
"USER_EMAIL_CONFLICT"
,
"normalized email matched multiple users"
)
}
return
matches
[
0
],
nil
}
func
oauthIdentityIssuer
(
session
*
dbent
.
PendingAuthSession
)
*
string
{
func
oauthIdentityIssuer
(
session
*
dbent
.
PendingAuthSession
)
*
string
{
if
session
==
nil
{
if
session
==
nil
{
return
nil
return
nil
...
@@ -1102,8 +1137,8 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
...
@@ -1102,8 +1137,8 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
}
}
email
:=
strings
.
TrimSpace
(
strings
.
ToLower
(
req
.
Email
))
email
:=
strings
.
TrimSpace
(
strings
.
ToLower
(
req
.
Email
))
existingUser
,
err
:=
client
.
User
.
Query
()
.
Where
(
dbuser
.
EmailEQ
(
email
))
.
Only
(
c
.
Request
.
Context
())
existingUser
,
err
:=
findUserByNormalizedEmail
(
c
.
Request
.
Context
()
,
client
,
email
)
if
err
!=
nil
&&
!
dbent
.
Is
NotFound
(
err
)
{
if
err
!=
nil
&&
!
errors
.
Is
(
err
,
service
.
ErrUser
NotFound
)
{
response
.
ErrorFrom
(
c
,
infraerrors
.
ServiceUnavailable
(
"SERVICE_UNAVAILABLE"
,
"service temporarily unavailable"
))
response
.
ErrorFrom
(
c
,
infraerrors
.
ServiceUnavailable
(
"SERVICE_UNAVAILABLE"
,
"service temporarily unavailable"
))
return
return
}
}
...
...
backend/internal/handler/auth_oauth_pending_flow_test.go
View file @
aaf4946b
...
@@ -642,6 +642,60 @@ func TestCreateOIDCOAuthAccountExistingEmailReturnsAdoptExistingUserByEmailState
...
@@ -642,6 +642,60 @@ func TestCreateOIDCOAuthAccountExistingEmailReturnsAdoptExistingUserByEmailState
require
.
Zero
(
t
,
identityCount
)
require
.
Zero
(
t
,
identityCount
)
}
}
func
TestCreateOIDCOAuthAccountExistingEmailNormalizesLegacySpacingAndCase
(
t
*
testing
.
T
)
{
handler
,
client
:=
newOAuthPendingFlowTestHandlerWithEmailVerification
(
t
,
false
,
"owner@example.com"
,
"135790"
)
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
(
"existing-email-normalized-session-token"
)
.
SetIntent
(
"login"
)
.
SetProviderType
(
"oidc"
)
.
SetProviderKey
(
"https://issuer.example"
)
.
SetProviderSubject
(
"oidc-existing-normalized-123"
)
.
SetBrowserSessionKey
(
"existing-email-normalized-browser-session-key"
)
.
SetUpstreamIdentityClaims
(
map
[
string
]
any
{
"username"
:
"oidc_user"
,
"suggested_display_name"
:
"Existing OIDC User"
,
"suggested_avatar_url"
:
"https://cdn.example/existing.png"
,
})
.
SetRedirectTo
(
"/dashboard"
)
.
SetExpiresAt
(
time
.
Now
()
.
UTC
()
.
Add
(
10
*
time
.
Minute
))
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
body
:=
bytes
.
NewBufferString
(
`{"email":"owner@example.com","verify_code":"135790","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
(
"existing-email-normalized-browser-session-key"
)})
ginCtx
.
Request
=
req
handler
.
CreateOIDCOAuthAccount
(
ginCtx
)
require
.
Equal
(
t
,
http
.
StatusOK
,
recorder
.
Code
)
var
payload
map
[
string
]
any
require
.
NoError
(
t
,
json
.
Unmarshal
(
recorder
.
Body
.
Bytes
(),
&
payload
))
require
.
Equal
(
t
,
"adopt_existing_user_by_email"
,
payload
[
"intent"
])
storedSession
,
err
:=
client
.
PendingAuthSession
.
Get
(
ctx
,
session
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
storedSession
.
TargetUserID
)
require
.
Equal
(
t
,
existingUser
.
ID
,
*
storedSession
.
TargetUserID
)
require
.
Equal
(
t
,
"owner@example.com"
,
storedSession
.
ResolvedEmail
)
}
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
()
...
@@ -884,6 +938,37 @@ func TestBindOIDCOAuthLoginAppliesFirstBindGrantOnce(t *testing.T) {
...
@@ -884,6 +938,37 @@ func TestBindOIDCOAuthLoginAppliesFirstBindGrantOnce(t *testing.T) {
require
.
Equal
(
t
,
1
,
countProviderGrantRecords
(
t
,
client
,
existingUser
.
ID
,
"oidc"
,
"first_bind"
))
require
.
Equal
(
t
,
1
,
countProviderGrantRecords
(
t
,
client
,
existingUser
.
ID
,
"oidc"
,
"first_bind"
))
}
}
func
TestResolvePendingOAuthTargetUserIDNormalizesLegacySpacingAndCase
(
t
*
testing
.
T
)
{
handler
,
client
:=
newOAuthPendingFlowTestHandler
(
t
,
false
)
_
=
handler
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
(
"resolve-target-session-token"
)
.
SetIntent
(
"login"
)
.
SetProviderType
(
"oidc"
)
.
SetProviderKey
(
"https://issuer.example"
)
.
SetProviderSubject
(
"oidc-target-123"
)
.
SetResolvedEmail
(
"owner@example.com"
)
.
SetBrowserSessionKey
(
"resolve-target-browser-session-key"
)
.
SetExpiresAt
(
time
.
Now
()
.
UTC
()
.
Add
(
10
*
time
.
Minute
))
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
resolvedUserID
,
err
:=
resolvePendingOAuthTargetUserID
(
ctx
,
client
,
session
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
existingUser
.
ID
,
resolvedUserID
)
}
func
TestBindOIDCOAuthLoginReturns2FAChallengeWhenUserHasTotp
(
t
*
testing
.
T
)
{
func
TestBindOIDCOAuthLoginReturns2FAChallengeWhenUserHasTotp
(
t
*
testing
.
T
)
{
totpCache
:=
&
oauthPendingFlowTotpCacheStub
{}
totpCache
:=
&
oauthPendingFlowTotpCacheStub
{}
handler
,
client
:=
newOAuthPendingFlowTestHandlerWithDependencies
(
t
,
oauthPendingFlowTestHandlerOptions
{
handler
,
client
:=
newOAuthPendingFlowTestHandlerWithDependencies
(
t
,
oauthPendingFlowTestHandlerOptions
{
...
...
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