Blitz has built in session management that can be used with any type of authentication or identity providers.
Session management performs the following functions:
You login, logout, and otherwise modify the session via the
SessionContext
object which is
accessible anywhere on the server.
For login you will have form component in your UI that will submit a login mutation like the one shown below.
// app/auth/mutations/login.ts
import { Ctx } from "blitz"
export default async function login(input: SomeTSInputType, ctx: Ctx) {
// 1. Validate input data
// 2. Validate user credentials
// 3. Fetch user data
// 4. Create a new session (log in)
await ctx.session.$create({ userId: user.id, role: user.role })
}
For logout you will have a button in your UI that will submit a logout mutation like the one shown below.
Revoking a session will immediately delete all client-side query cache causing all queries on the page to be refetched. This ensures any sensitive data in the cache is deleted.
// app/auth/mutations/logout.ts
import { Ctx } from "blitz"
export default async function logout(_: any, ctx: Ctx) {
// 1. Revoke the current user session, logging them out.
return await ctx.session.$revoke()
}
Each session has PublicData
, data which is available on
the client and has the potential to be read by third-parties because it's
stored in a cookie readable by any Javascript code. This is usually used
to store the current user id, user role, and perhaps current organization
id.
You can change the session public data in any query or mutation like this:
// app/mutations/someMutation.ts
import { Ctx } from "blitz"
export default async function someMutation(input: any, ctx: Ctx) {
// This merges the input data with whatever is already in current publicData
await ctx.session.$setPublicData({ orgId: 1 })
}
A session is automatically refreshed after 1/4th of the expiry time has
passed. The default expiry time is 30 days. If you change the session’s
PublicData
the session is also automatically refreshed.
You will need to update tokens (access and anti-CSRF) after a session
refresh, if you use them (e.g. in a mobile app).
SessionContext
is available off of ctx
which is
provided as the second parameter to all queries and mutations because of
the sessionMiddleware
that's in blitz.config.js
.
// app/queries/someQuery.ts
import { Ctx } from "blitz"
export default async function someQuery(input: any, ctx: Ctx) {
// Access the SessionContext class
ctx.session.userId
ctx.session.role
ctx.session.$create(/*...*/)
return
}
getServerSideProps
or API RoutesYou can also get the session context inside getServerSideProps
or inside
API routes with getSession
like this:
import { getSession } from "blitz"
export const getServerSideProps = async ({ req, res }) => {
const session = await getSession(req, res)
console.log("User ID:", session.userId)
return { props: {} }
}
Blitz provides a useSession()
hook that returns
PublicData
with isLoading
property. This hook can be
used anywhere in your application.
Note: useSession()
uses suspense by default, so you need a <Suspense>
component above it in the tree. Or you can set
useSession({suspense: false})
to disable suspense.
import { useSession } from "blitz"
function SomeComponent() {
const session = useSession()
session.userId
session.role
return /*... */
}
If you are using getServerSideProps
, then you
can pass the session public data to useSession()
with the
initialPublicData
option.
import { useSession, GetServerSideProps } from "blitz"
export const getServerSideProps: GetServerSideProps = async ({
req,
res,
}) => {
const session = await getSession(req, res)
return { props: { initialPublicData: session.$publicData } }
}
const SomePage: BlitzPage = ({ initialPublicData }) => {
const session = useSession({ initialPublicData })
return /*... */
}
In production, you must provide the SESSION_SECRET_KEY
environment
variable with at least 32 characters. This is your private key for signing
JWT tokens.
On macOS and Linux, you can generate it by running openssl rand -hex 16
in your terminal.
If a user is not logged in, an anonymous session will automatically be
created for them. You can use ctx.session.$setPublicData()
and
ctx.session.$setPrivateData()
for anonymous sessions the same as for
logged in users. Any data you set for an anonymous session will
automatically be transferred to an authentication session when a user logs
in.
Anonymous sessions are JWT tokens that are stored on the client as an httpOnly cookie that never expires.
PublicData
for anonymous sessions is kept in the session JWT and not
stored in the database. Anonymous sessions will only be saved in your
database if you call session.$setPrivateData()
.
The anonymous session will be created on the first network request,
whether SSR or via an API. This will happen as long as sessionMiddleware
is in your middleware chain for that request.
One use case for this is saving shopping cart items for anonymous users. If an anonymous user later signs up or logs in, the anonymous session data can be merged into their new authenticated session.
Anonymous session PublicData
looks like this:
{
userId: null,
}
If using TypeScript, first update the Session.PublicData
type in
types.ts
like this:
import {DefaultCtx, SessionContext, SimpleRolesIsAuthorized} from "blitz"
import {User} from "db"
// Note: You should switch to Postgres and then use a DB enum for role type
export type Role = "ADMIN" | "USER"
declare module "blitz" {
export interface Ctx extends DefaultCtx {
session: SessionContext
}
export interface Session {
isAuthorized: SimpleRolesIsAuthorized<Role>
PublicData: {
userId: User["id"]
role: Role
+ orgId: number
}
}
}
Then change all uses of ctx.session.$create()
to pass in the new fields.
ctx.session.$create({ userId: 1, role: "ADMIN", orgId: 1 })
You can also use ctx.session.$setPublicData()
to update session data for
an already logged in user. This will merge values with the existing
public data.
ctx.session.$setPublicData({ orgId: 1 })
To access public data on the client:
import { useSession } from "blitz"
function SomeComponent() {
const session = useSession()
session.orgId
return /*... */
}
To access public data on the server:
// app/queries/someQuery.ts
import { Ctx } from "blitz"
export default async function someQuery(input: any, ctx: Ctx) {
// Access the SessionContext class
ctx.session.orgId
return
}
You can customize session management by passing an object to the
sessionMiddleware
factory function.
// blitz.config.js
const { sessionMiddleware, simpleRolesIsAuthorized } = require("blitz")
module.exports = {
middleware: [
sessionMiddleware({
cookiePrefix: "my-app",
sessionExpiryMinutes: 1234,
isAuthorized: simpleRolesIsAuthorized,
}),
],
}
Available options:
type SessionConfig = {
cookiePrefix?: string /* Default: 'blitz' */
sessionExpiryMinutes?: number /* Default: 30 days */
sameSite?: "strict" | "lax" | "none" /* Default: 'lax'.
If you set this to 'none', you also have to set `secureCookies: true`.
See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite */
method?: "essential" | "advanced" /* Default: 'essential' */
secureCookies?: boolean /* Default: undefined. This flag is skipped if running app on hostname `localhost` */
domain?: string /* Default: undefined. Can set as `.yourDomain.com` to work across subdomains */
publicDataKeysToSyncAcrossSessions?: string[] /* Default: ['role', 'roles'] */
getSession: (handle: string) => Promise<SessionModel | null>
getSessions: (userId: string | number) => Promise<SessionModel[]>
createSession: (session: SessionModel) => Promise<SessionModel>
updateSession: (
handle: string,
session: Partial<SessionModel>
) => Promise<SessionModel>
deleteSession: (handle: string) => Promise<SessionModel>
isAuthorized: ({ ctx: any, args: [...unknown] }) => boolean
}
interface SessionModel extends Record<any, any> {
handle: string
userId?: string | number
expiresAt?: Date
hashedSessionToken?: string
antiCSRFToken?: string
publicData?: string
privateData?: string
}
By default, session persistence is zero-config with Prisma. However, you can customize this to save sessions somewhere else, like Redis. You can also customize this if you have Prisma but want to customize the attribute names on the user or session model.
Customize session persistence by overriding the database access functions
defined above in SessionConfig
. The functions can do anything, but they
must conform to the defined input and outputs types.
For reference, here's the default config that works with Prisma.
When making a request from the client to an API route, you need to include
the anti-CSRF token in the anti-csrf
header like this:
import { getAntiCSRFToken } from "blitz"
const antiCSRFToken = getAntiCSRFToken()
if (antiCSRFToken) {
// Set fetch request header["anti-csrf"] = antiCSRFToken
}
And then you can get the sessionContext in the API route like this:
import { getSession } from "blitz"
export default async function ({ req, res }) {
const session = await getSession(req, res)
console.log("User ID:", session.userId)
res.json({ userId })
}
Authenticated sessions use opaque tokens that are stored in the database.
string
.httpOnly
, secure
cookie.secure
cookie that can be read from Javascript.SessionContext
interface SessionContext extends PublicData {
/**
* null if anonymous
*/
userId: unknown
$handle: string | null
$publicData: PublicData
$authorize(
...args: IsAuthorizedArgs
): asserts this is AuthenticatedSessionContext
$isAuthorized: (
...args: IsAuthorizedArgs
) => this is AuthenticatedSessionContext
$create: (
publicData: PublicData,
privateData?: Record<any, any>
) => Promise<void>
$revoke: () => Promise<void>
$revokeAll: () => Promise<void>
$getPrivateData: () => Promise<Record<any, any>>
$setPrivateData: (data: Record<any, any>) => Promise<void>
$setPublicData: (
data: Partial<Omit<PublicData, "userId">>
) => Promise<void>
}