najm-auth 4.2.2 → 4.2.4
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/README.md +1372 -1372
- package/dist/client/index.js +3 -1
- package/dist/client/server/index.js +3 -1
- package/dist/index.d.ts +57 -18
- package/dist/index.js +81 -34
- package/package.json +4 -4
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
|
}>;
|
|
@@ -3172,6 +3210,7 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
|
|
|
3172
3210
|
/**
|
|
3173
3211
|
* Standalone function to seed authentication data.
|
|
3174
3212
|
* Handles Server setup, seeding, and cleanup automatically.
|
|
3213
|
+
* The caller retains ownership of db; stopping the temporary server leaves it open.
|
|
3175
3214
|
*
|
|
3176
3215
|
* @example
|
|
3177
3216
|
* ```typescript
|
|
@@ -3207,4 +3246,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
|
|
|
3207
3246
|
*/
|
|
3208
3247
|
declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
|
|
3209
3248
|
|
|
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 };
|
|
3249
|
+
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";
|
|
@@ -2312,11 +2312,7 @@ var TokenService = class TokenService2 {
|
|
|
2312
2312
|
this.rejectRefreshSession();
|
|
2313
2313
|
}
|
|
2314
2314
|
const presentedHash = this.hashToken(refreshToken);
|
|
2315
|
-
if (presentedHash === stored.token) {
|
|
2316
|
-
return { userId, tokenFamily };
|
|
2317
|
-
}
|
|
2318
|
-
const canRecover = stored.previousHash && presentedHash === stored.previousHash && stored.previousValidUntil && storedTimeMs(stored.previousValidUntil) > Date.now() && !stored.previousUsedAt;
|
|
2319
|
-
if (canRecover) {
|
|
2315
|
+
if (presentedHash === stored.token || this.withinGraceWindow(stored, presentedHash)) {
|
|
2320
2316
|
return { userId, tokenFamily };
|
|
2321
2317
|
}
|
|
2322
2318
|
this.rejectRefreshSession();
|
|
@@ -2443,10 +2439,22 @@ var TokenService = class TokenService2 {
|
|
|
2443
2439
|
const token = jwt.sign({ ...data, jti, type: "refresh", exp: expiresAt }, this.config.jwt.refreshSecret);
|
|
2444
2440
|
return { token, expiresAt };
|
|
2445
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
|
+
}
|
|
2446
2454
|
generateRefreshToken(data) {
|
|
2447
2455
|
return this.signRefreshToken({ userId: data.userId, tokenFamily: data.tokenFamily ?? nanoid4(16) }).token;
|
|
2448
2456
|
}
|
|
2449
|
-
async createTokenPair(userId, family) {
|
|
2457
|
+
async createTokenPair(userId, family, replaced) {
|
|
2450
2458
|
const { roleName, permissions } = await this.tokenRepository.getRoleAndPermissions(userId);
|
|
2451
2459
|
const accessTokenData = {
|
|
2452
2460
|
userId,
|
|
@@ -2455,7 +2463,7 @@ var TokenService = class TokenService2 {
|
|
|
2455
2463
|
tokenFamily: family
|
|
2456
2464
|
};
|
|
2457
2465
|
const access = await this.signAccessToken(accessTokenData);
|
|
2458
|
-
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 });
|
|
2459
2467
|
return {
|
|
2460
2468
|
userId,
|
|
2461
2469
|
tokenFamily: family,
|
|
@@ -2537,35 +2545,44 @@ var TokenService = class TokenService2 {
|
|
|
2537
2545
|
}
|
|
2538
2546
|
/**
|
|
2539
2547
|
* Rotate only the family row observed by refreshTokens(). This conditional
|
|
2540
|
-
* 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.
|
|
2541
2551
|
*/
|
|
2542
|
-
async rotateTokens(userId, tokenFamily, expectedCurrentHash) {
|
|
2543
|
-
const generated = await this.createTokenPair(userId, tokenFamily);
|
|
2544
|
-
const expireInSecond = timestring3(this.config.jwt.refreshExpiresIn, "s");
|
|
2552
|
+
async rotateTokens(userId, tokenFamily, replaced, expectedCurrentHash) {
|
|
2553
|
+
const generated = await this.createTokenPair(userId, tokenFamily, replaced);
|
|
2545
2554
|
const rotated = await this.tokenRepository.rotateRefreshToken({
|
|
2546
2555
|
userId,
|
|
2547
2556
|
token: this.hashToken(generated.refreshToken),
|
|
2548
2557
|
tokenFamily,
|
|
2549
|
-
expiresAt: new Date(
|
|
2558
|
+
expiresAt: new Date(generated.refreshTokenExpiresAt * 1e3).toISOString(),
|
|
2550
2559
|
previousHash: expectedCurrentHash,
|
|
2551
2560
|
previousValidUntil: new Date(Date.now() + TokenService_1.PREVIOUS_GRACE_SECONDS * 1e3).toISOString(),
|
|
2552
2561
|
previousUsedAt: null
|
|
2553
2562
|
}, expectedCurrentHash);
|
|
2554
|
-
if (!rotated?.length)
|
|
2555
|
-
|
|
2556
|
-
}
|
|
2563
|
+
if (!rotated?.length)
|
|
2564
|
+
return null;
|
|
2557
2565
|
if (!await this.invalidation.markFamilyIssued(tokenFamily, userId)) {
|
|
2558
2566
|
this.rejectRefreshSession();
|
|
2559
2567
|
}
|
|
2560
2568
|
return generated;
|
|
2561
2569
|
}
|
|
2562
2570
|
/**
|
|
2563
|
-
*
|
|
2564
|
-
*
|
|
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.
|
|
2565
2582
|
*/
|
|
2566
2583
|
async refreshTokens() {
|
|
2567
2584
|
const { refreshToken, userId, tokenFamily } = this.readRefreshSessionCookie();
|
|
2568
|
-
|
|
2585
|
+
let stored = await this.tokenRepository.getByFamily(tokenFamily);
|
|
2569
2586
|
await this.assertRefreshFamilyAllowed(tokenFamily, userId);
|
|
2570
2587
|
if (!stored || stored.userId !== userId) {
|
|
2571
2588
|
this.rejectRefreshSession();
|
|
@@ -2573,19 +2590,52 @@ var TokenService = class TokenService2 {
|
|
|
2573
2590
|
const presentedHash = this.hashToken(refreshToken);
|
|
2574
2591
|
await this.requireActiveRefreshUser(userId, tokenFamily);
|
|
2575
2592
|
if (presentedHash === stored.token) {
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
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);
|
|
2583
2599
|
}
|
|
2584
|
-
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);
|
|
2585
2604
|
}
|
|
2586
2605
|
await this.revokeSuspectRefreshFamily(userId, tokenFamily);
|
|
2587
2606
|
this.rejectRefreshSession();
|
|
2588
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
|
+
}
|
|
2589
2639
|
async requireActiveRefreshUser(userId, tokenFamily) {
|
|
2590
2640
|
const user = await this.tokenRepository.getUser(userId);
|
|
2591
2641
|
if (!user || user.status !== "active") {
|
|
@@ -2710,11 +2760,7 @@ var TokenService = class TokenService2 {
|
|
|
2710
2760
|
if (!stored || stored.userId !== userId)
|
|
2711
2761
|
return null;
|
|
2712
2762
|
const presentedHash = this.hashToken(refreshToken);
|
|
2713
|
-
|
|
2714
|
-
return decoded.tokenFamily;
|
|
2715
|
-
}
|
|
2716
|
-
const canRecover = stored.previousHash && presentedHash === stored.previousHash && stored.previousValidUntil && storedTimeMs(stored.previousValidUntil) > Date.now() && !stored.previousUsedAt;
|
|
2717
|
-
return canRecover ? decoded.tokenFamily : null;
|
|
2763
|
+
return presentedHash === stored.token || this.withinGraceWindow(stored, presentedHash) ? decoded.tokenFamily : null;
|
|
2718
2764
|
} catch {
|
|
2719
2765
|
return null;
|
|
2720
2766
|
}
|
|
@@ -3801,7 +3847,8 @@ var AuthService = class AuthService2 {
|
|
|
3801
3847
|
}
|
|
3802
3848
|
async refreshTokens() {
|
|
3803
3849
|
const generated = await this.tokenService.refreshTokens();
|
|
3804
|
-
|
|
3850
|
+
if (generated.refreshToken)
|
|
3851
|
+
this.cookieManager.setRefreshToken(generated.refreshToken);
|
|
3805
3852
|
const user = await this.tokenService.getUserById(generated.userId);
|
|
3806
3853
|
if (user) {
|
|
3807
3854
|
this.cookieManager.setSessionCookie({
|
|
@@ -8095,7 +8142,7 @@ async function seedAuthData(config) {
|
|
|
8095
8142
|
role: "admin"
|
|
8096
8143
|
};
|
|
8097
8144
|
try {
|
|
8098
|
-
server = new Server({ isolated: true }).use(database({ default: config.db }));
|
|
8145
|
+
server = new Server({ isolated: true }).use(database({ default: config.db }, { close: false }));
|
|
8099
8146
|
await server.init();
|
|
8100
8147
|
const report = await server.runAs(seedActor, async () => {
|
|
8101
8148
|
const seeder = server.container.get(SeedService);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "najm-auth",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.4",
|
|
4
4
|
"description": "Authentication and authorization library for najm framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -92,9 +92,9 @@
|
|
|
92
92
|
"dependencies": {
|
|
93
93
|
"bcryptjs": "^3.0.2",
|
|
94
94
|
"najm-cookies": "^2.0.2",
|
|
95
|
-
"najm-core": "^3.0.
|
|
96
|
-
"najm-database": "^2.0
|
|
97
|
-
"najm-guard": "^2.
|
|
95
|
+
"najm-core": "^3.0.2",
|
|
96
|
+
"najm-database": "^2.1.0",
|
|
97
|
+
"najm-guard": "^2.2.0",
|
|
98
98
|
"najm-i18n": "^2.1.2",
|
|
99
99
|
"najm-cache": "^2.2.0",
|
|
100
100
|
"najm-email": "^2.0.4",
|