Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ jobs:

env:
REDIS_ADDR: localhost:56379
AUTH_TEST_REDIS_ADDR: localhost:56379
RABBITMQ_STREAM_URL: rabbitmq-stream://guest:guest@localhost:55552
RABBITMQ_AMQP_URL: amqp://guest:guest@localhost:55672/
POSTGRES_DSN: postgres://aether:aether_test@localhost:5432/aether?sslmode=disable
Expand Down
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Aet

---

## [Unreleased]

### Added

- **[AUTHPROXY] Redis/Valkey browser-session inventory and revocation.** The optional `login.SessionManager` API lists session metadata and supports individual and per-user bulk revocation across replicas. Management IDs cannot authenticate as browser cookies. Session creation and legacy migration prune expired index entries; logout and individual revocation restore a finite index lifetime after its last nonexpiring session is removed.

### Upgrade notes

- **[AUTHPROXY] Coordinated upgrade required for Redis browser sessions**, including deployments that do not expose session management. Drain and stop every old replica before any new replica serves requests; mixed versions cannot consistently read sessions or enforce revocation. Keep the same Redis primary, DB and prefix to migrate valid legacy sessions on use. **Rollback, stale-backup restore, or recovery from acknowledged-write loss requires a fresh, never-used session prefix on every replica and new sign-ins.** Do not reset generation counters while retaining session records. See the [browser-session upgrade and rollback guide](server/docs/auth-proxy-sessions.md#upgrade-and-rollback). JWT mode is unaffected.

---

## [0.2.2] - Unreleased

Work landed since the **v0.2.1** release (2026-05-22); not yet tagged.
Expand Down Expand Up @@ -242,7 +254,7 @@ Initial public OSS release of the Aether gateway, SDKs (Go, Python, TypeScript),

---

[Unreleased]: https://github.com/scitrera/aether/compare/v0.1.60...HEAD
[Unreleased]: https://github.com/scitrera/aether/compare/v0.2.3...HEAD
[0.1.60]: https://github.com/scitrera/aether/compare/v0.1.59...v0.1.60
[0.1.59]: https://github.com/scitrera/aether/compare/v0.1.58...v0.1.59
[0.1.58]: https://github.com/scitrera/aether/releases/tag/v0.1.58
7 changes: 7 additions & 0 deletions docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,13 @@ disabling JWT signature verification during development.

### Auth Proxy — Browser login flow

**Redis browser-session upgrade:** drain and stop all older replicas before the
new session-store version serves requests, even if session management is not
exposed. Keep the same primary, DB and prefix to preserve valid sessions during
upgrade. Rollback or recovery from stale/lost revocation state requires a fresh,
never-used `AUTH_PROXY_SESSION_REDIS_PREFIX` on every replica and new sign-ins.
See [upgrade and rollback steps](../server/docs/auth-proxy-sessions.md#upgrade-and-rollback).

The login flow is **enabled iff** `AUTH_PROXY_LOGIN_PROVIDERS` is non-empty.
Each provider name maps to a set of per-provider variables (the provider
name is upper-cased and `-` is replaced with `_` when forming the key).
Expand Down
77 changes: 77 additions & 0 deletions server/docs/auth-proxy-sessions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Browser sessions

`SessionStore` handles cookie issuance, lookup and logout. The Redis/Valkey store
also implements the optional `SessionManager` interface for per-user listing,
single revocation and bulk revocation. JWT stores intentionally do not implement
that interface. Applications must expose management only behind their own
administrative authentication, authorization, CSRF protection and audit trail.

`ListSessions(ctx, subject, limit, offset)` returns only management ID, provider,
creation and expiry. The subject is normalized email when available, otherwise
UserID. Email normalization trims/lowercases and converts IDNA domains. Management
IDs are SHA-256 digests of 256-bit random cookies, never bearer credentials.
The store does not track presence or last activity. Pagination (limit 1–200,
offset 0–1000000) is a live view ordered by expiry then management ID.

`RevokeSession` is scoped to the subject and idempotent. `RevokeAllSessions`
atomically advances the subject's generation; later logins remain possible.
Every lookup verifies that generation. Session creation and indexing use a Lua
script to share an atomic boundary with bulk revocation. Redis failure never
returns an authenticated session or a successful empty inventory.

## Upgrade and rollback

**This storage change applies to every Redis/Valkey browser-session deployment,
even when no application exposes `SessionManager`.** JWT mode is unaffected.
Older replicas cannot read the new session records or enforce their revocation
generations. Mixing versions can cause intermittent authentication failures and
allow legacy sessions to authenticate after bulk revocation.

For an upgrade that preserves valid browser sessions:

1. Drain and stop all replicas running the older session store, including
applications that embed this package. Do this before the new version serves
requests; do not use a rolling deployment that serves both versions together.
2. Deploy the new version to every replica, using the same Redis/Valkey primary,
logical DB and `AUTH_PROXY_SESSION_REDIS_PREFIX` (or library constructor
prefix) as before. Restore traffic only after all serving replicas are upgraded.
3. Verify login, logout, and any application's session-management flow. Legacy
sessions under `<prefix><cookie>` migrate atomically when used and then appear
in the index. The inventory does not include unused legacy sessions, but bulk
revocation invalidates those sessions too.

For rollback, restoring a stale backup, or recovery from acknowledged-write loss:

1. Drain and stop all serving replicas.
2. Choose a fresh, never-used session prefix and configure it on every replica
before restoring traffic. For the standalone auth-proxy, set
`AUTH_PROXY_SESSION_REDIS_PREFIX`, for example `auth-session:recovery-20260911:`.
Library users must pass the new prefix to `NewRedisOpaqueSessionStore`.
3. Require all users to sign in again. Keep the old session records out of the new
prefix, and do not later reuse the old prefix or reset generation counters
while its session records remain. Restoring old records or losing revocation
state can otherwise make revoked sessions valid again.

Previously accepted requests and downstream/IdP sessions are outside this store's
revocation boundary.

## Storage lifetime

Finite session records expire at their session lifetime. Creation and legacy
migration prune expired index entries, so retention does not depend on anyone
calling `ListSessions`. Logout and individual revocation set the index lifetime
to the latest remaining finite expiry; an empty index is removed. An index stays
nonexpiring while it contains a nonexpiring session.

A user generation counter persists after bulk revocation. Bulk-revoked records
expire naturally; explicitly nonexpiring records (zero `ExpiresAt` in library
calls) remain stored. Keep generation state for as long as records under that
prefix can remain. Use persistence, `noeviction` and sufficient capacity. All
replicas must use the same primary, DB and prefix. The implementation targets a
single Redis/Valkey endpoint, not Redis Cluster.

## Testing

Tests use miniredis by default. Set `AUTH_TEST_REDIS_ADDR` to exercise the same
suite against real Redis/Valkey; each test owns a unique key prefix. Run
`go test -race ./pkg/authproxy/login ./pkg/authproxy ./internal/auth` from `server`.
6 changes: 3 additions & 3 deletions server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
golang.org/x/crypto v0.51.0
golang.org/x/net v0.55.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/sync v0.21.0
golang.org/x/time v0.15.0
google.golang.org/grpc v1.82.1
google.golang.org/protobuf v1.36.11
Expand Down Expand Up @@ -115,9 +116,8 @@ require (
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/text v0.39.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
modernc.org/libc v1.70.0 // indirect
Expand Down
16 changes: 8 additions & 8 deletions server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -240,26 +240,26 @@ golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 h1:yOzSCGPx+cp5VO7IxvZ9SBFF7j1tZVcNtlHR2iYKtVo=
Expand Down
Loading
Loading