Auth
Accounts and sessions: the auth engine, its services on every transport, the client kit (token store, OIDC with PKCE, device flow), and apps/auth-server.
What it is and when to use it#
Auth is a family of crates under features/auth. auth-proto declares
AuthService, OrganizationService and DeviceService once, as
#[architect::service] traits, so each has a vox face, an HTTP face
(POST /auth/<method>) and clients for both. auth is the engine
(ArchitectAuth): accounts, sessions (only a token’s hash is stored),
organizations, devices, OIDC provider, device flow, passkeys and two-factor.
auth-client is the wasm-clean client kit: a stored session, token stores,
middleware that attaches the bearer to every vox call, OIDC with PKCE and
refresh, and the RFC 8628 device flow. apps/auth-server is the deployable
server (deployed as fts-auth).
Use it to sign people in and to know who is calling. What a caller may do is
Permissions. There is no architect::auth
re-export; depend on the crates. Design: Auth;
the deployment is in skills/architect/references/fts-auth.md.
A minimal example#
architect-auth with feature full. From
features/auth/architect-auth/examples/vox_rpc.rs:
use architect::{LayerRouter, LocalServer, Scope};
use architect_auth::{ArchitectAuth, AuthServiceClient, SignUpEmailPassword, auth_service_layer,
client::{stored_session, FileTokenStore, TokenStore, TokenStoreMiddleware},
db::{AuthSeaOrmStorage, Migrator}, transport::vox::AuthVoxService};
use sea_orm_migration::MigratorTrait;
let db = sea_orm::Database::connect("sqlite::memory:").await?;
Migrator::up(&db, None).await?;
let auth = ArchitectAuth::builder()
.secret("a-secret-at-least-32-bytes-long!!") // shorter is refused
.storage(AuthSeaOrmStorage::new(db))
.build()?;
let router = LayerRouter::new().merge(auth_service_layer(AuthVoxService::new(auth)));
let scope = Scope::new();
let client: AuthServiceClient = LocalServer::serve(router, scope.clone()).establish().await?;
let bundle = client.sign_up_email_password(SignUpEmailPassword {
email: "a@b.c".into(), password: "correct horse battery staple".into(),
name: None, username: None, image: None, metadata_json: None,
ip_address: None, user_agent: None,
}).await?;
let store = std::sync::Arc::new(FileTokenStore::new("session.json"));
store.save(&stored_session(&bundle))?;
// Every later client: client.with_middleware(TokenStoreMiddleware::new(store.clone()))
A server protects its own services with AuthServerMiddleware (the handler
reads AuthVoxContext), or resolves the bearer for the permissions gate with
SessionIdentityResolver. Plain axum routes use require_session
(features/auth/architect-auth/examples/axum_http.rs).
Code tour#
features/auth/auth-proto/src/service.rsAuthService: sign up and in, device sign-in,current_session,refresh_session,whoami,sign_out, profile changes.features/auth/auth-proto/src/organizations.rsOrganizationServiceandfeatures/auth/auth-proto/src/device.rsDeviceService: orgs, members, invitations, roles; a person’s devices.features/auth/auth/src/config.rsArchitectAuthBuilderandfeatures/auth/auth/src/lib.rsArchitectAuth: the engine.features/auth/auth/src/transport.rsAuthVoxService,AuthServerMiddleware,AuthVoxContext: the engine as the traits, and the server side of the bearer.features/auth/auth/src/identity.rsSessionIdentityResolver: a bearer to aPrincipal, cached briefly.features/auth/auth-client/src/lib.rsStoredSession,TokenStore,FileTokenStore,MemoryTokenStore,TokenStoreMiddleware.features/auth/auth-client/src/oidc.rsauthorize_url,Pkce,refresh_if_needed;features/auth/auth-client/src/device.rsstart_body,poll_body.apps/auth-server/src/server.rsservices,build,serve: the three services on every face;apps/auth-server/src/oauth.rsthe OIDC routes and hosted pages.
A client signs in through AuthServiceClient or AuthServiceHttpClient.
AuthVoxService calls the engine, which returns a session bundle. The app
keeps it with stored_session in a TokenStore; TokenStoreMiddleware
reads the store on every call and sends authorization: Bearer <token>, so a
refresh lands on the next call. On the server, AuthServerMiddleware turns
that into an AuthVoxContext, or SessionIdentityResolver into a
Principal for the gate. A device without a browser posts the device-code
request, shows the code, and polls until the person approves it elsewhere.
Tests to read#
apps/auth-server/tests/surfaces.rs: one scenario over HTTP, in-process vox, WebSocket and iroh: sessions, a wrong password, org isolation, invitations and roles, orgs fetched live.features/auth/tests/native/src/lib.rs: the session surface overLocalServer(refresh rotates the token, sign-out); the token middleware matching the server middleware’s wire format; a short secret refused.apps/auth-server/tests/device_and_accounts.rs: the device code approved and refused; several accounts and switching.apps/auth-server/tests/http_surface.rs: the OIDC provider, pages, CORS.apps/auth-server/tests/mock_oauth.rsandapps/auth-server/tests/social.rs: social sign-in againstapps/mock-oauth.
Status#
Done in architect and deployed. Not yet: remote token introspection (until
then Task’s task:apps/server/src/central_auth.rs CentralAuth is the only
one); tokens others can verify offline (HS256 with no published keys); a key
ring instead of one secret; revocation reaching offline peers.
- Task runs the engine on its server (
task:apps/server/Cargo.toml), keeps sessions in aFileTokenStore, signs in withauth_client::oidc(task:crates/ui/src/central_login.rs) and usesDeviceService. Still to adopt:TokenStoreMiddlewareandrefresh_if_needed. - Studio uses only
auth-client: the device flow (studio:apps/session/daw/src/task_account.rs) and OIDC with PKCE (studio:features/signal/tone3000/src/account/session.rs).