Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple fixes/improvements to auth-nextjs #785

Merged
merged 9 commits into from
Nov 22, 2023
5 changes: 3 additions & 2 deletions packages/auth-core/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class Auth {
password: string,
verifyUrl: string
): Promise<
| { status: "complete"; tokenData: TokenData }
| { status: "complete"; verifier: string; tokenData: TokenData }
| { status: "verificationRequired"; verifier: string }
> {
const { challenge, verifier } = await pkce.createVerifierChallengePair();
Expand All @@ -122,6 +122,7 @@ export class Auth {
if ("code" in result) {
return {
status: "complete",
verifier,
tokenData: await this.getToken(result.code, verifier),
};
} else {
Expand All @@ -145,7 +146,7 @@ export class Auth {
}

async sendPasswordResetEmail(email: string, resetUrl: string) {
const { challenge, verifier } = pkce.createVerifierChallengePair();
const { challenge, verifier } = await pkce.createVerifierChallengePair();
return {
verifier,
...(await this._post<{ email_sent: string }>("send-reset-email", {
Expand Down
118 changes: 87 additions & 31 deletions packages/auth-nextjs/src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,53 @@ import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import type { NextRequest } from "next/server";

import { NextAuth, NextAuthSession, type NextAuthOptions } from "../shared";
import {
NextAuth,
NextAuthSession,
type NextAuthOptions,
BuiltinProviderNames,
} from "../shared";

export { NextAuthSession, type NextAuthOptions };
export {
NextAuthSession,
type NextAuthOptions,
type BuiltinProviderNames,
type TokenData,
};

type ParamsOrError<Result extends object> =
| ({ error: null } & Result)
| ({ error: Error } & { [Key in keyof Result]?: undefined });
type ParamsOrError<Result extends object, ErrorDetails extends object = {}> =
| ({ error: null } & { [Key in keyof ErrorDetails]?: undefined } & Result)
| ({ error: Error } & ErrorDetails & { [Key in keyof Result]?: undefined });

export interface CreateAuthRouteHandlers {
onOAuthCallback(
params: ParamsOrError<{ tokenData: TokenData; isSignUp: boolean }>
params: ParamsOrError<{
tokenData: TokenData;
provider: BuiltinOAuthProviderNames;
isSignUp: boolean;
}>
): void;
onEmailPasswordSignIn(params: ParamsOrError<{ tokenData: TokenData }>): void;
onEmailPasswordSignUp(
params: ParamsOrError<{ tokenData: TokenData | null }>
): void;
onEmailPasswordReset(params: ParamsOrError<{ tokenData: TokenData }>): void;
onEmailVerify(params: ParamsOrError<{ tokenData: TokenData }>): void;
onEmailVerify(
params: ParamsOrError<
{ tokenData: TokenData },
{ verificationToken?: string }
>
): void;
onBuiltinUICallback(
params: ParamsOrError<{ tokenData: TokenData | null; isSignUp: boolean }>
params: ParamsOrError<
(
| {
tokenData: TokenData;
provider: BuiltinProviderNames;
}
| { tokenData: null; provider: null }
) & { isSignUp: boolean }
>
): void;
onSignout(): void;
}
Expand All @@ -48,6 +75,10 @@ export class NextAppAuth extends NextAuth {
);
}

async getProvidersInfo() {
return (await this.core).getProvidersInfo();
}

createAuthRouteHandlers({
onOAuthCallback,
onEmailPasswordSignIn,
Expand Down Expand Up @@ -137,7 +168,14 @@ export class NextAppAuth extends NextAuth {
});
cookies().delete(this.options.pkceVerifierCookieName);

return onOAuthCallback({ error: null, tokenData, isSignUp });
return onOAuthCallback({
error: null,
tokenData,
provider: req.nextUrl.searchParams.get(
"provider"
) as BuiltinOAuthProviderNames,
isSignUp,
});
}
case "emailpassword/verify": {
if (!onEmailVerify) {
Expand All @@ -158,6 +196,7 @@ export class NextAppAuth extends NextAuth {
if (!verifier) {
return onEmailVerify({
error: new Error("no pkce verifier cookie found"),
verificationToken,
});
}
let tokenData: TokenData;
Expand All @@ -168,6 +207,7 @@ export class NextAppAuth extends NextAuth {
} catch (err) {
return onEmailVerify({
error: err instanceof Error ? err : new Error(String(err)),
verificationToken,
});
}
cookies().set({
Expand Down Expand Up @@ -203,6 +243,7 @@ export class NextAppAuth extends NextAuth {
return onBuiltinUICallback({
error: null,
tokenData: null,
provider: null,
isSignUp: true,
});
}
Expand Down Expand Up @@ -236,7 +277,14 @@ export class NextAppAuth extends NextAuth {
});
cookies().delete(this.options.pkceVerifierCookieName);

return onBuiltinUICallback({ error: null, tokenData, isSignUp });
return onBuiltinUICallback({
error: null,
tokenData,
provider: req.nextUrl.searchParams.get(
"provider"
) as BuiltinProviderNames,
isSignUp,
});
}
case "builtin/signin":
case "builtin/signup": {
Expand Down Expand Up @@ -328,6 +376,12 @@ export class NextAppAuth extends NextAuth {
error: err instanceof Error ? err : new Error(String(err)),
});
}
cookies().set({
name: this.options.pkceVerifierCookieName,
value: result.verifier,
httpOnly: true,
sameSite: "strict",
});
if (result.status === "complete") {
cookies().set({
name: this.options.authCookieName,
Expand All @@ -340,18 +394,12 @@ export class NextAppAuth extends NextAuth {
tokenData: result.tokenData,
});
} else {
cookies().set({
name: this.options.pkceVerifierCookieName,
value: result.verifier,
httpOnly: true,
sameSite: "strict",
});
return onEmailPasswordSignUp({ error: null, tokenData: null });
}
}
case "emailpassword/send-reset-email": {
if (!this.options.passwordResetUrl) {
throw new Error(`'passwordResetUrl' option not configured`);
if (!this.options.passwordResetPath) {
throw new Error(`'passwordResetPath' option not configured`);
}
const [email] = _extractParams(
await _getReqBody(req),
Expand All @@ -360,7 +408,13 @@ export class NextAppAuth extends NextAuth {
);
const { verifier } = await (
await this.core
).sendPasswordResetEmail(email, this.options.passwordResetUrl);
).sendPasswordResetEmail(
email,
new URL(
this.options.passwordResetPath,
this.options.baseUrl
).toString()
);
cookies().set({
name: this.options.pkceVerifierCookieName,
value: verifier,
Expand Down Expand Up @@ -465,6 +519,12 @@ export class NextAppAuth extends NextAuth {
password,
`${this._authRoute}/emailpassword/verify`
);
cookies().set({
name: this.options.pkceVerifierCookieName,
value: result.verifier,
httpOnly: true,
sameSite: "strict",
});
if (result.status === "complete") {
cookies().set({
name: this.options.authCookieName,
Expand All @@ -473,28 +533,24 @@ export class NextAppAuth extends NextAuth {
sameSite: "strict",
});
return result.tokenData;
} else {
cookies().set({
name: this.options.pkceVerifierCookieName,
value: result.verifier,
httpOnly: true,
sameSite: "strict",
});
return null;
}
return null;
},
emailPasswordSendPasswordResetEmail: async (
data: FormData | { email: string }
) => {
if (!this.options.passwordResetUrl) {
throw new Error(`'passwordResetUrl' option not configured`);
if (!this.options.passwordResetPath) {
throw new Error(`'passwordResetPath' option not configured`);
}
const [email] = _extractParams(data, ["email"], "email missing");
const { verifier } = await (
await this.core
).sendPasswordResetEmail(
email,
`${this.options.baseUrl}/${this.options.passwordResetUrl}`
new URL(
this.options.passwordResetPath,
this.options.baseUrl
).toString()
);
cookies().set({
name: this.options.pkceVerifierCookieName,
Expand All @@ -504,7 +560,7 @@ export class NextAppAuth extends NextAuth {
});
},
emailPasswordResetPassword: async (
data: FormData | { resetToken: string; password: string }
data: FormData | { reset_token: string; password: string }
) => {
const verifier = cookies().get(
this.options.pkceVerifierCookieName
Expand Down
26 changes: 19 additions & 7 deletions packages/auth-nextjs/src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { Client } from "edgedb";
import { Auth, BuiltinOAuthProviderNames } from "@edgedb/auth-core";
import {
Auth,
BuiltinOAuthProviderNames,
emailPasswordProviderName,
} from "@edgedb/auth-core";

export type BuiltinProviderNames =
| BuiltinOAuthProviderNames
| typeof emailPasswordProviderName;

export interface NextAuthOptions {
baseUrl: string;
authRoutesPath?: string;
authCookieName?: string;
pkceVerifierCookieName?: string;
passwordResetUrl?: string;
passwordResetPath?: string;
}

type OptionalOptions = "passwordResetUrl";
type OptionalOptions = "passwordResetPath";

export abstract class NextAuth {
/** @internal */
Expand All @@ -24,7 +32,7 @@ export abstract class NextAuth {
authCookieName: options.authCookieName ?? "edgedb-session",
pkceVerifierCookieName:
options.pkceVerifierCookieName ?? "edgedb-pkce-verifier",
passwordResetUrl: options.passwordResetUrl,
passwordResetPath: options.passwordResetPath,
};
}

Expand Down Expand Up @@ -66,8 +74,12 @@ export class NextAuthSession {

async isLoggedIn() {
if (!this.authToken) return false;
return (await this.client.querySingle(
`select exists global ext::auth::ClientTokenIdentity`
)) as boolean;
try {
return await this.client.querySingle<boolean>(
`select exists global ext::auth::ClientTokenIdentity`
);
} catch {
return false;
}
}
}