@nage-api/auth 1.0.0-beta.2
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/LICENSE +202 -0
- package/README.md +176 -0
- package/dist/api-key.service.d.ts +50 -0
- package/dist/api-key.service.js +110 -0
- package/dist/auth.controller.d.ts +48 -0
- package/dist/auth.controller.js +185 -0
- package/dist/auth.dto.d.ts +37 -0
- package/dist/auth.dto.js +117 -0
- package/dist/auth.guard.d.ts +29 -0
- package/dist/auth.guard.js +122 -0
- package/dist/auth.module.d.ts +61 -0
- package/dist/auth.module.js +226 -0
- package/dist/auth.service.d.ts +82 -0
- package/dist/auth.service.js +269 -0
- package/dist/authorization.guard.d.ts +24 -0
- package/dist/authorization.guard.js +107 -0
- package/dist/config.d.ts +71 -0
- package/dist/config.js +151 -0
- package/dist/decorators.d.ts +51 -0
- package/dist/decorators.js +70 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +96 -0
- package/dist/jwt.d.ts +50 -0
- package/dist/jwt.js +163 -0
- package/dist/lockout.service.d.ts +43 -0
- package/dist/lockout.service.js +94 -0
- package/dist/memory-stores.d.ts +84 -0
- package/dist/memory-stores.js +246 -0
- package/dist/otp.service.d.ts +47 -0
- package/dist/otp.service.js +137 -0
- package/dist/password.d.ts +51 -0
- package/dist/password.js +122 -0
- package/dist/policy.d.ts +44 -0
- package/dist/policy.js +61 -0
- package/dist/ports.d.ts +175 -0
- package/dist/ports.js +17 -0
- package/dist/principal.resolver.d.ts +52 -0
- package/dist/principal.resolver.js +125 -0
- package/dist/session.service.d.ts +71 -0
- package/dist/session.service.js +175 -0
- package/dist/tokens.d.ts +22 -0
- package/dist/tokens.js +23 -0
- package/package.json +66 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The retained auth endpoint surface (PLAN.md §15.1).
|
|
4
|
+
*
|
|
5
|
+
* Same paths as the legacy framework so existing clients keep working; the
|
|
6
|
+
* hardening is behind them. Three things are worth noticing here:
|
|
7
|
+
*
|
|
8
|
+
* - the login and OTP routes are `@Public()` **and** rate-limited harder than
|
|
9
|
+
* the global default, because they are the ones worth brute-forcing;
|
|
10
|
+
* - `forgot` and `send` always answer 202, whatever the address;
|
|
11
|
+
* - nothing in this file touches a password, a hash or a code — it parses a
|
|
12
|
+
* body, calls a service and returns a token pair.
|
|
13
|
+
*/
|
|
14
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
15
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
17
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
18
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
|
+
};
|
|
20
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
+
};
|
|
23
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
24
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.AuthController = void 0;
|
|
28
|
+
const common_1 = require("@nestjs/common");
|
|
29
|
+
const core_1 = require("@nage-api/core");
|
|
30
|
+
const auth_service_js_1 = require("./auth.service.js");
|
|
31
|
+
const auth_dto_js_1 = require("./auth.dto.js");
|
|
32
|
+
/** Tight enough to make online guessing useless, loose enough for a typo or two. */
|
|
33
|
+
const CREDENTIAL_RATE_LIMIT = { limit: 10, windowMs: 300_000 };
|
|
34
|
+
const CODE_RATE_LIMIT = { limit: 5, windowMs: 300_000 };
|
|
35
|
+
let AuthController = class AuthController {
|
|
36
|
+
auth;
|
|
37
|
+
constructor(auth) {
|
|
38
|
+
this.auth = auth;
|
|
39
|
+
}
|
|
40
|
+
async login(body, request) {
|
|
41
|
+
return this.auth.login({
|
|
42
|
+
email: body.email,
|
|
43
|
+
password: body.password,
|
|
44
|
+
context: sessionContext(request),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async refresh(body, request) {
|
|
48
|
+
return this.auth.refresh(body.refreshToken, sessionContext(request));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Public because a caller whose access token has already expired must still
|
|
52
|
+
* be able to log out; the refresh token in the body is the credential.
|
|
53
|
+
*/
|
|
54
|
+
async logout(body) {
|
|
55
|
+
await this.auth.logout(body.refreshToken);
|
|
56
|
+
}
|
|
57
|
+
async forgotPassword(body) {
|
|
58
|
+
// Accepted, always: a 404 here would confirm which addresses are registered.
|
|
59
|
+
await this.auth.forgotPassword(body.email);
|
|
60
|
+
}
|
|
61
|
+
async resetPassword(body) {
|
|
62
|
+
await this.auth.resetPassword({
|
|
63
|
+
email: body.email,
|
|
64
|
+
code: body.code,
|
|
65
|
+
password: body.password,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async sendOtp(body) {
|
|
69
|
+
await this.auth.sendLoginOtp(body.email);
|
|
70
|
+
}
|
|
71
|
+
async verifyOtp(body, request) {
|
|
72
|
+
return this.auth.verifyLoginOtp(body.email, body.code, sessionContext(request));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Administrative impersonation, retained from the legacy `/auth/user`.
|
|
76
|
+
*
|
|
77
|
+
* Not decorated with `@Roles`: which roles may impersonate is configuration
|
|
78
|
+
* (`auth.impersonation.allowedRoles`), and the service enforces it. Putting
|
|
79
|
+
* the role list in a decorator here would freeze one deployment's answer into
|
|
80
|
+
* the framework.
|
|
81
|
+
*/
|
|
82
|
+
async impersonate(body, actor, request) {
|
|
83
|
+
return this.auth.impersonate(actor, body.userId, sessionContext(request));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
exports.AuthController = AuthController;
|
|
87
|
+
__decorate([
|
|
88
|
+
(0, common_1.Post)('local'),
|
|
89
|
+
(0, core_1.Public)(),
|
|
90
|
+
(0, core_1.RateLimit)(CREDENTIAL_RATE_LIMIT),
|
|
91
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.OK),
|
|
92
|
+
__param(0, (0, common_1.Body)()),
|
|
93
|
+
__param(1, (0, common_1.Req)()),
|
|
94
|
+
__metadata("design:type", Function),
|
|
95
|
+
__metadata("design:paramtypes", [auth_dto_js_1.LoginDto, Object]),
|
|
96
|
+
__metadata("design:returntype", Promise)
|
|
97
|
+
], AuthController.prototype, "login", null);
|
|
98
|
+
__decorate([
|
|
99
|
+
(0, common_1.Post)('token'),
|
|
100
|
+
(0, core_1.Public)(),
|
|
101
|
+
(0, core_1.RateLimit)(CREDENTIAL_RATE_LIMIT),
|
|
102
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.OK),
|
|
103
|
+
__param(0, (0, common_1.Body)()),
|
|
104
|
+
__param(1, (0, common_1.Req)()),
|
|
105
|
+
__metadata("design:type", Function),
|
|
106
|
+
__metadata("design:paramtypes", [auth_dto_js_1.RefreshTokenDto, Object]),
|
|
107
|
+
__metadata("design:returntype", Promise)
|
|
108
|
+
], AuthController.prototype, "refresh", null);
|
|
109
|
+
__decorate([
|
|
110
|
+
(0, common_1.Post)('logout'),
|
|
111
|
+
(0, core_1.Public)(),
|
|
112
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.NO_CONTENT),
|
|
113
|
+
__param(0, (0, common_1.Body)()),
|
|
114
|
+
__metadata("design:type", Function),
|
|
115
|
+
__metadata("design:paramtypes", [auth_dto_js_1.RefreshTokenDto]),
|
|
116
|
+
__metadata("design:returntype", Promise)
|
|
117
|
+
], AuthController.prototype, "logout", null);
|
|
118
|
+
__decorate([
|
|
119
|
+
(0, common_1.Post)('password/forgot'),
|
|
120
|
+
(0, core_1.Public)(),
|
|
121
|
+
(0, core_1.RateLimit)(CODE_RATE_LIMIT),
|
|
122
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.ACCEPTED),
|
|
123
|
+
__param(0, (0, common_1.Body)()),
|
|
124
|
+
__metadata("design:type", Function),
|
|
125
|
+
__metadata("design:paramtypes", [auth_dto_js_1.ForgotPasswordDto]),
|
|
126
|
+
__metadata("design:returntype", Promise)
|
|
127
|
+
], AuthController.prototype, "forgotPassword", null);
|
|
128
|
+
__decorate([
|
|
129
|
+
(0, common_1.Post)('password/reset'),
|
|
130
|
+
(0, core_1.Public)(),
|
|
131
|
+
(0, core_1.RateLimit)(CODE_RATE_LIMIT),
|
|
132
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.NO_CONTENT),
|
|
133
|
+
__param(0, (0, common_1.Body)()),
|
|
134
|
+
__metadata("design:type", Function),
|
|
135
|
+
__metadata("design:paramtypes", [auth_dto_js_1.ResetPasswordDto]),
|
|
136
|
+
__metadata("design:returntype", Promise)
|
|
137
|
+
], AuthController.prototype, "resetPassword", null);
|
|
138
|
+
__decorate([
|
|
139
|
+
(0, common_1.Post)('otp/send'),
|
|
140
|
+
(0, core_1.Public)(),
|
|
141
|
+
(0, core_1.RateLimit)(CODE_RATE_LIMIT),
|
|
142
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.ACCEPTED),
|
|
143
|
+
__param(0, (0, common_1.Body)()),
|
|
144
|
+
__metadata("design:type", Function),
|
|
145
|
+
__metadata("design:paramtypes", [auth_dto_js_1.SendOtpDto]),
|
|
146
|
+
__metadata("design:returntype", Promise)
|
|
147
|
+
], AuthController.prototype, "sendOtp", null);
|
|
148
|
+
__decorate([
|
|
149
|
+
(0, common_1.Post)('otp/verify'),
|
|
150
|
+
(0, core_1.Public)(),
|
|
151
|
+
(0, core_1.RateLimit)(CODE_RATE_LIMIT),
|
|
152
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.OK),
|
|
153
|
+
__param(0, (0, common_1.Body)()),
|
|
154
|
+
__param(1, (0, common_1.Req)()),
|
|
155
|
+
__metadata("design:type", Function),
|
|
156
|
+
__metadata("design:paramtypes", [auth_dto_js_1.VerifyOtpDto, Object]),
|
|
157
|
+
__metadata("design:returntype", Promise)
|
|
158
|
+
], AuthController.prototype, "verifyOtp", null);
|
|
159
|
+
__decorate([
|
|
160
|
+
(0, common_1.Post)('user'),
|
|
161
|
+
(0, common_1.HttpCode)(common_1.HttpStatus.OK),
|
|
162
|
+
__param(0, (0, common_1.Body)()),
|
|
163
|
+
__param(1, (0, core_1.Owner)()),
|
|
164
|
+
__param(2, (0, common_1.Req)()),
|
|
165
|
+
__metadata("design:type", Function),
|
|
166
|
+
__metadata("design:paramtypes", [auth_dto_js_1.ImpersonateDto, Object, Object]),
|
|
167
|
+
__metadata("design:returntype", Promise)
|
|
168
|
+
], AuthController.prototype, "impersonate", null);
|
|
169
|
+
exports.AuthController = AuthController = __decorate([
|
|
170
|
+
(0, common_1.Controller)('auth'),
|
|
171
|
+
__metadata("design:paramtypes", [auth_service_js_1.AuthService])
|
|
172
|
+
], AuthController);
|
|
173
|
+
/** User agent and IP, recorded on the session so a user can review their logins. */
|
|
174
|
+
function sessionContext(request) {
|
|
175
|
+
const agent = request.headers?.['user-agent'];
|
|
176
|
+
const userAgent = Array.isArray(agent) ? agent[0] : agent;
|
|
177
|
+
// `clientIp` falls back to 'unknown' rather than undefined, and recording
|
|
178
|
+
// that as an address would be worse than recording nothing.
|
|
179
|
+
const ip = (0, core_1.clientIp)(request);
|
|
180
|
+
return {
|
|
181
|
+
...(userAgent === undefined ? {} : { userAgent: userAgent.slice(0, 256) }),
|
|
182
|
+
...(ip === 'unknown' ? {} : { ip }),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=auth.controller.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request bodies for the auth endpoints.
|
|
3
|
+
*
|
|
4
|
+
* `class-validator` rather than zod here, because these run through Nest's
|
|
5
|
+
* `ValidationPipe` alongside an application's own DTOs; zod validates the
|
|
6
|
+
* environment (§11.3), class-validator validates request bodies (§16.2).
|
|
7
|
+
*
|
|
8
|
+
* Every DTO is `whitelist`-friendly: a property the class does not declare is
|
|
9
|
+
* stripped before it reaches a service, so an extra `roles: ["admin"]` in a
|
|
10
|
+
* login body goes nowhere.
|
|
11
|
+
*/
|
|
12
|
+
export declare class LoginDto {
|
|
13
|
+
email: string;
|
|
14
|
+
password: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class RefreshTokenDto {
|
|
17
|
+
refreshToken: string;
|
|
18
|
+
}
|
|
19
|
+
export declare class ForgotPasswordDto {
|
|
20
|
+
email: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class ResetPasswordDto {
|
|
23
|
+
email: string;
|
|
24
|
+
code: string;
|
|
25
|
+
password: string;
|
|
26
|
+
}
|
|
27
|
+
export declare class SendOtpDto {
|
|
28
|
+
email: string;
|
|
29
|
+
}
|
|
30
|
+
export declare class VerifyOtpDto {
|
|
31
|
+
email: string;
|
|
32
|
+
code: string;
|
|
33
|
+
}
|
|
34
|
+
export declare class ImpersonateDto {
|
|
35
|
+
userId: string;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=auth.dto.d.ts.map
|
package/dist/auth.dto.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Request bodies for the auth endpoints.
|
|
4
|
+
*
|
|
5
|
+
* `class-validator` rather than zod here, because these run through Nest's
|
|
6
|
+
* `ValidationPipe` alongside an application's own DTOs; zod validates the
|
|
7
|
+
* environment (§11.3), class-validator validates request bodies (§16.2).
|
|
8
|
+
*
|
|
9
|
+
* Every DTO is `whitelist`-friendly: a property the class does not declare is
|
|
10
|
+
* stripped before it reaches a service, so an extra `roles: ["admin"]` in a
|
|
11
|
+
* login body goes nowhere.
|
|
12
|
+
*/
|
|
13
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
14
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
15
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
16
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
17
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
18
|
+
};
|
|
19
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.ImpersonateDto = exports.VerifyOtpDto = exports.SendOtpDto = exports.ResetPasswordDto = exports.ForgotPasswordDto = exports.RefreshTokenDto = exports.LoginDto = void 0;
|
|
24
|
+
const class_validator_1 = require("class-validator");
|
|
25
|
+
/** Long enough for a passphrase, bounded so a megabyte cannot be argon2-hashed. */
|
|
26
|
+
const MAX_PASSWORD_LENGTH = 256;
|
|
27
|
+
class LoginDto {
|
|
28
|
+
email;
|
|
29
|
+
password;
|
|
30
|
+
}
|
|
31
|
+
exports.LoginDto = LoginDto;
|
|
32
|
+
__decorate([
|
|
33
|
+
(0, class_validator_1.IsEmail)(),
|
|
34
|
+
(0, class_validator_1.MaxLength)(320),
|
|
35
|
+
__metadata("design:type", String)
|
|
36
|
+
], LoginDto.prototype, "email", void 0);
|
|
37
|
+
__decorate([
|
|
38
|
+
(0, class_validator_1.IsString)(),
|
|
39
|
+
(0, class_validator_1.IsNotEmpty)(),
|
|
40
|
+
(0, class_validator_1.MaxLength)(MAX_PASSWORD_LENGTH),
|
|
41
|
+
__metadata("design:type", String)
|
|
42
|
+
], LoginDto.prototype, "password", void 0);
|
|
43
|
+
class RefreshTokenDto {
|
|
44
|
+
refreshToken;
|
|
45
|
+
}
|
|
46
|
+
exports.RefreshTokenDto = RefreshTokenDto;
|
|
47
|
+
__decorate([
|
|
48
|
+
(0, class_validator_1.IsString)(),
|
|
49
|
+
(0, class_validator_1.IsNotEmpty)(),
|
|
50
|
+
(0, class_validator_1.MaxLength)(512),
|
|
51
|
+
__metadata("design:type", String)
|
|
52
|
+
], RefreshTokenDto.prototype, "refreshToken", void 0);
|
|
53
|
+
class ForgotPasswordDto {
|
|
54
|
+
email;
|
|
55
|
+
}
|
|
56
|
+
exports.ForgotPasswordDto = ForgotPasswordDto;
|
|
57
|
+
__decorate([
|
|
58
|
+
(0, class_validator_1.IsEmail)(),
|
|
59
|
+
(0, class_validator_1.MaxLength)(320),
|
|
60
|
+
__metadata("design:type", String)
|
|
61
|
+
], ForgotPasswordDto.prototype, "email", void 0);
|
|
62
|
+
class ResetPasswordDto {
|
|
63
|
+
email;
|
|
64
|
+
code;
|
|
65
|
+
password;
|
|
66
|
+
}
|
|
67
|
+
exports.ResetPasswordDto = ResetPasswordDto;
|
|
68
|
+
__decorate([
|
|
69
|
+
(0, class_validator_1.IsEmail)(),
|
|
70
|
+
(0, class_validator_1.MaxLength)(320),
|
|
71
|
+
__metadata("design:type", String)
|
|
72
|
+
], ResetPasswordDto.prototype, "email", void 0);
|
|
73
|
+
__decorate([
|
|
74
|
+
(0, class_validator_1.IsString)(),
|
|
75
|
+
(0, class_validator_1.Length)(6, 12),
|
|
76
|
+
__metadata("design:type", String)
|
|
77
|
+
], ResetPasswordDto.prototype, "code", void 0);
|
|
78
|
+
__decorate([
|
|
79
|
+
(0, class_validator_1.IsString)(),
|
|
80
|
+
(0, class_validator_1.MaxLength)(MAX_PASSWORD_LENGTH),
|
|
81
|
+
__metadata("design:type", String)
|
|
82
|
+
], ResetPasswordDto.prototype, "password", void 0);
|
|
83
|
+
class SendOtpDto {
|
|
84
|
+
email;
|
|
85
|
+
}
|
|
86
|
+
exports.SendOtpDto = SendOtpDto;
|
|
87
|
+
__decorate([
|
|
88
|
+
(0, class_validator_1.IsEmail)(),
|
|
89
|
+
(0, class_validator_1.MaxLength)(320),
|
|
90
|
+
__metadata("design:type", String)
|
|
91
|
+
], SendOtpDto.prototype, "email", void 0);
|
|
92
|
+
class VerifyOtpDto {
|
|
93
|
+
email;
|
|
94
|
+
code;
|
|
95
|
+
}
|
|
96
|
+
exports.VerifyOtpDto = VerifyOtpDto;
|
|
97
|
+
__decorate([
|
|
98
|
+
(0, class_validator_1.IsEmail)(),
|
|
99
|
+
(0, class_validator_1.MaxLength)(320),
|
|
100
|
+
__metadata("design:type", String)
|
|
101
|
+
], VerifyOtpDto.prototype, "email", void 0);
|
|
102
|
+
__decorate([
|
|
103
|
+
(0, class_validator_1.IsString)(),
|
|
104
|
+
(0, class_validator_1.Length)(6, 12),
|
|
105
|
+
__metadata("design:type", String)
|
|
106
|
+
], VerifyOtpDto.prototype, "code", void 0);
|
|
107
|
+
class ImpersonateDto {
|
|
108
|
+
userId;
|
|
109
|
+
}
|
|
110
|
+
exports.ImpersonateDto = ImpersonateDto;
|
|
111
|
+
__decorate([
|
|
112
|
+
(0, class_validator_1.IsString)(),
|
|
113
|
+
(0, class_validator_1.IsNotEmpty)(),
|
|
114
|
+
(0, class_validator_1.MaxLength)(128),
|
|
115
|
+
__metadata("design:type", String)
|
|
116
|
+
], ImpersonateDto.prototype, "userId", void 0);
|
|
117
|
+
//# sourceMappingURL=auth.dto.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The global authentication guard (PLAN.md §15.1).
|
|
3
|
+
*
|
|
4
|
+
* Registered by `NageAuthModule` as an `APP_GUARD`, so authentication is
|
|
5
|
+
* opt-**out**: a new controller is protected before anyone remembers to protect
|
|
6
|
+
* it. The legacy framework wired guards per controller, which meant every new
|
|
7
|
+
* file started life unauthenticated.
|
|
8
|
+
*
|
|
9
|
+
* The guard resolves a principal and writes it into the ambient request
|
|
10
|
+
* context, so `@Owner()`, services, repositories and queue publishers all see
|
|
11
|
+
* the same value without it being threaded through call signatures.
|
|
12
|
+
*/
|
|
13
|
+
import { type CanActivate, type ExecutionContext } from '@nestjs/common';
|
|
14
|
+
import { Reflector } from '@nestjs/core';
|
|
15
|
+
import type { ApiKeyService } from './api-key.service.js';
|
|
16
|
+
import type { PrincipalResolver } from './principal.resolver.js';
|
|
17
|
+
import type { TokenSigner } from './ports.js';
|
|
18
|
+
export interface AuthGuardOptions {
|
|
19
|
+
readonly signer: TokenSigner;
|
|
20
|
+
readonly principals: PrincipalResolver;
|
|
21
|
+
readonly reflector: Reflector;
|
|
22
|
+
readonly apiKeys?: ApiKeyService;
|
|
23
|
+
}
|
|
24
|
+
export declare class AuthGuard implements CanActivate {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(options: AuthGuardOptions);
|
|
27
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=auth.guard.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The global authentication guard (PLAN.md §15.1).
|
|
4
|
+
*
|
|
5
|
+
* Registered by `NageAuthModule` as an `APP_GUARD`, so authentication is
|
|
6
|
+
* opt-**out**: a new controller is protected before anyone remembers to protect
|
|
7
|
+
* it. The legacy framework wired guards per controller, which meant every new
|
|
8
|
+
* file started life unauthenticated.
|
|
9
|
+
*
|
|
10
|
+
* The guard resolves a principal and writes it into the ambient request
|
|
11
|
+
* context, so `@Owner()`, services, repositories and queue publishers all see
|
|
12
|
+
* the same value without it being threaded through call signatures.
|
|
13
|
+
*/
|
|
14
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
15
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
17
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
18
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
19
|
+
};
|
|
20
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.AuthGuard = void 0;
|
|
25
|
+
const common_1 = require("@nestjs/common");
|
|
26
|
+
const core_1 = require("@nage-api/core");
|
|
27
|
+
const decorators_js_1 = require("./decorators.js");
|
|
28
|
+
const jwt_js_1 = require("./jwt.js");
|
|
29
|
+
let AuthGuard = class AuthGuard {
|
|
30
|
+
#signer;
|
|
31
|
+
#principals;
|
|
32
|
+
#reflector;
|
|
33
|
+
#apiKeys;
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.#signer = options.signer;
|
|
36
|
+
this.#principals = options.principals;
|
|
37
|
+
this.#reflector = options.reflector;
|
|
38
|
+
this.#apiKeys = options.apiKeys;
|
|
39
|
+
}
|
|
40
|
+
async canActivate(context) {
|
|
41
|
+
// Only HTTP is guarded here; a websocket or RPC transport needs its own
|
|
42
|
+
// credential extraction, and silently passing them through would be worse
|
|
43
|
+
// than not supporting them.
|
|
44
|
+
if (context.getType() !== 'http')
|
|
45
|
+
return true;
|
|
46
|
+
const request = context.switchToHttp().getRequest();
|
|
47
|
+
const publicRoute = (0, core_1.isPublicRoute)(this.#reflector, context);
|
|
48
|
+
const bearer = readHeader(request, 'authorization');
|
|
49
|
+
const apiKey = readHeader(request, 'x-api-key');
|
|
50
|
+
if (bearer === undefined && apiKey === undefined) {
|
|
51
|
+
if (publicRoute)
|
|
52
|
+
return true;
|
|
53
|
+
throw new core_1.AuthenticationError('AUTH_REQUIRED', {
|
|
54
|
+
detail: 'No Authorization or X-Api-Key header on a protected route',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// A credential that was sent is always validated, even on a public route:
|
|
58
|
+
// silently ignoring an expired token would make `@OptionalOwner()` return
|
|
59
|
+
// `undefined` for a user who believes they are signed in.
|
|
60
|
+
const user = bearer !== undefined
|
|
61
|
+
? await this.#fromBearer(bearer)
|
|
62
|
+
: await this.#fromApiKey(apiKey ?? '', context);
|
|
63
|
+
(0, core_1.setContextValue)('user', user);
|
|
64
|
+
if (user.tenantId !== undefined)
|
|
65
|
+
(0, core_1.setContextValue)('tenantId', user.tenantId);
|
|
66
|
+
// Nest reads `request.user` in a few places (and so does application code
|
|
67
|
+
// migrating from the legacy framework), so mirror it there too.
|
|
68
|
+
request['user'] = user;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
async #fromBearer(header) {
|
|
72
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
73
|
+
if (match?.[1] === undefined) {
|
|
74
|
+
throw new core_1.AuthenticationError('AUTH_TOKEN_INVALID', {
|
|
75
|
+
detail: 'Authorization header is not a Bearer token',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const payload = await this.#signer.verify(match[1]);
|
|
79
|
+
const claims = (0, jwt_js_1.parseClaims)(payload);
|
|
80
|
+
const impersonatedBy = (0, jwt_js_1.actorId)(payload);
|
|
81
|
+
return this.#principals.resolve({
|
|
82
|
+
userId: claims.sub,
|
|
83
|
+
sessionId: claims.sid,
|
|
84
|
+
claimedRoles: claims.roles ?? [],
|
|
85
|
+
claimedPermissions: claims.permissions ?? [],
|
|
86
|
+
...(claims.tenantId === undefined ? {} : { tenantId: String(claims.tenantId) }),
|
|
87
|
+
...(impersonatedBy === undefined ? {} : { impersonatedBy }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async #fromApiKey(presented, context) {
|
|
91
|
+
if (this.#apiKeys === undefined) {
|
|
92
|
+
throw new core_1.AuthenticationError('AUTH_TOKEN_INVALID', {
|
|
93
|
+
detail: 'An API key was presented but api-key authentication is not enabled',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (!(0, decorators_js_1.isApiKeyAllowed)(this.#reflector, context)) {
|
|
97
|
+
// Fails closed: a key works only where a route has said it may.
|
|
98
|
+
throw new core_1.AuthenticationError('AUTH_REQUIRED', {
|
|
99
|
+
detail: 'This route does not accept API keys; decorate it with @AllowApiKey() if it should',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const record = await this.#apiKeys.verify(presented);
|
|
103
|
+
return {
|
|
104
|
+
id: record.userId ?? `api-key:${record.id}`,
|
|
105
|
+
roles: record.roles,
|
|
106
|
+
permissions: record.permissions,
|
|
107
|
+
sessionId: `api-key:${record.id}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
exports.AuthGuard = AuthGuard;
|
|
112
|
+
exports.AuthGuard = AuthGuard = __decorate([
|
|
113
|
+
(0, common_1.Injectable)(),
|
|
114
|
+
__metadata("design:paramtypes", [Object])
|
|
115
|
+
], AuthGuard);
|
|
116
|
+
function readHeader(request, name) {
|
|
117
|
+
const value = request.headers?.[name];
|
|
118
|
+
if (Array.isArray(value))
|
|
119
|
+
return value[0];
|
|
120
|
+
return value === undefined || value === '' ? undefined : value;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=auth.guard.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NageAuthModule` — one `forRoot` that registers both guards globally
|
|
3
|
+
* (PLAN.md §15.1, §11.1).
|
|
4
|
+
*
|
|
5
|
+
* The module owns the composition: an application supplies stores and secrets,
|
|
6
|
+
* and gets back a protected surface. It deliberately does **not** offer a way
|
|
7
|
+
* to register the controller without the guards — a package that lets you take
|
|
8
|
+
* the endpoints and skip the protection is a footgun with a `forRoot`.
|
|
9
|
+
*
|
|
10
|
+
* Every store defaults to its in-memory implementation so the module is usable
|
|
11
|
+
* (and testable) before a database exists; a real deployment passes its own,
|
|
12
|
+
* and `assertProductionStores` refuses the in-memory ones there.
|
|
13
|
+
*/
|
|
14
|
+
import { type DynamicModule } from '@nestjs/common';
|
|
15
|
+
import type { AuthConfig, NodeEnvironment, RoleMatrix } from '@nage-api/contracts';
|
|
16
|
+
import { type PolicyFunction } from './policy.js';
|
|
17
|
+
import type { ApiKeyStore, AuthAuditSink, AuthUserStore, Clock, LockoutStore, OtpChannel, OtpStore, PasswordHasher, SessionStore, TokenSigner } from './ports.js';
|
|
18
|
+
/** Secrets the package needs. All are required; none has a usable default. */
|
|
19
|
+
export interface AuthSecrets {
|
|
20
|
+
/** PEM private key (or the shared secret when the algorithm is HS256). */
|
|
21
|
+
readonly jwtPrivateKey: string;
|
|
22
|
+
/** PEM public key. Not needed for HS256. */
|
|
23
|
+
readonly jwtPublicKey?: string;
|
|
24
|
+
/** Pre-hash key for passwords; makes a dumped user table useless alone. */
|
|
25
|
+
readonly passwordPepper: string;
|
|
26
|
+
/** HMAC key for stored refresh tokens. */
|
|
27
|
+
readonly sessionPepper: string;
|
|
28
|
+
/** HMAC key for stored OTP codes. */
|
|
29
|
+
readonly otpPepper: string;
|
|
30
|
+
/** HMAC key for stored API keys. Required only when api keys are enabled. */
|
|
31
|
+
readonly apiKeyPepper?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface NageAuthModuleOptions {
|
|
34
|
+
readonly auth?: AuthConfig;
|
|
35
|
+
readonly environment: NodeEnvironment;
|
|
36
|
+
readonly secrets: AuthSecrets;
|
|
37
|
+
/** Role → permissions. Without it, permissions must be granted per user. */
|
|
38
|
+
readonly roles?: RoleMatrix;
|
|
39
|
+
readonly impersonation?: {
|
|
40
|
+
readonly enabled?: boolean;
|
|
41
|
+
readonly allowedRoles?: readonly string[];
|
|
42
|
+
readonly ttl?: string;
|
|
43
|
+
};
|
|
44
|
+
readonly policies?: Readonly<Record<string, PolicyFunction>>;
|
|
45
|
+
/** How long a resolved principal may be cached. Default 5s. */
|
|
46
|
+
readonly principalCacheTtlMs?: number;
|
|
47
|
+
readonly users?: AuthUserStore;
|
|
48
|
+
readonly sessions?: SessionStore;
|
|
49
|
+
readonly otpStore?: OtpStore;
|
|
50
|
+
readonly otpChannel?: OtpChannel;
|
|
51
|
+
readonly apiKeys?: ApiKeyStore;
|
|
52
|
+
readonly lockout?: LockoutStore;
|
|
53
|
+
readonly hasher?: PasswordHasher;
|
|
54
|
+
readonly signer?: TokenSigner;
|
|
55
|
+
readonly audit?: AuthAuditSink;
|
|
56
|
+
readonly clock?: Clock;
|
|
57
|
+
}
|
|
58
|
+
export declare class NageAuthModule {
|
|
59
|
+
static forRoot(options: NageAuthModuleOptions): DynamicModule;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=auth.module.d.ts.map
|