Skip to content

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) / InMemoryRevocationCache

Construct directly with new AuthClient({ ... }) (full control) or via AuthClient.build(config, transport, ...) for the common case.

Token validation flow

  1. validateBearer("Bearer eyJ...") strips the prefix.
  2. JwtValidator.validate(token): a. Decode header → alg. Refuse anything except HS256. b. Verify signature with the active JWT_ACCESS_SECRET. On signature mismatch only, retry with JWT_ACCESS_SECRET_PREVIOUS if set (zero-downtime rotation). c. Verify iss (ven-auth), aud (ven-platform), exp, nbf, iat against Clock. d. Read tv and check against the per-user token-version in RevocationCache (Redis-backed in prod, no-op in tests). e. Read jti and check the blacklist (legacy path; rarely populated).
  3. Map the claims to a normalized AuthenticatedUser or ServicePrincipal and return.

Error map

Every HTTP error from the auth-server (including non-JSON network errors) is mapped via ErrorMapper to one of:

ClassMaps from
TokenExpiredException401 with code token_expired
TokenRevokedException401 with code token_revoked
TokenInvalidException401 with code token_invalid
InvalidCredentialsException401 with code invalid_credentials
TwoFactorRequiredException401 with {requires_2fa: true}
UnauthorizedException401 (other)
ForbiddenException403
NotFoundException404
ConflictException409
ValidationException400 / 422
RateLimitedException429 (with retryAfter parsed from header)
ServerException5xx
NetworkExceptionfetch / DNS / connection failure
OfflineModeExceptionbootstrap 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:

  1. Try the request with the current access token.
  2. On 401 with code token_expired, call auth.refresh(refreshToken) under a single-flight mutex.
  3. Retry the request once with the new access token.
  4. 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

PortImplementWhen
Transportclass MyTransport implements HttpTransportPortCustom retry; injecting tracing headers; mocking HTTP for tests
RevocationCacheclass RedisRevocationCache implements RevocationCacheProduction deployments — share token-version across replicas via Redis
SessionStoreclass RedisSessionStore implements SessionStoreMulti-replica session sharing
Clocknew FixedClock(date)Deterministic tests
LoggerPortclass PinoLogger implements LoggerPortProduction logging stack