Passport & Branding

The Short Version

  • The Passport is a JWT that carries identity, org branding, and permissions. Bio-ID assembles it at token mint time. Services read req.user — no extra calls needed.
  • Vault branding is the single source of truth for how an org appears on every surface. It lives at profile.branding on vault entities.
  • When passport.includeBranding: true is declared in catalog-info.yaml, Bio-ID fetches vault branding at mint time (fail-open, 3s timeout) and bundles it into the JWT branding claim.
  • Consuming services read req.user.branding — no separate vault call needed for authenticated requests.

Vault Branding Schema

interface VaultBranding {
  // Self-serve (Claimed tier)
  displayName: string          // shown on all surfaces — REQUIRED on first write
  tagline?: string             // "Colorado's largest commercial agency"
  logoUrl?: string             // primary logo — SVG or PNG
  logoMarkUrl?: string         // icon/mark for small surfaces (32x32)
  primaryColor: string         // hex — buttons, accents (default: '#0F172A')
  secondaryColor?: string      // hex
  websiteUrl?: string
  phone?: string
  email?: string

  // White-Label Approved tier only
  customDomain?: string        // auth.acmeagency.com
  hideEcoBranding: boolean     // remove "Powered by InsurEco" — requires whiteLabelApproved
  emailFromName?: string       // magic link sender name
  customCss?: string           // injected into consent page, max 10KB

  // Platform-managed (only settable by platform org)
  verified: boolean
  whiteLabelApproved: boolean
}

Two Tiers

TierWhoWhat they can set
ClaimedAny entity after Bio-ID claim flowdisplayName, tagline, logoUrl, logoMarkUrl, colors, website, phone, email
White-Label ApprovedPlatform approval after verificationcustomDomain, hideEcoBranding, emailFromName, customCss

Vault API

GET  /v1/branding/:orgSlug                → 0 gas, no auth (public)
GET  /v1/entities/:iecHash/branding       → 0 gas, no auth (public)
PATCH /v1/entities/:iecHash/branding      → 2 gas, auth required (owner/admin)
  • Response always returns defaults for unset fields.
  • First-time branding write requires displayName.
  • verified and whiteLabelApproved can only be set by platform services — PATCH from a regular service returns 403 for those fields.
  • hideEcoBranding is silently ignored unless whiteLabelApproved: true.

Vault SDK

import { VaultClient } from '@insureco/bio-vault'
const vault = VaultClient.fromEnv()

// Read by orgSlug (most common — you have it from the JWT)
const branding = await vault.getBranding('acme-agency')

// Read by iecHash (for public entity pages)
const branding = await vault.getBrandingByHash('0x1a2b3c...')

// Update (owner/admin only)
await vault.updateBranding('0x1a2b3c...', {
  tagline: 'Updated tagline',
  primaryColor: '#1D3557',
})

Enriched Passport JWT

When an OAuth client has passportConfig.includeBranding: true, Bio-ID fetches vault branding at token mint time and includes it in the JWT.

passportConfig on OAuthClient

interface IPassportConfig {
  includeBranding: boolean  // fetch vault branding at mint time
  includeIecHash: boolean   // include entity iecHash in token
}

Services opt in via catalog-info.yaml:

spec:
  auth:
    mode: sso
    passport:
      includeBranding: true    # sets passportConfig.includeBranding on the OAuth client
      includeIecHash: false    # sets passportConfig.includeIecHash (default: false)

On deploy, the builder reads spec.auth.passport and sets passportConfig on the OAuth client. Bio-ID then enriches the JWT at token mint time based on these flags.

The shorthand branding: true is still accepted and maps to includeBranding: true. The explicit form is preferred for new services.

What goes into the JWT branding claim

When includeBranding: true, the JWT contains a branding claim with this shape:

{
  "branding": {
    "displayName": "Acme Insurance",
    "logoUrl": "https://...",
    "logoMarkUrl": "https://...",
    "primaryColor": "#0066CC",
    "secondaryColor": "#003366",
    "verified": true,
    "whiteLabelApproved": false
  }
}
// TypeScript interface — subset of VaultBranding, only display-relevant fields
interface PassportBranding {
  displayName: string
  logoUrl?: string
  logoMarkUrl?: string
  primaryColor: string
  secondaryColor?: string
  verified: boolean
  whiteLabelApproved: boolean
}

Fail-open behavior

Bio-ID fetches vault branding with a 3-second timeout. If the vault is unreachable at mint time, Bio-ID issues the token without a branding claim — auth still works. Branding is best-effort. Services should handle req.user.branding being undefined.

Propagation timing

Branding refreshes on every token mint and refresh. Since access tokens have a 1-hour TTL, branding changes propagate within 1 hour at most.


The Passport Object

The full Passport shape (identity + branding + permissions):

interface Passport {
  // Identity
  bioId: string
  email: string
  firstName: string
  lastName: string
  displayName: string
  avatar?: string
  userType: string          // 'user' | 'service' | 'admin'
  roles: string[]

  // Primary org
  orgSlug: string
  orgId: string             // ORG-xxx format
  orgName: string

  // All org memberships
  orgs: {
    orgSlug: string
    orgId: string
    orgName: string
    roles: string[]
    modules: string[]
  }[]

  modules: string[]

  // Branding — present when passport.includeBranding: true
  branding?: PassportBranding

  // Permissions — resolved from Koko at mint time
  permissions: { scopes: string[] }

  // Vault link
  iecHash?: string

  // Session metadata
  issuedAt: string
  connectedServices: string[]
}

Consuming Branding in Your Service

From the Passport (authenticated requests)

// req.user IS the Passport — populated by Janus or your middleware
const { branding, permissions } = req.user

if (branding) {
  document.title = branding.displayName
  headerLogo.src = branding.logoUrl
  root.style.setProperty('--primary', branding.primaryColor)

  // Trust badge
  if (branding.verified) showVerifiedBadge()

  // White-label features
  if (branding.whiteLabelApproved) hideEcoBranding()
}

// Permission gating
if (permissions.scopes.includes('raterspot:rate')) showRatingPanel()

From the vault directly (unauthenticated / background)

Only call vault when there is no Passport available:

// Public entity profile page — no user session
const branding = await vault.getBranding('acme-agency')

// Background job — no request context
const branding = await vault.getBranding(orgSlug)

Passport Socket (real-time updates)

wss://bio.insureco.io/passport?token=<access_token>
EventWhenPayload
identityOn connect{ type, passport }
passport_updatedRole/org/branding change{ type, passport }
revokedLogout or token invalidated{ type }
const ws = new WebSocket(`${BIO_ID_URL.replace(/^http/, 'ws')}/passport?token=${token}`)
ws.onmessage = (e) => {
  const { type, passport } = JSON.parse(e.data)
  if (type === 'identity' || type === 'passport_updated') applyBranding(passport.branding)
  if (type === 'revoked') clearSession()
}

Three Auth Modes

All three produce the same Passport. The mode determines the user experience.

Modecatalog-info.yamlBio-ID visible?
Brandedauth: mode: ssoYes — "Sign in with Bio-ID"
White-Labelauth: mode: sso + passport: includeBranding: trueNo — org's domain + branding
Headlessauth: mode: sso + passport: includeBranding: true, headless: trueNo — server-to-server, fully invisible

White-Label requires whiteLabelApproved: true on the vault entity for custom domain and hideEcoBranding features.


Gotchas

  • Branding only bundled if declared — without passport: includeBranding: true in catalog-info.yaml, the Passport carries identity only.
  • Handle missing branding — vault may be unreachable at mint time. Always check if (req.user.branding) before accessing fields.
  • Don't call vault for authenticated requests — read req.user.branding instead. Vault calls are for public/unauthenticated contexts only.
  • 1-hour propagation — branding changes take up to 1 hour to appear in tokens (access token TTL).
  • Platform-only fieldsverified and whiteLabelApproved return 403 when set by non-platform services.
  • hideEcoBranding requires approval — setting it without whiteLabelApproved: true has no effect.
  • First write needs displayName — PATCH without displayName on a new branding record fails validation.
  • customCss max 10KB — Bio-ID rejects larger payloads.
  • Socket closes on invalid token — don't retry without a fresh token.
  • Headless mode: server-to-server only — never expose Bio-ID embed URLs to the browser.

Last updated: July 15, 2026