🚀Announcing Flightcontrol - Optimized Deployment for Fullstack Blitz.js and Next.js 🚀
Back to Documentation Menu

Authorization & Security

Topics

Jump to a Topic

Authorization is the act of allowing or disallowing access to data and pages in your application.

Secure Your Data

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 "blitz"
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) => { // TODO: 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(),
// TODO: in multi-tenant app, you must add validation to ensure correct tenant const projects = await db.projects.create({data}) return projects }

Input Validation

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 "blitz"
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) => { // TODO: in multi-tenant app, you must add validation to ensure correct tenant const projects = await db.projects.create({ data: input }) return projects } )

Secure Your Pages

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

Redirecting Logged In Users

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 "blitz"

const Page: BlitzPage = () => {
  return <div>{/* ... */}</div>
}

// using full path Page.redirectAuthenticatedTo = "/" // using route manifest Page.redirectAuthenticatedTo = Routes.Home()
export default Page

UX for Unauthorized Users

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:

  • If 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.
  • If 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 {
  ErrorComponent,
  AuthenticationError,
  AuthorizationError,
  ErrorFallbackProps,
} from "blitz"

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.

Displaying Different Content Based on User Role

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 "blitz"

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/core/hooks/useCurrentUser"

const user = useCurrentUser()

if (user.isFunny) {
  return /* funny stuff */
} else {
  return /* normal stuff */
}

isAuthorized Adapters

The 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)

Setup

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 "blitz"

type Role = "ADMIN" | "USER"

declare module "blitz" {
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

Making a Custom Adapter

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
}
Example

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
}

Idea for improving this page? Edit it on GitHub.