Your backend

Add the routes the widget calls and receive webhooks. With the Block-Auth package for your server it's mostly configuration: it redeems the login code, keeps the session and verifies the webhooks for you. If you prefer, you can write it yourself.

How you integrate it

Add the backend routes

Install the Block-Auth package for your server:

npm install @block-auth.io/node express express-session

Create BlockAuthRoutes with your keys and the URL of your frontend, and mount it in your server:

import express from 'express'
import session from 'express-session'
import { BlockAuthRoutes } from '@block-auth.io/node'
import { blockauthRouter } from '@block-auth.io/node/express'

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

export const routes = new BlockAuthRoutes({
  apiKey: BLOCKAUTH_API_KEY,
  apiSecret: BLOCKAUTH_API_SECRET,
  appUrl: APP_URL,
})

export const app = express()
app.use(
  session({
    secret: SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: { httpOnly: true, sameSite: 'lax', secure: APP_URL.startsWith('https') },
  }),
)
app.use(blockauthRouter(routes))
app.use(express.json())

app.listen(4000)

That's all. Your server now answers these routes under /auth/blockauth, the path the widget calls by default:

RouteDoes
POST /codeRedeems the login code at Block-Auth, creates the session and returns { address, did, roles }
GET /meReturns the signed-in user, or 401
POST /logoutCloses the session in your app and in Block-Auth
GET and POST /linkLinks your existing users (only with the appUser option)
POST /webhookReceives the webhooks

What the package already does for you:

  • Trusts only Block-Auth. The user always comes from redeeming the code with your apiSecret, and the redirectUri must be on appUrl. If your frontend lives on several origins, pass allowedOrigins (a list) instead.
  • Blocks forged requests. It only accepts POST requests with a JSON body, so another site can't log your users in with a form.
  • Keeps the session short and clean. It renews the session ID on every login and ends it when the user's Block-Auth signature expires (signedExpiredAt). To end it earlier, set maxSessionSeconds (MaxSessionDuration in .NET).
  • Logs out everywhere. POST /logout also closes the user's Block-Auth sessions for your app, and keeps the rest of your session data.
  • Verifies every webhook. Signature, time and apiKey, before calling your code.

If your routes must live under another path, set it in basePath and the same path as authEndpoint in the widget. To turn off a route you don't use, pass routes: { link: false } (in .NET, add it to DisabledRoutes). Option names are snake_case in Python and PascalCase in .NET.

Use the session in your routes

From now on, the session is the source of truth. Read the user before doing anything that needs a signed-in user or a role. You get null when there is no session, it expired or it was closed by a webhook:

import { expressSessionStore } from '@block-auth.io/node/express'

const currentUser = (req) => routes.currentUser({ session: expressSessionStore(req) })

const requireRole = (slug) => async (req, res, next) => {
  const user = await currentUser(req)
  if (!user) return res.status(401).end()
  if (!user.roles.some((role) => role.slug === slug)) return res.status(403).end()
  next()
}

app.delete('/api/orders/:id', requireRole('acme-x7k2p9'), deleteOrder)

The user has address, did, roles ({ name, slug }), externalId (your user ID, if linked), loggedInAt, expiresAt (Unix seconds) and data. In .NET the roles are also role claims with the role slug. Use the role slug, not the name: see Roles.

To keep your own data in the session, or to refuse a login, add onLogin(user): return an object to save it as data, or false to reject the user (they are also signed out of Block-Auth). In .NET, AuthorizeLogin decides and OnLogin returns the data. onLogout(user) runs when the user logs out.

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.

This is how a sign-out made in Block-Auth (Identity, the extension or another device) reaches your page:

  1. 1Block-AuthThe user signs out
  2. 2Block-Auth → Your backendPOST /webhook · signout
  3. 3Your backendSaves the sign-out
  4. 4Your frontend → Your backendGET /me on focus or every 60 s
  5. 5Your backend → Your frontend401
  6. 6Your frontendShows the user signed out
  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 /auth/blockauth/webhook (your domain is already filled in).
  1. Tell the package how to remember a sign-out and how to check it. POST /webhook is already there: it verifies the request and calls onSignout or onUnlinked. Then isRevoked closes every session that started before that sign-out:
export const routes = new BlockAuthRoutes({
  apiKey: BLOCKAUTH_API_KEY,
  apiSecret: BLOCKAUTH_API_SECRET,
  appUrl: APP_URL,
  onSignout: (event) => saveSignout(event.address, event.iat),
  onUnlinked: async (event) => {
    await saveSignout(event.address, event.iat)
    await removeLink(event.externalId)
  },
  isRevoked: async (user) => ((await lastSignout(user.address.toLowerCase())) ?? -1) >= user.loggedInAt,
})

saveSignout, lastSignout and removeLink are yours: keep them in your database or a shared cache such as Redis, so every server instance sees them. address arrives in lowercase and iat and loggedInAt are Unix seconds.

  • isRevoked runs on every GET /me and every time you read the user. The widget asks /me when the tab gets focus and every minute, so the page signs out by itself.
  • To reject a repeated delivery, add isReplay(jti) and return true if you already saw that jti.