Skip to content

Commit

Permalink
Add WebAuthn and Magic Links in auth-nextjs (#897)
Browse files Browse the repository at this point in the history
  • Loading branch information
scotttrinh committed Mar 21, 2024
1 parent c5bcd4a commit 0306030
Show file tree
Hide file tree
Showing 13 changed files with 449 additions and 36 deletions.
17 changes: 10 additions & 7 deletions packages/auth-core/src/core.ts
Expand Up @@ -176,31 +176,34 @@ export class Auth {
async signupWithMagicLink(
email: string,
callbackUrl: string,
redirectOnFailure: string,
challenge: string
): Promise<void> {
redirectOnFailure: string
): Promise<{ verifier: string }> {
const { challenge, verifier } = await pkce.createVerifierChallengePair();
await this._post("magic-link/register", {
provider: magicLinkProviderName,
challenge,
email,
challenge,
callback_url: callbackUrl,
redirect_on_failure: redirectOnFailure,
});
return { verifier };
}

async signinWithMagicLink(
email: string,
callbackUrl: string,
redirectOnFailure: string,
challenge: string
): Promise<void> {
redirectOnFailure: string
): Promise<{ verifier: string }> {
const { challenge, verifier } = await pkce.createVerifierChallengePair();
await this._post("magic-link/email", {
provider: magicLinkProviderName,
challenge,
email,
callback_url: callbackUrl,
redirect_on_failure: redirectOnFailure,
});

return { verifier };
}

async resendVerificationEmail(verificationToken: string) {
Expand Down
4 changes: 2 additions & 2 deletions packages/auth-core/src/webauthn.ts
Expand Up @@ -94,7 +94,7 @@ export class WebAuthnClient {
);
}

async signIn(email: string): Promise<TokenData> {
async signIn(email: string): Promise<void> {
const options = await requestGET<PublicKeyCredentialRequestOptionsJSON>(
this.signinOptionsUrl,
{ email },
Expand Down Expand Up @@ -143,7 +143,7 @@ export class WebAuthnClient {
},
};

return await requestPOST<TokenData>(
await requestPOST(
this.signinUrl,
{
email,
Expand Down
1 change: 1 addition & 0 deletions packages/auth-nextjs/package.json
Expand Up @@ -15,6 +15,7 @@
],
"exports": {
"./app": "./dist/app/index.js",
"./app/client": "./dist/app/client.js",
"./pages/*": "./dist/pages/*.js"
},
"scripts": {
Expand Down
23 changes: 12 additions & 11 deletions packages/auth-nextjs/readme.md
Expand Up @@ -2,8 +2,6 @@

> Warning: This library is still in an alpha state, and so, bugs are likely and the api's should be considered unstable and may change in future releases.
> Note: Currently only the Next.js 'App Router' is supported.
### Setup

**Prerequisites**: Before adding EdgeDB auth to your Next.js app, you will first need to enable the `auth` extension in your EdgeDB schema, and have configured the extension with some providers. Refer to the auth extension docs for details on how to do this.
Expand All @@ -16,10 +14,7 @@
import { createClient } from "edgedb";
import createAuth from "@edgedb/auth-nextjs/app";

export const client = createClient({
// Note: when developing locally you will need to set tls security to insecure, because the development server uses self-signed certificates which will cause api calls with the fetch api to fail.
tlsSecurity: "insecure",
});
export const client = createClient();

export const auth = createAuth(client, {
baseUrl: "http://localhost:3000",
Expand All @@ -32,7 +27,8 @@
- `authRoutesPath?: string`, The path to the auth route handlers, defaults to `'auth'`, see below for more details.
- `authCookieName?: string`, The name of the cookie where the auth token will be stored, defaults to `'edgedb-session'`.
- `pkceVerifierCookieName?: string`: The name of the cookie where the verifier for the PKCE flow will be stored, defaults to `'edgedb-pkce-verifier'`
- `passwordResetUrl?: string`: The url of the the password reset page; needed if you want to enable password reset emails in your app.
- `passwordResetPath?: string`: The path relative to the `baseUrl` of the the password reset page; needed if you want to enable password reset emails in your app.
- `magicLinkFailurePath?: string`: The path relative to the `baseUrl` of the page we should redirect users to if there is an error when trying to sign in with a magic link. The page will get an `error` search parameter attached with an error message. This property is required if you use the Magic Link authentication feature.

2. Setup the auth route handlers, with `auth.createAuthRouteHandlers()`. Callback functions can be provided to handle various auth events, where you can define what to do in the case of successful signin's or errors. You only need to configure callback functions for the types of auth you wish to use in your app.

Expand Down Expand Up @@ -62,6 +58,9 @@
- `onEmailPasswordReset`
- `onEmailVerify`
- `onBuiltinUICallback`
- `onWebAuthnSignIn`
- `onWebAuthnSignUp`
- `onMagicLinkCallback`
- `onSignout`

By default the handlers expect to exist under the `/auth` path in your app, however if you want to place them elsewhere, you will also need to configure the `authRoutesPath` option of `createAuth` to match.
Expand All @@ -85,6 +84,8 @@
- `emailPasswordSendPasswordResetEmail`
- `emailPasswordResetPassword`
- `emailPasswordResendVerificationEmail`
- `magicLinkSignUp`
- `magicLinkSignIn`
- `signout`
- `isPasswordResetTokenValid(resetToken: string)`: Checks if a password reset token is still valid.

Expand All @@ -98,20 +99,20 @@ import { auth } from "@/edgedb";
export default async function Home() {
const session = await auth.getSession();

const loggedIn = await session.isSignedIn();
const isSignedIn = await session.isSignedIn();

return (
<main>
<h1>Home</h1>

{loggedIn ? (
{isSignedIn ? (
<>
<div>You are logged in</div>
<div>You are signed in</div>
{await session.client.queryJSON(`...`)}
</>
) : (
<>
<div>You are not logged in</div>
<div>You are not signed in</div>
<a href={auth.getBuiltinUIUrl()}>Sign in with Built-in UI</a>
</>
)}
Expand Down
18 changes: 18 additions & 0 deletions packages/auth-nextjs/src/app/client.ts
@@ -0,0 +1,18 @@
import {
type BuiltinProviderNames,
NextAuthHelpers,
type NextAuthOptions,
} from "../shared.client";

export * from "@edgedb/auth-core/errors";
export { type NextAuthOptions, type BuiltinProviderNames };

export default function createNextAppClientAuth(options: NextAuthOptions) {
return new NextAppClientAuth(options);
}

export class NextAppClientAuth extends NextAuthHelpers {
constructor(options: NextAuthOptions) {
super(options);
}
}
50 changes: 49 additions & 1 deletion packages/auth-nextjs/src/app/index.ts
Expand Up @@ -12,7 +12,7 @@ import {
NextAuth,
NextAuthSession,
type NextAuthOptions,
BuiltinProviderNames,
type BuiltinProviderNames,
type CreateAuthRouteHandlers,
_extractParams,
} from "../shared";
Expand Down Expand Up @@ -178,6 +178,54 @@ export class NextAppAuth extends NextAuth {
);
}
},
magicLinkSignUp: async (data: FormData | { email: string }) => {
if (!this.options.magicLinkFailurePath) {
throw new ConfigurationError(
`'magicLinkFailurePath' option not configured`
);
}
const [email] = _extractParams(data, ["email"], "email missing");
const { verifier } = await (
await this.core
).signupWithMagicLink(
email,
`${this._authRoute}/magiclink/callback?isSignUp=true`,
new URL(
this.options.magicLinkFailurePath,
this.options.baseUrl
).toString()
);
cookies().set({
name: this.options.pkceVerifierCookieName,
value: verifier,
httpOnly: true,
sameSite: "strict",
});
},
magicLinkSignIn: async (data: FormData | { email: string }) => {
if (!this.options.magicLinkFailurePath) {
throw new ConfigurationError(
`'magicLinkFailurePath' option not configured`
);
}
const [email] = _extractParams(data, ["email"], "email missing");
const { verifier } = await (
await this.core
).signinWithMagicLink(
email,
`${this._authRoute}/magiclink/callback`,
new URL(
this.options.magicLinkFailurePath,
this.options.baseUrl
).toString()
);
cookies().set({
name: this.options.pkceVerifierCookieName,
value: verifier,
httpOnly: true,
sameSite: "strict",
});
},
};
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/auth-nextjs/src/pages/client.ts
Expand Up @@ -61,6 +61,14 @@ export class NextPagesClientAuth extends NextAuthHelpers {
data
);
}

async magicLinkSignUp(data: { email: string } | FormData) {
return await apiRequest(`${this._authRoute}/magiclink/signup`, data);
}

async magicLinkSend(data: { email: string } | FormData) {
return await apiRequest(`${this._authRoute}/magiclink/send`, data);
}
}

async function apiRequest(url: string, _data: any) {
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-nextjs/src/pages/server.ts
@@ -1,4 +1,4 @@
import { Client } from "edgedb";
import { type Client } from "edgedb";
import { type TokenData } from "@edgedb/auth-core";
import {
type BuiltinProviderNames,
Expand Down
15 changes: 14 additions & 1 deletion packages/auth-nextjs/src/shared.client.ts
Expand Up @@ -2,6 +2,9 @@ import {
type BuiltinOAuthProviderNames,
type emailPasswordProviderName,
} from "@edgedb/auth-core";
import { WebAuthnClient } from "@edgedb/auth-core/webauthn";

export * as webauthn from "@edgedb/auth-core/webauthn";

export type BuiltinProviderNames =
| BuiltinOAuthProviderNames
Expand All @@ -13,14 +16,16 @@ export interface NextAuthOptions {
authCookieName?: string;
pkceVerifierCookieName?: string;
passwordResetPath?: string;
magicLinkFailurePath?: string;
}

type OptionalOptions = "passwordResetPath";
type OptionalOptions = "passwordResetPath" | "magicLinkFailurePath";

export abstract class NextAuthHelpers {
/** @internal */
readonly options: Required<Omit<NextAuthOptions, OptionalOptions>> &
Pick<NextAuthOptions, OptionalOptions>;
readonly webAuthnClient: WebAuthnClient;

/** @internal */
constructor(options: NextAuthOptions) {
Expand All @@ -31,7 +36,15 @@ export abstract class NextAuthHelpers {
pkceVerifierCookieName:
options.pkceVerifierCookieName ?? "edgedb-pkce-verifier",
passwordResetPath: options.passwordResetPath,
magicLinkFailurePath: options.magicLinkFailurePath,
};
this.webAuthnClient = new WebAuthnClient({
signupOptionsUrl: `${this._authRoute}/webauthn/signup/options`,
signupUrl: `${this._authRoute}/webauthn/signup`,
signinOptionsUrl: `${this._authRoute}/webauthn/signin/options`,
signinUrl: `${this._authRoute}/webauthn/signin`,
verifyUrl: `${this._authRoute}/webauthn/verify`,
});
}

protected get _authRoute() {
Expand Down

0 comments on commit 0306030

Please sign in to comment.