Platform auth + OAuth + per-user stores (closes #4)#6
Merged
thorwhalen merged 1 commit intomainfrom Apr 20, 2026
Merged
Conversation
Implements the auth + stores plan documented in
misc/docs/plan__auth_and_stores_phase_2_3.md. Apps remain auth- and
storage-unaware; the contract is just request.state.user_id and
request.state.store.
Phase 2a — Auth foundation:
- enlace/auth/ package with PlatformAuthMiddleware (pure ASGI, path
normalization, identity-header stripping, deny-by-default), CSRF
middleware (signed double-submit), SessionStore, argon2id passwords,
signed cookies via itsdangerous, and /auth/register|login|logout|
shared-login|whoami routes.
- AccessLevel on AppConfig ("public" | "protected:shared" |
"protected:user" | "local"); AuthConfig / StoreBackendConfig /
OAuthProviderConfig on PlatformConfig with TOML parsing.
- CLI: enlace auth-init, auth-generate-signing-key, auth-hash-password,
auth-list-sessions, auth-revoke-session. enlace check validates the
signing-key and shared-password env vars.
Phase 2b — OAuth:
- enlace/auth/oauth.py with lazy Authlib import. Google + GitHub presets;
on callback we create a local session and discard upstream tokens.
Phase 3 — Per-user stores:
- enlace/stores/ with PrefixedStore, key sanitization (path-traversal
guard), a file-backed MutableMapping factory, StoreInjectionMiddleware,
and a /api/{app_id}/store/{key} router.
Diagnostics: new categories SUBAPP_AUTH_MIDDLEWARE, CLIENT_IDENTITY_HEADER,
HARDCODED_USER_ID, SESSION_COOKIE_IN_SUBAPP, STORE_IMPORT_IN_APP,
UNSAFE_KEY_IN_STORE, MISSING_SIGNING_KEY.
Tests: 84 new tests (75 unit + 9 integration), including an e2e that
registers, logs in, hits a protected endpoint, round-trips the store,
and logs out, plus a standalone-preservation test proving apps still
work with ENLACE_MANAGED unset. Full suite: 152 passed.
Skills (enlace-auth-setup, enlace-user-stores) are deferred to a
follow-up PR per §13 open decisions. Open decisions resolved with plan
defaults: hand-rolled CSRF, dol as enlace[auth] soft dep (with stdlib
fallback), separate session/user-data store factories, no CAPTCHA, in-
repo skills.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements issue #4 — the auth and stores plan from
misc/docs/plan__auth_and_stores_phase_2_3.md. All three phases ship together.Phase 2a — Auth foundation
enlace/auth/withPlatformAuthMiddleware(pure ASGI, path normalization + identity-header stripping + deny-by-default),CSRFMiddleware(signed double-submit),SessionStore, argon2id passwords, signed cookies via itsdangerous, and/auth/{register,login,logout,shared-login,whoami}routes.AccessLevelonAppConfig;AuthConfig/StoreBackendConfig/OAuthProviderConfigonPlatformConfigwith TOML parsing.enlace auth-init | auth-generate-signing-key | auth-hash-password | auth-list-sessions | auth-revoke-session.enlace checknow validates signing-key and shared-password env vars.Phase 2b — OAuth
enlace/auth/oauth.pywith lazy Authlib import. Google and GitHub presets. Callback creates a local session and discards upstream tokens.Phase 3 — Per-user stores
enlace/stores/withPrefixedStore, key sanitization (path-traversal guard incl. URL-encoded variants), a file-backedMutableMappingfactory,StoreInjectionMiddleware, and a/api/{app_id}/store/{key}router.Diagnostics — seven new checks:
SUBAPP_AUTH_MIDDLEWARE,CLIENT_IDENTITY_HEADER,HARDCODED_USER_ID,SESSION_COOKIE_IN_SUBAPP,STORE_IMPORT_IN_APP,UNSAFE_KEY_IN_STORE,MISSING_SIGNING_KEY. Each has a standalone-preserving fix suggestion.Design invariants honored
import enlace. Contract:request.state.user_id+request.state.store.test_standalone_preservation.pyprovesENLACE_MANAGEDunset keeps the happy path working.BaseHTTPMiddleware.Open decisions (§13 of the plan)
Resolved with plan defaults:
dolas soft dep inenlace[auth]; stdlib fallback ships inenlace/stores/backends.pyso core install still works./auth/register.enlace-auth-setup,enlace-user-stores) deferred to a follow-up PR once the auth flow is proven on a real app.Tests
test_auth_middleware(19),test_csrf(5),test_sessions(5),test_passwords(5),test_prefixed_store(23),test_store_middleware(5),test_oauth(3),test_base_auth_config(3),test_diagnose_auth(7).test_auth_e2e(6 — register/login/protected access/store round-trip/logout, shared-password flow, identity-header stripping),test_standalone_preservation(3).ruff check/ruff format: clean.Test plan
pip install -e '.[auth,oauth,dev]'pytest enlace/tests/ tests/— 152 passenlace auth-generate-signing-keyprints a keyenlace auth-hash-passwordprompts and prints an argon2id hashenlace checksurfaces missing env vars whenauth.enabled=trueFollow-ups
enlace-auth-setup,enlace-user-stores)