diff --git a/packages/auth-core/src/core.ts b/packages/auth-core/src/core.ts index 3d387b0d1..b2efeb86a 100644 --- a/packages/auth-core/src/core.ts +++ b/packages/auth-core/src/core.ts @@ -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(); @@ -122,6 +122,7 @@ export class Auth { if ("code" in result) { return { status: "complete", + verifier, tokenData: await this.getToken(result.code, verifier), }; } else { @@ -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", { diff --git a/packages/auth-nextjs/src/app/index.ts b/packages/auth-nextjs/src/app/index.ts index df87428af..1598129ef 100644 --- a/packages/auth-nextjs/src/app/index.ts +++ b/packages/auth-nextjs/src/app/index.ts @@ -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 = - | ({ error: null } & Result) - | ({ error: Error } & { [Key in keyof Result]?: undefined }); +type ParamsOrError = + | ({ 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; } @@ -48,6 +75,10 @@ export class NextAppAuth extends NextAuth { ); } + async getProvidersInfo() { + return (await this.core).getProvidersInfo(); + } + createAuthRouteHandlers({ onOAuthCallback, onEmailPasswordSignIn, @@ -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) { @@ -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; @@ -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({ @@ -203,6 +243,7 @@ export class NextAppAuth extends NextAuth { return onBuiltinUICallback({ error: null, tokenData: null, + provider: null, isSignUp: true, }); } @@ -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": { @@ -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, @@ -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), @@ -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, @@ -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, @@ -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, @@ -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 diff --git a/packages/auth-nextjs/src/shared.ts b/packages/auth-nextjs/src/shared.ts index 0b8148dfd..545c733b6 100644 --- a/packages/auth-nextjs/src/shared.ts +++ b/packages/auth-nextjs/src/shared.ts @@ -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 */ @@ -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, }; } @@ -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( + `select exists global ext::auth::ClientTokenIdentity` + ); + } catch { + return false; + } } }