Skip to content

Tokens and refresh

Token parsing is local decoding and expiration bookkeeping. It does not verify signatures, issuer, audience or permissions; the service remains the authority for authentication.

JWT values and serialization

APIInput/defaultReturn/behavior
parseJwtPayload<T extends JwtPayload>(token)Three dot-separated partsT or null; base64url/UTF-8 JSON payload decoding; parse errors logged and return null; no claim-shape validation
isTokenExpired(token, earlyPeriod=0)String or CoSecJwtPayload; earlyPeriod in secondstrue for unparseable token, invalid/nonfinite exp, or now >= exp-earlyPeriod; absent/null exp is not expired
JwtToken<Payload>(token, earlyPeriod=0)Raw stringreadonly token/payload/earlyPeriod; isExpired computed against current clock
JwtCompositeToken(token, earlyPeriod=0, sessionId=random)CompositeTokenaccess and refresh JwtToken; isRefreshNeeded=access expired, isRefreshable=refresh valid, authenticated=access valid
JwtCompositeTokenSerializer(earlyPeriod=0)Expiry marginserialize→JSON of raw tokens plus sessionId; deserialize→JwtCompositeToken using configured margin
deserializeLegacy(value)Older object containing token.accessToken and token.refreshToken stringsRebuilds live class; throws TypeError for invalid legacy shape
jwtCompositeTokenSerializerSingleton, margin 0Default standalone serializer

JwtPayload declares required jti/sub/exp/iat and optional iss/aud/nbf. CoSecJwtPayload adds tenantId, policies, roles, attributes. IJwtToken<Payload> combines token/payload/isExpired with EarlyPeriodCapable. RefreshTokenStatusCapable supplies readonly isRefreshNeeded/isRefreshable. AccessToken and RefreshToken are one-string-field shapes; CompositeToken combines them.

JSON parse failures from deserialize propagate. New sign-ins receive a random sessionId; deserialization preserves a nonempty stored sessionId and derives a stable legacy identifier otherwise. The legacy identifier is session bookkeeping, not a cryptographic integrity check.

TokenStorage

TokenStorage(options={}) extends KeyStorage<JwtCompositeToken>. Options are partial KeyStorageOptions excluding serializer, plus earlyPeriod. Defaults: key=DEFAULT_COSEC_TOKEN_KEY (cosec-token), earlyPeriod=0, a broadcast bus with serial delegate named for the actual key, and inherited environment storage. Serialization is selected internally. Instances sharing an event bus must use the same earlyPeriod or construction throws.

MemberResult
signIn(compositeToken)void; sets a new JwtCompositeToken/session
setCompositeToken(compositeToken)Alias for signIn
signOut()void; removes stored token and emits inherited storage event
authenticatedtrue only when stored access token is not expired
currentUserCoSecJwtPayload or null; null while unauthenticated
get/set/remove/destroy/eventBusInherited KeyStorage API and cleanup ownership

Expiration alone does not schedule events or automatically refresh; status getters evaluate time when read. No background refresh timer is created.

Refresh manager and transport

new JwtTokenManager(tokenStorage, tokenRefresher) exposes both dependencies, currentToken (token/null), and status getters (false with no token). refresh(exchange?): Promise<void> rejects with Error('No token found') without a token. Concurrent refreshes for the same current token on this manager share a promise; this is not a cross-tab distributed lock. A same-session newer token wins over late refresh results. Sign-out or a different session prevents stale writeback and raises RefreshSessionChangedError(cause?).

An unsuccessful refresh of the still-current session removes that session's token and raises RefreshTokenError(token, cause?). The error exposes the old JwtCompositeToken; avoid logging its raw credentials. A failure of the retried business request propagates unchanged and does not remove successfully refreshed credentials. The pending promise is cleared in finally. Unauthorized notification ownership is coordinated with exchange error handlers, not a general event queue.

TokenRefresher.refresh(token): Promise<CompositeToken> is the custom transport contract. CoSecTokenRefresher({fetcher, endpoint}) requires both fields and POSTs the token object with JSON result extraction. Its concrete refresh method additionally accepts shouldNotifyUnauthorized?: () => boolean. It sets IGNORE_REFRESH_TOKEN_ATTRIBUTE_KEY to prevent recursive refresh; custom transports using a configured Fetcher must supply that attribute themselves.

Complete in-memory example

This example decodes deliberately synthetic tokens locally and never contacts an authentication service. It demonstrates storage status; it does not mint valid credentials.

ts
import { TokenStorage } from '@ahoo-wang/fetcher-cosec';
import { InMemoryStorage } from '@ahoo-wang/fetcher-storage';
import { SerialTypedEventBus } from '@ahoo-wang/fetcher-eventbus';

const tokens = new TokenStorage({
  storage: new InMemoryStorage(),
  eventBus: new SerialTypedEventBus('example-token'),
});
const payload = {
  jti: 'demo',
  sub: 'user-1',
  iat: 0,
  exp: Math.floor(Date.now() / 1000) + 3600,
};
const jwt = `e30.${btoa(JSON.stringify(payload)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')}.demo`;
try {
  tokens.signIn({ accessToken: jwt, refreshToken: jwt });
  console.log(tokens.authenticated, tokens.currentUser?.sub);
  tokens.signOut();
} finally {
  tokens.destroy();
}

JwtPayloadpackages/cosec/src/jwts.ts:17

CoSecJwtPayloadpackages/cosec/src/jwts.ts:61

parseJwtPayloadpackages/cosec/src/jwts.ts:91

EarlyPeriodCapablepackages/cosec/src/jwts.ts:122

isTokenExpiredpackages/cosec/src/jwts.ts:145

IJwtTokenpackages/cosec/src/jwtToken.ts:42

JwtTokenpackages/cosec/src/jwtToken.ts:77

RefreshTokenStatusCapablepackages/cosec/src/jwtToken.ts:125

JwtCompositeTokenpackages/cosec/src/jwtToken.ts:164

JwtCompositeTokenSerializerpackages/cosec/src/jwtToken.ts:253

jwtCompositeTokenSerializerpackages/cosec/src/jwtToken.ts:325

RefreshTokenErrorpackages/cosec/src/jwtTokenManager.ts:25

RefreshSessionChangedErrorpackages/cosec/src/jwtTokenManager.ts:37

JwtTokenManagerpackages/cosec/src/jwtTokenManager.ts:48

DEFAULT_COSEC_TOKEN_KEYpackages/cosec/src/tokenStorage.ts:27

TokenStorageOptionspackages/cosec/src/tokenStorage.ts:48

TokenStoragepackages/cosec/src/tokenStorage.ts:58

AccessTokenpackages/cosec/src/tokenRefresher.ts:28

RefreshTokenpackages/cosec/src/tokenRefresher.ts:41

CompositeTokenpackages/cosec/src/tokenRefresher.ts:58

TokenRefresherpackages/cosec/src/tokenRefresher.ts:74

CoSecTokenRefresherOptionspackages/cosec/src/tokenRefresher.ts:111

CoSecTokenRefresherpackages/cosec/src/tokenRefresher.ts:142

Released under the Apache License 2.0.