@flusys/nestjs-auth 9.1.1 → 9.2.0
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 +41 -9
- package/controllers/branch.controller.d.ts +1 -0
- package/controllers/company.controller.d.ts +1 -0
- package/controllers/social-auth-config.controller.d.ts +1 -0
- package/controllers/user.controller.d.ts +1 -0
- package/fesm/118.js +1 -1
- package/fesm/120.js +1 -1
- package/fesm/24.js +438 -264
- package/fesm/360.js +8 -2
- package/fesm/370.js +1 -0
- package/fesm/helpers/index.js +61 -20
- package/fesm/index.js +7 -3
- package/fesm/interceptors/index.js +1 -0
- package/helpers/cache-invalidation.helper.d.ts +3 -0
- package/helpers/token.helper.d.ts +4 -2
- package/interfaces/authentication.interface.d.ts +1 -0
- package/package.json +3 -3
- package/services/auth-email.service.d.ts +7 -2
- package/services/authentication.service.d.ts +3 -1
- package/services/branch.service.d.ts +1 -0
- package/services/company-selection-session.service.d.ts +7 -6
- package/services/company.service.d.ts +1 -0
- package/services/social-auth-config.service.d.ts +1 -0
- package/services/social-auth.service.d.ts +4 -1
- package/services/user-permission.service.d.ts +4 -3
- package/services/user.service.d.ts +3 -0
package/README.md
CHANGED
|
@@ -51,6 +51,15 @@ AuthModule.forRoot({
|
|
|
51
51
|
|
|
52
52
|
Browser logins get the refresh token as an HttpOnly cookie lasting `refreshTokenExpiration`. Sending `rememberMe: false` in the body of `POST /auth/login` or `POST /auth/select` makes it a session cookie instead (dropped when the browser closes).
|
|
53
53
|
|
|
54
|
+
### Tokens and sessions
|
|
55
|
+
|
|
56
|
+
- Access and refresh tokens are HS256 JWTs carrying `type: 'access' | 'refresh'`. `JwtStrategy` accepts only `access` tokens and `POST /auth/refresh` only `refresh` tokens, so one can never stand in for the other, even when both share a secret (outside production `refreshTokenSecret` falls back to `jwtSecret`).
|
|
57
|
+
- Refresh tokens are stateless and are not rotated: `POST /auth/refresh` returns a new access token only. They end at `refreshTokenExpiration` or when the user's sessions are revoked: logout, deactivation, a password reset and a social sign-in that claims an unverified account all record a revocation time, and every token issued before that moment is refused. Tokens carry an `iatMs` claim (issue time in milliseconds), so a token issued earlier in the same second as the revocation is refused while one issued right after it (signing straight back in) is kept; a token from before `iatMs` existed is judged by `iat` and, issued in the revocation's second, is refused. The revocation marker (`auth:revoked:<userId>`, epoch ms, kept for `refreshTokenExpiration`) is read and written past the in-process cache layer and never moves backwards. A company-selection session started before a revocation cannot complete `POST /auth/select` either.
|
|
58
|
+
- Password reset and email verification tokens are 32 random bytes, sent raw and stored only as their SHA-256 hash. A reset token lasts one hour and is single-use even under concurrent requests (it is claimed with one conditional update before the password changes); a verification link lasts 24 hours.
|
|
59
|
+
- Login answers an unknown email, a wrong password and a password-less (social-only) account with the same `auth.login.invalid.credentials`, running a bcrypt comparison in every case. A pending registration is only reported to someone who knows its password. `forgot-password` answers the same whether or not the email exists, a failing email provider included. Brute force is limited by the `@Throttle` limits on each route; there is no per-account lockout.
|
|
60
|
+
- With the company feature on, `POST /auth/register` with an email that already has an account joins or creates another company for that account only when the request carries the account's current password (otherwise it is refused like any taken email), the account is active, and, with email verification on, its email is verified.
|
|
61
|
+
- `POST /administration/users/profile` only ever edits the caller's own profile (an `id` naming anyone else is refused with `auth.profile.access.denied`); changing your password there always needs `oldPassword`, and changing your email clears `emailVerified`. Administrators edit other users through `POST /administration/users/update`.
|
|
62
|
+
|
|
54
63
|
#### Mode 2: Multi-Tenant
|
|
55
64
|
|
|
56
65
|
In multi-tenant mode you provide:
|
|
@@ -272,7 +281,7 @@ AuthModule.forRootAsync({ ..., providers: [userEnricherProvider] });
|
|
|
272
281
|
|
|
273
282
|
Google, Facebook, LinkedIn and Microsoft are built in. Nothing is enabled until an
|
|
274
283
|
administrator saves credentials, and no flag gates the feature: the
|
|
275
|
-
`social_auth_config`
|
|
284
|
+
`social_auth_config` table is always registered, and
|
|
276
285
|
`SocialAuthController` / `SocialAuthConfigController` are always mounted.
|
|
277
286
|
|
|
278
287
|
### Credentials
|
|
@@ -303,23 +312,36 @@ The `state` is a single-use, cache-backed token that also records which provider
|
|
|
303
312
|
and redirect URI the sign-in started with — providers echo back nothing but
|
|
304
313
|
`code` and `state`, so the callback carries no provider of its own. Redeeming a
|
|
305
314
|
state deletes it, and a redirect URI that does not match the one the sign-in
|
|
306
|
-
started with is rejected.
|
|
315
|
+
started with is rejected. The state is redeemed with an atomic `take`, so it (and the
|
|
316
|
+
`code` that comes with it) cannot be replayed on another instance. The state is not bound to the browser that started the
|
|
317
|
+
sign-in, so the client must keep the `state` it got from `authorize` and refuse a
|
|
318
|
+
callback whose `state` differs before posting it (login CSRF).
|
|
307
319
|
|
|
308
320
|
The callback returns exactly what `POST /auth/login` returns, company selection
|
|
309
321
|
included, because both routes finish through `AuthenticationService.issueLoginForUser`.
|
|
310
322
|
|
|
311
323
|
### How a user is matched
|
|
312
324
|
|
|
313
|
-
1. The
|
|
314
|
-
2. Otherwise
|
|
315
|
-
3. Otherwise a new account with no password.
|
|
325
|
+
1. The email, so an existing password account is adopted rather than duplicated.
|
|
326
|
+
2. Otherwise a new account with no password.
|
|
316
327
|
|
|
317
|
-
|
|
328
|
+
Both require the provider to have **verified** the address. Matching on an
|
|
318
329
|
unverified email would let anyone who registers someone else's address at the
|
|
319
330
|
provider walk into their FLUSYS account, so an unverified profile is rejected.
|
|
320
|
-
An account already linked at step 1 is unaffected.
|
|
321
331
|
|
|
322
|
-
|
|
332
|
+
Adopting an account whose own email was never verified in FLUSYS drops its
|
|
333
|
+
password and ends its sessions: whoever chose that password never proved they own
|
|
334
|
+
the address, so an account pre-registered with someone else's email cannot be kept
|
|
335
|
+
by its registrant once the real owner signs in. The owner can set a password again
|
|
336
|
+
through `forgot-password`.
|
|
337
|
+
|
|
338
|
+
Microsoft's `mail` claim is set by the directory's administrator, so it only counts
|
|
339
|
+
as verified when sign-in is pinned to one directory (`extraConfig.tenantId` set to
|
|
340
|
+
your tenant id or domain) or to personal accounts (`consumers`). With the default
|
|
341
|
+
`common` (or `organizations`) authority every Microsoft profile is treated as
|
|
342
|
+
unverified and refused - set `tenantId` to use Microsoft sign-in.
|
|
343
|
+
|
|
344
|
+
Step 2 — creating an account — is refused in two cases, each with its own message key:
|
|
323
345
|
|
|
324
346
|
| Condition | Result |
|
|
325
347
|
| ------------------------------------------- | -------------------------------------------- |
|
|
@@ -335,6 +357,16 @@ then works. Signing **in** is never affected by either — only account creation
|
|
|
335
357
|
A cache instance is required: `AuthModule` imports `CacheModule`, so this is
|
|
336
358
|
satisfied by default.
|
|
337
359
|
|
|
360
|
+
### Caching
|
|
361
|
+
|
|
362
|
+
`UserService`, `CompanyService`, `BranchService` and `SocialAuthConfigService` cache `findById` / `get-all` through `ApiService` version stamps (see `nestjs-shared` section 7). Notes specific to this package:
|
|
363
|
+
|
|
364
|
+
- Every write outside `ApiService`'s own CRUD moves the stamp of what it wrote, after commit: login (`lastLoginAt`), registration (user, plus company / branch / grants when it creates them), password change and reset, email verification, profile and status updates, social sign-in (new or claimed account), grant assign / revoke (`UserPermissionService`), and the grants removed by a company or branch delete.
|
|
365
|
+
- The user list depends on `appuser` and `user_company_permission` (the company-scoped list reads each user's grant). A list filtered by `actionCode` reads IAM grants, whose writes never move these stamps, so that read is not cached.
|
|
366
|
+
- `SocialAuthConfigService` returns `hasClientSecret` instead of the secret, so the client secret is never cached or sent.
|
|
367
|
+
- Login, token refresh and `JwtStrategy` read user, company and grant state from the database, never from the cache.
|
|
368
|
+
- One-time state never goes through the in-process layer: company-selection sessions (keyed by tenant in multi-tenant mode, consumed with `take`), social sign-in state (`take`) and revocation markers. Revocation markers are keyed by user id only, so in multi-tenant mode a revocation in one tenant also ends the sessions of a user with the same id in another tenant (only when tenant databases share user ids).
|
|
369
|
+
|
|
338
370
|
## 6. Branch Hierarchy
|
|
339
371
|
|
|
340
372
|
Branches nest through `parentId`. A parent must be in the same company and cannot create a cycle, and deleting a branch also deletes every branch under it. `BranchService` exposes the hierarchy, each lookup taking two queries whatever the depth:
|
|
@@ -349,7 +381,7 @@ With the company feature on, the module also provides and exports `BRANCH_HIERAR
|
|
|
349
381
|
|
|
350
382
|
## 7. Deactivating a User
|
|
351
383
|
|
|
352
|
-
With the company feature off, `UserService.updateStatus` sets `AppUser.isActive`, which blocks login, token refresh and every request. With it on, it deactivates the user in the admin's current company only, by setting `isActive` on that company grant (`UserCompanyPermission`, `permissionType: company`). Login, company select, select/switch, token refresh, `get-my-companies` / `get-my-company-branches` and `COMPANY_ACCESS_RESOLVER` all skip an inactive company grant, and deactivating (in either mode) also ends every session of the user at once, the same way logout does (`revokeUserSessions`, checked on refresh and by `JwtStrategy` on every request). The user can log straight back in to the companies they are still active in. A user with company grants never gets a company-less login (such a token is not filtered by company): login is refused when they are deactivated in every company (`auth.login.account.deactivated`) or none of their companies is active (`auth.company.not.found`), and a company-less refresh token is not renewed once the user has company grants. Branch grants carry no status of their own.
|
|
384
|
+
With the company feature off, `UserService.updateStatus` sets `AppUser.isActive`, which blocks login, token refresh and every request. With it on, it deactivates the user in the admin's current company only, by setting `isActive` on that company grant (`UserCompanyPermission`, `permissionType: company`). Login, company select, select/switch, token refresh, `get-my-companies` / `get-my-company-branches` and `COMPANY_ACCESS_RESOLVER` all skip an inactive company grant, and deactivating (in either mode) also ends every session of the user at once, the same way logout does (`revokeUserSessions`, checked on refresh and by `JwtStrategy` on every request). The user can log straight back in to the companies they are still active in. A user with company grants never gets a company-less login (such a token is not filtered by company): login is refused when they are deactivated in every company (`auth.login.account.deactivated`) or none of their companies is active (`auth.company.not.found`), and a company-less refresh token is not renewed once the user has company grants. Branch grants carry no status of their own. With the company feature on, a caller whose token has no company cannot change a status (`auth.company.required`) and sees no users in `get-all` / `lookup`, matching the company-less scoping of `nestjs-shared`; the company-select routes (`get-my-companies`, `get-my-company-branches`, `switch-company`) work from the token's user id alone, so such a caller can still pick a company.
|
|
353
385
|
|
|
354
386
|
## License
|
|
355
387
|
|
|
@@ -4,6 +4,7 @@ declare const BranchController_base: abstract new (service: BranchService) => {
|
|
|
4
4
|
readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
|
|
5
5
|
service: BranchService;
|
|
6
6
|
isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
|
|
7
|
+
assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
|
|
7
8
|
insert(addDto: CreateCompanyBranchDto, user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<CompanyBranchResponseDto>>;
|
|
8
9
|
insertMany(addDto: CreateCompanyBranchDto[], user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").BulkResponseDto<CompanyBranchResponseDto>>;
|
|
9
10
|
getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<CompanyBranchResponseDto>>;
|
|
@@ -4,6 +4,7 @@ declare const CompanyController_base: abstract new (service: CompanyService) =>
|
|
|
4
4
|
readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
|
|
5
5
|
service: CompanyService;
|
|
6
6
|
isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
|
|
7
|
+
assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
|
|
7
8
|
insert(addDto: CreateCompanyDto, user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<CompanyResponseDto>>;
|
|
8
9
|
insertMany(addDto: CreateCompanyDto[], user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").BulkResponseDto<CompanyResponseDto>>;
|
|
9
10
|
getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<CompanyResponseDto>>;
|
|
@@ -4,6 +4,7 @@ declare const SocialAuthConfigController_base: abstract new (service: SocialAuth
|
|
|
4
4
|
readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
|
|
5
5
|
service: SocialAuthConfigService;
|
|
6
6
|
isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
|
|
7
|
+
assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
|
|
7
8
|
insert(addDto: CreateSocialAuthConfigDto, user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<SocialAuthConfigResponseDto>>;
|
|
8
9
|
insertMany(addDto: CreateSocialAuthConfigDto[], user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").BulkResponseDto<SocialAuthConfigResponseDto>>;
|
|
9
10
|
getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<SocialAuthConfigResponseDto>>;
|
|
@@ -7,6 +7,7 @@ declare const UserController_base: abstract new (service: UserService) => {
|
|
|
7
7
|
readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
|
|
8
8
|
service: UserService;
|
|
9
9
|
isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
|
|
10
|
+
assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
|
|
10
11
|
insert(addDto: CreateUserDto, user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<UserResponseDto>>;
|
|
11
12
|
insertMany(addDto: CreateUserDto[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").BulkResponseDto<UserResponseDto>>;
|
|
12
13
|
getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<UserResponseDto>>;
|
package/fesm/118.js
CHANGED
|
@@ -752,7 +752,7 @@ _ts_decorate([
|
|
|
752
752
|
description: 'Whether a client secret is currently stored for this provider'
|
|
753
753
|
}),
|
|
754
754
|
(0,class_transformer__rspack_import_2.Expose)(),
|
|
755
|
-
(0,class_transformer__rspack_import_2.Transform)(({ obj })
|
|
755
|
+
(0,class_transformer__rspack_import_2.Transform)(({ obj })=>obj.hasClientSecret ?? !!obj.clientSecret),
|
|
756
756
|
_ts_metadata("design:type", Boolean)
|
|
757
757
|
], SocialAuthConfigResponseDto.prototype, "hasClientSecret", void 0);
|
|
758
758
|
_ts_decorate([
|
package/fesm/120.js
CHANGED
|
@@ -1474,7 +1474,6 @@ user_controller_ts_decorate([
|
|
|
1474
1474
|
(0,common_.Post)('profile-sections'),
|
|
1475
1475
|
(0,common_.UseGuards)(guards_.JwtAuthGuard),
|
|
1476
1476
|
(0,swagger_.ApiBearerAuth)(),
|
|
1477
|
-
(0,__rspack_external__flusys_nestjs_shared_decorators_c129b601.RequirePermission)(classes_.USER_PERMISSIONS.READ),
|
|
1478
1477
|
(0,swagger_.ApiOperation)({
|
|
1479
1478
|
summary: 'Get profile section definitions',
|
|
1480
1479
|
description: 'Returns available profile sections for multi-step profile (if enricher configured).'
|
|
@@ -2137,6 +2136,7 @@ class ClearToken {
|
|
|
2137
2136
|
ClearToken = _ts_decorate([
|
|
2138
2137
|
(0,_nestjs_common__rspack_import_1.Injectable)(),
|
|
2139
2138
|
_ts_param(0, (0,_nestjs_common__rspack_import_1.Optional)()),
|
|
2139
|
+
_ts_param(0, (0,_nestjs_common__rspack_import_1.Inject)(_services_auth_config_service_js__rspack_import_3/* .AuthConfigService */.W)),
|
|
2140
2140
|
_ts_metadata("design:type", Function),
|
|
2141
2141
|
_ts_metadata("design:paramtypes", [
|
|
2142
2142
|
typeof _services_auth_config_service_js__rspack_import_3/* .AuthConfigService */.W === "undefined" ? Object : _services_auth_config_service_js__rspack_import_3/* .AuthConfigService */.W
|