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.
| Endpoint | Path | Notes |
|---|---|---|
| Discovery | /.well-known/openid-configuration | Canonical metadata; everything below is advertised here |
| Authorization | /oauth/authorize | Browser redirect; consent screen for third-party clients |
| Token | /oauth/token | Code exchange + refresh; client secret over TLS |
| UserInfo | /oauth/userinfo | Bearer access token; same claim set as the ID token |
| JWKS | /oauth/discovery/keys | RSA public key(s), kid-tagged |
| Revocation | /oauth/revoke | RFC 7009; client-authenticated |
| End session | /logout/oidc | RP-initiated logout; validated post_logout_redirect_uri |
| DID document | /u/{handle}/did.json | did:web resolution target |
| Attestation | /u/{handle}/attestation.jwt | Fresh 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 types | authorization_code + refresh only. No implicit, no password grant, no client credentials. |
|---|---|
| PKCE | Required (S256), enforced server-side. Confidential clients send it too. |
| Clients | Confidential only, operator-registered. Exact redirect-URI matching; HTTPS outside development. |
| State / nonce | Round-tripped and validated. The nonce is embedded in the ID token. |
| Consent | Per-scope consent screen for third-party clients; first-party clients are pre-authorized. |
What the server issues
| Authorization code | Single use · expires in 60 seconds |
|---|---|
| Access token | Opaque (not a JWT) · 2 hours · revocable |
| Refresh token | Rotated on every use; the replaced token is invalidated |
| ID token | JWT, RS256, kid in header · 2 hours · aud = client id, iss = https://legal.id |
| Signing key | RSA-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. |
| Subject | Opaque stable UUID (sub), never the handle, so handles can change without breaking RP links |
Scopes and the claims they release
| Scope | Claims released (ID token + UserInfo) |
|---|---|
| openid | sub (opaque UUID), standard JWT claims (iss, aud, exp, iat, nonce, auth_time) |
email, email_verified (boolean — RPs are instructed not to link accounts when false) | |
| profile | name, 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.
| Tier | Verification performed | Recorded |
|---|---|---|
| Digital | Control of the email address proven via confirmation link | email_verified_at |
| Core | Identity corroborated by sign-in with a verified Google account | google_verified_at + provider uid |
| Verified | Form I-9 employment-eligibility check by Legal.io — an in-person review of government identity documents | The 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 endpointsPath 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 keyTen-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 methods | Passkeys (WebAuthn, discoverable credentials, counter-checked) · password (min 10 chars, bcrypt) with mandatory email verification · optional Google corroboration |
|---|---|
| Session store | Server-side records (IP + user agent), signed httponly cookie, SameSite=Lax, Secure in production |
| Lifetime | 30 days absolute · 14 days idle · users see all active sessions and can revoke any, or all others at once |
| Fixation defense | Rails session reset on every privilege transition |
Ending a session
| RP-initiated logout | end_session_endpoint in discovery. Ends the IdP session, redirects only to origins registered by a known client, round-trips state. |
|---|---|
| App revocation | Revoking an app from the dashboard invalidates its tokens and grants immediately. Clients can revoke via RFC 7009. |
| Recovery | Password reset by email, 30-minute single-use token. Completing a reset invalidates all other sessions. Responses are generic, so accounts cannot be enumerated. |
| Audit | Append-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
| Surface | Limit |
|---|---|
| Password login | 10/min per IP · 6 per 5 min per email |
| Signup | 8 per 5 min per IP. Invite redemption is atomic and capped |
| Token endpoint | 30/min per IP |
| Password reset | 5 per 15 min per IP |
| Passkey endpoints | 30/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.