Technical specifications

Legal.id

1 · System

One issuer, many relying parties.

An OpenID Connect provider operated by Lextronica, Inc. Relying parties — Jane first — integrate over standard OIDC, no proprietary SDK. Applications never see credentials, only signed tokens carrying consented claims. Every identity also publishes a did:web document that anything can resolve without an account here.

EndpointPathNotes
Discovery/.well-known/openid-configurationCanonical metadata; everything below is advertised here
Authorization/oauth/authorizeBrowser redirect; consent screen for third-party clients
Token/oauth/tokenCode exchange + refresh; client secret over TLS
UserInfo/oauth/userinfoBearer access token; same claim set as the ID token
JWKS/oauth/discovery/keysRSA public key(s), kid-tagged
Revocation/oauth/revokeRFC 7009; client-authenticated
End session/logout/oidcRP-initiated logout; validated post_logout_redirect_uri
DID document/u/{handle}/did.jsondid:web resolution target
Attestation/u/{handle}/attestation.jwtFresh EdDSA-signed JWS of the identity's public facts
Verify (human)/verify?u={handle}Public lookup page; performs and displays the JWS check

2 · Protocol

OAuth 2.0 + OIDC, under OAuth 2.1 rules.

How clients connect

Grant typesauthorization_code + refresh only. No implicit, no password grant, no client credentials.
PKCERequired (S256), enforced server-side. Confidential clients send it too.
ClientsConfidential only, operator-registered. Exact redirect-URI matching; HTTPS outside development.
State / nonceRound-tripped and validated. The nonce is embedded in the ID token.
ConsentPer-scope consent screen for third-party clients; first-party clients are pre-authorized.

What the server issues

Authorization codeSingle use · expires in 60 seconds
Access tokenOpaque (not a JWT) · 2 hours · revocable
Refresh tokenRotated on every use; the replaced token is invalidated
ID tokenJWT, RS256, kid in header · 2 hours · aud = client id, iss = https://legal.id
Signing keyRSA-2048, published at JWKS. Rotation is a deploy procedure: the new key issues immediately, old tokens age out within the 2-hour TTL. Multi-key JWKS is a follow-up.
SubjectOpaque stable UUID (sub), never the handle, so handles can change without breaking RP links

Scopes and the claims they release

ScopeClaims released (ID token + UserInfo)
openidsub (opaque UUID), standard JWT claims (iss, aud, exp, iat, nonce, auth_time)
emailemail, email_verified (boolean — RPs are instructed not to link accounts when false)
profilename, preferred_username (the handle), legal.id/tier, legal.id/did

3 · Assurance

Tiers state what was verified.

Each tier names the verification that produced it and travels in the token as legal.id/tier. Relying parties make their own trust decisions.

TierVerification performedRecorded
DigitalControl of the email address proven via confirmation linkemail_verified_at
CoreIdentity corroborated by sign-in with a verified Google accountgoogle_verified_at + provider uid
VerifiedForm I-9 employment-eligibility check by Legal.io — an in-person review of government identity documentsThe attestation event only: basis, attesting admin, timestamp. Documents, document numbers and SSNs are never stored or entered.

I-9 attestation semantics

Only administrators can attest, and only for people whose I-9 Legal.io has completed. An attestation sets a re-attestation date 36 months out (reattest_on, printed on the card) and writes an immutable audit event. Revoking one drops the account to the tier its remaining verifications support.

Unverified accounts

An account with an unconfirmed email is Unverified: the card renders, but tokens carry email_verified: false and relying parties are told not to link accounts on it.

4 · Identity

did:web.

Resolution

did:web:legal.id:u:{handle}  ⇩ resolves tohttps://legal.id/u/{handle}/did.json  ⇩ publishesEd25519 key (publicKeyMultibase, z6Mk…)profile + verification service endpoints

Path form, with an upgrade path to did:webvh. Key custody is server-side today, private keys encrypted at rest. Holder-controlled keys arrive with the wallet.

Attestation (JWS)

GET /u/{handle}/attestation.jwt  ⇩ compact JWS, alg=EdDSAheader: { alg, typ, kid: {did}#key-1 }payload: { iss, sub: {did}, handle,  legal.id/tier, legal.id/issued,  iat, exp: iat+600 }  ⇩ verify againstdid.json → Ed25519 public key

Ten-minute validity: a timestamped assertion of current status, not a bearer credential. Wallet-held credentials (OpenID4VCI/VP, selective disclosure) are next, and not claimed until they ship.

5 · Operations

Sessions, limits, and what we store.

Holding a session

Sign-in methodsPasskeys (WebAuthn, discoverable credentials, counter-checked) · password (min 10 chars, bcrypt) with mandatory email verification · optional Google corroboration
Session storeServer-side records (IP + user agent), signed httponly cookie, SameSite=Lax, Secure in production
Lifetime30 days absolute · 14 days idle · users see all active sessions and can revoke any, or all others at once
Fixation defenseRails session reset on every privilege transition

Ending a session

RP-initiated logoutend_session_endpoint in discovery. Ends the IdP session, redirects only to origins registered by a known client, round-trips state.
App revocationRevoking an app from the dashboard invalidates its tokens and grants immediately. Clients can revoke via RFC 7009.
RecoveryPassword reset by email, 30-minute single-use token. Completing a reset invalidates all other sessions. Responses are generic, so accounts cannot be enumerated.
AuditAppend-only: sign-ins and failures, sign-outs, token issue/refresh/revocation, consents, passkey add/remove/failed attempts, verification changes, attestations. Recent events are shown to the user. Write failures alarm rather than drop silently.

Rate limits

SurfaceLimit
Password login10/min per IP · 6 per 5 min per email
Signup8 per 5 min per IP. Invite redemption is atomic and capped
Token endpoint30/min per IP
Password reset5 per 15 min per IP
Passkey endpoints30/min per IP

TLS is forced (HSTS). All responses send frame-ancestors 'none'.

What we hold, what RPs get

Stored: email, handle, display name, credential material (password hash, passkey public keys, Google uid), an Ed25519 DID keypair with the private key encrypted at rest, security logs. Never stored: plaintext passwords, I-9 documents or numbers, SSNs. Relying parties receive only consented claims. Retention and rights are in the privacy policy and terms.

Get your Legal.id.

Invite-only. The best handles go first.

Claim your handle