OAuth callback no longer redirects to itself if things fail

This commit is contained in:
Stan Wohlwend 2024-07-28 06:00:58 -07:00
parent 29a24dcf3b
commit 51665a4162
6 changed files with 43 additions and 32 deletions

View File

@ -16,10 +16,12 @@ import { useEffect } from 'react';
export function AuthPage({
fullPage=false,
type,
automaticRedirect,
mockProject,
}: {
fullPage?: boolean,
type: 'sign-in' | 'sign-up',
automaticRedirect?: boolean,
mockProject?: {
config: {
credentialEnabled: boolean,
@ -36,10 +38,12 @@ export function AuthPage({
const project = mockProject || projectFromHook;
useEffect(() => {
if (user && !mockProject) {
runAsynchronously(type === 'sign-in' ? stackApp.redirectToAfterSignIn() : stackApp.redirectToAfterSignUp());
if (automaticRedirect) {
if (user && !mockProject) {
runAsynchronously(type === 'sign-in' ? stackApp.redirectToAfterSignIn() : stackApp.redirectToAfterSignUp());
}
}
}, [user, mockProject, stackApp]);
}, [user, mockProject, stackApp, automaticRedirect]);
if (user && !mockProject) {
return <PredefinedMessageCard type='signedIn' fullPage={fullPage} />;

View File

@ -5,6 +5,7 @@ import { useStackApp } from "..";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import { MessageCard } from "../components/message-cards/message-card";
import { StyledLink } from "@stackframe/stack-ui";
import { captureError } from "@stackframe/stack-shared/dist/utils/errors";
export function OAuthCallback(props: { fullPage?: boolean }) {
const app = useStackApp();
@ -19,10 +20,11 @@ export function OAuthCallback(props: { fullPage?: boolean }) {
try {
hasRedirected = await app.callOAuthCallback();
} catch (e: any) {
captureError("<OAuthCallback />", e);
setError(e);
}
if (!hasRedirected && (!error || process.env.NODE_ENV === 'production')) {
await app.redirectToSignIn();
await app.redirectToSignIn({ noRedirectBack: true });
}
}), []);

View File

@ -2,7 +2,7 @@ import { SignUp } from "./sign-up";
import { SignIn } from "./sign-in";
import { RedirectType, notFound, redirect } from 'next/navigation';
import { EmailVerification } from "./email-verification";
import { StackServerApp } from "..";
import { AuthPage, StackServerApp } from "..";
import { MessageCard } from "../components/message-cards/message-card";
import { HandlerUrls } from "../lib/stack-app";
import { SignOut } from "./sign-out";
@ -67,11 +67,11 @@ export default async function StackHandler<HasTokenStore extends boolean>({
switch (path) {
case availablePaths.signIn: {
redirectIfNotHandler('signIn');
return <SignIn fullPage={fullPage} />;
return <AuthPage fullPage={fullPage} type='sign-in' automaticRedirect />;
}
case availablePaths.signUp: {
redirectIfNotHandler('signUp');
return <SignUp fullPage={fullPage} />;
return <AuthPage fullPage={fullPage} type='sign-up' automaticRedirect />;
}
case availablePaths.emailVerification: {
redirectIfNotHandler('emailVerification');

View File

@ -45,14 +45,14 @@ export function PredefinedMessageCard({
case 'passwordReset': {
title = "Password reset successfully!";
message = 'Your password has been reset. You can now sign in with your new password.';
primaryAction = () => stackApp.redirectToSignIn();
primaryAction = () => stackApp.redirectToSignIn({ noRedirectBack: true });
primaryButton = "Sign in";
break;
}
case 'emailVerified': {
title = "Email verified!";
message = 'Your have successfully verified your email.';
primaryAction = () => stackApp.redirectToSignIn();
primaryAction = () => stackApp.redirectToSignIn({ noRedirectBack: true });
primaryButton = "Sign in";
break;
}

View File

@ -1,6 +1,6 @@
import { StackClientInterface } from "@stackframe/stack-shared";
import { InternalSession } from "@stackframe/stack-shared/dist/sessions";
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
import { neverResolve } from "@stackframe/stack-shared/dist/utils/promises";
import { constructRedirectUrl } from "../utils/url";
import { getVerifierAndState, saveVerifierAndState } from "./cookie";
@ -64,6 +64,7 @@ function consumeOAuthCallbackQueryParams(expectedState: string): null | URL {
const originalUrl = new URL(window.location.href);
for (const param of requiredParams) {
if (!originalUrl.searchParams.has(param)) {
captureError("consumeOAuthCallbackQueryParams", new Error(`Missing required query parameter on OAuth callback: ${param}`));
return null;
}
}
@ -71,6 +72,7 @@ function consumeOAuthCallbackQueryParams(expectedState: string): null | URL {
if (expectedState !== originalUrl.searchParams.get("state")) {
// If the state doesn't match, then the callback wasn't meant for us.
// Maybe the website uses another OAuth library?
captureError("consumeOAuthCallbackQueryParams", new Error(`Invalid OAuth callback state: Was this meant for someone else, or did cookies fail?`));
return null;
}

View File

@ -881,29 +881,31 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
throw new Error(`No URL for handler name ${handlerName}`);
}
if (handlerName === "afterSignIn" || handlerName === "afterSignUp") {
if (isReactServer || typeof window === "undefined") {
try {
await this._checkFeatureSupport("rsc-handler-" + handlerName, {});
} catch (e) {}
} else {
const queryParams = new URLSearchParams(window.location.search);
url = queryParams.get("after_auth_return_to") || url;
}
} else if (handlerName === "signIn" || handlerName === "signUp") {
if (isReactServer || typeof window === "undefined") {
try {
await this._checkFeatureSupport("rsc-handler-" + handlerName, {});
} catch (e) {}
} else {
const currentUrl = new URL(window.location.href);
const nextUrl = new URL(url, currentUrl);
if (currentUrl.searchParams.has("after_auth_return_to")) {
nextUrl.searchParams.set("after_auth_return_to", currentUrl.searchParams.get("after_auth_return_to")!);
} else if (currentUrl.protocol === nextUrl.protocol && currentUrl.host === nextUrl.host) {
nextUrl.searchParams.set("after_auth_return_to", getRelativePart(currentUrl));
if (!options?.noRedirectBack) {
if (handlerName === "afterSignIn" || handlerName === "afterSignUp") {
if (isReactServer || typeof window === "undefined") {
try {
await this._checkFeatureSupport("rsc-handler-" + handlerName, {});
} catch (e) {}
} else {
const queryParams = new URLSearchParams(window.location.search);
url = queryParams.get("after_auth_return_to") || url;
}
} else if (handlerName === "signIn" || handlerName === "signUp") {
if (isReactServer || typeof window === "undefined") {
try {
await this._checkFeatureSupport("rsc-handler-" + handlerName, {});
} catch (e) {}
} else {
const currentUrl = new URL(window.location.href);
const nextUrl = new URL(url, currentUrl);
if (currentUrl.searchParams.has("after_auth_return_to")) {
nextUrl.searchParams.set("after_auth_return_to", currentUrl.searchParams.get("after_auth_return_to")!);
} else if (currentUrl.protocol === nextUrl.protocol && currentUrl.host === nextUrl.host) {
nextUrl.searchParams.set("after_auth_return_to", getRelativePart(currentUrl));
}
url = getRelativePart(nextUrl);
}
url = getRelativePart(nextUrl);
}
}
@ -2555,6 +2557,7 @@ type _______________VARIOUS_______________ = never; // this is a marker for VSC
type RedirectToOptions = {
replace?: boolean,
noRedirectBack?: boolean,
};
type AsyncStoreProperty<Name extends string, Args extends any[], Value, IsMultiple extends boolean> =