Authorization is the act of allowing or disallowing access to data and pages in your application.
You secure data by calling
ctx.session.$authorize()
inside all the queries
and mutations that you want secured. (Or if using
resolver.pipe
, by using
resolver.authorize
).
You can also secure API routes the same way.
Those will throw AuthenticationError
if the user is not logged in, and
it will throw AuthorizationError
if the user is logged in but doesn't
have the required permissions.
import { resolver } from "@blitzjs/rpc"
import db from "db"
import * as z from "zod"
const CreateProject = z
.object({
name: z.string(),
})
.nonstrict()
export default resolver.pipe(
resolver.zod(CreateProject),
resolver.authorize(),
async (input, ctx) => {
// *: in multi-tenant app, you must add validation to ensure correct tenant
const projects = await db.projects.create({ data: input })
return projects
}
)
or
import {Ctx} from "blitz"
import db from "db"
import * as z from "zod"
const CreateProject = z
.object({
name: z.string(),
}).nonstrict()
type CreateProjectType = z.infer<typeof CreateProject>
export default function createProject(input: CreateProjectType, ctx: Ctx) {
const data = CreateProject.parse(input)
ctx.session.$authorize(),
// *: in multi-tenant app, you must add validation to ensure correct tenant
const projects = await db.projects.create({data})
return projects
}
For security, it's very important to validate all input values in your mutations. We recommended using zod, which we include in all our code generation templates. Without this, users may be able to access data or perform actions that are forbidden.
import { resolver } from "@blitzjs/rpc"
import db from "db"
import * as z from "zod"
const CreateProject = z
.object({
name: z.string(),
})
.nonstrict()
export default resolver.pipe(
resolver.zod(CreateProject),
resolver.authorize(),
async (input, ctx) => {
// *: in multi-tenant app, you must add validation to ensure correct tenant
const projects = await db.projects.create({ data: input })
return projects
}
)
Set Page.authenticate = true
on all pages that require a user to be
logged in. If a user is not logged in, an AuthenticationError
will be
thrown and caught by your top level Error Boundary.
Or if instead you want to redirect the user, set
Page.authenticate = {redirectTo: '/login'}
const Page: BlitzPage = () => {
return <div>{/* ... */}</div>
}
Page.authenticate = true
// or Page.authenticate = {redirectTo: '/login'}
// or Page.authenticate = {redirectTo: Routes.Login()}
export default Page
For pages that are only for logged out users, such as login and signup
pages, set Page.redirectAuthenticatedTo = '/'
to automatically redirect
logged in users to another page.
import { Routes } from "@blitzjs/next"
const Page: BlitzPage = () => {
return <div>{/* ... */}</div>
}
// using full path
Page.redirectAuthenticatedTo = "/"
// using route manifest
Page.redirectAuthenticatedTo = Routes.Home()
// using function
Page.redirectAuthenticatedTo = ({ session }) =>
session.role === "admin" ? "/admin" : Routes.Home()
export default Page
You can secure layouts as you secure pages:
const Layout = ({ children }) => {
return <div>{children}</div>
}
Layout.authenticate = true
// or Layout.authenticate = {redirectTo: '/login'}
// or Layout.authenticate = {redirectTo: Routes.Login()}
// or Layout.redirectAuthenticatedTo = Routes.Home()
const Page = () => {
return <div>{/* ... */}</div>
}
Page.getLayout = (page) => <Layout>{page}</Layout>
export default Page
Blitz will take the first component it finds from the response of
getLayout
with either a authenticate
key or a
redirectAuthenticatedTo
key and uses its values for authentication.
These values can be overwritten in a per-page basis with
Page.authenticate
or Page.redirectAuthenticatedTo
:
const Layout: BlitzLayout = ({ children }) => {
return <div>{children}</div>
}
Layout.authenticate = true
const Page: BlitzPage = () => {
return <div>{/* ... */}</div>
}
Page.getLayout = (page) => <Layout>{page}</Layout>
Page.authenticate = false
export default Page
While you can use redirects to and from a /login
page, we recommended to
use Error Boundaries instead of redirects.
In React, the way you catch errors in your UI is to use an error boundary.
You should have a top level error boundary inside _app.tsx
so that these
errors are handled from everywhere in our app. And then if you need, you
can also place more error boundaries at other places in your app.
The default error handling setup in new Blitz apps is as follows:
AuthenticationError
is thrown, directly show the user a login form
instead of redirecting to a separate route. This prevents the need to
manage redirect URLs. Once the user logs in, the error boundary will
reset and the user can access the original page they wanted to access.AuthorizationError
is thrown, display an error stating such.And here's the default RootErrorFallback
that's in app/pages/_app.tsx
.
You can customize it as required for your needs.
import { AuthenticationError, AuthorizationError } from "blitz"
import { ErrorComponent, ErrorFallbackProps } from "@blitzjs/next"
function RootErrorFallback({
error,
resetErrorBoundary,
}: ErrorFallbackProps) {
if (error instanceof AuthenticationError) {
return <LoginForm onSuccess={resetErrorBoundary} />
} else if (error instanceof AuthorizationError) {
return (
<ErrorComponent
statusCode={error.statusCode}
title="Sorry, you are not authorized to access this"
/>
)
} else {
return (
<ErrorComponent
statusCode={error.statusCode || 400}
title={error.message || error.name}
/>
)
}
}
For more information on error handling in Blitz, see the Error Handling documentation.
There's two approaches you can use to check the user role in your UI.
useSession()
The first way is to use the
useSession()
hook to read
the user role from the session's publicData
.
This is available on the client without making a network call to the
backend, so it's available faster than the useCurrentUser()
approach
described below.
Note: due to the nature of static pre-rendering, the session
will not
exist on the very first render on the client. This causes a quick "flash"
on first load. You can fix that by setting
Page.suppressFirstRenderFlicker = true
import { useSession } from "@blitzjs/auth"
const session = useSession()
if (session.role === "admin") {
return /* admin stuff */
} else {
return /* normal stuff */
}
useCurrentUser()
The second way is to use the useCurrentUser()
hook. New Blitz apps by
default have a useCurrentUser()
hook and a corresponding
getCurrentUser
query. Unlike the useSession()
approach above,
useCurrentUser()
will require a network call and thus be slower.
However, if you need access to user data that would be insecure to store
in the session's publicData
, you would need to use useCurrentUser()
instead of useSession()
.
import { useCurrentUser } from "app/users/hooks/useCurrentUser"
const user = useCurrentUser()
if (user.isFunny) {
return /* funny stuff */
} else {
return /* normal stuff */
}
isAuthorized
AdaptersThe implementation of ctx.session.$isAuthorized()
and
ctx.session.$authorize()
are defined by an adapter which you set in the
sessionMiddleware()
config.
ctx.session.$isAuthorized()
Always returns a boolean indicating if user is authorized
ctx.session.$authorize()
Throws an error if the user is not authorized. This is what you most commonly use to secure your queries and mutations.
import { Ctx } from "blitz"
import { GetUserInput } from "./somewhere"
export default async function getUser({ where }: GetUserInput, ctx: Ctx) {
ctx.session.$authorize("admin")
return await db.user.findOne({ where })
}
simpleRolesIsAuthorized
(default in new apps)To use, add it to your sessionMiddleware
configuration (this is already
set up by default in new apps).
// blitz.config.js
const { sessionMiddleware, simpleRolesIsAuthorized } = require("blitz")
module.exports = {
middleware: [
sessionMiddleware({
isAuthorized: simpleRolesIsAuthorized,
}),
],
}
And if using TypeScript, set the type in types.ts
like this:
import { SimpleRolesIsAuthorized } from "@blitzjs/auth"
type Role = "ADMIN" | "USER"
declare module "@blitzjs/auth" {
export interface Session {
isAuthorized: SimpleRolesIsAuthorized<Role>
}
}
ctx.session.$isAuthorized(roleOrRoles?: string | string[])
Example usage:
// User not logged in
ctx.session.$isAuthorized() // false
// User logged in with 'customer' role
ctx.session.$isAuthorized() // true
ctx.session.$isAuthorized("customer") // true
ctx.session.$isAuthorized("admin") // false
ctx.session.$authorize(roleOrRoles?: string | string[])
Example usage:
// User not logged in
ctx.session.$authorize() // throws AuthenticationError
// User logged in with 'customer' role
ctx.session.$authorize() // success - no error
ctx.session.$authorize("customer") // success - no error
ctx.session.$authorize("admin") // throws AuthorizationError
ctx.session.$authorize(["admin", "customer"]) // success - no error
An isAuthorized
adapter must conform to the following function
signature.
type CustomIsAuthorizedArgs = {
ctx: any
args: [/* args that you want for session.$authorize(...args) */]
}
export function customIsAuthorized({
ctx,
args,
}: CustomIsAuthorizedArgs) {
// can access ctx.session, ctx.session.userId, etc
}
Here's the source code for the simpleRolesIsAuthorized
adapter include
in Blitz core as of Jan 26, 2021.
type SimpleRolesIsAuthorizedArgs = {
ctx: any
args: [roleOrRoles?: string | string[]]
}
export function simpleRolesIsAuthorized({
ctx,
args,
}: SimpleRolesIsAuthorizedArgs) {
const [roleOrRoles, options = {}] = args
const condition = options.if ?? true
// No roles required, so all roles allowed
if (!roleOrRoles) return true
// Don't enforce the roles if condition is false
if (!condition) return true
const rolesToAuthorize = []
if (Array.isArray(roleOrRoles)) {
rolesToAuthorize.push(...roleOrRoles)
} else if (roleOrRoles) {
rolesToAuthorize.push(roleOrRoles)
}
for (const role of rolesToAuthorize) {
if ((ctx.session as SessionContext).$publicData.roles!.includes(role))
return true
}
return false
}