VettiGuard Passkeys as a Service
VettiGuard Passkeys as a Service lets a protected web application add phishing-resistant WebAuthn authentication without exposing its VettiGuard server credential to the browser.
The relying application remains the account system of record. VettiGuard stores only a keyed, site-scoped representation of the application's subject_id and returns a pairwise subject_ref for correlation. Raw subject identifiers are not persisted by this feature.
Security boundary
- Use a scoped server credential with
site.passkeys. - Registration and authentication ceremonies require user verification (
UV). - Registration requests privacy-preserving
attestation: noneand accepts ES256 or RS256 credentials. - Passkeys are bound to a verified protected-site origin and its exact WebAuthn RP ID.
- Production origins must use HTTPS.
http://localhostand*.localhostare accepted only for local development. - Challenges expire after 5 minutes and can be completed once.
- Authentication evidence uses the
vgpk.*format, expires after 5 minutes, and is one-use by default. - Authentication counters are checked where the authenticator supplies a non-zero counter. A non-advancing counter is rejected.
- The relying application's raw
subject_idis not stored.
1. Register a passkey
Your backend requests registration options:
POST /api/v1/passkeys/registration/options
Authorization: Bearer vgk_site_...
Content-Type: application/json{
"subject_id": "customer-1842",
"origin": "https://app.example.com",
"display_name": "Customer account"
}VettiGuard returns a normal PublicKeyCredentialCreationOptions object under publicKey, plus a short-lived challenge_token.
Your backend passes only publicKey and the opaque challenge token to the browser. The browser invokes:
const credential = await navigator.credentials.create({ publicKey });Your backend then submits the browser response to:
POST /api/v1/passkeys/registration/completeThe response contains a VettiGuard credential UUID and the pairwise subject_ref. Store those references with the relying application's account if you need credential-management UI.
2. Authenticate with a known subject
POST /api/v1/passkeys/authentication/options{
"subject_id": "customer-1842",
"origin": "https://app.example.com",
"action": "sign-in",
"context": "login-9942"
}The returned publicKey contains the active credential descriptors for that subject.
The browser invokes:
const credential = await navigator.credentials.get({ publicKey });Your backend sends the response to:
POST /api/v1/passkeys/authentication/completeA successful response includes:
{
"authenticated": true,
"authentication_method": "passkey",
"user_verification": true,
"subject_ref": "vgsub_...",
"credential_id": "...",
"evidence": "vgpk...."
}3. Discoverable/usernameless sign-in
Omit subject_id from the authentication-options request. VettiGuard omits allowCredentials, allowing the browser/authenticator to select a discoverable passkey.
For this mode VettiGuard requires the WebAuthn assertion to return a valid user handle. The completed response returns only the pairwise subject_ref; the application should map that value to its account record. VettiGuard does not return or retain the application's original subject identifier.
4. Verify passkey evidence
Passkey completion is already a server-authenticated VettiGuard response. For workflows that need portable, action-bound evidence, verify the short-lived vgpk.* token:
POST /api/v1/passkeys/evidence/verify{
"evidence": "vgpk.payload.signature",
"action": "sign-in",
"subject_id": "customer-1842",
"context": "login-9942",
"consume": true
}By default evidence is consumed on successful verification. Set consume: false only for a read-only preflight that cannot authorize the protected action.
vgpk.* is VettiGuard authentication evidence, not a legal electronic signature.
5. Revoke a credential
POST /api/v1/passkeys/credentials/revoke{
"credential_id": "VETTIGUARD-CREDENTIAL-UUID",
"reason": "customer-request"
}Revocation is scoped to the protected site that owns the API credential.
Browser serialization
WebAuthn uses binary ArrayBuffer values. Convert binary values to base64url before sending them to your backend. The backend—not the browser—calls the VettiGuard API using the private site.passkeys credential.
Never embed a vgk_site_*, legacy site secret, or VettiGuard evidence-signing secret in JavaScript.
Privacy model
VettiGuard stores:
- a keyed site/workspace-scoped subject hash;
- a pairwise
subject_ref; - the WebAuthn credential identifier and public key;
- authenticator algorithm/counter/backup metadata;
- bounded challenge/evidence state.
VettiGuard does not store the raw application subject_id for Passkeys as a Service.
Credential limit
A subject can have up to 10 active passkeys per protected site. Revoke unused credentials before registering additional credentials.
Error handling
| Error | Meaning | Handling |
|---|---|---|
passkey-origin-not-allowed | Origin is not registered for the protected site | Fix site-domain configuration; do not retry unchanged |
passkey-origin-requires-https | Non-local origin is not HTTPS | Use HTTPS |
passkey-challenge-expired-or-consumed | Ceremony is stale or already completed | Start a new ceremony |
passkey-not-found-for-subject | Known-subject authentication has no active passkey | Offer another governed authentication method |
passkey-credential-limit-reached | Subject already has 10 active passkeys | Revoke an old credential |
passkey-service-unavailable | Required migration/storage unavailable | Fail closed and retry after service recovery |
Migration
Apply:
202609210002_passkeys_as_a_servicebefore enabling the public endpoints.