Preserve continue URL on auth error re-login

Failed OIDC, magic-link, and SAML sign-ins sent users to /auth/error
without the post-login destination, so Sign in dropped OAuth flows
and deep links. Propagate a validated continue query through auth
error redirects, recover it from OIDC state when the IdP denies or
cancels login, and forward it from AuthErrorPage to /auth/login.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
Cursor Agent
2026-07-27 10:27:38 +00:00
committed by Bryan Frimin
parent 428d28fade
commit 9abea50507
8 changed files with 205 additions and 29 deletions

View File

@@ -381,6 +381,53 @@ func (s *Service) InitiateLogin(
return authURL, nil
}
// ContinueURLFromCallbackState loads and removes a pending login state and returns
// its continue URL. Used when the IdP callback fails before an authorization code
// is exchanged (user denied consent, cancelled login, etc.).
func (s *Service) ContinueURLFromCallbackState(
ctx context.Context,
provider coredata.OIDCProvider,
stateParam string,
) (string, error) {
if stateParam == "" {
return "", NewInvalidStateError()
}
var oidcState coredata.OIDCState
err := s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
if err := oidcState.LoadByIDForUpdate(ctx, tx, stateParam); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return NewInvalidStateError()
}
return fmt.Errorf("cannot load oidc state: %w", err)
}
if err := oidcState.Delete(ctx, tx); err != nil {
return fmt.Errorf("cannot delete oidc state: %w", err)
}
return nil
},
)
if err != nil {
return "", err
}
if time.Now().After(oidcState.ExpiresAt) {
return "", NewInvalidStateError()
}
if oidcState.Provider != provider {
return "", NewInvalidStateError()
}
return oidcState.ContinueURL, nil
}
func (s *Service) HandleCallback(
ctx context.Context,
provider coredata.OIDCProvider,
@@ -417,11 +464,11 @@ func (s *Service) HandleCallback(
}
if time.Now().After(oidcState.ExpiresAt) {
return nil, "", nil, NewInvalidStateError()
return nil, oidcState.ContinueURL, nil, NewInvalidStateError()
}
if oidcState.Provider != provider {
return nil, "", nil, NewInvalidStateError()
return nil, oidcState.ContinueURL, nil, NewInvalidStateError()
}
token, err := info.oauth2Config.Exchange(
@@ -430,26 +477,26 @@ func (s *Service) HandleCallback(
oauth2.SetAuthURLParam("code_verifier", oidcState.CodeVerifier),
)
if err != nil {
return nil, "", nil, NewCodeExchangeError(err)
return nil, oidcState.ContinueURL, nil, NewCodeExchangeError(err)
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, "", nil, NewIDTokenMissingError()
return nil, oidcState.ContinueURL, nil, NewIDTokenMissingError()
}
claims, err := s.verifyAndParseIDToken(ctx, info, rawIDToken, oidcState.Nonce)
if err != nil {
return nil, "", nil, fmt.Errorf("cannot verify id token: %w", err)
return nil, oidcState.ContinueURL, nil, fmt.Errorf("cannot verify id token: %w", err)
}
if err := validateIDTokenClaims(info, claims); err != nil {
return nil, "", nil, err
return nil, oidcState.ContinueURL, nil, err
}
email, err := mail.ParseAddr(claims.Email)
if err != nil {
return nil, "", nil, fmt.Errorf("cannot parse email from id token: %w", err)
return nil, oidcState.ContinueURL, nil, fmt.Errorf("cannot parse email from id token: %w", err)
}
var identity *coredata.Identity
@@ -496,7 +543,7 @@ func (s *Service) HandleCallback(
},
)
if err != nil {
return nil, "", nil, err
return nil, oidcState.ContinueURL, nil, err
}
return identity, oidcState.ContinueURL, oidcState.OrganizationID, nil