How auth-server-ts works
auth-server-ts is deliberately port-based — every external
dependency is an injectable interface with a small bundled
implementation. This lets you swap the transport, the cache, the session
store, the clock, or the logger without touching call sites.
The composition
AuthClient ├── Config (authServerUrl, appCode, jwtIssuer, jwtAudience, …) ├── Transport — HttpTransport (default) / InMemoryTransport ├── TokenValidator — JwtValidator (HS256 + rotation + jti gate + tv gate) ├── SessionStore — InMemorySessionStore (default) ├── Clock — SystemClock / FixedClock ├── Logger — NoOpLogger (default) └── RevocationCache — NullRevocationCache (default) / InMemoryRevocationCacheConstruct directly with new AuthClient({ ... }) (full control) or via
AuthClient.build(config, transport, ...) for the common case.
Token validation flow
validateBearer("Bearer eyJ...")strips the prefix.JwtValidator.validate(token): a. Decode header →alg. Refuse anything except HS256. b. Verify signature with the activeJWT_ACCESS_SECRET. On signature mismatch only, retry withJWT_ACCESS_SECRET_PREVIOUSif set (zero-downtime rotation). c. Verifyiss(ven-auth),aud(ven-platform),exp,nbf,iatagainstClock. d. Readtvand check against the per-user token-version inRevocationCache(Redis-backed in prod, no-op in tests). e. Readjtiand check the blacklist (legacy path; rarely populated).- Map the claims to a normalized
AuthenticatedUserorServicePrincipaland return.
Error map
Every HTTP error from the auth-server (including non-JSON network
errors) is mapped via ErrorMapper to one of:
| Class | Maps from |
|---|---|
TokenExpiredException | 401 with code token_expired |
TokenRevokedException | 401 with code token_revoked |
TokenInvalidException | 401 with code token_invalid |
InvalidCredentialsException | 401 with code invalid_credentials |
TwoFactorRequiredException | 401 with {requires_2fa: true} |
UnauthorizedException | 401 (other) |
ForbiddenException | 403 |
NotFoundException | 404 |
ConflictException | 409 |
ValidationException | 400 / 422 |
RateLimitedException | 429 (with retryAfter parsed from header) |
ServerException | 5xx |
NetworkException | fetch / DNS / connection failure |
OfflineModeException | bootstrap is offline |
VenAuthException is the root; every flow method declares throws VenAuthException and consumers can instanceof to narrow.
Refresh handling
auth.authenticatedRequest({ ... }) (when implemented in your service)
should:
- Try the request with the current access token.
- On 401 with code
token_expired, callauth.refresh(refreshToken)under a single-flight mutex. - Retry the request once with the new access token.
- On any second 401, throw
UnauthenticatedException.
The mutex is critical. The auth-server’s family-aware reuse detector revokes the entire refresh-token family if it sees a token presented twice — a naïve “refresh per request” loop would log the user out the first time two requests overlap.
Permission registration
Flows.registerPermissions(manifest) (or PermissionRegistrar) lets
your service POST its full permission catalog at boot to
/admin/permissions/register. The auth-server reconciles — upserts
declared, prunes withdrawn. Idempotent. Required for any service that
owns custom permissions beyond core.
Where to plug in custom adapters
| Port | Implement | When |
|---|---|---|
Transport | class MyTransport implements HttpTransportPort | Custom retry; injecting tracing headers; mocking HTTP for tests |
RevocationCache | class RedisRevocationCache implements RevocationCache | Production deployments — share token-version across replicas via Redis |
SessionStore | class RedisSessionStore implements SessionStore | Multi-replica session sharing |
Clock | new FixedClock(date) | Deterministic tests |
LoggerPort | class PinoLogger implements LoggerPort | Production logging stack |