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

Add WebAuthn and Magic Links in auth-express #915

Merged
merged 5 commits into from Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/auth-express/package.json
@@ -1,7 +1,7 @@
{
"name": "@edgedb/auth-express",
"description": "Helper library to integrate the EdgeDB Auth extension with Express",
"version": "0.1.0",
"version": "0.2.0-beta.1",
"type": "module",
"author": "EdgeDB <info@edgedb.com>",
"repository": {
Expand All @@ -11,15 +11,17 @@
},
"license": "Apache-2.0",
"sideEffects": false,
"files": [
"/dist"
],
"files": ["/dist"],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./webauthn": {
"types": "./dist/webauthn.d.ts",
"default": "./dist/webauthn.js"
}
},
"scripts": {
Expand Down
169 changes: 169 additions & 0 deletions packages/auth-express/readme.md
Expand Up @@ -226,6 +226,102 @@ app.use(oAuthRouter);
// - GET /auth/oauth/callback
```

### Custom UI: Magic Link

- `routerPath: string`, required, This is the path relative to the `baseUrl` configured when creating the `ExpressAuth` object. This path is used to build the URL for the callback path configured by the router factory.
- `failureUrl: string`, required, URL to redirect to in case of a failure during the Magic Link process.
- `callback: (express.RouteHandler | express.ErrorHandler)[]`, required, Once the authentication flow completes, this callback will be called, and you must return a terminating Express route handler here. Typically, you'll redirect to elsewhere in your app based on `req.isSignUp`.
- `send: (express.RouteHandler | express.ErrorHandler)[]`, this route handler stack will be called when a request is made to send a magic link to a registered email address. Typically, you'll return some HTML or a redirect here that indicates that the user should check their email.
- `signup: (express.RouteHandler | express.ErrorHandler)[]`, this route handler stack will be called when a request is made to register an email address. Typically, you'll return some HTML or a redirect here that indicates that the user should check their email.

```ts
const magicLinkRouter = auth.createMagicLinkRouter(
"/auth/magic-link",
"/login-failure",
{
callback: [
(req: expressAuth.CallbackRequest, res) => {
res.redirect("/");
},
],
send: [
(req, res) => {
res.redirect("/check-email.html");
},
],
signUp: [
(req, res) => {
res.redirect("/check-email.html");
}
]
}
);

app.use(magicLinkRouter);
// This creates the following routes:
// - POST /auth/magic-link/send
// - POST /auth/magic-link/signup
// - GET /auth/magic-link/callback
```

### Custom UI: WebAuthn

Unlike the other authentication methods, WebAuthn requires a client-side script that runs in the browser. This script requests JSON from the EdgeDB Auth server that gets options to pass to the Web Authentication API built into the browser, and then after successfully creating new credentials or retrieving existing credentials, it calls back to the endpoints you're configuring below.

In order to facilitate the sign in and sign up ceremonies, we export a helper class called `WebAuthnClient` that you must configure with some relevant paths based on how you set up your routing below.

```ts
import { WebAuthnClient } from "@edgedb/auth-express";

const webAuthnClient = new WebAuthnClient({
signupOptionsUrl: "http://localhost:3000/auth/webauthn/signup/options",
signupUrl: "http://localhost:3000/auth/webauthn/signup",
signinOptionsUrl: "http://localhost:3000/auth/webauthn/signin/options",
signinUrl: "http://localhost:3000/auth/webauthn/signin",
verifyUrl: "http://localhost:3000/auth/webauthn/verify",
});
```

- `routerPath: string`, required, This is the path relative to the `baseUrl` configured when creating the `ExpressAuth` object. This path is used to build the URL for the callback path configured by the router factory.
- `signIn: (express.RouteHandler | express.ErrorHandler)[]`, required, Attached middleware executes when sign-in attempt succeeds. Typically you will redirect the user to your application here.
- `signUp: (express.RouteHandler | express.ErrorHandler)[]`, required, Attached middleware executes when sign-up attempt succeeds. Typically you will redirect the user to your application here.
- `verify: (express.RouteHandler | express.ErrorHandler)[]`, Attached middleware executes after the user verifies their email successfully. Typically you will redirect the user to your application here.
- `signInOptions: (express.RouteHandler | express.ErrorHandler)[]`, This redirects the user to the appropriate URL of the EdgeDB server to retrieve the WebAuthn sign in options.
- `signUpOptions: (express.RouteHandler | express.ErrorHandler)[]`, This redirects the user to the appropriate URL of the EdgeDB server to retrieve the WebAuthn sign up options.

```ts
const webAuthnRouter = auth.createWebAuthnRouter(
"/auth/webauthn",
{
signInOptions: [],
signIn: [
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
},
],
signUpOptions: [],
signUp: [
(req: expressAuth.AuthRequest, res) => {
res.redirect("/onboarding");
},
],
verify: [
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
},
],
}
);

app.use(webAuthnRouter);
// This creates the following routes:
// - GET /auth/webauthn/signin/options
// - POST /auth/webauthn/signin
// - GET /auth/webauthn/signup/options
// - POST /auth/webauthn/signup
// - GET /auth/webauthn/verify
```

### Custom router

Each route is also available as a middleware itself for maximum customization, to allow custom route names, per-route middleware customization, and integration with advanced Express patterns. Here are examples that are equivalent to the ones given in the router factory section:
Expand Down Expand Up @@ -329,6 +425,78 @@ const oAuthRouter = Router()
app.use("/auth/oauth", oAuthRouter);
```

### Custom UI: Magic Link

```ts
const magicLinkRoute = Router()
.post(
"/send",
auth.magicLink.send(
// URL of the callback endpoint configured below
"http://localhost:3000/auth/magic-link/callback",
// URL of the route in your app that should receive login errors
"/login-failure",
),
)
.post(
"/signup",
auth.magicLink.signUp(
// URL of the callback endpoint configured below
"http://localhost:3000/auth/magic-link/callback",
// URL of the route in your app that should receive login errors
"/login-failure",
),
)
.get(
"/callback",
auth.magicLink.callback,
(req: expressAuth.CallbackRequest, res) => {
// Custom logic after successful authentication
res.redirect("/");
},
);


app.use("/auth/magic-link", router);
```

#### Custom UI: WebAuthn

```ts
const webAuthnRouter = Router()
.get(
"/signin/options",
auth.webauthn.signInOptions
)
.post(
"/signin",
auth.webauthn.signIn,
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
}
)
.get(
"/signup/options",
auth.webauthn.signUpOptions
)
.post(
"/signup",
auth.webauthn.signUp,
(req: expressAuth.AuthRequest, res) => {
res.redirect("/onboarding");
}
)
.get(
"/verify",
auth.webauthn.verify,
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
}
);

app.use("/auth/webauthn", webAuthnRouter);
```

### Error handling

If an error occurs during the authentication flow, it will be passed to the Express error handler. You can use Express error handlers to handle errors either on individual routes by adding the error handlers to the middleware arrays or in your route definitions, or define a router-wide error handler. Any Express error handling pattern is available here, but let's examine a quick example of handling error with the built-in UI flow:
Expand Down Expand Up @@ -399,3 +567,4 @@ This is an extension of the Express `Request` interface containing some optional

- `session?: ExpressAuthSession`
- `tokenData?: TokenData`