diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts
index f6b21ff6..bd633707 100644
--- a/server/routers/idp/validateOidcCallback.ts
+++ b/server/routers/idp/validateOidcCallback.ts
@@ -192,11 +192,71 @@ export async function validateOidcCallback(
state
});
- const tokens = await client.validateAuthorizationCode(
- ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl),
- code,
- codeVerifier
- );
+ let tokens: arctic.OAuth2Tokens;
+ try {
+ tokens = await client.validateAuthorizationCode(
+ ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl),
+ code,
+ codeVerifier
+ );
+ } catch (err: unknown) {
+ if (err instanceof arctic.OAuth2RequestError) {
+ logger.warn("OIDC provider rejected the authorization code", {
+ error: err.code,
+ description: err.description,
+ uri: err.uri,
+ state: err.state
+ });
+ return next(
+ createHttpError(
+ HttpCode.UNAUTHORIZED,
+ err.description ||
+ `OIDC provider rejected the request (${err.code})`
+ )
+ );
+ }
+
+ if (err instanceof arctic.UnexpectedResponseError) {
+ logger.error(
+ "OIDC provider returned an unexpected response during token exchange",
+ { status: err.status }
+ );
+ return next(
+ createHttpError(
+ HttpCode.BAD_GATEWAY,
+ "Received an unexpected response from the identity provider while exchanging the authorization code."
+ )
+ );
+ }
+
+ if (err instanceof arctic.UnexpectedErrorResponseBodyError) {
+ logger.error(
+ "OIDC provider returned an unexpected error payload during token exchange",
+ { status: err.status, data: err.data }
+ );
+ return next(
+ createHttpError(
+ HttpCode.BAD_GATEWAY,
+ "Identity provider returned an unexpected error payload while exchanging the authorization code."
+ )
+ );
+ }
+
+ if (err instanceof arctic.ArcticFetchError) {
+ logger.error(
+ "Failed to reach OIDC provider while exchanging authorization code",
+ { error: err.message }
+ );
+ return next(
+ createHttpError(
+ HttpCode.BAD_GATEWAY,
+ "Unable to reach the identity provider while exchanging the authorization code. Please try again."
+ )
+ );
+ }
+
+ throw err;
+ }
const idToken = tokens.idToken();
logger.debug("ID token", { idToken });
diff --git a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx
index 3d646084..7b3ccabf 100644
--- a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx
+++ b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx
@@ -14,8 +14,11 @@ export const dynamic = "force-dynamic";
export default async function Page(props: {
params: Promise<{ orgId: string; idpId: string }>;
searchParams: Promise<{
- code: string;
- state: string;
+ code?: string;
+ state?: string;
+ error?: string;
+ error_description?: string;
+ error_uri?: string;
}>;
}) {
const params = await props.params;
@@ -61,6 +64,14 @@ export default async function Page(props: {
}
}
+ const providerError = searchParams.error
+ ? {
+ error: searchParams.error,
+ description: searchParams.error_description,
+ uri: searchParams.error_uri
+ }
+ : undefined;
+
return (
<>
>
);
diff --git a/src/components/ValidateOidcToken.tsx b/src/components/ValidateOidcToken.tsx
index 8f61cdd1..3677f625 100644
--- a/src/components/ValidateOidcToken.tsx
+++ b/src/components/ValidateOidcToken.tsx
@@ -26,6 +26,11 @@ type ValidateOidcTokenParams = {
stateCookie: string | undefined;
idp: { name: string };
loginPageId?: number;
+ providerError?: {
+ error: string;
+ description?: string | null;
+ uri?: string | null;
+ };
};
export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
@@ -35,14 +40,65 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ const [isProviderError, setIsProviderError] = useState(false);
const { licenseStatus, isLicenseViolation } = useLicenseStatusContext();
const t = useTranslations();
useEffect(() => {
- async function validate() {
+ let isCancelled = false;
+
+ async function runValidation() {
setLoading(true);
+ setIsProviderError(false);
+
+ if (props.providerError?.error) {
+ const providerMessage =
+ props.providerError.description ||
+ t("idpErrorOidcProviderRejected", {
+ error: props.providerError.error,
+ defaultValue:
+ "The identity provider returned an error: {error}."
+ });
+ const suffix = props.providerError.uri
+ ? ` (${props.providerError.uri})`
+ : "";
+ if (!isCancelled) {
+ setIsProviderError(true);
+ setError(`${providerMessage}${suffix}`);
+ setLoading(false);
+ }
+ return;
+ }
+
+ if (!props.code) {
+ if (!isCancelled) {
+ setIsProviderError(false);
+ setError(
+ t("idpErrorOidcMissingCode", {
+ defaultValue:
+ "The identity provider did not return an authorization code."
+ })
+ );
+ setLoading(false);
+ }
+ return;
+ }
+
+ if (!props.expectedState || !props.stateCookie) {
+ if (!isCancelled) {
+ setIsProviderError(false);
+ setError(
+ t("idpErrorOidcMissingState", {
+ defaultValue:
+ "The login request is missing state information. Please restart the login process."
+ })
+ );
+ setLoading(false);
+ }
+ return;
+ }
console.log(t("idpOidcTokenValidating"), {
code: props.code,
@@ -57,22 +113,28 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
try {
const response = await validateOidcUrlCallbackProxy(
props.idpId,
- props.code || "",
- props.expectedState || "",
- props.stateCookie || "",
+ props.code,
+ props.expectedState,
+ props.stateCookie,
props.loginPageId
);
if (response.error) {
- setError(response.message);
- setLoading(false);
+ if (!isCancelled) {
+ setIsProviderError(false);
+ setError(response.message);
+ setLoading(false);
+ }
return;
}
const data = response.data;
if (!data) {
- setError("Unable to validate OIDC token");
- setLoading(false);
+ if (!isCancelled) {
+ setIsProviderError(false);
+ setError("Unable to validate OIDC token");
+ setLoading(false);
+ }
return;
}
@@ -82,8 +144,11 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
router.push(env.app.dashboardUrl);
}
- setLoading(false);
- await new Promise((resolve) => setTimeout(resolve, 100));
+ if (!isCancelled) {
+ setIsProviderError(false);
+ setLoading(false);
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ }
if (redirectUrl.startsWith("http")) {
window.location.href = data.redirectUrl; // this is validated by the parent using this component
@@ -92,18 +157,27 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
}
} catch (e: any) {
console.error(e);
- setError(
- t("idpErrorOidcTokenValidating", {
- defaultValue:
- "An unexpected error occurred. Please try again."
- })
- );
+ if (!isCancelled) {
+ setIsProviderError(false);
+ setError(
+ t("idpErrorOidcTokenValidating", {
+ defaultValue:
+ "An unexpected error occurred. Please try again."
+ })
+ );
+ }
} finally {
- setLoading(false);
+ if (!isCancelled) {
+ setLoading(false);
+ }
}
}
- validate();
+ runValidation();
+
+ return () => {
+ isCancelled = true;
+ };
}, []);
return (
@@ -134,12 +208,16 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
-
- {t("idpErrorConnectingTo", {
- name: props.idp.name
- })}
+
+ {isProviderError
+ ? error
+ : t("idpErrorConnectingTo", {
+ name: props.idp.name
+ })}
- {error}
+ {!isProviderError && (
+ {error}
+ )}
)}