跳到內容
開源

Conformance

This file maps every MUST and SHOULD that Zeabur Identity implements to the code that implements it and the test that proves it. Normative words follow RFC 2119 and RFC 8174. The design reasons and the full citations are in each crate's documentation (cargo doc --workspace --open).

Status values: Met (code and test), Partial (explained in the row), Not implemented, N/A (with the reason).

Code paths are relative to crates/. Tests are given as crate: module::tests::name for unit tests and crate/tests/file.rs: name for integration tests. Tests marked (cluster) run against k3s v1.36.5 (--run-ignored only); (e2e) is zeabur-identity-e2e/tests/e2e.rs, which runs Zeabur Identity from its container image.

SpecificationVersion / status (2026-10)Status
OpenID Connect Discovery 1.0Final (errata set 2)Met
RFC 8414 OAuth 2.0 Authorization Server MetadataRFCMet
RFC 7515 JWS, RFC 7517 JWK, RFC 7518 JWA, RFC 7638 thumbprintsRFCMet
RFC 7519 JWT, RFC 8725 JWT BCP, RFC 9068 JWT access tokensRFCMet
KEP-3331 Structured Authentication Configuration (apiserver.config.k8s.io/v1)GA in Kubernetes 1.34Met on k3s; RKE2 configuration generation implemented, clean release installation unverified
RFC 6749 OAuth 2.0 (authorization code, refresh)RFCMet
RFC 7636 PKCERFCMet (S256 only)
RFC 9700 OAuth 2.0 Security BCPBCP 240Partial: sender-constrained tokens (DPoP) not implemented
RFC 6750 Bearer token usageRFCMet for UserInfo
RFC 8707 Resource IndicatorsRFCMet (one resource per request)
RFC 7009 Token RevocationRFCMet (refresh tokens; access tokens answered with unsupported_token_type)
RFC 9126 PARRFCMet
RFC 9207 Authorization server issuer identificationRFCMet
OpenID Connect Core 1.0Final (errata set 2)Partial: no single sign-on sessions, so every authorization request asks for a sign-in
W3C WebAuthn Level 3RecommendationMet for registration, discoverable sign-in and conditional mediation
WebAuthn Signal API (signalAllAcceptedCredentials, signalUnknownCredential)WebAuthn Level 3Met where the browser has it (feature-detected)
W3C A Well-Known URL for Relying Party Passkey EndpointsWorking DraftMet
NIST SP 800-63B-4Final, Jul. 2025Partial: user-verified passkeys (AAL2-capable); with the admin-aal3 setting, admin accounts need a device-bound passkey (BE unset, self-reported: attestation is none); refresh sessions end after 1 h idle (15 min for admins with several teams or AAL3) and 12 h after sign-in; account-page sessions end after one hour and need a fresh verified passkey within five minutes to add or remove one, or to link or unlink an upstream account
Cookies (draft-ietf-httpbis-rfc6265bis)DraftMet for the __Host- cookies used
RFC 9457 Problem DetailsRFCMet for the sign-in endpoints
RFC 8176 Authentication Method Reference valuesRFCMet (swk, mfa for passkeys)
WCAG 2.2 AAW3C RecommendationPartial: contrast computed and met for every colour pair used; no automated browser audit or screen-reader pass yet
OpenID Connect RP-Initiated Logout 1.0FinalNot implemented (no sessions)
OpenID Connect Back-Channel Logout 1.0FinalNot implemented
RFC 9449 DPoPRFCNot implemented: kube-apiserver accepts only bearer tokens
RFC 9396 Rich Authorization RequestsRFCN/A: kube-apiserver cannot evaluate authorization_details; not advertised
RFC 9470 Step-up Authentication ChallengeRFCN/A: kube-apiserver never sends the challenge. Admin rights come from a separate, passkey-only admin account instead
OAuth 2.1 (draft-ietf-oauth-v2-1)DraftFollowed where it tightens OAuth 2.0 (PKCE always, S256 only)
OpenID Foundation conformance suite, oidcc-basic-certification-test-plan—Not run: the suite's browser automation cannot perform a passkey ceremony, and Zeabur Identity has no password login to give it
RequirementCodeCheckStatus
unsafe_code = "forbid"Cargo.toml [workspace.lints.rust]; #![forbid(unsafe_code)] in both binariesCI clippy, unsafe-reportMet
Only OSI-approved licencesdeny.toml [licenses]CI cargo-denyMet
Known vulnerabilities rejecteddeny.toml [advisories]CI cargo-denyMet
Dependency unsafe/native code listed for approvalscripts/unsafe-report.sh, supply-chain/unsafe-allowlist.tomlCI unsafe-reportMet
clippy pedantic + restriction set, rustfmtCargo.toml [workspace.lints], clippy.toml, rustfmt.tomlCI clippy, format, docsMet
DCO sign-offCONTRIBUTING.md, scripts/check-dco.shCI dcoMet
One pinned Rust toolchainflake.nix, flake.lockCI clippy, format in nix developMet
Image: static binary, non-root, no shellContainerfile, zeabur-identity-k8s/src/deployment.rszeabur-identity-k8s: deployment::tests::deployment_is_locked_down; (e2e)Met
Image size and idle memory measured in CIscripts/build-image.sh, e2e measurementCI kubernetes job summaryMet (about 20 MB; pod working set within the 30 MiB budget)
Image signing (Sigstore) and SBOM.github/workflows/release.yml, scripts/release-{build,publish,verify}.shExact-digest signature/SBOM verification and public pull in both native release rehearsal jobsPartial: pipeline implemented; public OIDC publication evidence requires a successful tag-triggered run (release procedure)
Versioned CLI archives, checksums, signed provenance and SBOMsSame workflow; scripts/release-smoke.shSigned checksum manifest; downloaded archive execution and Kubernetes-backed container startup on Linux amd64/arm64 while release stays draft; separate maintainer publication after installation acceptancePartial: pipeline implemented; local smoke is not evidence of a published release; #43 remains unverified
§RequirementLevelCodeTestStatus
Disc. 3issuer: https, no query or fragmentREQUIREDzeabur-identity-core/src/config.rs BaseUrl::parsezeabur-identity-core: config::tests::rejects_everything_elseMet
Disc. 3authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri (https)REQUIREDzeabur-identity-oauth/src/discovery.rszeabur-identity-oauth: discovery::tests::required_members_are_presentMet
Disc. 3id_token_signing_alg_values_supported includes RS256MUSTdiscovery.rs, zeabur-identity-keys Algorithm::ALLsameMet
Disc. 3scopes_supported includes openidMUSTdiscovery.rssameMet
Disc. 3Defaults that would over-claim set explicitly—discovery.rszeabur-identity-oauth: discovery::tests::does_not_inherit_misleading_defaultsMet
Disc. 4, 4.3Document at /.well-known/openid-configuration; issuer identicalMUSTzeabur-identity-oauth/src/metadata.rszeabur-identity-oauth: metadata::tests::both_metadata_locations_serve_the_same_documentMet
8414 §3, §3.3Well-known location; identical issuer; RFC 9068 §4 consistencyMUSTone document at both pathssameMet
Disc. 7.1TLS for discoveryMUSTTLS at the ingress; the internal listener for kube-apiserver uses a private CA that cert-manager keepszeabur-identity: internal::tests::serves_metadata_with_the_issued_certificate_and_renews_in_placeMet
§RequirementLevelCodeTestStatus
7515 §4.1.1, §4.1.4alg present; kid identifies the keyMUSTzeabur-identity-keys/src/jws.rszeabur-identity-keys: jws::tests::independent_library_verifies_with_published_jwkMet
7515 §4.1.11Unknown critical extensions rejectedMUSTzeabur-identity-keys/src/verify.rszeabur-identity-keys: verify::tests::rejects_tampering_wrong_keys_and_algorithmsMet
7517 §4.5, §4.3, §5Distinct kids; not use with key_ops; set formatSHOULDzeabur-identity-keys/src/jwk.rszeabur-identity-keys: jwk::tests::*, rotation::tests::schedule_invariantsMet
7518 §3.3, §3.4, §3.6RSA ≥ 2048 bits (3072 generated); fixed-width ES256; no noneMUSTzeabur-identity-keys/src/signing.rs, verify.rszeabur-identity-keys: signing::tests::*, verify::tests::*Met
7638 §3Thumbprint canonical formMUSTjwk.rszeabur-identity-keys: jwk::tests::rfc7638_example_thumbprintMet
8725 §3.1One algorithm per keyMUSTsigning.rs, verify.rszeabur-identity-keys: signing::tests::rejects_key_for_the_wrong_algorithm, verify::tests::an_rsa_key_never_verifies_an_es256_headerMet
8725 §3.10, §3.11No jku/x5u/jwk headers; explicit typ— / RECOMMENDEDjws.rs (at+jwt, JWT)zeabur-identity-oauth/tests/oauth.rs: code_flow_issues_tokens_kubernetes_can_useMet
OIDC Core §10.1.1Keep decommissioned keys published for a whileSHOULDzeabur-identity-keys/src/rotation.rszeabur-identity-keys: rotation::tests::*Met
SourceRequirementCodeTestStatus
oidc.goRS/ES/PS algorithms onlyES256 default, RS256—Met
go-oidc v2 key cacheA new key is visible before it signszeabur-identity-keys/src/rotation.rs publish-ahead; max-age below itzeabur-identity-keys: rotation::tests::schedule_invariants, zeabur-identity: cli::tests::rejects_bad_issuers_and_inconsistent_timingsMet
validation.godiscoveryURL https and different from urlinternal TLS listener at the Service's ClusterIP(e2e)Met
oidc.goInternal discovery keeps issuer; jwks_uri from that documentzeabur-identity-oauth/src/discovery.rszeabur-identity-oauth: discovery::tests::internal_variant_changes_only_the_jwks_uri; (e2e)Met
AuthenticationConfigurationPrefixed usernames and groups; no system:; access tokens only (has(claims.client_id)); lifetime boundzeabur-identity-k8s/src/kube_config.rszeabur-identity-k8s: kube_config::tests::*; (e2e) passkey_sign_in_then_kubectl_auth_whoamiMet
Distributionsanonymous per distribution; install steps for K3s/RKE2kube_config.rszeabur-identity-k8s: kube_config::tests::*Partial: configuration generation tested; cluster E2E covers k3s only, RKE2 release installation unverified
Project requirementkubectl auth whoami on k3s—(e2e)Met
SourceRequirementCodeTestStatus
API conceptsUpdate with resourceVersion is compare-and-swap; delete preconditions; create of an existing name failszeabur-identity-k8s/src/objects.rszeabur-identity-k8s/tests/kubernetes.rs: exactly_one_racer_wins_each_primitive (cluster)Met
CRD validationCEL within the cost budgetzeabur-identity-k8s/src/crd/*.rsadmission_rejects_invalid_objects (cluster)Met
RBACLeast privilege: Secrets only by name, no create on Secrets, no RBAC verbs; create on core Events and update/patch on credentials/status for the account pagezeabur-identity-k8s/src/rbac.rsservice_account_has_least_privilege (cluster), zeabur-identity-k8s: rbac::tests::*Met
RFC 9700 §2.1; RFC 8252 §7.3; RFC 6749 §3.1.2Redirect URIs are https:// with a host, or a loopback http:// with a path; printable ASCII, no fragment or user information. Admission checks this with CEL isURL()/url(); zeabur-identityctl client create additionally requires a valid WHATWG URL written in its serialized form with well-formed percent-encoding, so exact matching sees the string a browser uses and admission accepts itzeabur-identity-k8s/src/crd/client.rs REDIRECT_URI_RULE, zeabur-identity-k8s/src/clients.rs redirect_uri_allowedzeabur-identity-k8s: clients::tests::redirect_uris_are_https_or_loopback_in_the_form_a_browser_uses, clients::tests::invalid_registrations_are_rejected_with_the_offending_value; tests/redirect_uris.rs (property: accepted URIs survive the WHATWG and RFC 3986 parsers and the authorization response, loopback ports match, the browser's form is accepted); admission_rejects_invalid_objects, admission_and_zeabur-identityctl_agree_on_generated_redirect_uris (cluster: generated candidates; what zeabur-identityctl accepts is admitted, and what admission lets in is never a non-https, non-loopback, fragment or credential target to a browser)Met
RFC 6749 §10.10Generated client secrets: 256 bits from the CSPRNG (guessing probability at most 2^-128 is a MUST, at most 2^-160 a SHOULD); only the hash is storedzeabur-identity-k8s/src/clients.rs registerzeabur-identity-k8s: clients::tests::confidential_clients_get_a_fresh_random_secret_and_only_its_hash, clients::tests::emitted_objects_satisfy_the_schema_rules; oauth_flow_on_kubernetes_storage (cluster: the printed secret authenticates, a wrong one is invalid_client)Met
Project requirementExpired objects are cleaned upzeabur-identity-k8s/src/gc.rsgarbage_collector_deletes_expired_objects_only (cluster)Met
Project requirementSigning keys in a Secret, rotation with overlapzeabur-identity-k8s/src/keys.rssigning_keys_persist_and_only_one_writer_wins (cluster)Met
WebAuthn Level 3 §7.1A known credential ID fails registrationzeabur-identity-store passkey_credential_name, create-oncezeabur-identity-signin/tests/signin.rs; setup_link_is_single_use_and_replaceable (cluster)Met
RFC 9700 §4.14.2Refresh rotation with reuse detectionzeabur-identity-k8s/src/grants.rs, zeabur-identity-store classifygrant_rotation_is_linearizable (cluster: 1 of 64 rotates)Met
Project account-recovery policy; RFC 9700 §4.14.2 (security-event revocation, MAY)Reset deletes pending invitations, then credentials, then refresh-token families before writing a setup link, including zero-credential retries after partial failurezeabur-identity-k8s/src/setup.rsAccount-recovery testsMet
  • zeabur-identity-k8s/tests/setup_reset.rs: reset_revokes_invitations_before_anything_else, reset_retries_grant_deletion_after_credentials_are_gone and reset_revokes_old_grants_without_credentials exercise partial-failure recovery using a mock Kubernetes HTTP API and the real OAuth router.
  • zeabur-identity-k8s/tests/kubernetes.rs: setup_link_is_single_use_and_replaceable checks on a cluster that reset revokes only the target user's invitation.
  • zeabur-identity-e2e/tests/e2e.rs: passkey_sign_in_then_kubectl_auth_whoami checks on a cluster that refresh succeeds before reset and fails afterward.

Tests are in zeabur-identity-oauth/tests/oauth.rs unless noted.

Spec §RequirementLevelCode (zeabur-identity-oauth/src/)TestStatus
6749 §3.1, §3.2Parameters not repeatedMUSTparams.rsauthorization_endpoint_errorsMet
6749 §3.1.2.3; 9700 §2.1Exact redirect URI match (loopback port excepted)MUSTauthorize.rszeabur-identity-oauth: authorize::tests::redirect_uri_rulesMet
6749 §4.1.2.1No redirect for an invalid client or redirect URIMUST NOTendpoints.rs (HTML error page)authorization_endpoint_errors; zeabur-identity-signin/tests/pages.rs: untrusted_authorization_requests_show_an_error_pageMet
6749 §4.1.2Codes short-lived and single use; reuse revokes issued tokensMUSTcode.rs, endpoints.rscode_is_single_use_and_reuse_revokes_what_it_issued, exactly_one_concurrent_redemption_succeedsMet
6749 §4.1.3Code bound to client and redirect_uriMUSTendpoints.rspkce_and_code_binding_are_enforcedMet
6749 §2.3.1, §5.2Client authentication; WWW-Authenticate for BasicMUSTclient_auth.rsconfidential_client_gets_rs256_id_tokens_and_must_authenticateMet
6749 §5.1Cache-Control: no-storeMUSTerror.rs no_storecode_flow_issues_tokens_kubernetes_can_useMet
6749 §10.10Token guessing ≤ 2^-128MUST256-bit secrets; sealed codes—Met
6749 §10.12CSRF protection for the authorization endpointMUSTapproval is a same-origin JSON sign-in bound to the browser's sealed flow cookiezeabur-identity-signin/tests/signin.rs: requests_from_other_sites_are_refused, sign_in_needs_a_pending_authorization_requestMet
7636 §4.1, §4.4.1, §4.6Verifier format; challenge required; S256 compareMUSTpkce.rs, authorize.rszeabur-identity-oauth: pkce::tests::*, authorize::tests::*Met
9700 §2.1.1, §2.2.2PKCE required; public-client refresh tokens rotatedMUSTauthorize.rs, endpoints.rspkce_and_code_binding_are_enforced, refresh_rotates_and_reuse_revokes_the_familyMet
9700 §4.11.2303, never 307MUST NOTendpoints.rscode_flow_issues_tokens_kubernetes_can_useMet
9068 §2.1, §2.2, §5at+jwt; required claims; distinct aud per resourceMUSTtokens.rs, authorize.rscode_flow_issues_tokens_kubernetes_can_use, authorization_endpoint_errorsMet
8707 §2invalid_target for disallowed resources—authorize.rszeabur-identity-oauth: authorize::tests::*Met
9126 §2.1, §2.2, §4Authenticated PAR; one-time, client-bound, expiring request URIMUSTendpoints.rspushed_authorization_requestsMet
7009 §2.1, §2.2Revocation by the owning client with constant-time proof of a current or previous refresh secret; 200 for invalid tokens without deleting a grantMUSTendpoints.rs, refresh.rsrevocation, guessed_refresh_secrets_cannot_revoke_a_known_grant, current_and_previous_refresh_secrets_authorize_family_revocation; zeabur-identity-e2e: passkey_sign_in_then_kubectl_auth_whoami (forged-secret smoke)Met
6749 §6Validate refresh token and client binding; wrong-client family deletion requires constant-time proof of a current or previous secret, never a grant name aloneMUST; deletion policyendpoints.rs, refresh.rsguessed_refresh_secrets_cannot_revoke_a_known_grant, current_and_previous_refresh_secrets_authorize_family_revocation; zeabur-identity-e2e: passkey_sign_in_then_kubectl_auth_whoami (second public-client forged-secret smoke)Met
Kubernetes Object Names and IDsA refresh token renews only for the User whose UID the grant recorded; a grant without a recorded UID belongs to no one, and none renews for a new User with the same nameProject requirementendpoints.rs refresha_session_left_from_a_removed_user_does_not_refresh_as_a_new_user_of_that_nameMet
Kubernetes Object Names and IDsAn authorization code is redeemed only for the User whose UID it sealed at sign-in, never for a new User with the same name; no grant is createdProject requirementcode.rs CodeClaims, endpoints.rs exchange_codea_code_for_a_removed_user_is_not_redeemed_for_a_new_user_of_that_nameMet
9207 §2iss in authorization responsesMUSTauthorize.rscode_flow_issues_tokens_kubernetes_can_useMet
OIDC Core §2, §3.1.3.6ID token claims; aud is the client; azpREQUIREDtokens.rscode_flow_issues_tokens_kubernetes_can_useMet
OIDC Core §3.1.2.1GET and POST; prompt=none without a session failsMUSTendpoints.rs, authorize.rsauthorization_endpoint_accepts_post, authorization_endpoint_errorsMet
OIDC Core §3.1.2.1ui_localesOPTIONALerror pagezeabur-identity-signin/tests/pages.rs: untrusted_authorization_requests_show_an_error_pagePartial: the sign-in page does not use it
OIDC Core §5.3; RFC 6750 §2.1, §3UserInfo with bearer access tokens; challenges with error codes; current claims by scopeMUSTuserinfo.rsuserinfo_returns_current_claims_for_access_tokens_onlyMet
OIDC Core §5.3.2Always return the grant's sub; resolve metadata by UID and verify the directly read user's UID and enabled stateMUST; binding policyuserinfo.rs, zeabur-identity-k8s/src/directory.rsuserinfo_returns_current_claims_for_access_tokens_only, userinfo_rejects_deleted_and_recreated_users, userinfo_rejects_unknown_and_ambiguous_subjectsMet (client-side ID token subject comparison remains the client's responsibility)
OIDC Core §12.2Refreshed ID token keeps auth_time, no nonceMUSTendpoints.rsrefresh_rotates_and_reuse_revokes_the_familyMet
OIDC Core §15.1max_age, prompt=loginMUSTevery sign-in is a fresh ceremony (no sessions)zeabur-identity-signin/tests/signin.rs: setup_link_then_passkey_sign_in_issues_tokens; auth_time_is_the_sign_in_and_survives_refresh (a request with max_age shows the sign-in page)Met while there are no sessions

OAuth tests are in zeabur-identity-oauth/tests/oauth.rs unless noted.

RequirementCodeTestStatus
Admin accounts sign in only with a passkey; with admin-aal3, only with a device-bound passkey (BE unset); regular accounts are unaffectedzeabur-identity-core/src/account.rs allowszeabur-identity-core: account::tests::admin_accounts_sign_in_with_passkeys_only, account::tests::aal3_refuses_synced_passkeys_for_admins_onlyMet
The token endpoint applies the same policy at code exchange and at every refresh, so a session that no longer qualifies ends at its next refreshzeabur-identity-oauth/src/endpoints.rsadmin_accounts_need_a_passkey_and_with_aal3_a_device_bound_oneMet
Admin accounts neither sign in with nor link GitHub or Googlezeabur-identity-signin/src/upstream.rs, lib.rszeabur-identity-signin/tests/upstream.rs: admin_accounts_neither_sign_in_with_nor_link_github_or_googleMet
With admin-aal3, a synced passkey can neither sign in to nor be enrolled for an admin accountzeabur-identity-signin/src/lib.rs, account.rszeabur-identity-signin/tests/signin.rs: with_aal3_an_admin_cannot_sign_in_with_a_synced_passkey, with_aal3_an_admin_enrols_only_a_device_bound_passkeyMet
Only admin accounts' tokens carry the admins group, and their tokens are shorter (5 min vs 10 min); the CRD refuses admins in spec.groupszeabur-identity-oauth/src/tokens.rs, zeabur-identity-k8s/src/crd/user.rsonly_admin_accounts_carry_the_admin_group_and_their_tokens_are_shorterMet
Refresh idle: 1 h, or 15 min for admins with teams: multiple or admin-aal3; every family ends 12 h after sign-in; refresh_expires_in in the token responsezeabur-identity-oauth/src/lib.rs Lifetimes::for_account, endpoints.rszeabur-identity-oauth: tests::lifetimes_per_account_type, refresh_idle_follows_the_account_type_and_the_settings; zeabur-identity-core: account::tests::short_admin_idle_follows_teams_or_aal3Met
auth_time is the passkey or upstream sign-in and survives refresh; the 12 h limit counts from itendpoints.rsauth_time_is_the_sign_in_and_survives_refreshMet
Settings come from the ConfigMap zeabur-identity-settings, read at every sign-in and refresh; missing means default, a value not understood means the stricter settingzeabur-identity-core/src/account.rs Settings::from_data, zeabur-identity-k8s/src/directory.rszeabur-identity-core: account::tests::settings_from_a_config_mapMet
First admin: setup-link creates an admin account; zeabur:admins is bound to cluster-admin, so the admin has full rights without a second sign-in; a refresh needs no passkey and follows the account type (a regular account loses the rights)zeabur-identity-k8s/src/rbac.rs, setup.rs(e2e) passkey_sign_in_then_kubectl_auth_whoamiMet
Section titled “Sign-in: passkeys, enrolment links, invitations”

Tests are in zeabur-identity-signin/tests/signin.rs unless noted.

SourceRequirementLevelCode (zeabur-identity-signin/src/)TestStatus
Project requirementSetup link works once and expires; no first-visitor adminMUSTlib.rs (consume before storing), zeabur-identity-k8s/src/setup.rssetup_token_is_single_use, concurrent_enrolments_with_one_token_add_one_passkey, setup_token_expires_and_must_match, setup_link_is_single_use_and_replaceable (cluster, 64 concurrent)Met
Project requirementInvitations work once, only for the invited user; the service account cannot create themMUSTlib.rs, zeabur-identity-k8s/src/setup.rs, invitations.rsinvitation_enrols_the_invited_user_once, invitations_are_single_use_and_admin_only (cluster)Met
WebAuthn §5.4.4, §5.4.6Discoverable credential, user verification required—passkeys.rszeabur-identity-signin: passkeys::tests::registration_requires_resident_keys_and_uvMet
WebAuthn §7.1, §7.2UV verified; user handle is the owner's; challenge single useMUSTpasskeys.rs, lib.rspasskeys_without_user_verification_are_refused (registration, client side), sign_in_without_user_verification_is_refused (signed assertion with UV clear, refused with no code or tokens; UV-positive control completes), unknown_passkeys_and_disabled_users_cannot_sign_in, assertions_cannot_be_replayedMet
WebAuthn conditional mediationAutofill (autocomplete="username webauthn")—passkeys.rs, ui/src/login.tszeabur-identity-signin/tests/pages.rs: login_offers_passkey_autofill_for_a_pending_requestMet
RFC 9457 §3Problem details—problem.rsrequests_from_other_sites_are_refusedMet
Fetch MetadataCross-site state-changing requests refused—lib.rs check_requestrequests_from_other_sites_are_refused, zeabur-identity-signin: tests::same_origin_checksMet
RFC 6265bis__Host- cookies: Secure, Path=/, no DomainMUSTlib.rs, upstream.rscode_flow_issues_tokens_kubernetes_can_useMet

/account, where a user manages their own passkeys and sessions. Tests are in zeabur-identity-signin/tests/signin.rs (tests::account::*) unless noted.

SourceRequirementLevelCodeTestStatus
Project requirementEvery read and write is filtered to the session's user by name and UID; names in requests only select among thoseMUSTzeabur-identity-signin/src/account.rs, zeabur-identity-k8s/src/credentials.rs owned_passkeys, grants.rs listthe_page_lists_only_the_session_users_passkeys, changes_cannot_reach_another_users_objects; zeabur-identity-k8s/tests/account.rs: only_the_users_own_passkeys_are_listed_or_changed, grants_are_listed_by_uid_and_reauthenticated_by_secretMet
Project requirement; NIST SP 800-63B-4 §2.2.3Adding or removing a passkey needs a user-verified assertion from the last 5 minutes, by one of the user's own passkeys, recorded on the sessionMUSTaccount.rs is_fresh, reauthadding_and_removing_need_a_recent_verified_passkey, changes_cannot_reach_another_users_objectsMet
Project requirementThe last passkey is never removed, including by concurrent removalsMUSTzeabur-identity-k8s/src/credentials.rs remove (revision bump on the others, then delete with preconditions)the_last_passkey_cannot_be_removed, concurrent_removals_keep_one_passkey; zeabur-identity-k8s/tests/account.rs: interleaved_removals_keep_a_passkey, sequential_removals_stop_at_the_last_passkeyMet
Project requirementRemoving a passkey revokes every session and grant that signed in or re-authenticated with itMUSTaccount.rs revoke_by_credentialremoving_a_passkey_revokes_what_it_signed_inMet
OWASP CSRF PreventionChanges need the session cookie, same-origin Fetch Metadata and a per-session tokenMUSTaccount.rs signed_in, SessionToken::csrfrename_needs_the_csrf_token_and_same_originMet
RFC 6265bisSession cookie __Host-, HttpOnly, SameSite=Lax; only the secret's hash is stored; one hour, not extended; ends when the user is disabled or recreatedMUSTaccount.rssessions_end_after_an_hour_and_with_the_user, sessions_can_be_revokedMet
WebAuthn §5.4.3excludeCredentials lists the user's passkeys; resident key and UV requiredSHOULDaccount.rs add_options, passkeys.rsadding_and_removing_need_a_recent_verified_passkeyMet
WebAuthn Signal APIsignalAllAcceptedCredentials only with a complete, non-empty list; signalUnknownCredential for an unregistered credential—lib.rs accepted, Refused; ui/src/webauthn.ts signalremoving_a_passkey_revokes_what_it_signed_inMet
Passkey Endpoints well-known URL/.well-known/passkey-endpoints returns enroll and manage, 200 JSON, no redirect—account.rs passkey_endpointspasskey_endpoints_are_publishedMet
Project requirementAdd, rename, remove and session revocation leave a zeabur_identity::audit log line and an Event on the User (actor, credential, AAGUID, client IP, user agent)MUSTaccount.rs log_outcome, record; zeabur-identity-k8s/src/credentials.rs recordadding_and_removing_need_a_recent_verified_passkey, rename_needs_the_csrf_token_and_same_origin; zeabur-identity-k8s/tests/account.rs: only_the_users_own_passkeys_are_listed_or_changedMet
Project requirementA passkey's label is the one the user typed; without one, the account page names it by its creation date—lib.rs new_passkey; zeabur-identity-ui/templates/account.htmla_passkey_without_a_label_shows_its_creation_dateMet

Tests are in zeabur-identity-signin/tests/upstream.rs.

SourceRequirementCode (zeabur-identity-signin/src/)TestStatus
RFC 6749 §10.12; RFC 9700 §2.1state bound to the browser, compared in constant time, used onceupstream.rsunlinked_disabled_and_replayed_sign_ins_failMet
RFC 7636PKCE towards the providerupstream/provider.rsa_linked_github_account_signs_in_and_gets_a_code (fake provider checks the verifier)Met
OIDC Core §3.1.3.7ID token: signature from the provider's JWK set, iss, aud exactly the client ID (no other audience trusted), azp if present equal to it, exp, iat, nonceupstream/provider.rsgoogle_id_tokens_are_validatedMet
Project requirementOnly linked accounts sign in; linking only through an enrolment link, once, or from a signed-in account pageupstream.rsan_invitation_links_an_account_once, unlinked_disabled_and_replayed_sign_ins_fail, a_signed_in_user_links_an_account_on_the_account_pageMet
RFC 6749 §10.12A link from the account page starts only with a fresh verified passkey and the CSRF token; its sealed state names the session, user and UID, and the callback links nothing unless that session is still signed in for that userupstream.rs account_start, Boundlinking_needs_a_fresh_confirmation_and_the_session_that_started_itMet
Project requirementAn upstream account already linked to any user is not linked again; admin accounts cannot link, checked at the start and at the callbackupstream.rs finish_account_linkan_account_linked_to_anyone_is_not_linked_again, admin_accounts_cannot_link_from_the_account_pageMet
Project requirementUnlinking needs a fresh verified passkey, never removes the user's last credential (also under concurrent unlinks), revokes what the account signed in, and records AccountUnlinkedaccount.rs unlink; zeabur-identity-k8s/src/credentials.rs unlinkunlinking_keeps_a_way_to_sign_in_and_ends_what_the_account_signed_in; zeabur-identity-k8s/tests/account.rs: the_last_credential_is_never_unlinked, interleaved_unlinks_keep_a_credential, only_linked_accounts_the_user_owns_are_listed_or_unlinkedMet
Kubernetes Object Names and IDsA linked account signs in only as the User whose UID its owner reference names, never as a new User with the same nameupstream.rs callback; zeabur-identity-k8s/src/credentials.rs linked_account, linka_link_left_from_a_removed_user_does_not_sign_in_as_a_new_user_of_that_name; zeabur-identity-k8s/tests/account.rs: a_linked_account_signs_in_only_as_the_user_that_owns_itMet
Kubernetes Object Names and IDsAn invitation or setup link enrols a passkey or links an account only for the User whose UID its owner reference nameslib.rs valid_setup_token; zeabur-identity-k8s/src/setup.rs load_setup, load_invitationsignin.rs: a_link_left_from_a_removed_user_does_not_enrol_a_new_user_of_that_name; zeabur-identity-k8s/tests/account.rs: an_invitation_enrols_only_the_user_that_owns_itMet

Tests are in zeabur-identity-signin/tests/pages.rs; every page test checks headers and that the markup needs nothing the CSP forbids.

SourceRequirementLevelCodeTestStatus
RFC 9700 §4.16Prevent clickjacking (CSP frame-ancestors, X-Frame-Options)MUSTzeabur-identity-ui/src/lib.rs hardenall page testsMet
CSP Level 3, Trusted TypesNo inline script, style or handlers; no string-to-HTML sinks—zeabur-identity-ui, ui/src/*.tsall page testsMet
RFC 9110 §12.5.4, §12.4.2Accept-Language with quality values—zeabur-identity-ui/src/i18n.rszeabur-identity-ui: i18n::tests::*, language_follows_parameter_then_headerMet
Project requirementAssets embedded, no CDN; built output checked against source; size measuredMUSTzeabur-identity-ui/src/assets.rs, scripts/build-ui.sh, scripts/ui-budget.shassets_are_embedded_versioned_and_immutable; CI uiMet
Project requirementNo tokens in browser storage; cookies HttpOnly, Secure, SameSiteMUSTui/src/*.ts, zeabur-identity-signinbun run typecheck in ui/, reviewMet
Project requirementFallback without passkey support or script; provider buttons only when configuredMUSTtemplateslogin_offers_passkey_autofill_for_a_pending_request, provider_buttons_appear_only_when_configuredMet
Project requirementDark mode; en, zh-TW, zh-CNMUSTui/styles/app.css, zeabur-identity-ui/src/i18n.rslanguage_follows_parameter_then_headerMet
EscapingUser-influenced text cannot inject markup—askama auto-escapinguser_controlled_text_is_escapedMet
WCAG 2.2 1.4.3, 1.4.11Text ≥ 4.5:1; boundaries and focus ≥ 3:1AAthe design system's tokens, ui/styles/app.csscontrast.py (CI ui), computed table in the zeabur-identity-ui docsMet for every pair used
WCAG 2.2 2.4.7, 2.5.8, 3.1.1, 3.1.2, 3.3.1, 4.1.3Focus, target size, language, errors, status messagesA / AAtemplateslanguage_follows_parameter_then_header; reviewMet by inspection
WCAG 2.2Automated audit and assistive-technology pass———Not done