For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt, and this page is available as Markdown at https://dotpress.dev/auth/protecting-routes.md.

Protecting routes

The auth plugin reads the access token of every request. When it is valid, ctx.user and ctx.auth are set; otherwise, the request stays anonymous. Guards then decide which routes need a user.

Guards

// One route
defineRoute<"/profile", EmptyObject, false, readonly [AuthenticatedContext]>(route: RouteConfig<"/profile", EmptyObject, EmptyObject, false, readonly [AuthenticatedContext], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<"/profile", EmptyObject, EmptyObject, false, readonly [AuthenticatedContext], unknown>.path: "/profile"
path
: '/profile',
RouteConfig<"/profile", EmptyObject, EmptyObject, false, readonly [AuthenticatedContext], unknown>.middlewares?: readonly [Middleware<AuthenticatedContext>] | undefined

Route middlewares, run after global and group middlewares. Context properties they guarantee (defineMiddleware()) are typed in the handler.

middlewares
: [
const requireAuth: Middleware<AuthenticatedContext>

Requires an authenticated user (401 UNAUTHORIZED otherwise). In the handlers of the routes using it, ctx.user and ctx.auth are defined.

requireAuth
],
RouteConfig<"/profile", EmptyObject, EmptyObject, false, readonly [AuthenticatedContext], unknown>.handler: RouteHandler<EmptyObject, {}, false, AuthenticatedContext>
handler
: ({
user: AuthUser

Authenticated user (authPlugin). Use requireAuth to require it.

user
}) => ({
email: string
email
:
user: AuthUser

Authenticated user (authPlugin). Use requireAuth to require it.

user
.
AuthUser.email: string
email
}),
}) // Every route of a group const
const admin: RouteGroup<EmptyObject & {}, AuthenticatedContext>
admin
=
createRouteGroup<"/admin", readonly [AuthenticatedContext]>(prefix: "/admin", middlewares?: readonly [Middleware<AuthenticatedContext>] | undefined): RouteGroup<EmptyObject & {}, AuthenticatedContext>

Creates a nested group, inheriting the prefix and middlewares. Context properties guaranteed by the group middlewares are typed in the handlers of its routes.

createRouteGroup
('/admin', [
function requireRole(roles_0: string, ...roles: string[]): Middleware<AuthenticatedContext>

Requires an authenticated user with at least one of the roles (401 UNAUTHORIZED without user, 403 FORBIDDEN without role). Roles come from the getUserRoles setting of createAuth().

requireRole
('admin')])
const admin: RouteGroup<EmptyObject & {}, AuthenticatedContext>
admin
.
RouteGroup<EmptyObject & {}, AuthenticatedContext>.defineRoute<"/stats", EmptyObject, false, unknown[]>(route: RouteConfig<"/stats", EmptyObject, EmptyObject & {}, false, unknown[], AuthenticatedContext>): void
defineRoute
({
RouteConfig<"/stats", EmptyObject, EmptyObject & {}, false, unknown[], AuthenticatedContext>.path: "/stats"
path
: '/stats',
RouteConfig<"/stats", EmptyObject, EmptyObject & {}, false, unknown[], AuthenticatedContext>.handler: RouteHandler<EmptyObject, {}, false, AuthenticatedContext>
handler
: ({
user: AuthUser

Authenticated user (authPlugin). Use requireAuth to require it.

user
,
auth: AuthSession

Access token data of the authenticated user (authPlugin).

auth
}) => ({
admin: string
admin
:
user: AuthUser

Authenticated user (authPlugin). Use requireAuth to require it.

user
.
AuthUser.id: string
id
,
roles: readonly string[]
roles
:
auth: AuthSession

Access token data of the authenticated user (authPlugin).

auth
.
AuthSession.roles: readonly string[]

Roles of the user, used by requireRole().

roles
}),
})
GuardWithout userWithout the right
requireAuth401—
requireRole(...roles)401403 without one of the roles
requireClient(...clientIds)401403 when the token was issued for another client, see Clients

A 401 carries a WWW-Authenticate: Bearer header (RFC 6750), with error="invalid_token" when the request sent an expired, revoked or invalid token: the client knows it must refresh. Every 401 of the app gets this header, unless it already has one.

The plugin reads the access token from the Authorization: Bearer header, then from its cookie in cookie mode. It never reads the request body: a route of the app can receive an accessToken field of its own, a third-party token for instance, without side effects.

Guards are typed middlewares: in the routes using them, directly or through a group, ctx.user and ctx.auth are defined. They also declare the bearerAuth security scheme (AUTH_SECURITY_SCHEMES), without meta.security: the OpenAPI document marks these routes as secured, and the API reference shows an Authorize button.

The type of the user

ctx.user is what dataProvider.findUserById() returns. Describe it once:

declare module '@dotpress/auth' {
  interface AuthUser {
    id: string
    email: string
    roles: string[]
  }
}

ctx.auth holds the access token data:

FieldContent
userIdThe sub claim
sessionIdThe session (sid claim): see Sessions and tokens
clientIdThe client application, with the clients setting
rolesThe roles of the user
tokenIdThe jti claim
usernameOnly with the usernameClaim setting
claimsEvery claim of the token

Roles

requireRole() checks the roles array of the user, or its role string. When roles are stored elsewhere, getUserRoles(user) returns them:

createAuth({
  // …
  getUserRoles: (user) => user.memberships.map((membership) => membership.role),
})

Routes for everyone, better for users

Without a guard, a route serves anonymous and authenticated users alike: ctx.user is then AuthUser | undefined.

defineRoute({
  path: '/articles/:id',
  handler: async ({ req, user }) => {
    const article = await articles.find(req.params.id)
    return { ...article, bookmarked: user ? await bookmarks.has(user.id, article.id) : false }
  },
})

Authentication is not authorization

requireAuth proves who the user is, not what they may access. A route that reads /invoices/:id must still check that this invoice belongs to the user: otherwise, any user can read any invoice by changing the ID in the URL. It is the most common vulnerability of APIs (OWASP API1: Broken Object Level Authorization).

const 
const myInvoices: RouteGroup<EmptyObject & {}, AuthenticatedContext>
myInvoices
=
createRouteGroup<"/invoices", readonly [AuthenticatedContext]>(prefix: "/invoices", middlewares?: readonly [Middleware<AuthenticatedContext>] | undefined): RouteGroup<EmptyObject & {}, AuthenticatedContext>

Creates a nested group, inheriting the prefix and middlewares. Context properties guaranteed by the group middlewares are typed in the handlers of its routes.

createRouteGroup
('/invoices', [
const requireAuth: Middleware<AuthenticatedContext>

Requires an authenticated user (401 UNAUTHORIZED otherwise). In the handlers of the routes using it, ctx.user and ctx.auth are defined.

requireAuth
])
const myInvoices: RouteGroup<EmptyObject & {}, AuthenticatedContext>
myInvoices
.
RouteGroup<EmptyObject & {}, AuthenticatedContext>.defineRoute<"/:id", EmptyObject, false, unknown[]>(route: RouteConfig<"/:id", EmptyObject, EmptyObject & {}, false, unknown[], AuthenticatedContext>): void
defineRoute
({
RouteConfig<"/:id", EmptyObject, EmptyObject & {}, false, unknown[], AuthenticatedContext>.path: "/:id"
path
: '/:id',
RouteConfig<"/:id", EmptyObject, EmptyObject & {}, false, unknown[], AuthenticatedContext>.handler: RouteHandler<EmptyObject, {
    id: string;
}, false, AuthenticatedContext>
handler
: async ({
req: RouteRequest<EmptyObject, {
    id: string;
}, false>
req
,
user: AuthUser

Authenticated user (authPlugin). Use requireAuth to require it.

user
}) => {
const
const invoice: {
    id: string;
    customerId: string;
    total: number;
} | undefined
invoice
= await
const invoices: {
    find(id: string): Promise<{
        id: string;
        customerId: string;
        total: number;
    } | undefined>;
}
invoices
.
function find(id: string): Promise<{
    id: string;
    customerId: string;
    total: number;
} | undefined>
find
(
req: RouteRequest<EmptyObject, {
    id: string;
}, false>
req
.
DotpressRequest<{ id: string; }, QueryValues, unknown, UploadedFiles>.params: {
    id: string;
}
params
.
id: string
id
)
// Not found, rather than forbidden: do not reveal that the invoice exists. if (!
const invoice: {
    id: string;
    customerId: string;
    total: number;
} | undefined
invoice
||
const invoice: {
    id: string;
    customerId: string;
    total: number;
}
invoice
.
customerId: string
customerId
!==
user: AuthUser

Authenticated user (authPlugin). Use requireAuth to require it.

user
.
AuthUser.id: string
id
) return
function notFoundError(message?: string, data?: ErrorData): HttpError
notFoundError
('Invoice not found')
return
const invoice: {
    id: string;
    customerId: string;
    total: number;
}
invoice
}, })

Check access in the query itself when you can (WHERE id = ? AND customer_id = ?), or in a middleware of the group for a whole resource: the Tasks API example checks the membership of a project once, on the /projects/:projectId group.

Reading the cookies yourself

In cookie mode, a route of the app that reads the auth cookies itself must apply the same protection against cross-site requests as the plugin: await auth.isCookieAuthenticationAllowed(ctx.req), which resolves to false for a request the plugin would refuse. See Cookie mode and CSRF.