fix(auth): require explicit choice for third-party signup
This commit is contained in:
@@ -32,6 +32,7 @@ const (
|
||||
oauthPendingSessionCookiePath = "/api/v1/auth/oauth"
|
||||
oauthPendingSessionCookieName = "oauth_pending_session"
|
||||
oauthPendingCookieMaxAgeSec = 10 * 60
|
||||
oauthPendingChoiceStep = "choose_account_action_required"
|
||||
|
||||
oauthCompletionResponseKey = "completion_response"
|
||||
)
|
||||
@@ -431,8 +432,9 @@ func (h *AuthHandler) createOAuthEmailRequiredPendingSession(
|
||||
BrowserSessionKey: browserSessionKey,
|
||||
UpstreamIdentityClaims: upstreamClaims,
|
||||
CompletionResponse: map[string]any{
|
||||
"redirect": redirectTo,
|
||||
"step": "email_required",
|
||||
"redirect": strings.TrimSpace(redirectTo),
|
||||
"step": oauthPendingChoiceStep,
|
||||
"adoption_required": true,
|
||||
"force_email_on_signup": true,
|
||||
"email_binding_required": true,
|
||||
"existing_account_bindable": true,
|
||||
@@ -492,7 +494,7 @@ func (h *AuthHandler) SendPendingOAuthVerifyCode(c *gin.Context) {
|
||||
|
||||
email := strings.TrimSpace(strings.ToLower(req.Email))
|
||||
if existingUser, err := findUserByNormalizedEmail(c.Request.Context(), client, email); err == nil && existingUser != nil {
|
||||
session, err = h.transitionPendingOAuthAccountToBindLogin(c, client, session, email, oauthAdoptionDecisionRequest{})
|
||||
session, err = h.transitionPendingOAuthAccountToChoiceState(c, client, session, email)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
@@ -1206,12 +1208,13 @@ func readPendingOAuthBrowserSession(c *gin.Context, h *AuthHandler) (*service.Au
|
||||
}
|
||||
|
||||
func buildPendingOAuthSessionStatusPayload(session *dbent.PendingAuthSession) gin.H {
|
||||
completionResponse := normalizePendingOAuthCompletionResponse(mergePendingCompletionResponse(session, nil))
|
||||
payload := gin.H{
|
||||
"auth_result": "pending_session",
|
||||
"provider": strings.TrimSpace(session.ProviderType),
|
||||
"intent": strings.TrimSpace(session.Intent),
|
||||
}
|
||||
for key, value := range mergePendingCompletionResponse(session, nil) {
|
||||
for key, value := range completionResponse {
|
||||
payload[key] = value
|
||||
}
|
||||
if email := strings.TrimSpace(session.ResolvedEmail); email != "" {
|
||||
@@ -1220,38 +1223,58 @@ func buildPendingOAuthSessionStatusPayload(session *dbent.PendingAuthSession) gi
|
||||
return payload
|
||||
}
|
||||
|
||||
func (h *AuthHandler) transitionPendingOAuthAccountToBindLogin(
|
||||
func normalizePendingOAuthCompletionResponse(payload map[string]any) map[string]any {
|
||||
normalized := clonePendingMap(payload)
|
||||
step := strings.ToLower(strings.TrimSpace(pendingSessionStringValue(normalized, "step")))
|
||||
switch step {
|
||||
case "choice", "choose_account_action", "choose_account", "choose", "email_required", "bind_login_required":
|
||||
normalized["step"] = oauthPendingChoiceStep
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(pendingSessionStringValue(normalized, "step")), oauthPendingChoiceStep) {
|
||||
normalized["adoption_required"] = true
|
||||
}
|
||||
if _, exists := normalized["adoption_required"]; !exists {
|
||||
if _, hasChoiceFields := normalized["email_binding_required"]; hasChoiceFields {
|
||||
normalized["adoption_required"] = true
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func pendingOAuthChoiceCompletionResponse(session *dbent.PendingAuthSession, email string) map[string]any {
|
||||
response := mergePendingCompletionResponse(session, map[string]any{
|
||||
"step": oauthPendingChoiceStep,
|
||||
"adoption_required": true,
|
||||
"force_email_on_signup": true,
|
||||
"email_binding_required": true,
|
||||
"existing_account_bindable": true,
|
||||
})
|
||||
if email = strings.TrimSpace(email); email != "" {
|
||||
response["email"] = email
|
||||
response["resolved_email"] = email
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *AuthHandler) transitionPendingOAuthAccountToChoiceState(
|
||||
c *gin.Context,
|
||||
client *dbent.Client,
|
||||
session *dbent.PendingAuthSession,
|
||||
email string,
|
||||
decision oauthAdoptionDecisionRequest,
|
||||
) (*dbent.PendingAuthSession, error) {
|
||||
existingUser, err := findUserByNormalizedEmail(c.Request.Context(), client, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
completionResponse := mergePendingCompletionResponse(session, map[string]any{
|
||||
"step": "bind_login_required",
|
||||
"email": email,
|
||||
})
|
||||
session, err = updatePendingOAuthSessionProgress(
|
||||
completionResponse := pendingOAuthChoiceCompletionResponse(session, email)
|
||||
session, err := updatePendingOAuthSessionProgress(
|
||||
c.Request.Context(),
|
||||
client,
|
||||
session,
|
||||
"adopt_existing_user_by_email",
|
||||
strings.TrimSpace(session.Intent),
|
||||
email,
|
||||
&existingUser.ID,
|
||||
nil,
|
||||
completionResponse,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, infraerrors.InternalServer("PENDING_AUTH_SESSION_UPDATE_FAILED", "failed to update pending oauth session").WithCause(err)
|
||||
}
|
||||
|
||||
if _, err := h.ensurePendingOAuthAdoptionDecision(c, session.ID, decision); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
@@ -1365,12 +1388,20 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
|
||||
|
||||
email := strings.TrimSpace(strings.ToLower(req.Email))
|
||||
existingUser, err := findUserByNormalizedEmail(c.Request.Context(), client, email)
|
||||
if err != nil && !errors.Is(err, service.ErrUserNotFound) {
|
||||
response.ErrorFrom(c, infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "service temporarily unavailable"))
|
||||
return
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrUserNotFound):
|
||||
existingUser = nil
|
||||
case infraerrors.Code(err) >= http.StatusBadRequest && infraerrors.Code(err) < http.StatusInternalServerError:
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
default:
|
||||
response.ErrorFrom(c, infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "service temporarily unavailable"))
|
||||
return
|
||||
}
|
||||
}
|
||||
if existingUser != nil {
|
||||
session, err = h.transitionPendingOAuthAccountToBindLogin(c, client, session, email, req.adoptionDecision())
|
||||
session, err = h.transitionPendingOAuthAccountToChoiceState(c, client, session, email)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
@@ -1393,7 +1424,7 @@ func (h *AuthHandler) createPendingOAuthAccount(c *gin.Context, provider string)
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrEmailExists) {
|
||||
session, err = h.transitionPendingOAuthAccountToBindLogin(c, client, session, email, req.adoptionDecision())
|
||||
session, err = h.transitionPendingOAuthAccountToChoiceState(c, client, session, email)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
@@ -1548,6 +1579,7 @@ func (h *AuthHandler) ExchangePendingOAuthCompletion(c *gin.Context) {
|
||||
response.ErrorFrom(c, infraerrors.InternalServer("PENDING_AUTH_COMPLETION_INVALID", "pending auth completion payload is invalid"))
|
||||
return
|
||||
}
|
||||
payload = normalizePendingOAuthCompletionResponse(payload)
|
||||
if strings.TrimSpace(session.RedirectTo) != "" {
|
||||
if _, exists := payload["redirect"]; !exists {
|
||||
payload["redirect"] = session.RedirectTo
|
||||
|
||||
Reference in New Issue
Block a user