Login

Add the login routes to your server and the links to your pages.

Add the login routes

Add these four routes. The callback URL (/blockauth/callback) must be on your validated domain.

RouteDoes
GET /blockauth/loginCreates a random state, keeps it in the session and redirects to the Block-Auth login
GET /blockauth/callbackChecks the state, redeems the code and creates your session
GET /api/meReturns the signed-in user, or 401
POST /blockauth/logoutCloses the Block-Auth sessions and yours
import crypto from 'node:crypto'
import express from 'express'
import session from 'express-session'
import { AuthCodeClient } from '@block-auth.io/node'

const { APP_URL, BLOCKAUTH_API_KEY, BLOCKAUTH_API_SECRET, SESSION_SECRET } = process.env

export const blockauth = new AuthCodeClient(BLOCKAUTH_API_KEY, BLOCKAUTH_API_SECRET)

const sessionUser = (user) => ({
  address: user.address,
  roles: user.roles,
  expiresAt: Date.parse(`${user.signedExpiredAt.replace(' ', 'T')}Z`),
})

export const currentUser = (req) => {
  const user = req.session.user
  if (user && Date.now() < user.expiresAt) return user
  delete req.session.user
  return null
}

export const app = express()

app.use(
  session({
    secret: SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: { httpOnly: true, sameSite: 'lax', secure: APP_URL.startsWith('https') },
  }),
)

const CALLBACK_URL = `${APP_URL}/blockauth/callback`

app.get('/blockauth/login', async (req, res) => {
  const state = crypto.randomBytes(16).toString('hex')
  req.session.oauthState = state
  try {
    const { url } = await blockauth.authorize(CALLBACK_URL, state)
    res.redirect(url)
  } catch {
    res.redirect('/login?error=blockauth')
  }
})

app.get('/blockauth/callback', async (req, res) => {
  const { code, state } = req.query
  const expected = req.session.oauthState
  delete req.session.oauthState
  if (typeof code !== 'string' || !expected || state !== expected) {
    return res.redirect('/login?error=invalid_state')
  }
  try {
    const user = await blockauth.exchangeCode(code, CALLBACK_URL)
    req.session.regenerate((err) => {
      if (err) return res.redirect('/login?error=session')
      req.session.user = sessionUser(user)
      res.redirect('/')
    })
  } catch {
    res.redirect('/login?error=blockauth')
  }
})

app.get('/api/me', (req, res) => {
  const user = currentUser(req)
  if (!user) return res.status(401).json({ error: 'unauthenticated' })
  res.json({ address: user.address, roles: user.roles })
})

app.post('/blockauth/logout', async (req, res) => {
  const user = req.session.user
  if (user) await blockauth.signOut(user.address).catch(() => {})
  req.session.destroy(() => res.redirect('/'))
})

app.listen(4000)

Your session ends when the user's Block-Auth signature does: signedExpiredAt, in UTC with the format YYYY-MM-DD HH:mm:ss. That's why the session keeps that moment and every route checks it before trusting the user (how long a session lasts).

Add the links to your pages

In your templates, the login is a plain link and the logout a form. To know who is signed in, read your session (or call GET /api/me from your scripts).

HTML

<a href="/blockauth/login">Sign in with Block-Auth</a>

<form method="post" action="/blockauth/logout">
  <button type="submit">Sign out</button>
</form>

Receive webhooks

Block-Auth calls your backend when a user signs out of your app from Block-Auth (signout) or unlinks your app (account.unlinked), so you can close their session too.

  1. In the Dashboard, open your application's Authentication settings. In the third URL field, shown as "When user fails to log in", type the path /blockauth/webhook (your domain is already filled in).
  2. Add the route. It checks the signature (headers X-BlockAuth-Timestamp and X-BlockAuth-Signature), checks that the apiKey is yours and ignores repeated jti:

Body

{
  "event": "account.unlinked",
  "apiKey": "your_api_key",
  "address": "0x8f2a…c41d",
  "externalId": "42",
  "iat": 1762790400,
  "jti": "5c1e2b7a-…"
}
const seen = new Map()

app.post('/blockauth/webhook', express.raw({ type: '*/*' }), (req, res) => {
  let event
  try {
    event = blockauth.verifyWebhook(req.body, req.get('X-BlockAuth-Timestamp'), req.get('X-BlockAuth-Signature'))
  } catch {
    return res.status(401).end()
  }
  if (event.apiKey !== BLOCKAUTH_API_KEY || seen.has(event.jti)) return res.status(409).end()
  seen.set(event.jti, Date.now())

  endSessionsOf(event.address)
  if (event.event === 'account.unlinked') markUnlinked(event.externalId)
  res.json({ ok: true })
})

endSessionsOf, markUnlinked and alreadyProcessed are yours: they depend on how your app stores sessions and users.