HTTP API · 5. Identity and passports
Twenty-five registrations answer one question in four different ways: which principal is acting, what it is entitled to, and how that binding was established.
Planes covered: Auth rails (unified login) · Identity links and candidates · Passports, mint requests and presence · Principal resolution and capability policy.
Read chapter 0 first for the auth model, request-field notation and error shape.
Three things worth knowing before the tables:
- The whole
/v1/auth/*plane isPublicby route-auth contract. That is by construction, a login rail that required a token would be useless. One route on it nonetheless enforcesadmin:writein its own handler; see §5.1. - Seven of these routes are gated off by default. The identity-link plane needs
CORECRUXD_IDENTITY_LINKS; mint requests needCORECRUXD_FEATURE_PASSPORT_MINT_REQUESTS. Both default off. - Presence is populated by middleware, not by a route. Any request carrying
X-Corecrux-Passport-Idtouches presence in the background (mod.rs:1570).
5.1 Auth rails (unified login)
Seven registrations. Two rails: an OAuth-style device-code flow, and a Tailscale identity exchange.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
POST | /v1/auth/device/start | auth_device.rs:204 post_device_start | public · Public | body: client_name (String, dflt) | keys device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval | 500 | - | W |
POST | /v1/auth/device/token | auth_device.rs:353 post_device_token | public · Public | body: device_code (String, req) | Token response, or a pending or denied status | 400, 500 | - | W |
POST | /v1/auth/device/approve | auth_device.rs:283 post_device_approve | admin:write in the handler, Public by contract | body: user_code (String, req), tenant_id (String, req), scopes (Vec String, req), deny (bool, dflt) | keys ok, decision, user_code, client_name | 400, 404, 409, 500 | - | W |
POST | /v1/auth/device/refresh | auth_device.rs:453 post_device_refresh | public · Public | body: refresh_token (String, req) | keys access_token, token_type, expires_in, scopes, tenant_id, rail | 401, 500, 503 | - | W |
POST | /v1/auth/device/revoke | auth_device.rs:506 post_device_revoke | public · Public | body: refresh_token (String, req) | keys revoked | 500 | - | W |
POST | /v1/auth/tailscale/token | auth_rails.rs:207 post_tailscale_token | public · Public | , (identity comes from the Tailscale connection) | keys access_token, token_type, expires_in, scopes, sub, tenant_id, rail | 401, 403, 503 | - | W |
GET | /v1/auth/whoami | auth_rails.rs:180 get_whoami | public · Public | - | keys login, rail, source, trusted, allowlisted | - | , | R |
POST /v1/auth/device/approve is the exception on this plane. The route-auth contract classes the whole /v1/auth/* prefix as Public, but the handler itself calls require_http_scopes with admin:write (auth_device.rs:292). An unauthenticated caller reaches the handler and is refused there. That is the correct outcome, approval is the step where scopes are granted, but it means the protection is the handler, not the contract. Under CORECRUXD_AUTH_MODE=off the handler check passes for everyone, and Public means the middleware will not stop it either.
The browser page that drives approval is /activate, a static asset outside the API contract whose form POSTs to this route (console.rs:314). See chapter 9.
GET /v1/auth/whoami reports trusted and allowlisted separately. trusted means the rail authenticated the caller; allowlisted means an operator has additionally permitted that identity. Both must be true for the Tailscale rail to issue a token.
503 on the token routes means the rail is not configured on this daemon, not that it failed.
5.2 Passports and presence
Six registrations. A passport is the acting identity a request declares through X-Corecrux-Passport-Id.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/passports | passports.rs:211 get_passports | admin:read · Read | query: category (String, opt) | keys passports, category_filter | 400 | - | R |
POST | /v1/passports | passports.rs:494 post_passport | admin:write · AdminWrite | body: id (String, req), category (String, req), sponsor_id (String, dflt), agent_work_gate (bool, dflt), is_default_for_category (bool, dflt), name (String, dflt), owner (String, dflt), position (String, dflt), company (String, dflt), notes (String, dflt) | Passport record | 400, 409 | - | W · facts |
GET | /v1/passports/{passportId} | passports.rs:477 get_passport | admin:read · Read | path passportId | Passport record | 404 | - | R |
PATCH | /v1/passports/{passportId} | passports.rs:531 patch_passport | admin:read in the handler · Write | path passportId; body: category (String, dflt), agent_work_gate (bool, dflt), is_default_for_category (bool, dflt), sponsor_id (nullable String, dflt), reputation_tier (String, dflt), receipt_count (u64, dflt), name (nullable String, dflt), owner (nullable String, dflt), position (nullable String, dflt), company (nullable String, dflt), notes (nullable String, dflt) | Updated passport record | 400, 404 | - | W · receipt, facts |
DELETE | /v1/passports/{passportId} | passports.rs:568 delete_passport | admin:read in the handler · Write | path passportId | Deletion result | 404, 500 | - | W · facts |
GET | /v1/passports/presence | passports.rs:591 get_presence | admin:read · Read | - | keys presence, count | - | , | R |
PATCH and DELETE on a passport check admin:read in the handler, not admin:write. The route-auth contract classes them Write and accepts admin:write, facts:write or integrations:install, so under enforce a write-class scope is still needed to reach the handler. Under the default shadow mode the middleware does not block, and the handler's own check is a read scope. On a default install, an admin:read token can delete a passport. If that matters to you, CORECRUXD_ROUTE_AUTH=enforce closes it.
POST /v1/passports is tenant-bound against the literal tenant default (passports.rs:152). 409 means the id already exists.
Presence is not written by any route on this plane. presence_middleware spawns a background touch on any request that carries X-Corecrux-Passport-Id, and skips the lock entirely when the header is absent (mod.rs:1570). A passport that never sends the header is invisible to GET /v1/passports/presence no matter how much traffic it generates.
5.3 Passport mint requests
Three registrations, gated off by default. This is the request-and-approve path for minting a passport without handing out admin:write.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/passport/mint-requests/pending | passports.rs:241 get_pending_mint_requests | admin:read · FeatureGated | query: by_passport (String, opt) | keys pending, count | - | CORECRUXD_FEATURE_PASSPORT_MINT_REQUESTS, default off | R |
POST | /v1/passport/mint-requests/{request_id}/approve | passports.rs:271 post_mint_request_approve | admin:write · FeatureGated | path request_id; body: approver_passport (String, dflt), category (String, dflt), name (String, dflt) | keys status, minted, request_id, requester_id, category, passport_operation, passport_record_hash, passport_mutation_hash, receipt_id, receipt_record_id, receipt_session_id | 409, 500 | CORECRUXD_FEATURE_PASSPORT_MINT_REQUESTS, default off | W · receipt, facts |
POST | /v1/passport/mint-requests/{request_id}/reject | passports.rs:408 post_mint_request_reject | admin:write · FeatureGated | path request_id; body: approver_passport (String, dflt), category (String, dflt), name (String, dflt) | keys status, minted, request_id, requester_id, receipt_id, receipt_record_id, receipt_session_id | - | CORECRUXD_FEATURE_PASSPORT_MINT_REQUESTS, default off | W · receipt, facts |
With the flag off the routes return 404 without touching state (mod.rs:230).
Both resolution routes emit a receipt whether the decision was approve or reject, and the reject path still returns a receipt_id. A rejection is evidence too.
409 on approve means the request has already been resolved. Approving twice does not mint twice.
5.4 Identity links and candidates
Seven registrations, all gated off by default. An identity link binds a local passport to a remote fingerprint, and it requires both parties' signatures.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/identity/links | identity_links.rs:460 get_identity_links | any-of admin:read, admin:write · Read | - | keys links, link, link_id, live and revoked | 401, 404 | CORECRUXD_IDENTITY_LINKS, default off | R |
POST | /v1/identity/links | identity_links.rs:97 post_identity_link | admin:write · Write | body: local_passport_id (String, req), remote_fpr (String, req), remote_public_key_hex (String, req), created_at (String, req), sig_local (String, req), sig_remote (String, req) | 201, keys link, link_id | 401, 403, 404, 409 | CORECRUXD_IDENTITY_LINKS, default off | W |
POST | /v1/identity/links/{linkId}/revoke | identity_links.rs:487 post_identity_link_revoke | admin:write · Write | path linkId | keys link, link_id | 401, 404 | CORECRUXD_IDENTITY_LINKS, default off | W |
GET | /v1/identity/candidates | identity_links.rs:147 get_identity_candidates | any-of admin:read, admin:write · AdminRead | query: status (String, dflt) | keys candidates, candidate, candidate_id | 400, 401, 403, 404 | CORECRUXD_IDENTITY_LINKS, default off | R |
POST | /v1/identity/candidates/propose | identity_links.rs:265 post_identity_candidates_propose | admin:write · AdminWrite | - | keys created, examined, observations, by_source | 401, 403, 404, 500 | CORECRUXD_IDENTITY_LINKS, default off | W |
POST | /v1/identity/candidates/{candidateId}/confirm | identity_links.rs:364 post_identity_candidate_confirm | admin:write · AdminWrite | path candidateId; body: local_passport_id (String, req), remote_fpr (String, req), remote_public_key_hex (String, req), created_at (String, req), sig_local (String, req), sig_remote (String, req) | 201, keys candidate, candidate_id, link, link_id | 400, 401, 403, 404, 409 | CORECRUXD_IDENTITY_LINKS, default off | W |
POST | /v1/identity/candidates/{candidateId}/reject | identity_links.rs:419 post_identity_candidate_reject | admin:write · AdminWrite | path candidateId | keys candidate, candidate_id | 400, 401, 404 | CORECRUXD_IDENTITY_LINKS, default off | W |
A candidate is not a link and does not resolve. The candidate store records observations that two identities might be the same, produced by proposers you run explicitly with POST /v1/identity/candidates/propose. Nothing in the daemon treats a candidate as a binding. Only confirm, which requires both signatures, exactly as the direct link route does, creates one.
Reject preserves the audit trail. POST .../reject marks the candidate rejected without deleting it. That is deliberate: the record that someone proposed and someone declined is itself evidence.
403 on link creation means a signature did not verify. 409 means the link already exists. Revocation is idempotent.
With CORECRUXD_IDENTITY_LINKS off, the default, all seven routes return 404.
5.5 Principal resolution and capability policy
Two registrations.
| Method | Path | Handler | Auth | Request | Success response | Errors | Gate | R/W |
|---|---|---|---|---|---|---|---|---|
GET | /v1/principal/resolve | principal.rs:30 get_resolve_principal | any-of admin:read, sessions:read · Read | query: session_id (String, opt), passport_id (String, opt), include_candidates (String, opt) | Resolved principal | 400, 500 | CORECRUXD_IDENTITY_LINKS extends the response with candidates; default off | R |
GET | /v1/policy/capabilities | policy.rs:16 get_policy_capabilities | any-of query:read, facts:read, admin:read · Read | - | Capability policy view | - | , | R |
/v1/principal/resolve works with the identity-link flag off; the flag only controls whether the include_candidates expansion has anything to return (principal.rs:89). Requesting candidates on a daemon with the flag off is not an error; you get a resolution without them.
/v1/policy/capabilities is the widest read on this plane: any of three read scopes will do.
5.6 Failure modes on this plane
| Symptom | Most likely cause |
|---|---|
503 from /v1/auth/device/refresh or /v1/auth/tailscale/token | The rail is not configured on this daemon. Not a credential problem. |
403 from /v1/auth/tailscale/token with a valid Tailscale identity | trusted is true but allowlisted is false. Check GET /v1/auth/whoami. |
An admin:read token successfully deleting a passport | Expected under the default shadow route-auth mode: the handler checks admin:read. Set CORECRUXD_ROUTE_AUTH=enforce. |
A passport with heavy traffic missing from GET /v1/passports/presence | Presence only records requests that carry X-Corecrux-Passport-Id. |
404 on every /v1/identity/* route | CORECRUXD_IDENTITY_LINKS is off. It is off by default. |
404 on /v1/passport/mint-requests/* | CORECRUXD_FEATURE_PASSPORT_MINT_REQUESTS is off. It is off by default. |
403 on POST /v1/identity/links | One of the two signatures did not verify. Both are required. |
409 on POST /v1/passport/mint-requests/{id}/approve | Already resolved. Approving twice does not mint twice. |
include_candidates returning nothing | The identity-link flag is off, so there are no candidates to include. This is not an error. |
Sources
- crates/corecruxd/src/http/auth_device.rs:292,
admin:writeinside aPublic-classed route - crates/corecruxd/src/http/route_auth.rs:77,
/v1/auth/*classifiedPublic - crates/corecruxd/src/http/passports.rs:152, passport create, tenant-bound
admin:write - crates/corecruxd/src/http/mod.rs:230,
CORECRUXD_FEATURE_PASSPORT_MINT_REQUESTS - crates/corecruxd/src/http/mod.rs:1570,
presence_middleware - crates/corecruxd/src/http/identity_links.rs:105, link create scope
- crates/corecruxd/src/http/identity_links.rs:155, candidate read scopes
- crates/corecruxd/src/http/principal.rs:89, candidate expansion behind the flag
- crates/corecruxd/src/http/policy.rs:17, policy read scopes
- crates/corecruxd/src/config.rs:1375,
CORECRUXD_IDENTITY_LINKSdefault - crates/corecruxd/src/console.rs:314, the
/activateapproval page

