najm-auth 4.2.1 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.js +3 -1
- package/dist/client/server/index.js +3 -1
- package/dist/index.d.ts +56 -18
- package/dist/index.js +99 -34
- package/package.json +1 -1
package/dist/client/index.js
CHANGED
|
@@ -552,7 +552,9 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
552
552
|
if (generation !== this.authGeneration) return;
|
|
553
553
|
const shouldOpenCircuit = this.registerRefreshFailure(err);
|
|
554
554
|
if (shouldOpenCircuit) {
|
|
555
|
-
|
|
555
|
+
if (!(err instanceof AuthError && err.status === 401)) {
|
|
556
|
+
await this.requestServerLogout().catch(() => void 0);
|
|
557
|
+
}
|
|
556
558
|
this.resetState();
|
|
557
559
|
this.emit("sessionExpired", null);
|
|
558
560
|
if (err instanceof AuthError && err.status === 401) {
|
|
@@ -1281,7 +1281,9 @@ var NajmAuthClient = class _NajmAuthClient {
|
|
|
1281
1281
|
if (generation !== this.authGeneration) return;
|
|
1282
1282
|
const shouldOpenCircuit = this.registerRefreshFailure(err);
|
|
1283
1283
|
if (shouldOpenCircuit) {
|
|
1284
|
-
|
|
1284
|
+
if (!(err instanceof AuthError && err.status === 401)) {
|
|
1285
|
+
await this.requestServerLogout().catch(() => void 0);
|
|
1286
|
+
}
|
|
1285
1287
|
this.resetState();
|
|
1286
1288
|
this.emit("sessionExpired", null);
|
|
1287
1289
|
if (err instanceof AuthError && err.status === 401) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1281,6 +1281,22 @@ interface SetPasswordToken {
|
|
|
1281
1281
|
userId: string;
|
|
1282
1282
|
jti: string;
|
|
1283
1283
|
}
|
|
1284
|
+
/**
|
|
1285
|
+
* The outcome of a refresh. `refreshToken` is absent only when the request
|
|
1286
|
+
* presented a token that a release before 4.2.3 had just replaced: that
|
|
1287
|
+
* rotation's token cannot be rebuilt, and its own response set the cookie.
|
|
1288
|
+
*/
|
|
1289
|
+
interface RefreshedSession {
|
|
1290
|
+
userId: string;
|
|
1291
|
+
tokenFamily: string;
|
|
1292
|
+
roles: string[];
|
|
1293
|
+
permissions: string[];
|
|
1294
|
+
sessionVersion: number;
|
|
1295
|
+
accessToken: string;
|
|
1296
|
+
accessTokenExpiresAt: number;
|
|
1297
|
+
refreshToken?: string;
|
|
1298
|
+
refreshTokenExpiresAt?: number;
|
|
1299
|
+
}
|
|
1284
1300
|
declare class TokenService {
|
|
1285
1301
|
private tokenRepository;
|
|
1286
1302
|
private cookieManager;
|
|
@@ -1394,6 +1410,13 @@ declare class TokenService {
|
|
|
1394
1410
|
* family so rotation/revocation can target a single session.
|
|
1395
1411
|
*/
|
|
1396
1412
|
private signRefreshToken;
|
|
1413
|
+
/**
|
|
1414
|
+
* The refresh token that replaces `replaced`. Its jti is derived from the
|
|
1415
|
+
* replaced token and its lifetime is the one the family row records, so a
|
|
1416
|
+
* request that presents the replaced token inside the grace window can
|
|
1417
|
+
* rebuild the token its rotation issued and hand the browser that one.
|
|
1418
|
+
*/
|
|
1419
|
+
private successorRefreshToken;
|
|
1397
1420
|
generateRefreshToken(data: {
|
|
1398
1421
|
userId: string;
|
|
1399
1422
|
tokenFamily?: string;
|
|
@@ -1431,24 +1454,35 @@ declare class TokenService {
|
|
|
1431
1454
|
storeRefreshToken(userId: string, refreshToken: string, tokenFamily: string): Promise<void>;
|
|
1432
1455
|
/**
|
|
1433
1456
|
* Rotate only the family row observed by refreshTokens(). This conditional
|
|
1434
|
-
* update
|
|
1457
|
+
* update returns null if logout deleted the family or another refresh won.
|
|
1458
|
+
* The row records the new token's own expiry, which serveReplacedToken
|
|
1459
|
+
* reads back to rebuild it.
|
|
1435
1460
|
*/
|
|
1436
1461
|
private rotateTokens;
|
|
1437
1462
|
/**
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1463
|
+
* Rotate the refresh token the cookie presents.
|
|
1464
|
+
*
|
|
1465
|
+
* Browser tabs share one cookie jar, so several requests can present the
|
|
1466
|
+
* same token at once: tabs whose access tokens were issued together, or a
|
|
1467
|
+
* tab and a server render. The first rotates it and sets the new cookie.
|
|
1468
|
+
* The others present the token it replaced within the grace window and get
|
|
1469
|
+
* that same new token back without a second rotation, so however many there
|
|
1470
|
+
* are, and in whatever order their responses land, the browser ends up with
|
|
1471
|
+
* the one current token and none is mistaken for a stolen one. The same path
|
|
1472
|
+
* recovers a rotation whose response never reached the browser. Reuse after
|
|
1473
|
+
* the window, or of a token the family never issued, revokes the family.
|
|
1474
|
+
*/
|
|
1475
|
+
refreshTokens(): Promise<RefreshedSession>;
|
|
1476
|
+
/** Whether `presentedHash` is the token this family replaced less than the grace window ago. */
|
|
1477
|
+
private withinGraceWindow;
|
|
1478
|
+
/**
|
|
1479
|
+
* Serve a request that presented the token another request just replaced.
|
|
1480
|
+
* It rotates nothing: it returns a new access token and the refresh token
|
|
1481
|
+
* that rotation issued, rebuilt from the replaced one. A current token that
|
|
1482
|
+
* is not that successor (a rotation by an earlier release) is left alone and
|
|
1483
|
+
* only the access token is returned.
|
|
1484
|
+
*/
|
|
1485
|
+
private serveReplacedToken;
|
|
1452
1486
|
private requireActiveRefreshUser;
|
|
1453
1487
|
/** Revoke every refresh session for a user (password change/reset, logout-all). */
|
|
1454
1488
|
revokeAllForUser(userId: string): Promise<any>;
|
|
@@ -1917,7 +1951,9 @@ declare class AuthService {
|
|
|
1917
1951
|
establishSession(user: SanitizedUser): Promise<TokenPair & {
|
|
1918
1952
|
user: SanitizedUser;
|
|
1919
1953
|
}>;
|
|
1920
|
-
refreshTokens(): Promise<TokenPair
|
|
1954
|
+
refreshTokens(): Promise<Omit<TokenPair, 'refreshToken'> & {
|
|
1955
|
+
refreshToken?: string;
|
|
1956
|
+
}>;
|
|
1921
1957
|
/**
|
|
1922
1958
|
* Reissue the short-lived signed session snapshot from a fully validated
|
|
1923
1959
|
* refresh session. This path never creates or returns access/refresh tokens
|
|
@@ -2059,7 +2095,9 @@ declare class AuthController {
|
|
|
2059
2095
|
} & {
|
|
2060
2096
|
emailSent: boolean;
|
|
2061
2097
|
}>;
|
|
2062
|
-
refreshTokens(): Promise<TokenPair
|
|
2098
|
+
refreshTokens(): Promise<Omit<TokenPair, "refreshToken"> & {
|
|
2099
|
+
refreshToken?: string;
|
|
2100
|
+
}>;
|
|
2063
2101
|
recoverSession(recoveryRequest: string | undefined, ctx: Context): Promise<{
|
|
2064
2102
|
recovered: true;
|
|
2065
2103
|
}>;
|
|
@@ -3207,4 +3245,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
|
|
|
3207
3245
|
*/
|
|
3208
3246
|
declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
|
|
3209
3247
|
|
|
3210
|
-
export { AUTH_CONFIG, AUTH_CORE_MODULE, en as AUTH_EN, AUTH_LOCALES, AUTH_LOGIN_RATE_LIMIT_ENV, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AdministrativeDelivery, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthLoginRateLimitConfig, type AuthPluginConfig, AuthQueries, type AuthRateLimitEnvironment, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, AuthSessionService, type AuthUser, CREDENTIAL_SETUP_CODES, CREDENTIAL_SETUP_MODULE, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, type ConsumedSetPasswordToken, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type CredentialSetupChangeDto, type CredentialSetupCode, type CredentialSetupConfig, CredentialSetupController, type CredentialSetupOptions, type CredentialSetupPasswordOptions, type CredentialSetupPending, CredentialSetupRepository, CredentialSetupRequirementRepository, type CredentialSetupRequirementRow, CredentialSetupRequirementService, CredentialSetupService, type CredentialSetupSessionInfo, type CredentialSetupStarted, DEFAULT_AUTH_LOGIN_RATE_LIMIT, DEFAULT_CREDENTIAL_SETUP_COOKIE_NAME, DEFAULT_CREDENTIAL_SETUP_TTL_MS, type DefineRolesOptions, type EmailParam, EncryptionService, type GitHubOAuthConfig, type GoogleOAuthConfig, IdentityConfig, type IdentityResolver, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, type LoginResult, NewPermission, NewRoleEntity, NewUser, type OAuthConfig, type OAuthProvider, Owned, type OwnedMethods, type OwnedWhere, type OwnershipConditionMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipReadContext, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, PASSWORD_SETUP_PURPOSE, PUBLIC_REGISTRATION_MODULE, PasswordSetupService, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, type ProvisionUserWithPasswordInput, type ProvisionUserWithSetupInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RegisterDto, RegistrationController, type ResetPasswordDto, type ResolvedCredentialSetupConfig, ResolvedIdentityConfig, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type RunAsUser, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, SessionInvalidationService, type SetPasswordToken, type SetPasswordTokenType, TOKEN_STATUS, TOKEN_TYPE, TemporaryCredentialInput, type TemporaryCredentialReset, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, type UserListQuery, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authEmailRateLimitKey, authIdentityRateLimitKey, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createIdentityResolver, createPermissionDto, createRoleDto, createTokenDto, createUserDto, credentialSetupChangeDto, credentialSetupError, defaultCredentialSetupPasswordSchema, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmailIdentifier, isEmpty, isFile, isPath, join, languageParam, loginDto, normalizeAuthIdentifier, normalizeSetupPurpose, own, ownershipCondition, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, resolveAuthLoginRateLimitConfig, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
|
|
3248
|
+
export { AUTH_CONFIG, AUTH_CORE_MODULE, en as AUTH_EN, AUTH_LOCALES, AUTH_LOGIN_RATE_LIMIT_ENV, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AdministrativeDelivery, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthLoginRateLimitConfig, type AuthPluginConfig, AuthQueries, type AuthRateLimitEnvironment, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, AuthSessionService, type AuthUser, CREDENTIAL_SETUP_CODES, CREDENTIAL_SETUP_MODULE, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, type ConsumedSetPasswordToken, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type CredentialSetupChangeDto, type CredentialSetupCode, type CredentialSetupConfig, CredentialSetupController, type CredentialSetupOptions, type CredentialSetupPasswordOptions, type CredentialSetupPending, CredentialSetupRepository, CredentialSetupRequirementRepository, type CredentialSetupRequirementRow, CredentialSetupRequirementService, CredentialSetupService, type CredentialSetupSessionInfo, type CredentialSetupStarted, DEFAULT_AUTH_LOGIN_RATE_LIMIT, DEFAULT_CREDENTIAL_SETUP_COOKIE_NAME, DEFAULT_CREDENTIAL_SETUP_TTL_MS, type DefineRolesOptions, type EmailParam, EncryptionService, type GitHubOAuthConfig, type GoogleOAuthConfig, IdentityConfig, type IdentityResolver, type InviteUserDto, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, type LoginResult, NewPermission, NewRoleEntity, NewUser, type OAuthConfig, type OAuthProvider, Owned, type OwnedMethods, type OwnedWhere, type OwnershipConditionMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipReadContext, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, PASSWORD_SETUP_PURPOSE, PUBLIC_REGISTRATION_MODULE, PasswordSetupService, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, type ProvisionUserInput, type ProvisionUserWithPasswordInput, type ProvisionUserWithSetupInput, ROLES, ROLE_GROUPS, type RefreshTokenDto, type RefreshedSession, type RegisterDto, RegistrationController, type ResetPasswordDto, type ResolvedCredentialSetupConfig, ResolvedIdentityConfig, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type RunAsUser, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, SessionInvalidationService, type SetPasswordToken, type SetPasswordTokenType, TOKEN_STATUS, TOKEN_TYPE, TemporaryCredentialInput, type TemporaryCredentialReset, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, type UserListQuery, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authEmailRateLimitKey, authIdentityRateLimitKey, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createIdentityResolver, createPermissionDto, createRoleDto, createTokenDto, createUserDto, credentialSetupChangeDto, credentialSetupError, defaultCredentialSetupPasswordSchema, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, inviteUserDto, isAdmin, isAdministrator, isAuth, isEmailIdentifier, isEmpty, isFile, isPath, join, languageParam, loginDto, normalizeAuthIdentifier, normalizeSetupPurpose, own, ownershipCondition, parseSchema, permissionIdParam, pickProps, refreshTokenDto, registerDto, resetPasswordDto, resolveAuthLoginRateLimitConfig, revokeTokenDto, roleIdParam, runAsUser, seedAuthData, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, userListQuery, verifyTokenDto, where };
|
package/dist/index.js
CHANGED
|
@@ -2040,7 +2040,7 @@ UserService = __decorate10([
|
|
|
2040
2040
|
import { Injectable as Injectable7, Inject as Inject9 } from "najm-core";
|
|
2041
2041
|
import { I18n as I18n5 } from "najm-i18n";
|
|
2042
2042
|
import { CacheService as CacheService2 } from "najm-cache";
|
|
2043
|
-
import { createHash } from "crypto";
|
|
2043
|
+
import { createHash, createHmac } from "crypto";
|
|
2044
2044
|
import jwt from "jsonwebtoken";
|
|
2045
2045
|
import { nanoid as nanoid4 } from "nanoid";
|
|
2046
2046
|
import timestring3 from "timestring";
|
|
@@ -2118,6 +2118,24 @@ CredentialSetupRequirementRepository = __decorate11([
|
|
|
2118
2118
|
// src/credentialSetup/types.ts
|
|
2119
2119
|
var PASSWORD_SETUP_PURPOSE = "password";
|
|
2120
2120
|
|
|
2121
|
+
// src/shared/storedTime.ts
|
|
2122
|
+
function storedTimeMs(value) {
|
|
2123
|
+
if (value instanceof Date)
|
|
2124
|
+
return value.getTime();
|
|
2125
|
+
const text3 = value.trim().replace(" ", "T");
|
|
2126
|
+
const time = text3.split("T")[1];
|
|
2127
|
+
if (!time)
|
|
2128
|
+
return Date.parse(text3);
|
|
2129
|
+
if (/Z$/i.test(time))
|
|
2130
|
+
return Date.parse(text3);
|
|
2131
|
+
const offset = /([+-])(\d{2}):?(\d{2})?$/.exec(time);
|
|
2132
|
+
if (!offset)
|
|
2133
|
+
return Date.parse(`${text3}Z`);
|
|
2134
|
+
const [zone, sign, hours, minutes = "00"] = offset;
|
|
2135
|
+
return Date.parse(`${text3.slice(0, -zone.length)}${sign}${hours}:${minutes}`);
|
|
2136
|
+
}
|
|
2137
|
+
__name(storedTimeMs, "storedTimeMs");
|
|
2138
|
+
|
|
2121
2139
|
// src/tokens/TokenService.ts
|
|
2122
2140
|
import { Err as Err7 } from "najm-core";
|
|
2123
2141
|
var __decorate12 = function(decorators, target, key, desc) {
|
|
@@ -2294,11 +2312,7 @@ var TokenService = class TokenService2 {
|
|
|
2294
2312
|
this.rejectRefreshSession();
|
|
2295
2313
|
}
|
|
2296
2314
|
const presentedHash = this.hashToken(refreshToken);
|
|
2297
|
-
if (presentedHash === stored.token) {
|
|
2298
|
-
return { userId, tokenFamily };
|
|
2299
|
-
}
|
|
2300
|
-
const canRecover = stored.previousHash && presentedHash === stored.previousHash && stored.previousValidUntil && new Date(stored.previousValidUntil).getTime() > Date.now() && !stored.previousUsedAt;
|
|
2301
|
-
if (canRecover) {
|
|
2315
|
+
if (presentedHash === stored.token || this.withinGraceWindow(stored, presentedHash)) {
|
|
2302
2316
|
return { userId, tokenFamily };
|
|
2303
2317
|
}
|
|
2304
2318
|
this.rejectRefreshSession();
|
|
@@ -2425,10 +2439,22 @@ var TokenService = class TokenService2 {
|
|
|
2425
2439
|
const token = jwt.sign({ ...data, jti, type: "refresh", exp: expiresAt }, this.config.jwt.refreshSecret);
|
|
2426
2440
|
return { token, expiresAt };
|
|
2427
2441
|
}
|
|
2442
|
+
/**
|
|
2443
|
+
* The refresh token that replaces `replaced`. Its jti is derived from the
|
|
2444
|
+
* replaced token and its lifetime is the one the family row records, so a
|
|
2445
|
+
* request that presents the replaced token inside the grace window can
|
|
2446
|
+
* rebuild the token its rotation issued and hand the browser that one.
|
|
2447
|
+
*/
|
|
2448
|
+
successorRefreshToken(replaced, data, expiresAt) {
|
|
2449
|
+
const jti = createHmac("sha256", this.config.jwt.refreshSecret).update(`successor:${replaced}`).digest("base64url").slice(0, 16);
|
|
2450
|
+
const iat = expiresAt - Math.floor(timestring3(this.config.jwt.refreshExpiresIn, "s"));
|
|
2451
|
+
const token = jwt.sign({ ...data, jti, type: "refresh", iat, exp: expiresAt }, this.config.jwt.refreshSecret);
|
|
2452
|
+
return { token, expiresAt };
|
|
2453
|
+
}
|
|
2428
2454
|
generateRefreshToken(data) {
|
|
2429
2455
|
return this.signRefreshToken({ userId: data.userId, tokenFamily: data.tokenFamily ?? nanoid4(16) }).token;
|
|
2430
2456
|
}
|
|
2431
|
-
async createTokenPair(userId, family) {
|
|
2457
|
+
async createTokenPair(userId, family, replaced) {
|
|
2432
2458
|
const { roleName, permissions } = await this.tokenRepository.getRoleAndPermissions(userId);
|
|
2433
2459
|
const accessTokenData = {
|
|
2434
2460
|
userId,
|
|
@@ -2437,7 +2463,7 @@ var TokenService = class TokenService2 {
|
|
|
2437
2463
|
tokenFamily: family
|
|
2438
2464
|
};
|
|
2439
2465
|
const access = await this.signAccessToken(accessTokenData);
|
|
2440
|
-
const refresh = this.signRefreshToken({ userId, tokenFamily: family });
|
|
2466
|
+
const refresh = replaced ? this.successorRefreshToken(replaced, { userId, tokenFamily: family }, this.expiresAt(this.config.jwt.refreshExpiresIn)) : this.signRefreshToken({ userId, tokenFamily: family });
|
|
2441
2467
|
return {
|
|
2442
2468
|
userId,
|
|
2443
2469
|
tokenFamily: family,
|
|
@@ -2519,35 +2545,44 @@ var TokenService = class TokenService2 {
|
|
|
2519
2545
|
}
|
|
2520
2546
|
/**
|
|
2521
2547
|
* Rotate only the family row observed by refreshTokens(). This conditional
|
|
2522
|
-
* update
|
|
2548
|
+
* update returns null if logout deleted the family or another refresh won.
|
|
2549
|
+
* The row records the new token's own expiry, which serveReplacedToken
|
|
2550
|
+
* reads back to rebuild it.
|
|
2523
2551
|
*/
|
|
2524
|
-
async rotateTokens(userId, tokenFamily, expectedCurrentHash) {
|
|
2525
|
-
const generated = await this.createTokenPair(userId, tokenFamily);
|
|
2526
|
-
const expireInSecond = timestring3(this.config.jwt.refreshExpiresIn, "s");
|
|
2552
|
+
async rotateTokens(userId, tokenFamily, replaced, expectedCurrentHash) {
|
|
2553
|
+
const generated = await this.createTokenPair(userId, tokenFamily, replaced);
|
|
2527
2554
|
const rotated = await this.tokenRepository.rotateRefreshToken({
|
|
2528
2555
|
userId,
|
|
2529
2556
|
token: this.hashToken(generated.refreshToken),
|
|
2530
2557
|
tokenFamily,
|
|
2531
|
-
expiresAt: new Date(
|
|
2558
|
+
expiresAt: new Date(generated.refreshTokenExpiresAt * 1e3).toISOString(),
|
|
2532
2559
|
previousHash: expectedCurrentHash,
|
|
2533
2560
|
previousValidUntil: new Date(Date.now() + TokenService_1.PREVIOUS_GRACE_SECONDS * 1e3).toISOString(),
|
|
2534
2561
|
previousUsedAt: null
|
|
2535
2562
|
}, expectedCurrentHash);
|
|
2536
|
-
if (!rotated?.length)
|
|
2537
|
-
|
|
2538
|
-
}
|
|
2563
|
+
if (!rotated?.length)
|
|
2564
|
+
return null;
|
|
2539
2565
|
if (!await this.invalidation.markFamilyIssued(tokenFamily, userId)) {
|
|
2540
2566
|
this.rejectRefreshSession();
|
|
2541
2567
|
}
|
|
2542
2568
|
return generated;
|
|
2543
2569
|
}
|
|
2544
2570
|
/**
|
|
2545
|
-
*
|
|
2546
|
-
*
|
|
2571
|
+
* Rotate the refresh token the cookie presents.
|
|
2572
|
+
*
|
|
2573
|
+
* Browser tabs share one cookie jar, so several requests can present the
|
|
2574
|
+
* same token at once: tabs whose access tokens were issued together, or a
|
|
2575
|
+
* tab and a server render. The first rotates it and sets the new cookie.
|
|
2576
|
+
* The others present the token it replaced within the grace window and get
|
|
2577
|
+
* that same new token back without a second rotation, so however many there
|
|
2578
|
+
* are, and in whatever order their responses land, the browser ends up with
|
|
2579
|
+
* the one current token and none is mistaken for a stolen one. The same path
|
|
2580
|
+
* recovers a rotation whose response never reached the browser. Reuse after
|
|
2581
|
+
* the window, or of a token the family never issued, revokes the family.
|
|
2547
2582
|
*/
|
|
2548
2583
|
async refreshTokens() {
|
|
2549
2584
|
const { refreshToken, userId, tokenFamily } = this.readRefreshSessionCookie();
|
|
2550
|
-
|
|
2585
|
+
let stored = await this.tokenRepository.getByFamily(tokenFamily);
|
|
2551
2586
|
await this.assertRefreshFamilyAllowed(tokenFamily, userId);
|
|
2552
2587
|
if (!stored || stored.userId !== userId) {
|
|
2553
2588
|
this.rejectRefreshSession();
|
|
@@ -2555,19 +2590,52 @@ var TokenService = class TokenService2 {
|
|
|
2555
2590
|
const presentedHash = this.hashToken(refreshToken);
|
|
2556
2591
|
await this.requireActiveRefreshUser(userId, tokenFamily);
|
|
2557
2592
|
if (presentedHash === stored.token) {
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
Err7(this.t("errors.refreshTokenInvalid"), 401);
|
|
2593
|
+
const rotated = await this.rotateTokens(userId, tokenFamily, refreshToken, stored.token);
|
|
2594
|
+
if (rotated)
|
|
2595
|
+
return rotated;
|
|
2596
|
+
stored = await this.tokenRepository.getByFamily(tokenFamily);
|
|
2597
|
+
if (stored?.userId === userId && this.withinGraceWindow(stored, presentedHash)) {
|
|
2598
|
+
return this.serveReplacedToken(userId, tokenFamily, refreshToken, stored);
|
|
2565
2599
|
}
|
|
2566
|
-
return this.
|
|
2600
|
+
return Err7(this.t("errors.refreshTokenInvalid"), 401);
|
|
2601
|
+
}
|
|
2602
|
+
if (this.withinGraceWindow(stored, presentedHash)) {
|
|
2603
|
+
return this.serveReplacedToken(userId, tokenFamily, refreshToken, stored);
|
|
2567
2604
|
}
|
|
2568
2605
|
await this.revokeSuspectRefreshFamily(userId, tokenFamily);
|
|
2569
2606
|
this.rejectRefreshSession();
|
|
2570
2607
|
}
|
|
2608
|
+
/** Whether `presentedHash` is the token this family replaced less than the grace window ago. */
|
|
2609
|
+
withinGraceWindow(stored, presentedHash) {
|
|
2610
|
+
return Boolean(stored.previousHash && presentedHash === stored.previousHash && stored.previousValidUntil && storedTimeMs(stored.previousValidUntil) > Date.now());
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Serve a request that presented the token another request just replaced.
|
|
2614
|
+
* It rotates nothing: it returns a new access token and the refresh token
|
|
2615
|
+
* that rotation issued, rebuilt from the replaced one. A current token that
|
|
2616
|
+
* is not that successor (a rotation by an earlier release) is left alone and
|
|
2617
|
+
* only the access token is returned.
|
|
2618
|
+
*/
|
|
2619
|
+
async serveReplacedToken(userId, tokenFamily, replaced, stored) {
|
|
2620
|
+
const successor = this.successorRefreshToken(replaced, { userId, tokenFamily }, Math.floor(storedTimeMs(stored.expiresAt) / 1e3));
|
|
2621
|
+
const refresh = this.hashToken(successor.token) === stored.token ? successor : void 0;
|
|
2622
|
+
const { roleName, permissions } = await this.tokenRepository.getRoleAndPermissions(userId);
|
|
2623
|
+
const roles = roleName ? [roleName] : [];
|
|
2624
|
+
const access = await this.signAccessToken({ userId, roles, permissions: permissions ?? [], tokenFamily });
|
|
2625
|
+
if (!await this.invalidation.markFamilyIssued(tokenFamily, userId)) {
|
|
2626
|
+
this.rejectRefreshSession();
|
|
2627
|
+
}
|
|
2628
|
+
return {
|
|
2629
|
+
userId,
|
|
2630
|
+
tokenFamily,
|
|
2631
|
+
roles,
|
|
2632
|
+
permissions: permissions ?? [],
|
|
2633
|
+
sessionVersion: access.sessionVersion,
|
|
2634
|
+
accessToken: access.token,
|
|
2635
|
+
accessTokenExpiresAt: access.expiresAt,
|
|
2636
|
+
...refresh && { refreshToken: refresh.token, refreshTokenExpiresAt: refresh.expiresAt }
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2571
2639
|
async requireActiveRefreshUser(userId, tokenFamily) {
|
|
2572
2640
|
const user = await this.tokenRepository.getUser(userId);
|
|
2573
2641
|
if (!user || user.status !== "active") {
|
|
@@ -2692,11 +2760,7 @@ var TokenService = class TokenService2 {
|
|
|
2692
2760
|
if (!stored || stored.userId !== userId)
|
|
2693
2761
|
return null;
|
|
2694
2762
|
const presentedHash = this.hashToken(refreshToken);
|
|
2695
|
-
|
|
2696
|
-
return decoded.tokenFamily;
|
|
2697
|
-
}
|
|
2698
|
-
const canRecover = stored.previousHash && presentedHash === stored.previousHash && stored.previousValidUntil && new Date(stored.previousValidUntil).getTime() > Date.now() && !stored.previousUsedAt;
|
|
2699
|
-
return canRecover ? decoded.tokenFamily : null;
|
|
2763
|
+
return presentedHash === stored.token || this.withinGraceWindow(stored, presentedHash) ? decoded.tokenFamily : null;
|
|
2700
2764
|
} catch {
|
|
2701
2765
|
return null;
|
|
2702
2766
|
}
|
|
@@ -3503,7 +3567,7 @@ var AuthService = class AuthService2 {
|
|
|
3503
3567
|
isLockoutActive(lockoutUntil) {
|
|
3504
3568
|
if (!lockoutUntil)
|
|
3505
3569
|
return false;
|
|
3506
|
-
return
|
|
3570
|
+
return storedTimeMs(lockoutUntil) > Date.now();
|
|
3507
3571
|
}
|
|
3508
3572
|
nextLockoutUntil() {
|
|
3509
3573
|
const durationMs = timestring4(this.config.lockout.duration, "ms");
|
|
@@ -3783,7 +3847,8 @@ var AuthService = class AuthService2 {
|
|
|
3783
3847
|
}
|
|
3784
3848
|
async refreshTokens() {
|
|
3785
3849
|
const generated = await this.tokenService.refreshTokens();
|
|
3786
|
-
|
|
3850
|
+
if (generated.refreshToken)
|
|
3851
|
+
this.cookieManager.setRefreshToken(generated.refreshToken);
|
|
3787
3852
|
const user = await this.tokenService.getUserById(generated.userId);
|
|
3788
3853
|
if (user) {
|
|
3789
3854
|
this.cookieManager.setSessionCookie({
|