feat(oidc): provide reason of autolink failure
This commit is contained in:
@@ -56,6 +56,14 @@ pub enum OidcCallbackResult {
|
|||||||
/// `/profile?link_error=<reason>` redirect. See
|
/// `/profile?link_error=<reason>` redirect. See
|
||||||
/// docs/plan/oidc-account-linking.md § Safety checks.
|
/// docs/plan/oidc-account-linking.md § Safety checks.
|
||||||
LinkRefused { reason: &'static str },
|
LinkRefused { reason: &'static str },
|
||||||
|
/// Auto-link decision refused during the OIDC LOGIN callback path
|
||||||
|
/// (existing local user matched by email but the decision tree
|
||||||
|
/// rejected). `reason` is one of `auto_link_disabled`,
|
||||||
|
/// `auto_link_email_not_verified`, `already_linked_elsewhere`;
|
||||||
|
/// the handler maps each to a distinct CamelCase `error_type`
|
||||||
|
/// on the 409 response so the login page can switch on it.
|
||||||
|
/// See docs/plan/oidc-account-linking.md § Auto-link.
|
||||||
|
AutoLinkRefused { reason: &'static str },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Outcome of a successful magic-link redemption. The auth tokens are
|
/// Outcome of a successful magic-link redemption. The auth tokens are
|
||||||
@@ -3877,14 +3885,15 @@ impl AuthApplicationService {
|
|||||||
reason = reason,
|
reason = reason,
|
||||||
"🔗 auto-link refused",
|
"🔗 auto-link refused",
|
||||||
);
|
);
|
||||||
return Err(DomainError::new(
|
// Ok(AutoLinkRefused) rather than Err(AlreadyExists)
|
||||||
ErrorKind::AlreadyExists,
|
// so the handler can map each reason to a distinct
|
||||||
"OIDC",
|
// stable CamelCase error_type (AutoLinkDisabled /
|
||||||
format!(
|
// AutoLinkEmailNotVerified / AutoLinkAlreadyLinked-
|
||||||
"A user with email '{}' already exists. Contact admin to link your OIDC identity.",
|
// Elsewhere). Bubbling as a generic AlreadyExists
|
||||||
oidc_email
|
// would collapse all three reasons into "Already
|
||||||
),
|
// Exists" on the wire and leave the SPA without a
|
||||||
));
|
// switch arm for user-facing copy.
|
||||||
|
return Ok(OidcCallbackResult::AutoLinkRefused { reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
// All checks passed — commit the auto-link, re-fetch
|
// All checks passed — commit the auto-link, re-fetch
|
||||||
|
|||||||
@@ -1582,6 +1582,27 @@ pub async fn oidc_callback(
|
|||||||
);
|
);
|
||||||
Ok(Redirect::temporary(&redirect_url).into_response())
|
Ok(Redirect::temporary(&redirect_url).into_response())
|
||||||
}
|
}
|
||||||
|
// Map each auto-link refusal reason to a distinct stable
|
||||||
|
// CamelCase `error_type`. The SPA switches on this to render
|
||||||
|
// targeted copy (contact-admin vs. verify-email-at-IdP vs.
|
||||||
|
// already-linked-elsewhere) rather than a generic error toast.
|
||||||
|
// Status stays 409 (CONFLICT) — semantically an existing user
|
||||||
|
// blocks the auto-provision path.
|
||||||
|
OidcCallbackResult::AutoLinkRefused { reason } => {
|
||||||
|
let error_type = match reason {
|
||||||
|
"auto_link_disabled" => "AutoLinkDisabled",
|
||||||
|
"auto_link_email_not_verified" => "AutoLinkEmailNotVerified",
|
||||||
|
"already_linked_elsewhere" => "AutoLinkAlreadyLinkedElsewhere",
|
||||||
|
_ => "AutoLinkRefused",
|
||||||
|
};
|
||||||
|
Err(AppError::new(
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
"OIDC login blocked — a local account with this email already exists. \
|
||||||
|
Contact your administrator, or sign in with your existing credentials \
|
||||||
|
and connect SSO from your profile.",
|
||||||
|
error_type,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,9 @@
|
|||||||
# (iss, sub) miss but email matches admin, so it auto-
|
# (iss, sub) miss but email matches admin, so it auto-
|
||||||
# links + logs admin in.
|
# links + logs admin in.
|
||||||
# 2. Auto-link refused — email_verified=false. Callback
|
# 2. Auto-link refused — email_verified=false. Callback
|
||||||
# returns HTTP 409 with error_type "Already Exists" (the
|
# returns HTTP 409 with error_type "AutoLinkEmailNotVerified"
|
||||||
# "contact admin to link your OIDC identity" refusal).
|
# (one of three distinct auto-link refusal error_types —
|
||||||
|
# see auth_handler.rs AutoLinkRefused arm).
|
||||||
#
|
#
|
||||||
# [OIDC-only user]
|
# [OIDC-only user]
|
||||||
# 10. `oidc_user` unlink refused (would lock them out) with
|
# 10. `oidc_user` unlink refused (would lock them out) with
|
||||||
@@ -491,12 +492,10 @@ HTTP 200
|
|||||||
# "contact admin to link your OIDC identity" text that surfaces
|
# "contact admin to link your OIDC identity" text that surfaces
|
||||||
# in the SPA login form's error toast.
|
# in the SPA login form's error toast.
|
||||||
#
|
#
|
||||||
# NOTE: `error_type` here is "Already Exists" (with a space)
|
# The handler maps each auto-link refusal reason to a distinct
|
||||||
# because it comes from `ErrorKind::as_str()`, not from a
|
# CamelCase error_type — AutoLinkDisabled /
|
||||||
# handler-set stable key. The auto-link refusal branch is
|
# AutoLinkEmailNotVerified / AutoLinkAlreadyLinkedElsewhere —
|
||||||
# reusing the generic AlreadyExists mapping — a follow-up
|
# so the SPA can render targeted copy per refusal reason.
|
||||||
# could give it a dedicated `error_type` like
|
|
||||||
# `AutoLinkEmailNotVerified` for the SPA to switch on.
|
|
||||||
# ═════════════════════════════════════════════════════════════
|
# ═════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
|
||||||
@@ -522,7 +521,10 @@ location-trusted: true
|
|||||||
|
|
||||||
HTTP 409
|
HTTP 409
|
||||||
[Asserts]
|
[Asserts]
|
||||||
jsonpath "$.error_type" == "Already Exists"
|
# Distinct CamelCase key per auto-link refusal reason — the SPA
|
||||||
|
# switches on this to render "verify your email at the IdP" copy
|
||||||
|
# rather than the generic contact-admin fallback.
|
||||||
|
jsonpath "$.error_type" == "AutoLinkEmailNotVerified"
|
||||||
|
|
||||||
|
|
||||||
# Belt-and-braces invariant: admin's row is still un-linked
|
# Belt-and-braces invariant: admin's row is still un-linked
|
||||||
|
|||||||
Reference in New Issue
Block a user