mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Added handling for user canceling the oauth process (#260)
* added oauth cancel handling * added translation * fixed tests * fixed reviews
This commit is contained in:
parent
e3a8b43fd5
commit
8ea8ff383e
@ -20,7 +20,11 @@ const redirectOrThrowError = (error: KnownError, project: ProjectsCrud["Admin"][
|
||||
throw error;
|
||||
}
|
||||
|
||||
redirect(`${errorRedirectUrl}?errorCode=${error.errorCode}&message=${error.message}&details=${error.details}`);
|
||||
const url = new URL(errorRedirectUrl);
|
||||
url.searchParams.set("errorCode", error.errorCode);
|
||||
url.searchParams.set("message", error.message);
|
||||
url.searchParams.set("details", error.details ? JSON.stringify(error.details) : JSON.stringify({}));
|
||||
redirect(url.toString());
|
||||
};
|
||||
|
||||
const handler = createSmartRouteHandler({
|
||||
@ -92,14 +96,24 @@ const handler = createSmartRouteHandler({
|
||||
}
|
||||
|
||||
const providerObj = await getProvider(provider);
|
||||
const { userInfo, tokenSet } = await providerObj.getCallback({
|
||||
codeVerifier: innerCodeVerifier,
|
||||
state: innerState,
|
||||
callbackParams: {
|
||||
...query,
|
||||
...body,
|
||||
},
|
||||
});
|
||||
let callbackResult: Awaited<ReturnType<typeof providerObj.getCallback>>;
|
||||
try {
|
||||
callbackResult = await providerObj.getCallback({
|
||||
codeVerifier: innerCodeVerifier,
|
||||
state: innerState,
|
||||
callbackParams: {
|
||||
...query,
|
||||
...body,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof KnownErrors['OAuthProviderAccessDenied']) {
|
||||
redirectOrThrowError(error, project, errorRedirectUrl);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { userInfo, tokenSet } = callbackResult;
|
||||
|
||||
if (type === "link") {
|
||||
if (!projectUserId) {
|
||||
|
||||
@ -154,6 +154,9 @@ export abstract class OAuthBaseProvider {
|
||||
captureError("inner-oauth-callback", error);
|
||||
throw new KnownErrors.InvalidAuthorizationCode();
|
||||
}
|
||||
if (error?.error === 'access_denied') {
|
||||
throw new KnownErrors.OAuthProviderAccessDenied();
|
||||
}
|
||||
throw new StackAssertionError(`Inner OAuth callback failed due to error: ${error}`, undefined, { cause: error });
|
||||
}
|
||||
|
||||
|
||||
@ -128,7 +128,7 @@ it("should redirect to error callback url when inner callback has invalid author
|
||||
NiceResponse {
|
||||
"status": 307,
|
||||
"headers": Headers {
|
||||
"location": "http://stack-test.localhost/some-callback-url/callback-error?errorCode=INVALID_AUTHORIZATION_CODE&message=The%20given%20authorization%20code%20is%20invalid.&details=undefined",
|
||||
"location": "http://stack-test.localhost/some-callback-url/callback-error?errorCode=INVALID_AUTHORIZATION_CODE&message=The+given+authorization+code+is+invalid.&details=%7B%7D",
|
||||
"set-cookie": <deleting cookie 'stack-oauth-inner-<stripped cookie name key>' at path '/'>,
|
||||
<some fields may have been hidden>,
|
||||
},
|
||||
|
||||
@ -1119,6 +1119,16 @@ const InvalidAuthorizationCode = createKnownErrorConstructor(
|
||||
() => [] as const,
|
||||
);
|
||||
|
||||
const OAuthProviderAccessDenied = createKnownErrorConstructor(
|
||||
KnownError,
|
||||
"OAUTH_PROVIDER_ACCESS_DENIED",
|
||||
() => [
|
||||
400,
|
||||
"The OAuth provider denied access to the user.",
|
||||
] as const,
|
||||
() => [] as const,
|
||||
);
|
||||
|
||||
export type KnownErrors = {
|
||||
[K in keyof typeof KnownErrors]: InstanceType<typeof KnownErrors[K]>;
|
||||
};
|
||||
@ -1209,6 +1219,7 @@ export const KnownErrors = {
|
||||
InvalidStandardOAuthProviderId,
|
||||
InvalidAuthorizationCode,
|
||||
TeamPermissionNotFound,
|
||||
OAuthProviderAccessDenied,
|
||||
} satisfies Record<string, KnownErrorConstructor<any, any>>;
|
||||
|
||||
|
||||
|
||||
@ -18,13 +18,14 @@ export function ErrorPage(props: { fullPage?: boolean, searchParams: Record<stri
|
||||
|
||||
const unknownErrorCard = <PredefinedMessageCard type='unknownError' fullPage={!!props.fullPage} />;
|
||||
|
||||
if (!errorCode || !message || !details) {
|
||||
if (!errorCode || !message) {
|
||||
return unknownErrorCard;
|
||||
}
|
||||
|
||||
let error;
|
||||
try {
|
||||
error = KnownError.fromJson({ code: errorCode, message, details });
|
||||
const detailJson = details ? JSON.parse(details) : {};
|
||||
error = KnownError.fromJson({ code: errorCode, message, details: detailJson });
|
||||
} catch (e) {
|
||||
return unknownErrorCard;
|
||||
}
|
||||
@ -49,9 +50,9 @@ export function ErrorPage(props: { fullPage?: boolean, searchParams: Record<stri
|
||||
// TODO: add "Connect again" button
|
||||
return (
|
||||
<MessageCard
|
||||
title="Failed to connect account"
|
||||
title={t("Failed to connect account")}
|
||||
fullPage={!!props.fullPage}
|
||||
primaryButtonText="Go to Home"
|
||||
primaryButtonText={t("Go to Home")}
|
||||
primaryAction={() => stackApp.redirectToHome()}
|
||||
>
|
||||
<Typography>
|
||||
@ -61,5 +62,22 @@ export function ErrorPage(props: { fullPage?: boolean, searchParams: Record<stri
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof KnownErrors.OAuthProviderAccessDenied) {
|
||||
return (
|
||||
<MessageCard
|
||||
title={t("OAuth provider access denied")}
|
||||
fullPage={!!props.fullPage}
|
||||
primaryButtonText={t("Sign in again")}
|
||||
primaryAction={() => stackApp.redirectToSignIn()}
|
||||
secondaryButtonText={t("Go to Home")}
|
||||
secondaryAction={() => stackApp.redirectToHome()}
|
||||
>
|
||||
<Typography>
|
||||
{t("The sign-in operation has been cancelled. Please try again. [access_denied]")}
|
||||
</Typography>
|
||||
</MessageCard>
|
||||
);
|
||||
}
|
||||
|
||||
return <KnownErrorMessageCard error={error} fullPage={!!props.fullPage} />;
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { Typography } from "@stackframe/stack-ui";
|
||||
import { useStackApp } from "../..";
|
||||
import { MessageCard } from "./message-card";
|
||||
import { useTranslation } from "../../lib/translations";
|
||||
|
||||
export function PredefinedMessageCard({
|
||||
type,
|
||||
@ -12,6 +13,7 @@ export function PredefinedMessageCard({
|
||||
fullPage?: boolean,
|
||||
}) {
|
||||
const stackApp = useStackApp();
|
||||
const { t } = useTranslation();
|
||||
|
||||
let title: string;
|
||||
let message: string | null = null;
|
||||
@ -22,53 +24,53 @@ export function PredefinedMessageCard({
|
||||
|
||||
switch (type) {
|
||||
case 'signedIn': {
|
||||
title = "You are already signed in";
|
||||
title = t("You are already signed in");
|
||||
primaryAction = () => stackApp.redirectToHome();
|
||||
secondaryAction = () => stackApp.redirectToSignOut();
|
||||
primaryButton = "Go to home";
|
||||
secondaryButton = "Sign out";
|
||||
primaryButton = t("Go to home");
|
||||
secondaryButton = t("Sign out");
|
||||
break;
|
||||
}
|
||||
case 'signedOut': {
|
||||
title = "You are not currently signed in.";
|
||||
title = t("You are not currently signed in.");
|
||||
primaryAction = () => stackApp.redirectToSignIn();
|
||||
primaryButton = "Sign in";
|
||||
primaryButton = t("Sign in");
|
||||
break;
|
||||
}
|
||||
case 'signUpDisabled': {
|
||||
title = "Sign up for new users is not enabled at the moment.";
|
||||
title = t("Sign up for new users is not enabled at the moment.");
|
||||
primaryAction = () => stackApp.redirectToHome();
|
||||
secondaryAction = () => stackApp.redirectToSignIn();
|
||||
primaryButton = "Go to home";
|
||||
secondaryButton = "Sign in";
|
||||
primaryButton = t("Go to home");
|
||||
secondaryButton = t("Sign in");
|
||||
break;
|
||||
}
|
||||
case 'emailSent': {
|
||||
title = "Email sent!";
|
||||
message = 'If the user with this e-mail address exists, an e-mail was sent to your inbox. Make sure to check your spam folder.';
|
||||
title = t("Email sent!");
|
||||
message = t("If the user with this e-mail address exists, an e-mail was sent to your inbox. Make sure to check your spam folder.");
|
||||
primaryAction = () => stackApp.redirectToHome();
|
||||
primaryButton = "Go to home";
|
||||
primaryButton = t("Go to home");
|
||||
break;
|
||||
}
|
||||
case 'passwordReset': {
|
||||
title = "Password reset successfully!";
|
||||
message = 'Your password has been reset. You can now sign in with your new password.';
|
||||
title = t("Password reset successfully!");
|
||||
message = t("Your password has been reset. You can now sign in with your new password.");
|
||||
primaryAction = () => stackApp.redirectToSignIn({ noRedirectBack: true });
|
||||
primaryButton = "Sign in";
|
||||
primaryButton = t("Sign in");
|
||||
break;
|
||||
}
|
||||
case 'emailVerified': {
|
||||
title = "Email verified!";
|
||||
message = 'Your have successfully verified your email.';
|
||||
title = t("Email verified!");
|
||||
message = t("Your have successfully verified your email.");
|
||||
primaryAction = () => stackApp.redirectToSignIn({ noRedirectBack: true });
|
||||
primaryButton = "Sign in";
|
||||
primaryButton = t("Sign in");
|
||||
break;
|
||||
}
|
||||
case 'unknownError': {
|
||||
title = "An unknown error occurred";
|
||||
message = 'Please try again and if the problem persists, contact support.';
|
||||
title = t("An unknown error occurred");
|
||||
message = t("Please try again and if the problem persists, contact support.");
|
||||
primaryAction = () => stackApp.redirectToHome();
|
||||
primaryButton = "Go to home";
|
||||
primaryButton = t("Go to home");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -307,7 +307,7 @@ export function OAuthButton({
|
||||
<div className='flex items-center w-full gap-4'>
|
||||
{style.icon}
|
||||
<span className='flex-1'>
|
||||
{type === 'sign-up' ? t('Sign up with ') : t('Sign in with ')}{style.name}
|
||||
{type === 'sign-up' ? t('Sign up with') : t('Sign in with')} {style.name}
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user