@travetto/auth 2.2.4 → 3.0.0-rc.1
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 +17 -179
- package/index.ts +1 -4
- package/package.json +3 -7
- package/src/internal/types.ts +0 -3
- package/src/types/authenticator.ts +2 -1
- package/src/types/principal.ts +1 -1
- package/src/util.ts +13 -66
- package/src/extension/model.ts +0 -199
- package/test-support/model.ts +0 -89
package/README.md
CHANGED
|
@@ -63,7 +63,8 @@ As referenced above, a [Principal Structure](https://github.com/travetto/travett
|
|
|
63
63
|
```typescript
|
|
64
64
|
export interface Authenticator<T = unknown, P extends Principal = Principal, C = unknown> {
|
|
65
65
|
/**
|
|
66
|
-
* Verify the payload,
|
|
66
|
+
* Verify the payload, ensuring the payload is correctly identified.
|
|
67
|
+
*
|
|
67
68
|
* @returns Valid principal if authenticated
|
|
68
69
|
* @returns undefined if authentication is valid, but incomplete (multi-step)
|
|
69
70
|
* @throws AppError if authentication fails
|
|
@@ -105,7 +106,7 @@ Overall, the structure is simple, but drives home the primary use cases of the f
|
|
|
105
106
|
* To have access to the principal
|
|
106
107
|
|
|
107
108
|
## Common Utilities
|
|
108
|
-
The [AuthUtil](https://github.com/travetto/travetto/tree/main/module/auth/src/util.ts#
|
|
109
|
+
The [AuthUtil](https://github.com/travetto/travetto/tree/main/module/auth/src/util.ts#L11) provides the following functionality:
|
|
109
110
|
|
|
110
111
|
**Code: Auth util structure**
|
|
111
112
|
```typescript
|
|
@@ -113,42 +114,16 @@ import * as crypto from 'crypto';
|
|
|
113
114
|
import * as util from 'util';
|
|
114
115
|
import { AppError, Util } from '@travetto/base';
|
|
115
116
|
const pbkdf2 = util.promisify(crypto.pbkdf2);
|
|
116
|
-
type PermSet = Set<string> | ReadonlySet<string>;
|
|
117
|
-
type PermissionChecker = {
|
|
118
|
-
all: (perms: PermSet) => boolean;
|
|
119
|
-
any: (perms: PermSet) => boolean;
|
|
120
|
-
};
|
|
121
|
-
type PermissionCheckerSet = {
|
|
122
|
-
includes: (perms: PermSet) => boolean;
|
|
123
|
-
excludes: (perms: PermSet) => boolean;
|
|
124
|
-
check: (value: PermSet) => boolean;
|
|
125
|
-
};
|
|
126
117
|
/**
|
|
127
118
|
* Standard auth utilities
|
|
128
119
|
*/
|
|
129
120
|
export class AuthUtil {
|
|
130
121
|
/**
|
|
131
|
-
* Build
|
|
132
|
-
*
|
|
133
|
-
* @param perms Set of permissions to check
|
|
134
|
-
* @param defaultIfEmpty If no perms passed, default to empty
|
|
135
|
-
*/
|
|
136
|
-
/**
|
|
137
|
-
* Build a permission checker off of an include, and exclude set
|
|
138
|
-
*
|
|
139
|
-
* @param include Which permissions to include
|
|
140
|
-
* @param exclude Which permissions to exclude
|
|
141
|
-
* @param matchAll Whether not all permissions should be matched
|
|
142
|
-
*/
|
|
143
|
-
static permissionChecker(include: Iterable<string>, exclude: Iterable<string>, mode: 'all' | 'any' = 'any'): PermissionCheckerSet ;
|
|
144
|
-
/**
|
|
145
|
-
* Build a permission checker off of an include, and exclude set
|
|
122
|
+
* Build matcher for role permissions in allow/deny fashion
|
|
146
123
|
*
|
|
147
|
-
* @param
|
|
148
|
-
* @param exclude Which permissions to exclude
|
|
149
|
-
* @param matchAll Whether not all permissions should be matched
|
|
124
|
+
* @param roles Roles to build matcher for
|
|
150
125
|
*/
|
|
151
|
-
static
|
|
126
|
+
static roleMatcher(roles: string[]): (perms: Set<string>) => boolean ;
|
|
152
127
|
/**
|
|
153
128
|
* Generate a hash for a given value
|
|
154
129
|
*
|
|
@@ -170,155 +145,18 @@ export class AuthUtil {
|
|
|
170
145
|
}
|
|
171
146
|
```
|
|
172
147
|
|
|
173
|
-
`
|
|
148
|
+
`roleMatcher` is probably the only functionality that needs to be explained. The function extends the core allow/deny matcher functionality from [Base](https://github.com/travetto/travetto/tree/main/module/base#readme "Application phase management, environment config and common utilities for travetto applications.")'s Util class.
|
|
174
149
|
|
|
175
|
-
|
|
176
|
-
* The user is logged in
|
|
177
|
-
* If `matchAll` is false:
|
|
178
|
-
* The user does not have any permissions in the exclusion list
|
|
179
|
-
* The include list is empty, or the user has at least one permission in the include list.
|
|
180
|
-
* Else
|
|
181
|
-
* The user does not have all permissions in the exclusion list
|
|
182
|
-
* The include list is empty, or the user has all permissions in the include list.
|
|
183
|
-
|
|
184
|
-
## Extension - Model
|
|
185
|
-
|
|
186
|
-
This module also supports the integration between the [Authentication](https://github.com/travetto/travetto/tree/main/module/auth#readme "Authentication scaffolding for the travetto framework") module and the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.").
|
|
187
|
-
|
|
188
|
-
The asset module requires an [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11) to provide functionality for reading and storing user information. You can use any existing providers to serve as your [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11), or you can roll your own.
|
|
189
|
-
|
|
190
|
-
**Install: provider**
|
|
191
|
-
```bash
|
|
192
|
-
npm install @travetto/model-{provider}
|
|
193
|
-
```
|
|
150
|
+
An example of role checks could be:
|
|
194
151
|
|
|
195
|
-
Currently, the following are packages that provide [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11):
|
|
196
152
|
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* [MongoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-mongo#readme "Mongo backing for the travetto model module.") @travetto/model-mongo
|
|
201
|
-
* [Redis Model Support](https://github.com/travetto/travetto/tree/main/module/model-redis#readme "Redis backing for the travetto model module.") @travetto/model-redis
|
|
202
|
-
* [S3 Model Support](https://github.com/travetto/travetto/tree/main/module/model-s3#readme "S3 backing for the travetto model module.") @travetto/model-s3
|
|
203
|
-
* [SQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-sql#readme "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.") @travetto/model-sql
|
|
204
|
-
|
|
205
|
-
The module itself is fairly straightforward, and truly the only integration point for this module to work is defined at the model level. The contract for authentication is established in code as providing translation to and from a [Registered Principal](https://github.com/travetto/travetto/tree/main/module/auth/src/extension/model.ts#L15)
|
|
206
|
-
|
|
207
|
-
A registered principal extends the base concept of an principal, by adding in additional fields needed for local registration, specifically password management information.
|
|
208
|
-
|
|
209
|
-
**Code: Registered Principal**
|
|
210
|
-
```typescript
|
|
211
|
-
export interface RegisteredPrincipal extends Principal {
|
|
212
|
-
/**
|
|
213
|
-
* Password hash
|
|
214
|
-
*/
|
|
215
|
-
hash?: string;
|
|
216
|
-
/**
|
|
217
|
-
* Password salt
|
|
218
|
-
*/
|
|
219
|
-
salt?: string;
|
|
220
|
-
/**
|
|
221
|
-
* Temporary Reset Token
|
|
222
|
-
*/
|
|
223
|
-
resetToken?: string;
|
|
224
|
-
/**
|
|
225
|
-
* End date for the reset token
|
|
226
|
-
*/
|
|
227
|
-
resetExpires?: Date;
|
|
228
|
-
/**
|
|
229
|
-
* The actual password, only used on password set/update
|
|
230
|
-
*/
|
|
231
|
-
password?: string;
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**Code: A valid user model**
|
|
236
|
-
```typescript
|
|
237
|
-
import { Model } from '@travetto/model';
|
|
238
|
-
import { RegisteredPrincipal } from '@travetto/auth';
|
|
239
|
-
|
|
240
|
-
@Model()
|
|
241
|
-
export class User implements RegisteredPrincipal {
|
|
242
|
-
id: string;
|
|
243
|
-
source: string;
|
|
244
|
-
details: Record<string, unknown>;
|
|
245
|
-
password?: string;
|
|
246
|
-
salt: string;
|
|
247
|
-
hash: string;
|
|
248
|
-
resetToken?: string;
|
|
249
|
-
resetExpires?: Date;
|
|
250
|
-
permissions: string[];
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## Configuration
|
|
255
|
-
|
|
256
|
-
Additionally, there exists a common practice of mapping various external security principals into a local contract. These external identities, as provided from countless authentication schemes, need to be homogenized for use. This has been handled in other frameworks by using external configuration, and creating a mapping between the two set of fields. Within this module, the mappings are defined as functions in which you can translate to the model from an identity or to an identity from a model.
|
|
257
|
-
|
|
258
|
-
**Code: Principal Source configuration**
|
|
259
|
-
```typescript
|
|
260
|
-
import { InjectableFactory } from '@travetto/di';
|
|
261
|
-
import { ModelAuthService, RegisteredPrincipal } from '@travetto/auth';
|
|
262
|
-
import { ModelCrudSupport } from '@travetto/model';
|
|
263
|
-
|
|
264
|
-
import { User } from './model';
|
|
265
|
-
|
|
266
|
-
class AuthConfig {
|
|
267
|
-
@InjectableFactory()
|
|
268
|
-
static getModelAuthService(svc: ModelCrudSupport) {
|
|
269
|
-
return new ModelAuthService(
|
|
270
|
-
svc,
|
|
271
|
-
User,
|
|
272
|
-
(u: User) => ({ // This converts User to a RegisteredPrincipal
|
|
273
|
-
source: 'model',
|
|
274
|
-
provider: 'model',
|
|
275
|
-
id: u.id,
|
|
276
|
-
permissions: u.permissions,
|
|
277
|
-
hash: u.hash,
|
|
278
|
-
salt: u.salt,
|
|
279
|
-
resetToken: u.resetToken,
|
|
280
|
-
resetExpires: u.resetExpires,
|
|
281
|
-
password: u.password,
|
|
282
|
-
details: u,
|
|
283
|
-
}),
|
|
284
|
-
(u: Partial<RegisteredPrincipal>) => User.from(({ // This converts a RegisteredPrincipal to a User
|
|
285
|
-
id: u.id,
|
|
286
|
-
permissions: [...(u.permissions || [])],
|
|
287
|
-
hash: u.hash,
|
|
288
|
-
salt: u.salt,
|
|
289
|
-
resetToken: u.resetToken,
|
|
290
|
-
resetExpires: u.resetExpires,
|
|
291
|
-
})
|
|
292
|
-
)
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
**Code: Sample usage**
|
|
299
|
-
```typescript
|
|
300
|
-
import { AppError } from '@travetto/base';
|
|
301
|
-
import { Injectable, Inject } from '@travetto/di';
|
|
302
|
-
import { ModelAuthService } from '@travetto/auth';
|
|
153
|
+
* Admin
|
|
154
|
+
* !Editor
|
|
155
|
+
* Owner+Author
|
|
303
156
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
private auth: ModelAuthService<User>;
|
|
311
|
-
|
|
312
|
-
async authenticate(identity: User) {
|
|
313
|
-
try {
|
|
314
|
-
return await this.auth.authenticate(identity);
|
|
315
|
-
} catch (err) {
|
|
316
|
-
if (err instanceof AppError && err.category === 'notfound') {
|
|
317
|
-
return await this.auth.register(identity);
|
|
318
|
-
} else {
|
|
319
|
-
throw err;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
```
|
|
157
|
+
The code would check the list in order, which would result in the following logic:
|
|
158
|
+
|
|
159
|
+
* If the user is an admin, always allow
|
|
160
|
+
* If the user has the editor role, deny
|
|
161
|
+
* If the user is both an owner and an author allow
|
|
162
|
+
* By default, deny due to the presence of positive checks
|
package/index.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
export * from './src/util';
|
|
2
2
|
export * from './src/types/authenticator';
|
|
3
3
|
export * from './src/types/authorizer';
|
|
4
|
-
export * from './src/types/principal';
|
|
5
|
-
|
|
6
|
-
// Named export needed for proxying
|
|
7
|
-
export { ModelAuthService, RegisteredPrincipal } from './src/extension/model';
|
|
4
|
+
export * from './src/types/principal';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/auth",
|
|
3
3
|
"displayName": "Authentication",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "3.0.0-rc.1",
|
|
5
5
|
"description": "Authentication scaffolding for the travetto framework",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"authentication",
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"index.ts",
|
|
19
|
-
"src"
|
|
20
|
-
"test-support"
|
|
19
|
+
"src"
|
|
21
20
|
],
|
|
22
21
|
"main": "index.ts",
|
|
23
22
|
"repository": {
|
|
@@ -25,10 +24,7 @@
|
|
|
25
24
|
"directory": "module/auth"
|
|
26
25
|
},
|
|
27
26
|
"dependencies": {
|
|
28
|
-
"@travetto/base": "^
|
|
29
|
-
},
|
|
30
|
-
"optionalPeerDependencies": {
|
|
31
|
-
"@travetto/model": "^2.2.4"
|
|
27
|
+
"@travetto/base": "^3.0.0-rc.1"
|
|
32
28
|
},
|
|
33
29
|
"private": false,
|
|
34
30
|
"publishConfig": {
|
package/src/internal/types.ts
CHANGED
|
@@ -7,7 +7,8 @@ import { Principal } from './principal';
|
|
|
7
7
|
*/
|
|
8
8
|
export interface Authenticator<T = unknown, P extends Principal = Principal, C = unknown> {
|
|
9
9
|
/**
|
|
10
|
-
* Verify the payload,
|
|
10
|
+
* Verify the payload, ensuring the payload is correctly identified.
|
|
11
|
+
*
|
|
11
12
|
* @returns Valid principal if authenticated
|
|
12
13
|
* @returns undefined if authentication is valid, but incomplete (multi-step)
|
|
13
14
|
* @throws AppError if authentication fails
|
package/src/types/principal.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* A user principal, including permissions and details
|
|
3
3
|
*
|
|
4
4
|
* @concrete ../internal/types:PrincipalTarget
|
|
5
|
-
* @augments `@trv:rest/Context`
|
|
5
|
+
* @augments `@trv:rest/Context`
|
|
6
6
|
*/
|
|
7
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
8
|
export interface Principal<D = { [key: string]: any }> {
|
package/src/util.ts
CHANGED
|
@@ -5,85 +5,32 @@ import { AppError, Util } from '@travetto/base';
|
|
|
5
5
|
|
|
6
6
|
const pbkdf2 = util.promisify(crypto.pbkdf2);
|
|
7
7
|
|
|
8
|
-
type PermSet = Set<string> | ReadonlySet<string>;
|
|
9
|
-
|
|
10
|
-
type PermissionChecker = {
|
|
11
|
-
all: (perms: PermSet) => boolean;
|
|
12
|
-
any: (perms: PermSet) => boolean;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
type PermissionCheckerSet = {
|
|
16
|
-
includes: (perms: PermSet) => boolean;
|
|
17
|
-
excludes: (perms: PermSet) => boolean;
|
|
18
|
-
check: (value: PermSet) => boolean;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
8
|
/**
|
|
22
9
|
* Standard auth utilities
|
|
23
10
|
*/
|
|
24
11
|
export class AuthUtil {
|
|
25
12
|
|
|
26
|
-
static #
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
* @param perms Set of permissions to check
|
|
33
|
-
* @param defaultIfEmpty If no perms passed, default to empty
|
|
34
|
-
*/
|
|
35
|
-
static #buildChecker(perms: Iterable<string>, defaultIfEmpty: boolean): PermissionChecker {
|
|
36
|
-
const permArr = [...perms].map(x => x.toLowerCase());
|
|
37
|
-
let all = (_: PermSet): boolean => defaultIfEmpty;
|
|
38
|
-
let any = (_: PermSet): boolean => defaultIfEmpty;
|
|
39
|
-
if (permArr.length) {
|
|
40
|
-
all = (uPerms: PermSet): boolean => permArr.every(x => uPerms.has(x));
|
|
41
|
-
any = (uPerms: PermSet): boolean => permArr.some(x => uPerms.has(x));
|
|
13
|
+
static #matchPermissionSet(rule: string[], perms: Set<string>): boolean {
|
|
14
|
+
for (const el of rule) {
|
|
15
|
+
if (!perms.has(el)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
42
18
|
}
|
|
43
|
-
return
|
|
19
|
+
return true;
|
|
44
20
|
}
|
|
45
21
|
|
|
46
22
|
/**
|
|
47
|
-
* Build
|
|
23
|
+
* Build matcher for role permissions in allow/deny fashion
|
|
48
24
|
*
|
|
49
|
-
* @param
|
|
50
|
-
* @param exclude Which permissions to exclude
|
|
51
|
-
* @param matchAll Whether not all permissions should be matched
|
|
25
|
+
* @param roles Roles to build matcher for
|
|
52
26
|
*/
|
|
53
|
-
static
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.#checkIncCache.set(incKey, this.#buildChecker(include, true));
|
|
59
|
-
}
|
|
60
|
-
if (!this.#checkExcCache.has(excKey)) {
|
|
61
|
-
this.#checkExcCache.set(excKey, this.#buildChecker(exclude, false));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const includes = this.#checkIncCache.get(incKey)![mode];
|
|
65
|
-
const excludes = this.#checkExcCache.get(excKey)![mode];
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
includes, excludes, check: (perms: PermSet) => includes(perms) && !excludes(perms)
|
|
69
|
-
};
|
|
27
|
+
static roleMatcher(roles: string[]): (perms: Set<string>) => boolean {
|
|
28
|
+
return Util.allowDenyMatcher<string[], [Set<string>]>(roles,
|
|
29
|
+
x => x.split('|'),
|
|
30
|
+
this.#matchPermissionSet.bind(this),
|
|
31
|
+
);
|
|
70
32
|
}
|
|
71
33
|
|
|
72
|
-
/**
|
|
73
|
-
* Build a permission checker off of an include, and exclude set
|
|
74
|
-
*
|
|
75
|
-
* @param include Which permissions to include
|
|
76
|
-
* @param exclude Which permissions to exclude
|
|
77
|
-
* @param matchAll Whether not all permissions should be matched
|
|
78
|
-
*/
|
|
79
|
-
static checkPermissions(permissions: Iterable<string>, include: Iterable<string>, exclude: Iterable<string>, mode: 'all' | 'any' = 'any'): void {
|
|
80
|
-
const { check } = this.permissionChecker(include, exclude, mode);
|
|
81
|
-
if (!check(!(permissions instanceof Set) ? new Set(permissions) : permissions)) {
|
|
82
|
-
throw new AppError('Insufficient permissions', 'permissions');
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
34
|
/**
|
|
88
35
|
* Generate a hash for a given value
|
|
89
36
|
*
|
package/src/extension/model.ts
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
// @file-if @travetto/model
|
|
2
|
-
import { AppError, Util, Class } from '@travetto/base';
|
|
3
|
-
import { ModelCrudSupport, ModelType, NotFoundError, OptionalId } from '@travetto/model';
|
|
4
|
-
import { EnvUtil } from '@travetto/boot';
|
|
5
|
-
import { isStorageSupported } from '@travetto/model/src/internal/service/common';
|
|
6
|
-
|
|
7
|
-
import { Principal } from '../types/principal';
|
|
8
|
-
import { Authenticator } from '../types/authenticator';
|
|
9
|
-
import { Authorizer } from '../types/authorizer';
|
|
10
|
-
import { AuthUtil } from '../util';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A set of registration data
|
|
14
|
-
*/
|
|
15
|
-
export interface RegisteredPrincipal extends Principal {
|
|
16
|
-
/**
|
|
17
|
-
* Password hash
|
|
18
|
-
*/
|
|
19
|
-
hash?: string;
|
|
20
|
-
/**
|
|
21
|
-
* Password salt
|
|
22
|
-
*/
|
|
23
|
-
salt?: string;
|
|
24
|
-
/**
|
|
25
|
-
* Temporary Reset Token
|
|
26
|
-
*/
|
|
27
|
-
resetToken?: string;
|
|
28
|
-
/**
|
|
29
|
-
* End date for the reset token
|
|
30
|
-
*/
|
|
31
|
-
resetExpires?: Date;
|
|
32
|
-
/**
|
|
33
|
-
* The actual password, only used on password set/update
|
|
34
|
-
*/
|
|
35
|
-
password?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* A model-based auth service
|
|
40
|
-
*/
|
|
41
|
-
export class ModelAuthService<T extends ModelType> implements
|
|
42
|
-
Authenticator<T, RegisteredPrincipal>,
|
|
43
|
-
Authorizer<RegisteredPrincipal>
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
#modelService: ModelCrudSupport;
|
|
47
|
-
#cls: Class<T>;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Build a Model Principal Source
|
|
51
|
-
*
|
|
52
|
-
* @param cls Model class for the principal
|
|
53
|
-
* @param toPrincipal Convert a model to an principal
|
|
54
|
-
* @param fromPrincipal Convert an identity to the model
|
|
55
|
-
*/
|
|
56
|
-
constructor(
|
|
57
|
-
modelService: ModelCrudSupport,
|
|
58
|
-
cls: Class<T>,
|
|
59
|
-
public toPrincipal: (t: T) => RegisteredPrincipal,
|
|
60
|
-
public fromPrincipal: (t: Partial<RegisteredPrincipal>) => Partial<T>,
|
|
61
|
-
) {
|
|
62
|
-
this.#modelService = modelService;
|
|
63
|
-
this.#cls = cls;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Retrieve user by id
|
|
68
|
-
* @param userId The user id to retrieve
|
|
69
|
-
*/
|
|
70
|
-
async #retrieve(userId: string): Promise<T> {
|
|
71
|
-
return await this.#modelService.get<T>(this.#cls, userId);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Convert identity to a principal
|
|
76
|
-
* @param ident The registered identity to resolve
|
|
77
|
-
*/
|
|
78
|
-
async #resolvePrincipal(ident: RegisteredPrincipal): Promise<RegisteredPrincipal> {
|
|
79
|
-
const user = await this.#retrieve(ident.id);
|
|
80
|
-
return this.toPrincipal(user);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Authenticate password for model id
|
|
85
|
-
* @param userId The user id to authenticate against
|
|
86
|
-
* @param password The password to authenticate against
|
|
87
|
-
*/
|
|
88
|
-
async #authenticate(userId: string, password: string): Promise<RegisteredPrincipal> {
|
|
89
|
-
const ident = await this.#resolvePrincipal({ id: userId, details: {} });
|
|
90
|
-
|
|
91
|
-
const hash = await AuthUtil.generateHash(password, ident.salt!);
|
|
92
|
-
if (hash !== ident.hash) {
|
|
93
|
-
throw new AppError('Invalid password', 'authentication');
|
|
94
|
-
} else {
|
|
95
|
-
delete ident.password;
|
|
96
|
-
return ident;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async postConstruct(): Promise<void> {
|
|
101
|
-
if (isStorageSupported(this.#modelService) && EnvUtil.isDynamic()) {
|
|
102
|
-
await this.#modelService.createModel?.(this.#cls);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Register a user
|
|
108
|
-
* @param user The user to register
|
|
109
|
-
*/
|
|
110
|
-
async register(user: OptionalId<T>): Promise<T> {
|
|
111
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
112
|
-
const ident = this.toPrincipal(user as T);
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
if (ident.id) {
|
|
116
|
-
await this.#retrieve(ident.id);
|
|
117
|
-
throw new AppError('That id is already taken.', 'data');
|
|
118
|
-
}
|
|
119
|
-
} catch (err) {
|
|
120
|
-
if (!(err instanceof NotFoundError)) {
|
|
121
|
-
throw err;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const fields = await AuthUtil.generatePassword(ident.password!);
|
|
126
|
-
|
|
127
|
-
ident.password = undefined; // Clear it out on set
|
|
128
|
-
|
|
129
|
-
Object.assign(user, this.fromPrincipal(fields));
|
|
130
|
-
|
|
131
|
-
const res: T = await this.#modelService.create(this.#cls, user);
|
|
132
|
-
return res;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Change a password
|
|
137
|
-
* @param userId The user id to affect
|
|
138
|
-
* @param password The new password
|
|
139
|
-
* @param oldPassword The old password
|
|
140
|
-
*/
|
|
141
|
-
async changePassword(userId: string, password: string, oldPassword?: string): Promise<T> {
|
|
142
|
-
const user = await this.#retrieve(userId);
|
|
143
|
-
const ident = this.toPrincipal(user);
|
|
144
|
-
|
|
145
|
-
if (oldPassword === ident.resetToken) {
|
|
146
|
-
if (ident.resetExpires && ident.resetExpires.getTime() < Date.now()) {
|
|
147
|
-
throw new AppError('Reset token has expired', 'data');
|
|
148
|
-
}
|
|
149
|
-
} else if (oldPassword !== undefined) {
|
|
150
|
-
const pw = await AuthUtil.generateHash(oldPassword, ident.salt!);
|
|
151
|
-
if (pw !== ident.hash) {
|
|
152
|
-
throw new AppError('Old password is required to change', 'authentication');
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const fields = await AuthUtil.generatePassword(password);
|
|
157
|
-
Object.assign(user, this.fromPrincipal(fields));
|
|
158
|
-
|
|
159
|
-
return await this.#modelService.update(this.#cls, user);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Generate a reset token
|
|
164
|
-
* @param userId The user to reset for
|
|
165
|
-
*/
|
|
166
|
-
async generateResetToken(userId: string): Promise<RegisteredPrincipal> {
|
|
167
|
-
const user = await this.#retrieve(userId);
|
|
168
|
-
const ident = this.toPrincipal(user);
|
|
169
|
-
const salt = await Util.uuid();
|
|
170
|
-
|
|
171
|
-
ident.resetToken = await AuthUtil.generateHash(Util.uuid(), salt, 25000, 32);
|
|
172
|
-
ident.resetExpires = Util.timeFromNow('1h');
|
|
173
|
-
|
|
174
|
-
Object.assign(user, this.fromPrincipal(ident));
|
|
175
|
-
|
|
176
|
-
await this.#modelService.update(this.#cls, user);
|
|
177
|
-
|
|
178
|
-
return ident;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Authorize principal into known user
|
|
183
|
-
* @param principal
|
|
184
|
-
* @returns Authorized principal
|
|
185
|
-
*/
|
|
186
|
-
authorize(principal: RegisteredPrincipal): Promise<RegisteredPrincipal> {
|
|
187
|
-
return this.#resolvePrincipal(principal);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Authenticate entity into a principal
|
|
192
|
-
* @param payload
|
|
193
|
-
* @returns Authenticated principal
|
|
194
|
-
*/
|
|
195
|
-
authenticate(payload: T): Promise<RegisteredPrincipal> {
|
|
196
|
-
const { id, password } = this.toPrincipal(payload);
|
|
197
|
-
return this.#authenticate(id, password!);
|
|
198
|
-
}
|
|
199
|
-
}
|
package/test-support/model.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// @file-if @travetto/model
|
|
2
|
-
import * as assert from 'assert';
|
|
3
|
-
|
|
4
|
-
import { AppError, Class } from '@travetto/base';
|
|
5
|
-
import { Suite, Test } from '@travetto/test';
|
|
6
|
-
import { Inject, InjectableFactory } from '@travetto/di';
|
|
7
|
-
import { ModelCrudSupport, Model } from '@travetto/model';
|
|
8
|
-
import { InjectableSuite } from '@travetto/di/test-support/suite';
|
|
9
|
-
import { ModelSuite } from '@travetto/model/test-support/suite';
|
|
10
|
-
|
|
11
|
-
import { ModelAuthService, RegisteredPrincipal } from '..';
|
|
12
|
-
|
|
13
|
-
export const TestModelSvcⲐ = Symbol.for('@trv:auth/test-model-svc');
|
|
14
|
-
|
|
15
|
-
@Model({ autoCreate: false })
|
|
16
|
-
class User implements RegisteredPrincipal {
|
|
17
|
-
id: string;
|
|
18
|
-
password?: string;
|
|
19
|
-
salt?: string;
|
|
20
|
-
hash?: string;
|
|
21
|
-
resetToken?: string;
|
|
22
|
-
resetExpires?: Date;
|
|
23
|
-
permissions?: string[];
|
|
24
|
-
details: Record<string, unknown>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
class TestConfig {
|
|
28
|
-
@InjectableFactory()
|
|
29
|
-
static getAuthService(@Inject(TestModelSvcⲐ) svc: ModelCrudSupport): ModelAuthService<User> {
|
|
30
|
-
const src = new ModelAuthService<User>(
|
|
31
|
-
svc,
|
|
32
|
-
User,
|
|
33
|
-
u => ({ ...u, details: u, source: 'model' }),
|
|
34
|
-
reg => User.from({ ...reg })
|
|
35
|
-
);
|
|
36
|
-
return src;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
@Suite()
|
|
41
|
-
@ModelSuite()
|
|
42
|
-
@InjectableSuite()
|
|
43
|
-
export abstract class AuthModelServiceSuite {
|
|
44
|
-
|
|
45
|
-
serviceClass: Class;
|
|
46
|
-
configClass: Class;
|
|
47
|
-
|
|
48
|
-
@Inject()
|
|
49
|
-
authService: ModelAuthService<User>;
|
|
50
|
-
|
|
51
|
-
@Inject(TestModelSvcⲐ)
|
|
52
|
-
svc: ModelCrudSupport;
|
|
53
|
-
|
|
54
|
-
@Test()
|
|
55
|
-
async register() {
|
|
56
|
-
const pre = User.from({
|
|
57
|
-
password: 'bob',
|
|
58
|
-
details: {}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
const user = await this.authService.register(pre);
|
|
62
|
-
assert.ok(user.hash);
|
|
63
|
-
assert.ok(user.id);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
@Test()
|
|
67
|
-
async authenticate() {
|
|
68
|
-
const pre = User.from({
|
|
69
|
-
id: this.svc.uuid(),
|
|
70
|
-
password: 'bob',
|
|
71
|
-
details: {}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await this.authService.authenticate(pre);
|
|
76
|
-
assert.fail('Should not have gotten here');
|
|
77
|
-
} catch (err) {
|
|
78
|
-
if (err instanceof AppError && err.category === 'notfound') {
|
|
79
|
-
const user = await this.authService.register(pre);
|
|
80
|
-
assert.ok(user.hash);
|
|
81
|
-
assert.ok(user.id);
|
|
82
|
-
} else {
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
await assert.doesNotReject(() => this.authService.authenticate(pre));
|
|
88
|
-
}
|
|
89
|
-
}
|