@edgedb/auth-express
This library provides a set of utilities to help you integrate authentication into your Express application. It supports various authentication methods including OAuth, email/password, and session-based authentication.
Installation
This package is published on npm and can be installed with your package manager of choice.
npm install @edgedb/auth-express
Note: We have tested this library against the latest version of Express v4 with the types provided at DefinitelyTyped and have set up peerDependencies
based on typical usage with npm.
Setup
Prerequisites:
- Node v18+
- Note: Due to using the
crypto
global, you will need to start Node with --experimental-global-webcrypto
. You can add this option to your NODE_OPTIONS
environment variable, like NODE_OPTIONS='--experimental-global-webcrypto'
in the appropriate .env
file.
- Before adding EdgeDB auth to your Express app, you will first need to enable the
auth
extension in your EdgeDB schema, and have configured the extension with some providers (you can do this in CLI or EdgeDB UI). Refer to the auth extension docs for details on how to do this. - We depend on the following middleware being installed in your Express app:
body-parser
: both JSON and urlencodedcookie-parser
import express from "express";
import cookieParser from "cookie-parser";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
This library provides a few affordances for adding EdgeDB Auth to your existing Express app:
- Routers and route handlers to handle the authentication flow required by EdgeDB.
- Middleware to attach sessions to requests.
- Some additional utility functions for handling various auth related concerns.
Configuring the ExpressAuth
class
After configuring the EdgeDB Auth extension, you can set up the various auth routes with our route builders.
Start by instantiating an ExpressAuth
object by passing it a configured EdgeDB client and some options:
import { createClient } from "edgedb";
import createExpressAuth from "@edgedb/auth-express";
import * as expressAuth from "@edgedb/auth-express";
const client = createClient();
const auth = createExpressAuth(client, {
baseUrl: "http://localhost:3000",
});
Using the session middleware: createSessionMiddleware
We provide a middleware factory that will attach an ExpressAuthSession
object to your request object, which you can use to make authenticated queries, or protect routes.
app.use(auth.createSessionMiddleware());
You can then use the SessionRequest
type for your route's request parameter, which extends the Express Request
type with session?: ExpressAuthSession
.
app.get("/dashboard", (req: expressAuth.SessionRequest, res) => {
if (!(await req.session?.isSignedIn())) {
return res.redirect("/auth/builtin/signin");
}
res.render(
"dashboard",
{ title: "Dashboard", message: "Welcome to your dashboard!" }
);
});
Create your own requireAuth
middleware
You can centralize the logic to redirect unauthenticated routes into a custom middleware like this:
function requireAuth(
req: expressAuth.SessionRequest,
res: Response,
next: NextFunction
) {
if (!(await req.session?.isSignedIn())) {
return res.redirect("/auth/builtin/signin");
}
next();
}
Then you can use this to protect your route like this:
app.get("/dashboard", requireAuth, (req: expressAuth.SessionRequest, res) => {
res.render(
"dashboard",
{ title: "Dashboard", message: "Welcome to your dashboard!" }
);
});
Adding route handlers
Route handlers can be added either using one or more of our "router factories", or by constructing your own Express Router
object, and attaching the necessary middleware to routes that you configure yourself. The factory pattern makes it easy to quickly get a standardized set of routes to handle either the built-in UI, or some combination of email/password and/or OAuth routes. For maximum control over route locations, custom middleware patterns, or integration into existing router structures, you can use the router middleware.
signout
We provide a "signout" route handler that is used to reset the req.session
, and remove the authentication cookie from the browser session. It works across all of the various authentication strategies, so it is included as a bare middleware that you add to your Express router yourself, adding a route handler for redirecting after the sign-out completes.
app.get("/signout", expressAuth.signout, (req, res) => {
res.redirect("/goodbye");
});
Router factories
Built-in UI: createBuiltinRouter
When using the built-in UI, be sure to set your redirect_to
and redirect_to_on_signup
values in the configuration to the route that the callback is mounted to, adding ?isSignUp=true
to the redirect_to_on_signup
value.
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
.
const builtinRouter = auth.createBuiltinRouter({
callback: [
(req: expressAuth.CallbackRequest, res, next) => {
if (req.isSignUp) {
return res.redirect("/onboarding");
}
res.redirect("/");
},
],
});
app.use("/auth", builtinRouter);
Custom UI: Email and password createEmailPasswordRouter
routerPath: string
, required, This is the path relative to the baseUrl
given that was configured when creating the ExpressAuth
object that this router will be attached to. This is used to build the URL to the email verification 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.sendPasswordResetEmail: (express.RouteHandler | express.ErrorHandler)[]
, Attached middleware executes after the user sends a password reset. Typically you will redirect the user to some success page containing instructions to look for a reset email.resetPassword: (express.RouteHandler | express.ErrorHandler)[]
, Attached middleware executes after the user successfully resets their password. Typically you will redirect the user to your application here.resendVerificationEmail: (express.RouteHandler | express.ErrorHandler)[]
, Attached middleware executes after the user resends their email verification email. Typically you will redirect the user to some success page, or if you do not require verification to use your app, you can redirect to your app here.
const emailPasswordRouter = auth.createEmailPasswordRouter(
"/auth/email-password",
"/forgot-password",
{
signIn: [
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
},
],
signUp: [
(req: expressAuth.AuthRequest, res) => {
res.redirect("/onboarding");
},
],
verify: [
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
},
],
sendPasswordResetEmail: [
(req: expressAuth.AuthRequest, res) => {
res.redirect("/email-success");
},
],
resetPassword: [
(req: expressAuth.AuthRequest, res) => {
res.redirect("/email-success");
},
],
resendVerificationEmail: [
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
},
],
}
);
app.use(emailPasswordRouter);
Custom UI: OAuth createOAuthRouter
routerPath: string
, required, This is the path relative to the baseUrl
given that was configured when creating the ExpressAuth
object that this router will be attached to. This is used to build the URL to the callback path configured by the router factory.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
.
const oAuthRouter = auth.createOAuthRouter("/auth/oauth", {
callback: [
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
},
],
});
app.use(oAuthRouter);
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.
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);
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.
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.
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);
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:
Built-in UI
const builtinRouter = Router()
.get("/signin", auth.builtin.signIn)
.get("/signup", auth.builtin.signUp)
.get(
"/callback",
auth.builtin.callback,
(req: expressAuth.CallbackRequest, res, next) => {
if (req.isSignUp) {
return res.redirect("/onboarding");
}
res.redirect("/");
}
);
app.use("/auth", builtinRouter);
Custom UI: Email and password
const emailPasswordRouter = Router()
.post(
"/signin",
auth.emailPassword.signIn,
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
}
)
.post(
"/signup",
auth.emailPassword.signUp(
"http://localhost:3000/auth/email-password/verify"
),
(req: expressAuth.AuthRequest, res) => {
res.redirect("/onboarding");
}
)
.get(
"/verify",
auth.emailPassword.verify,
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
}
)
.post(
"/send-password-reset-email",
auth.emailPassword.sendPasswordResetEmail(
"http://localhost:3000/auth/email-password/reset-password"
),
(req: expressAuth.AuthRequest, res) => {
res.redirect("/email-success");
}
)
.post(
"/reset-password",
auth.emailPassword.resetPassword,
(req: expressAuth.AuthRequest, res) => {
res.redirect("/email-success");
}
)
.post(
"/resend-verification-email",
auth.emailPassword.resendVerificationEmail,
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
}
);
app.use("/auth/email-password", emailPasswordRouter);
Custom UI: OAuth
const oAuthRouter = Router()
.get(
"/",
auth.oAuth.redirect(
"http://localhost:3000/auth/oauth/callback"
)
)
.get(
"/callback",
auth.oAuth.callback,
(req: expressAuth.AuthRequest, res) => {
res.redirect("/");
}
);
app.use("/auth/oauth", oAuthRouter);
Custom UI: Magic Link
const magicLinkRoute = Router()
.post(
"/send",
auth.magicLink.send(
"http://localhost:3000/auth/magic-link/callback",
"/login-failure",
),
)
.post(
"/signup",
auth.magicLink.signUp(
"http://localhost:3000/auth/magic-link/callback",
"/login-failure",
),
)
.get(
"/callback",
auth.magicLink.callback,
(req: expressAuth.CallbackRequest, res) => {
res.redirect("/");
},
);
app.use("/auth/magic-link", router);
Custom UI: WebAuthn
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:
const builtinRouter = auth.createBuiltinRouter({
callback: [
(req: expressAuth.CallbackRequest, res, next) => {
if (req.isSignUp) {
return res.redirect("/onboarding");
}
res.redirect("/");
},
(error: any, req, res, next) => {
res.redirect(`/error?error=${encodeURIComponent(error.message)}`);
},
],
});
Reference
createExpressAuth
(default package export)
baseUrl: string
, required, The url of your application; needed for various auth flows (eg. OAuth) to redirect back to.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.
Returns: ExpressAuth
ExpressAuth
isPasswordResetTokenValid(resetToken: string)
: Checks if the provided password reset token is valid.getSession(req: ExpressRequest)
: Returns the session for the specified request.getProvidersInfo()
: Returns information about the available providers.createSessionMiddleware()
: Creates a session middleware.
For the router factories and route middleware, see the sections above for configuring routes for full documentation and usage instructions.
ExpressAuthSession
client: Client
: ext::auth::auth_token
global set based on the auth token that is present in the authentication cookie. If there is no cookie, the Client
will be the original client that was passed when creating the ExpressAuth
object.isSignedIn: () => Promise<boolean>
: Checks to see if this Request has a valid, unexpired auth token.
TokenData
Represents the result of successfully completing authentication.
auth_token: string
: The raw JWT returned by the server.identity_id: string | null
: The UUID of the ext::auth::Identity
provider_token: string | null
: If the identity is an OAuth identity, this is the original auth token provided by the Identity Provider.provider_refresh_token: string | null
: If the identity is an OAuth identity, this is the original refresh token provided by the Identity Provider, if it was provided by the provider.
CallbackRequest
This is an extension of the Express Request
interface containing some optional data added by auth callback middleware (either from the built-in UI or in an OAuth callback flow):
req.session?: ExpressAuthSession
req.tokenData?: TokenData
req.provider?: ProviderName
: The name of the provider type that this authentication event is for.req.isSignUp?: boolean
: If this authentication event represents a new user signing up, or an existing user signing in.
AuthRequest
This is an extension of the Express Request
interface containing some optional data added by the various auth middleware.
session?: ExpressAuthSession
tokenData?: TokenData