Sign in from your app
Sign the person already signed in to your own app into a tablewalk App, with no second login.
{ "name": "portal", "kind": "embed", "issuer": "https://portal.example.com", "secret": "${PORTAL_EMBED_SECRET}", "origins": ["https://portal.example.com"] }Your server signs a minute-long assertion of who is there:
import { createHmac, randomUUID } from 'node:crypto';
const part = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');export function assertionFor(user, page = 'https://portal.example.com') { const now = Math.floor(Date.now() / 1000); const body = `${part({ alg: 'HS256', typ: 'JWT' })}.${part({ iss: 'https://portal.example.com', aud: 'compass', origin: page, sub: user.id, email: user.email, email_verified: true, name: user.name, jti: randomUUID(), iat: now, exp: now + 60, })}`; return `${body}.${createHmac('sha256', process.env.PORTAL_EMBED_SECRET).update(body).digest('base64url')}`;}In Node, mintEmbedAssertion from tablewalk/handler signs the same assertion, HS256 with a secret or ES256 and EdDSA with a private key:
import { mintEmbedAssertion } from 'tablewalk/handler';
const assertion = await mintEmbedAssertion({ issuer: 'https://portal.example.com', key: process.env.PORTAL_EMBED_SECRET, app: 'compass', origin: 'https://portal.example.com', subject: user.id, email: user.email, emailVerified: true, name: user.name,});Your page posts it to the App and goes on signed in:
const reply = await fetch('https://crm.example.com/api/auth/embed/exchange', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ assertion }),});const { token } = await reply.json(); // only from another origin: send it as `Authorization: Bearer <token>`What you’ll see
Section titled “What you’ll see”- On the App’s own origin (you proxy or mount it): the App’s ordinary session cookie, the same grants as a password sign-in, and no sign-in screen.
- From another origin in
origins: no cookie, but a bearer good for an hour, only from that page and only on the App’s browser routes. The bearer is not the session cookie: it opens a session of its own that lapses with it, so a copy replayed as a cookie after the hour is nobody.
When to use it, and when not
Section titled “When to use it, and when not”- Use it when your own app already knows who is there and the App sits inside it, behind your proxy or in your pages.
- Use an identity provider instead when people should sign in at the App itself with your organization’s accounts.
Options
Section titled “Options”| Entry key | What it does | Default | Status |
|---|---|---|---|
issuer |
Exactly the assertions’ iss |
required | Stable |
secret or jwks |
A ${VARIABLE} holding an HS256 secret of 32 bytes or more, or your JWKS URL (ES256, EdDSA); a literal secret is refused |
one required | Stable |
origins |
The pages that may exchange; an assertion names one in its origin claim |
required | Stable |
apps |
The App ids (aud) it may sign people into |
every App | Stable |
accounts |
separate keeps your subject apart from existing accounts; link-verified-email links one when both sides verified the address |
separate |
Stable |
createAccounts |
false signs in only accounts that exist already |
accounts made at first sign-in | Stable |
roles |
{ "<App role>": ["<your role>"] }: the assertion’s roles claim counts only through this map |
ignored | Stable |
insecure |
Allows an http JWKS on a trusted network |
false |
Stable |
The server checks the signature, issuer, audience, origin, a lifetime of five minutes at most and thirty seconds of clock skew, and takes each jti once, kept in the App’s auth store so every instance on it refuses a replay. Each issuer has a budget of thirty exchanges a minute per client address. A tenant App’s exchange works the same way from either origin and gives no membership. POST /api/auth/embed/sign-out ends the session when yours ends: with the bearer from another origin, or the cookie from the App’s own page.
Common mistakes
Section titled “Common mistakes”assertion_origin→ theoriginclaim must be the page that posts it, and listed inorigins.rate_limited→ exchange once per page load and keep the session; a bearer lasts an hour.account_exists→ an account already has that email: set"accounts": "link-verified-email"and sendemail_verified: true.[embed-issuer-no-auth]→ the App does not sign people in: give itauth.
Related
Section titled “Related”- Sign-in: the sessions and roles this exchange hands out.
- Identity providers: when people sign in at the App itself.
- Tenant Apps: membership stays the tenant store’s.
- Secrets and hidden columns: how
${VARIABLE}references are read.