Skip to main content

BearerTransport

WIP

Opaque session token in the Authorization header. For mobile, native, and SPA workloads.

When to use

BearerTransport is the right pick when:

  • You are serving a mobile or native client that stores the token in the OS keychain.
  • You are a cross-origin SPA (e.g. a *.app.com frontend talking to api.app.com) and want to avoid the cookie cross-site complexity.
  • You expose a machine-to-machine API where credentials are exchanged for an opaque token via client-credentials.

There are no cookies, so no CSRF defence is needed on the server.

Constructor

import { BearerTransport } from '@gentleduck/auth/core/transport'

new BearerTransport({
  header: 'authorization',  // default
  scheme: 'Bearer',         // default
})

The token rides on every request as:

Authorization: Bearer 01HJX7B2T9XKAZQP1G...

Wire format

The token is a 256-bit random value (base64url-encoded). At rest, the session store records a hash of the token - even a database leak does not yield usable bearer tokens.

The token is returned to the client once, in the JSON intent on issue:

const intents = await auth.flows.signIn({
  providerId: 'password',
  input: { email, password },
})

// One of the intents is a JSON body containing the bearer token:
// { kind: 'json', status: 200, body: { session: { token: '01HJX7B2T9...' }, identity: {...} } }

The client persists the token. Subsequent requests carry it on the Authorization header until sign-out or expiry.

Sign-out

import { authMountSignOut } from '@gentleduck/auth/server/express'

app.post('/auth/signout', authMountSignOut(auth))

The handler reads the Authorization header, revokes the session row, and the next request will see AUTH/SESSION_REVOKED.

Mobile client pattern

iOS / Android: store the bearer token in the OS-managed credential store (Keychain / Keystore). Never in user defaults / shared preferences or in plain disk files.

// iOS - store
KeychainAccess.set(token, forKey: "duck-auth-session")

// iOS - read
let token = KeychainAccess.get("duck-auth-session")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

SPA pattern

If your SPA serves from a different origin than your API and you want to avoid CORS-credential complexity:

// In the SPA
const { session } = await client.signIn({ email, password })
localStorage.setItem('duck-sid', session.token)   // <- see warning below

// Or, better, in-memory + service worker:
const inMemoryToken = session.token  // SW intercepts fetch and adds header

(warning) localStorage is readable by any script on the page; an XSS gives an attacker the bearer token. Prefer in-memory plus a service worker, or move to a same-origin cookie deployment with CookieTransport.

Combining with DPoP

BearerTransport plus DPoPVerifier produces sender-constrained tokens - the bearer is bound to a client public key, and a stolen token is useless without the matching private key:

import { BearerTransport, DPoPVerifier } from '@gentleduck/auth/core/transport'

const transport = new BearerTransport({})
const dpop = new DPoPVerifier({
  publicKey: clientPublicKey,
  expectedAudience: 'https://api.example.com',
})

// On request handlers:
await dpop.verify(jwt, req.headers.get('dpop')!)

See DPoP ->.