mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-27 23:36:39 +00:00
Add OIDC authentication error response support
This commit is contained in:
@@ -192,11 +192,71 @@ export async function validateOidcCallback(
|
|||||||
state
|
state
|
||||||
});
|
});
|
||||||
|
|
||||||
const tokens = await client.validateAuthorizationCode(
|
let tokens: arctic.OAuth2Tokens;
|
||||||
ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl),
|
try {
|
||||||
code,
|
tokens = await client.validateAuthorizationCode(
|
||||||
codeVerifier
|
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();
|
const idToken = tokens.idToken();
|
||||||
logger.debug("ID token", { idToken });
|
logger.debug("ID token", { idToken });
|
||||||
|
|||||||
@@ -14,8 +14,11 @@ export const dynamic = "force-dynamic";
|
|||||||
export default async function Page(props: {
|
export default async function Page(props: {
|
||||||
params: Promise<{ orgId: string; idpId: string }>;
|
params: Promise<{ orgId: string; idpId: string }>;
|
||||||
searchParams: Promise<{
|
searchParams: Promise<{
|
||||||
code: string;
|
code?: string;
|
||||||
state: string;
|
state?: string;
|
||||||
|
error?: string;
|
||||||
|
error_description?: string;
|
||||||
|
error_uri?: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
@@ -59,6 +62,14 @@ export default async function Page(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const providerError = searchParams.error
|
||||||
|
? {
|
||||||
|
error: searchParams.error,
|
||||||
|
description: searchParams.error_description,
|
||||||
|
uri: searchParams.error_uri
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ValidateOidcToken
|
<ValidateOidcToken
|
||||||
@@ -69,6 +80,7 @@ export default async function Page(props: {
|
|||||||
expectedState={searchParams.state}
|
expectedState={searchParams.state}
|
||||||
stateCookie={stateCookie}
|
stateCookie={stateCookie}
|
||||||
idp={{ name: foundIdp.name }}
|
idp={{ name: foundIdp.name }}
|
||||||
|
providerError={providerError}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ type ValidateOidcTokenParams = {
|
|||||||
stateCookie: string | undefined;
|
stateCookie: string | undefined;
|
||||||
idp: { name: string };
|
idp: { name: string };
|
||||||
loginPageId?: number;
|
loginPageId?: number;
|
||||||
|
providerError?: {
|
||||||
|
error: string;
|
||||||
|
description?: string | null;
|
||||||
|
uri?: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
||||||
@@ -35,14 +40,65 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
|||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isProviderError, setIsProviderError] = useState(false);
|
||||||
|
|
||||||
const { licenseStatus, isLicenseViolation } = useLicenseStatusContext();
|
const { licenseStatus, isLicenseViolation } = useLicenseStatusContext();
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function validate() {
|
let isCancelled = false;
|
||||||
|
|
||||||
|
async function runValidation() {
|
||||||
setLoading(true);
|
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"), {
|
console.log(t("idpOidcTokenValidating"), {
|
||||||
code: props.code,
|
code: props.code,
|
||||||
@@ -57,22 +113,28 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
|||||||
try {
|
try {
|
||||||
const response = await validateOidcUrlCallbackProxy(
|
const response = await validateOidcUrlCallbackProxy(
|
||||||
props.idpId,
|
props.idpId,
|
||||||
props.code || "",
|
props.code,
|
||||||
props.expectedState || "",
|
props.expectedState,
|
||||||
props.stateCookie || "",
|
props.stateCookie,
|
||||||
props.loginPageId
|
props.loginPageId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
setError(response.message);
|
if (!isCancelled) {
|
||||||
setLoading(false);
|
setIsProviderError(false);
|
||||||
|
setError(response.message);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
if (!data) {
|
if (!data) {
|
||||||
setError("Unable to validate OIDC token");
|
if (!isCancelled) {
|
||||||
setLoading(false);
|
setIsProviderError(false);
|
||||||
|
setError("Unable to validate OIDC token");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +144,11 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
|||||||
router.push(env.app.dashboardUrl);
|
router.push(env.app.dashboardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
if (!isCancelled) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
setIsProviderError(false);
|
||||||
|
setLoading(false);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
|
||||||
if (redirectUrl.startsWith("http")) {
|
if (redirectUrl.startsWith("http")) {
|
||||||
window.location.href = data.redirectUrl; // this is validated by the parent using this component
|
window.location.href = data.redirectUrl; // this is validated by the parent using this component
|
||||||
@@ -92,18 +157,39 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
|||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
setError(
|
if (!isCancelled) {
|
||||||
t("idpErrorOidcTokenValidating", {
|
setIsProviderError(false);
|
||||||
defaultValue: "An unexpected error occurred. Please try again."
|
setError(
|
||||||
})
|
t("idpErrorOidcTokenValidating", {
|
||||||
);
|
defaultValue:
|
||||||
|
"An unexpected error occurred. Please try again."
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (!isCancelled) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validate();
|
runValidation();
|
||||||
}, []);
|
|
||||||
|
return () => {
|
||||||
|
isCancelled = true;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
env.app.dashboardUrl,
|
||||||
|
isLicenseViolation,
|
||||||
|
props.code,
|
||||||
|
props.expectedState,
|
||||||
|
props.idpId,
|
||||||
|
props.loginPageId,
|
||||||
|
props.providerError,
|
||||||
|
props.stateCookie,
|
||||||
|
router,
|
||||||
|
t
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
@@ -133,12 +219,16 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) {
|
|||||||
<Alert variant="destructive" className="w-full">
|
<Alert variant="destructive" className="w-full">
|
||||||
<AlertCircle className="h-5 w-5" />
|
<AlertCircle className="h-5 w-5" />
|
||||||
<AlertDescription className="flex flex-col space-y-2">
|
<AlertDescription className="flex flex-col space-y-2">
|
||||||
<span>
|
<span className="text-sm font-medium">
|
||||||
{t("idpErrorConnectingTo", {
|
{isProviderError
|
||||||
name: props.idp.name
|
? error
|
||||||
})}
|
: t("idpErrorConnectingTo", {
|
||||||
|
name: props.idp.name
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs">{error}</span>
|
{!isProviderError && (
|
||||||
|
<span className="text-xs">{error}</span>
|
||||||
|
)}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user