stable

oidc

Federated OIDC login (Google, Apple, LINE, Okta, ...) via a split-key key-connector bridge — the server never sees the unwrapped KEK or plaintext.

@noy-db/on-oidc · pnpm add @noy-db/on-oidc Split-key OIDC bridge for federated "login with X" unlock.

What it does

enrollOidc(keyring, vault, config, idToken) generates a random device secret (persisted to localStorage by default), HKDF-derives a device half from it, XORs that against the padded serialized keyring payload to get a server half, and PUTs the AES-GCM-encrypted server half to a key-connector server you operate. unlockOidc(enrollment, config, idToken) fetches the server half back, re-derives the device half locally, and XORs them to reconstruct the keyring. The server never sees the unwrapped KEK or plaintext — only an encrypted half it cannot combine alone. Since 0.10.0-pre.2 the device secret and id are stored per (provider, sub, vault), so enrolling a second vault no longer overwrites the first.

enrollOidc also HMAC-tags the padded payload under a key derived from the device secret, sub and vault, and returns it on the enrollment record as integrityTag (never sent to the server); unlockOidc verifies it before parsing and throws OidcEnrollmentIntegrityError (reason: 'tampered') on a mismatch. That closes a real attack: the key-connector holds the transport key, and XOR is bit-malleable, so it could flip a known offset — 'ro' to 'rw'. ⚠️ Breaking for devices enrolled before 0.10.0-pre.2: a record without integrityTag is refused (reason: 'legacy-enrollment') because its role and permissions cannot be verified; each such device enrolls again through a re-invite (enrollOidcFromInvite).

knownProviders ships ready-made configs for line, google, and apple — pass your client ID and key-connector URL.

This package is the client side only. Using it as a tier-2 unlock requires you to run a key-connector server that verifies ID tokens against the issuer's JWKS and stores per-user server halves — no hosted reference implementation ships. If you don't want to operate a server, on-webauthn's platform passkey gives a similar "login with X" UX with no server infrastructure.

What the provider is responsible for

Federating to an OIDC provider delegates exactly one thing: proving who you are. The whole sign-in ceremony — email and password, an emailed code, a passkey, a biometric — happens at the provider; noy-db never sees it and only ever consumes the resulting ID token. So the provider is the trust anchor for identity, and whatever assurance it enforces (MFA, biometric, device binding) flows into your tier-2 unlock for free.

What the provider is not responsible for is your decryption key. Because noy-db is zero-knowledge, an ID token cannot decrypt anything on its own. on-oidc XOR-splits the key into a device half — HKDF-derived from a per-device secret that never leaves the device and the provider never sees — and a server half, stored on your key-connector encrypted under a key derived from the ID token. Login unlocks only the server half; the device half is XOR'd in locally. So a compromised provider — or a malicious key-connector — still cannot read your vault, because neither ever holds both halves. That is the whole point of the split-key design: you delegate identity to the provider without handing it your data.

Two consequences worth stating plainly:

  • It is a tier-2 unlock, with tier-2 limits. Like every tier-2 authenticator, the OIDC unlock yields the DEK set with kek: null — the session can read and write, but cannot grant, rotate the secret, enroll another authenticator, or open a second vault (those need a real tier-1 unlock). See Authentication → Can a method serve a different tier?.
  • Lose the device half and OIDC alone can't get you back in. The provider re-proves your identity, not your device secret — so a new device needs a recovery profile (paper / Shamir) or a re-enrollment from a tier-1 unlock.

knownProviders ships ready-made configs for google, apple, and line; any other standards-compliant OIDC issuer (Microsoft Entra ID, Okta / Auth0, Keycloak, AWS Cognito, Meta's OIDC flows, …) works by passing a generic config with your issuer URL, client ID, and key-connector URL.

Usage

import { knownProviders, unlockOidc } from '@noy-db/on-oidc'

const config = knownProviders.google('<client-id>', 'https://key-connector.example.com')
const keyring = await unlockOidc(enrollment, config, idToken)

See also