You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
56 lines
2.0 KiB
56 lines
2.0 KiB
import { AUTH_DEFAULTS, AUTH_EVENT_NAMES, AUTH_REVOKE_REASONS } from '$libs/auth/consts';
|
|
import type { AuthClockPort, AuthCryptoPort, AuthStoreAdapter } from '$libs/auth/contracts';
|
|
import type { AuthRefreshFamilyId, AuthRefreshTokenId } from '$libs/auth/types';
|
|
import { AuthTokenReuseDetectedError } from '$libs/auth/errors';
|
|
import { createAuthRefreshTokenId } from '$libs/auth/ids';
|
|
import { createAuthRandomToken, hashAuthToken } from '$libs/auth/tokens';
|
|
|
|
export interface AuthRefreshRotationInput {
|
|
readonly store: AuthStoreAdapter;
|
|
readonly crypto: AuthCryptoPort;
|
|
readonly clock: AuthClockPort;
|
|
readonly token: string;
|
|
}
|
|
|
|
export interface AuthRefreshRotationResult {
|
|
readonly refreshToken: string;
|
|
readonly refreshTokenId: AuthRefreshTokenId;
|
|
readonly familyId: AuthRefreshFamilyId;
|
|
}
|
|
|
|
export async function rotateAuthRefreshToken(
|
|
input: AuthRefreshRotationInput
|
|
): Promise<AuthRefreshRotationResult> {
|
|
const nowMs = input.clock.nowMs();
|
|
const tokenHash = await hashAuthToken(input.crypto, input.token);
|
|
const current = await input.store.findRefreshTokenForUpdate({ tokenHash });
|
|
if (!current || current.revokedAt)
|
|
throw new AuthTokenReuseDetectedError(undefined, {
|
|
eventName: AUTH_EVENT_NAMES.REFRESH_REUSE_DETECTED
|
|
});
|
|
if (current.consumedAt && nowMs - current.consumedAt > AUTH_DEFAULTS.REFRESH_REUSE_GRACE_MS) {
|
|
await input.store.revokeRefreshFamily({
|
|
familyId: current.familyId,
|
|
nowMs,
|
|
reason: AUTH_REVOKE_REASONS.REFRESH_REUSE
|
|
});
|
|
throw new AuthTokenReuseDetectedError(undefined, {
|
|
eventName: AUTH_EVENT_NAMES.REFRESH_REUSE_DETECTED
|
|
});
|
|
}
|
|
const refreshToken = createAuthRandomToken(input.crypto);
|
|
const child = {
|
|
id: createAuthRefreshTokenId(input.crypto),
|
|
familyId: current.familyId,
|
|
tokenHash: await hashAuthToken(input.crypto, refreshToken),
|
|
parentTokenId: current.id,
|
|
issuedAt: nowMs
|
|
};
|
|
const result = await input.store.rotateRefreshToken({
|
|
currentTokenId: current.id,
|
|
childToken: child,
|
|
consumedAt: nowMs
|
|
});
|
|
return { refreshToken, refreshTokenId: result.child.id, familyId: result.child.familyId };
|
|
}
|