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.
| Route | Does |
|---|---|
GET /blockauth/login | Creates a random state, keeps it in the session and redirects to the Block-Auth login |
GET /blockauth/callback | Checks the state, redeems the code and creates your session |
GET /api/me | Returns the signed-in user, or 401 |
POST /blockauth/logout | Closes 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)
The state protects your callback against CSRF: always check it before
redeeming the code. The login link must be opened within 5 minutes, and the
code is valid for 60 seconds and only once. All the parameters are in User
login.
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.
- 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). - Add the route. It checks the signature (headers
X-BlockAuth-TimestampandX-BlockAuth-Signature), checks that theapiKeyis yours and ignores repeatedjti:
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.

