auth
Login, tokens, DPoP proofs (RFC 9449), single-flight refresh and step-up of the apps, without Flutter.
Kind: pure Dart package, a member of the Dart workspace at the repository root
(resolution: workspace, publish_to: none). It performs HTTP only through the api_client it
is given; keys, secrets, the browser, the app redirect, passkeys and time come in through ports,
so every flow runs against fakes in the tests.
Dependencies
Allowed members (docs/spec/15-client-architecture.md, 3.1; tools/arch-check/rules.yaml):
api_client, domain (arrows of the diagram). Every other member is out of reach; make arch
fails on a dependency or an import of it. From pub.dev: crypto (SHA-256 for ath, PKCE and the
JWK thumbprint) and the types of the api_client surface (dio, built_value,
built_collection). pointycastle is a dev dependency of the software signer in test/support/
only.
Further rules checked by make arch:
- no import of
package:flutter/,dart:ui(rule 1). - no native source (rule 4).
Ports to implement per platform
| Port | Purpose | Implemented by |
|---|---|---|
DeviceKeySigner |
ES256 key that returns the JOSE R || S signature (64 bytes) |
M4-10 (Apple), M4-11 (Windows), M5-01 (Android) |
DeviceKeys |
the pair dpop (no confirmation) and stepUp (user presence) |
same |
SecretStore |
refresh token and device ID by key; a write is durable when its future completes | M4-18 |
BrowserLauncher |
opens the OIDC URL in the system browser, never a web view | M4-18 |
RedirectReceiver |
the app_redirect_uri (loopback 127.0.0.1 on desktop) and the redirect back |
M4-18 |
PasskeyAuthenticator |
WebAuthn get and create, only for instances with a public domain | M4-18 |
StepUpPrompt |
explains the step-up in the person's language before the step-up key signs | M4-18 |
Clock |
time and polling waits (SystemClock in the app) |
this package |
Session and interceptor order
One AuthSession per server address owns the token state, the nonce store, the refresh
coordinator, the step-up service and the login flows; there is no global state. It builds its
client through createApiClient (M4-02) and fills the auth slot in this order:
RefreshInterceptor: refreshes before a request whose access token has expired by the clock, refreshes once and repeats once on401 token_expiredor401 unauthenticated, and ends the session on401 session_revoked.DpopInterceptor: addsAuthorization: DPoP <token>and a fresh proof withath, gives the unauthenticated login completions a proof withoutath, adopts everyDPoP-Nonceand repeats once on401 use_dpop_nonce.StepUpInterceptor: on401 step_up_requiredruns the step-up once and repeats the original request with the sameIdempotency-Keyand body, a new proof and the current token.
The refresh runs before the proof is built, so ath always hashes the token that is sent. Every
interceptor repeats a request at most once; a second identical problem goes to the caller.
DpopProofFactory.forGateway builds the gateway proof (htm = GET, htu the HTTPS form of the
gateway URL, the nonce of hello) for M4-13; RefreshCoordinator.refreshNow serves the gateway's
reauth_required, and AuthSession.sessionRevoked the close code 4012.
Flows
LoginService has one method per flow, each returning a LoginOutcome (LoginSignedIn,
LoginPending, LoginNeedsSecondFactor, LoginFailed):
- OIDC with PKCE (
beginOidc):client_challenge(S256), system browser, redirect withlogin_code,completeOidcLoginwithclient_verifier, the proof and the device fields. - Password with TOTP (
loginWithPassword): without a TOTP code nothing is sent and the outcome isLoginNeedsSecondFactor. - Proxy header (
loginWithTrustedHeader), passkey (loginWithPasskey), enrollment code (redeemEnrollmentCode,enrollWithPasskey), pairing (beginPairing,pollPairing). - Pending device (
pollPendingDevice) andlogout, which clears the store and signs out even offline.
Codes are normalized before they are sent (normalizeCode: spaces and hyphens removed, a leading
en_ in lower case, the rest in upper case). Polling waits with client.outbox_retry_base
doubling up to client.outbox_retry_cap from AppConfig.
Events
AuthSession.events is a broadcast stream of SignedIn, DevicePending, SignedOut(reason)
and RefreshFailed. SignedOut.deleteLocalData is true for a revoked session or device; the app
then deletes the local data of that server (R-SY-14).
Secrets
The access token stays in memory; the refresh token lives in the SecretStore and is written
before a rotated access token is published. No toString, exception or log line of this package
contains a token, proof, code, verifier, password or TOTP value; they print the kind and the
length only (test/no_secrets_test.dart).
Layout
| Path | Content |
|---|---|
lib/auth.dart |
the public library; every public symbol has a doc comment (make lint, public_member_api_docs) |
lib/src/ |
ports, JOSE, proofs, nonce store, tokens, refresh, step-up, interceptors, login flows, events |
test/ |
tests with a software P-256 signer and a fake API, run by make test with coverage (quality.coverage.dart_min_percent) |
Run from the repository root: make lint test coverage arch COMPONENTS=apps. The API pages
come from make docs (Dart API reference).
Libraries
- auth
- Login, tokens, DPoP proofs, step-up and refresh of the apps, without Flutter (15 3.1, 15 8).