@travetto/auth-model 2.0.0-alpha.2 → 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 +36 -33
- package/index.ts +1 -4
- package/package.json +19 -22
- package/src/model.ts +194 -0
- package/test-support/model.ts +88 -0
- package/src/extension/auth-rest.ts +0 -21
- package/src/identity.ts +0 -27
- package/src/principal.ts +0 -145
- package/test-support/service.ts +0 -85
package/README.md
CHANGED
|
@@ -1,47 +1,49 @@
|
|
|
1
|
-
<!-- This file was generated by
|
|
2
|
-
<!-- Please modify https://github.com/travetto/travetto/tree/
|
|
3
|
-
# Model
|
|
4
|
-
##
|
|
1
|
+
<!-- This file was generated by @travetto/doc and should not be modified directly -->
|
|
2
|
+
<!-- Please modify https://github.com/travetto/travetto/tree/main/module/auth-model/doc.ts and execute "npx trv doc" to rebuild -->
|
|
3
|
+
# Authentication Model
|
|
4
|
+
## Authentication model support for the travetto framework
|
|
5
5
|
|
|
6
6
|
**Install: @travetto/auth-model**
|
|
7
7
|
```bash
|
|
8
8
|
npm install @travetto/auth-model
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
This module
|
|
11
|
+
This module 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.").
|
|
12
12
|
|
|
13
|
-
The asset module requires
|
|
13
|
+
The asset module requires a [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11)-model 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.
|
|
14
14
|
|
|
15
15
|
**Install: provider**
|
|
16
16
|
```bash
|
|
17
17
|
npm install @travetto/model-{provider}
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Currently, the following are packages that provide [CRUD](https://github.com/travetto/travetto/tree/
|
|
20
|
+
Currently, the following are packages that provide [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11):
|
|
21
21
|
|
|
22
|
-
* [DynamoDB Model Support](https://github.com/travetto/travetto/tree/
|
|
23
|
-
* [Elasticsearch Model Source](https://github.com/travetto/travetto/tree/
|
|
24
|
-
* [Firestore Model Support](https://github.com/travetto/travetto/tree/
|
|
25
|
-
* [MongoDB Model Support](https://github.com/travetto/travetto/tree/
|
|
26
|
-
* [Redis Model Support](https://github.com/travetto/travetto/tree/
|
|
27
|
-
* [S3 Model Support](https://github.com/travetto/travetto/tree/
|
|
28
|
-
* [
|
|
22
|
+
* [DynamoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-dynamodb#readme "DynamoDB backing for the travetto model module.") - @travetto/model-dynamodb
|
|
23
|
+
* [Elasticsearch Model Source](https://github.com/travetto/travetto/tree/main/module/model-elasticsearch#readme "Elasticsearch backing for the travetto model module, with real-time modeling support for Elasticsearch mappings.") @travetto/model-elasticsearch
|
|
24
|
+
* [Firestore Model Support](https://github.com/travetto/travetto/tree/main/module/model-firestore#readme "Firestore backing for the travetto model module.") @travetto/model-firestore
|
|
25
|
+
* [MongoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-mongo#readme "Mongo backing for the travetto model module.") @travetto/model-mongo
|
|
26
|
+
* [Redis Model Support](https://github.com/travetto/travetto/tree/main/module/model-redis#readme "Redis backing for the travetto model module.") @travetto/model-redis
|
|
27
|
+
* [S3 Model Support](https://github.com/travetto/travetto/tree/main/module/model-s3#readme "S3 backing for the travetto model module.") @travetto/model-s3
|
|
28
|
+
* [MySQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-mysql#readme "MySQL backing for the travetto model module, with real-time modeling support for SQL schemas.") @travetto/model-mysql
|
|
29
|
+
* [PostgreSQL Model Service](https://github.com/travetto/travetto/tree/main/module/model-postgres#readme "PostgreSQL backing for the travetto model module, with real-time modeling support for SQL schemas.") @travetto/model-postgres
|
|
30
|
+
* [SQLite Model Service](https://github.com/travetto/travetto/tree/main/module/model-sqlite#readme "SQLite backing for the travetto model module, with real-time modeling support for SQL schemas.") @travetto/model-sqlite
|
|
29
31
|
|
|
30
|
-
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 [
|
|
32
|
+
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-model/src/model.ts#L10)
|
|
31
33
|
|
|
32
|
-
A registered
|
|
34
|
+
A registered principal extends the base concept of an principal, by adding in additional fields needed for local registration, specifically password management information.
|
|
33
35
|
|
|
34
|
-
**Code: Registered
|
|
36
|
+
**Code: Registered Principal**
|
|
35
37
|
```typescript
|
|
36
|
-
export interface
|
|
38
|
+
export interface RegisteredPrincipal extends Principal {
|
|
37
39
|
/**
|
|
38
40
|
* Password hash
|
|
39
41
|
*/
|
|
40
|
-
hash
|
|
42
|
+
hash?: string;
|
|
41
43
|
/**
|
|
42
44
|
* Password salt
|
|
43
45
|
*/
|
|
44
|
-
salt
|
|
46
|
+
salt?: string;
|
|
45
47
|
/**
|
|
46
48
|
* Temporary Reset Token
|
|
47
49
|
*/
|
|
@@ -59,11 +61,11 @@ export interface RegisteredIdentity extends Identity {
|
|
|
59
61
|
|
|
60
62
|
**Code: A valid user model**
|
|
61
63
|
```typescript
|
|
62
|
-
import { Model
|
|
63
|
-
import {
|
|
64
|
+
import { Model } from '@travetto/model';
|
|
65
|
+
import { RegisteredPrincipal } from '@travetto/auth-model';
|
|
64
66
|
|
|
65
67
|
@Model()
|
|
66
|
-
export class User
|
|
68
|
+
export class User implements RegisteredPrincipal {
|
|
67
69
|
id: string;
|
|
68
70
|
source: string;
|
|
69
71
|
details: Record<string, unknown>;
|
|
@@ -78,21 +80,23 @@ export class User extends BaseModel implements RegisteredIdentity {
|
|
|
78
80
|
|
|
79
81
|
## Configuration
|
|
80
82
|
|
|
81
|
-
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
|
|
83
|
+
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.
|
|
82
84
|
|
|
83
85
|
**Code: Principal Source configuration**
|
|
84
86
|
```typescript
|
|
85
87
|
import { InjectableFactory } from '@travetto/di';
|
|
86
|
-
import {
|
|
88
|
+
import { ModelAuthService, RegisteredPrincipal } from '@travetto/auth-model';
|
|
89
|
+
import { ModelCrudSupport } from '@travetto/model';
|
|
87
90
|
|
|
88
91
|
import { User } from './model';
|
|
89
92
|
|
|
90
93
|
class AuthConfig {
|
|
91
94
|
@InjectableFactory()
|
|
92
|
-
static
|
|
93
|
-
return new
|
|
95
|
+
static getModelAuthService(svc: ModelCrudSupport) {
|
|
96
|
+
return new ModelAuthService(
|
|
97
|
+
svc,
|
|
94
98
|
User,
|
|
95
|
-
(u: User) => ({ // This converts User to a
|
|
99
|
+
(u: User) => ({ // This converts User to a RegisteredPrincipal
|
|
96
100
|
source: 'model',
|
|
97
101
|
provider: 'model',
|
|
98
102
|
id: u.id,
|
|
@@ -104,7 +108,7 @@ class AuthConfig {
|
|
|
104
108
|
password: u.password,
|
|
105
109
|
details: u,
|
|
106
110
|
}),
|
|
107
|
-
(u: Partial<
|
|
111
|
+
(u: Partial<RegisteredPrincipal>) => User.from(({ // This converts a RegisteredPrincipal to a User
|
|
108
112
|
id: u.id,
|
|
109
113
|
permissions: [...(u.permissions || [])],
|
|
110
114
|
hash: u.hash,
|
|
@@ -122,7 +126,7 @@ class AuthConfig {
|
|
|
122
126
|
```typescript
|
|
123
127
|
import { AppError } from '@travetto/base';
|
|
124
128
|
import { Injectable, Inject } from '@travetto/di';
|
|
125
|
-
import {
|
|
129
|
+
import { ModelAuthService } from '@travetto/auth-model';
|
|
126
130
|
|
|
127
131
|
import { User } from './model';
|
|
128
132
|
|
|
@@ -130,11 +134,11 @@ import { User } from './model';
|
|
|
130
134
|
class UserService {
|
|
131
135
|
|
|
132
136
|
@Inject()
|
|
133
|
-
private auth:
|
|
137
|
+
private auth: ModelAuthService<User>;
|
|
134
138
|
|
|
135
139
|
async authenticate(identity: User) {
|
|
136
140
|
try {
|
|
137
|
-
return await this.auth.authenticate(identity
|
|
141
|
+
return await this.auth.authenticate(identity);
|
|
138
142
|
} catch (err) {
|
|
139
143
|
if (err instanceof AppError && err.category === 'notfound') {
|
|
140
144
|
return await this.auth.register(identity);
|
|
@@ -145,4 +149,3 @@ class UserService {
|
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
```
|
|
148
|
-
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,39 +1,36 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"publishConfig": {
|
|
7
|
-
"access": "public"
|
|
8
|
-
},
|
|
9
|
-
"dependencies": {
|
|
10
|
-
"@travetto/auth": "^2.0.0-alpha.2",
|
|
11
|
-
"@travetto/model": "^2.0.0-alpha.2"
|
|
12
|
-
},
|
|
13
|
-
"title": "Model Auth Source",
|
|
14
|
-
"description": "Model-based authentication and registration support for the travetto framework",
|
|
15
|
-
"optionalPeerDependencies": {
|
|
16
|
-
"@travetto/auth-rest": "^2.0.0-alpha.0"
|
|
17
|
-
},
|
|
18
|
-
"homepage": "https://travetto.io",
|
|
2
|
+
"name": "@travetto/auth-model",
|
|
3
|
+
"displayName": "Authentication Model",
|
|
4
|
+
"version": "3.0.0-rc.1",
|
|
5
|
+
"description": "Authentication model support for the travetto framework",
|
|
19
6
|
"keywords": [
|
|
20
7
|
"authentication",
|
|
21
8
|
"model",
|
|
22
9
|
"travetto",
|
|
23
|
-
"
|
|
10
|
+
"typescript"
|
|
24
11
|
],
|
|
12
|
+
"homepage": "https://travetto.io",
|
|
25
13
|
"license": "MIT",
|
|
26
|
-
"
|
|
14
|
+
"author": {
|
|
15
|
+
"email": "travetto.framework@gmail.com",
|
|
16
|
+
"name": "Travetto Framework"
|
|
17
|
+
},
|
|
27
18
|
"files": [
|
|
28
19
|
"index.ts",
|
|
29
20
|
"src",
|
|
30
21
|
"test-support"
|
|
31
22
|
],
|
|
32
|
-
"
|
|
23
|
+
"main": "index.ts",
|
|
33
24
|
"repository": {
|
|
34
25
|
"url": "https://github.com/travetto/travetto.git",
|
|
35
26
|
"directory": "module/auth-model"
|
|
36
27
|
},
|
|
37
|
-
"
|
|
38
|
-
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@travetto/auth": "^3.0.0-rc.0",
|
|
30
|
+
"@travetto/model": "^3.0.0-rc.1"
|
|
31
|
+
},
|
|
32
|
+
"private": false,
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
39
36
|
}
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { AppError, Util, Class } from '@travetto/base';
|
|
2
|
+
import { ModelCrudSupport, ModelType, NotFoundError, OptionalId } from '@travetto/model';
|
|
3
|
+
import { EnvUtil } from '@travetto/boot';
|
|
4
|
+
import { AuthUtil, Principal, Authenticator, Authorizer } from '@travetto/auth';
|
|
5
|
+
import { isStorageSupported } from '@travetto/model/src/internal/service/common';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A set of registration data
|
|
9
|
+
*/
|
|
10
|
+
export interface RegisteredPrincipal extends Principal {
|
|
11
|
+
/**
|
|
12
|
+
* Password hash
|
|
13
|
+
*/
|
|
14
|
+
hash?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Password salt
|
|
17
|
+
*/
|
|
18
|
+
salt?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Temporary Reset Token
|
|
21
|
+
*/
|
|
22
|
+
resetToken?: string;
|
|
23
|
+
/**
|
|
24
|
+
* End date for the reset token
|
|
25
|
+
*/
|
|
26
|
+
resetExpires?: Date;
|
|
27
|
+
/**
|
|
28
|
+
* The actual password, only used on password set/update
|
|
29
|
+
*/
|
|
30
|
+
password?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A model-based auth service
|
|
35
|
+
*/
|
|
36
|
+
export class ModelAuthService<T extends ModelType> implements
|
|
37
|
+
Authenticator<T, RegisteredPrincipal>,
|
|
38
|
+
Authorizer<RegisteredPrincipal>
|
|
39
|
+
{
|
|
40
|
+
|
|
41
|
+
#modelService: ModelCrudSupport;
|
|
42
|
+
#cls: Class<T>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a Model Principal Source
|
|
46
|
+
*
|
|
47
|
+
* @param cls Model class for the principal
|
|
48
|
+
* @param toPrincipal Convert a model to an principal
|
|
49
|
+
* @param fromPrincipal Convert an identity to the model
|
|
50
|
+
*/
|
|
51
|
+
constructor(
|
|
52
|
+
modelService: ModelCrudSupport,
|
|
53
|
+
cls: Class<T>,
|
|
54
|
+
public toPrincipal: (t: T) => RegisteredPrincipal,
|
|
55
|
+
public fromPrincipal: (t: Partial<RegisteredPrincipal>) => Partial<T>,
|
|
56
|
+
) {
|
|
57
|
+
this.#modelService = modelService;
|
|
58
|
+
this.#cls = cls;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Retrieve user by id
|
|
63
|
+
* @param userId The user id to retrieve
|
|
64
|
+
*/
|
|
65
|
+
async #retrieve(userId: string): Promise<T> {
|
|
66
|
+
return await this.#modelService.get<T>(this.#cls, userId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert identity to a principal
|
|
71
|
+
* @param ident The registered identity to resolve
|
|
72
|
+
*/
|
|
73
|
+
async #resolvePrincipal(ident: RegisteredPrincipal): Promise<RegisteredPrincipal> {
|
|
74
|
+
const user = await this.#retrieve(ident.id);
|
|
75
|
+
return this.toPrincipal(user);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Authenticate password for model id
|
|
80
|
+
* @param userId The user id to authenticate against
|
|
81
|
+
* @param password The password to authenticate against
|
|
82
|
+
*/
|
|
83
|
+
async #authenticate(userId: string, password: string): Promise<RegisteredPrincipal> {
|
|
84
|
+
const ident = await this.#resolvePrincipal({ id: userId, details: {} });
|
|
85
|
+
|
|
86
|
+
const hash = await AuthUtil.generateHash(password, ident.salt!);
|
|
87
|
+
if (hash !== ident.hash) {
|
|
88
|
+
throw new AppError('Invalid password', 'authentication');
|
|
89
|
+
} else {
|
|
90
|
+
delete ident.password;
|
|
91
|
+
return ident;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async postConstruct(): Promise<void> {
|
|
96
|
+
if (isStorageSupported(this.#modelService) && EnvUtil.isDynamic()) {
|
|
97
|
+
await this.#modelService.createModel?.(this.#cls);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Register a user
|
|
103
|
+
* @param user The user to register
|
|
104
|
+
*/
|
|
105
|
+
async register(user: OptionalId<T>): Promise<T> {
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
107
|
+
const ident = this.toPrincipal(user as T);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
if (ident.id) {
|
|
111
|
+
await this.#retrieve(ident.id);
|
|
112
|
+
throw new AppError('That id is already taken.', 'data');
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (!(err instanceof NotFoundError)) {
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const fields = await AuthUtil.generatePassword(ident.password!);
|
|
121
|
+
|
|
122
|
+
ident.password = undefined; // Clear it out on set
|
|
123
|
+
|
|
124
|
+
Object.assign(user, this.fromPrincipal(fields));
|
|
125
|
+
|
|
126
|
+
const res: T = await this.#modelService.create(this.#cls, user);
|
|
127
|
+
return res;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Change a password
|
|
132
|
+
* @param userId The user id to affect
|
|
133
|
+
* @param password The new password
|
|
134
|
+
* @param oldPassword The old password
|
|
135
|
+
*/
|
|
136
|
+
async changePassword(userId: string, password: string, oldPassword?: string): Promise<T> {
|
|
137
|
+
const user = await this.#retrieve(userId);
|
|
138
|
+
const ident = this.toPrincipal(user);
|
|
139
|
+
|
|
140
|
+
if (oldPassword === ident.resetToken) {
|
|
141
|
+
if (ident.resetExpires && ident.resetExpires.getTime() < Date.now()) {
|
|
142
|
+
throw new AppError('Reset token has expired', 'data');
|
|
143
|
+
}
|
|
144
|
+
} else if (oldPassword !== undefined) {
|
|
145
|
+
const pw = await AuthUtil.generateHash(oldPassword, ident.salt!);
|
|
146
|
+
if (pw !== ident.hash) {
|
|
147
|
+
throw new AppError('Old password is required to change', 'authentication');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const fields = await AuthUtil.generatePassword(password);
|
|
152
|
+
Object.assign(user, this.fromPrincipal(fields));
|
|
153
|
+
|
|
154
|
+
return await this.#modelService.update(this.#cls, user);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Generate a reset token
|
|
159
|
+
* @param userId The user to reset for
|
|
160
|
+
*/
|
|
161
|
+
async generateResetToken(userId: string): Promise<RegisteredPrincipal> {
|
|
162
|
+
const user = await this.#retrieve(userId);
|
|
163
|
+
const ident = this.toPrincipal(user);
|
|
164
|
+
const salt = await Util.uuid();
|
|
165
|
+
|
|
166
|
+
ident.resetToken = await AuthUtil.generateHash(Util.uuid(), salt, 25000, 32);
|
|
167
|
+
ident.resetExpires = Util.timeFromNow('1h');
|
|
168
|
+
|
|
169
|
+
Object.assign(user, this.fromPrincipal(ident));
|
|
170
|
+
|
|
171
|
+
await this.#modelService.update(this.#cls, user);
|
|
172
|
+
|
|
173
|
+
return ident;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Authorize principal into known user
|
|
178
|
+
* @param principal
|
|
179
|
+
* @returns Authorized principal
|
|
180
|
+
*/
|
|
181
|
+
authorize(principal: RegisteredPrincipal): Promise<RegisteredPrincipal> {
|
|
182
|
+
return this.#resolvePrincipal(principal);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Authenticate entity into a principal
|
|
187
|
+
* @param payload
|
|
188
|
+
* @returns Authenticated principal
|
|
189
|
+
*/
|
|
190
|
+
authenticate(payload: T): Promise<RegisteredPrincipal> {
|
|
191
|
+
const { id, password } = this.toPrincipal(payload);
|
|
192
|
+
return this.#authenticate(id, password!);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as assert from 'assert';
|
|
2
|
+
|
|
3
|
+
import { AppError, Class } from '@travetto/base';
|
|
4
|
+
import { Suite, Test } from '@travetto/test';
|
|
5
|
+
import { Inject, InjectableFactory } from '@travetto/di';
|
|
6
|
+
import { ModelCrudSupport, Model } from '@travetto/model';
|
|
7
|
+
import { InjectableSuite } from '@travetto/di/test-support/suite';
|
|
8
|
+
import { ModelSuite } from '@travetto/model/test-support/suite';
|
|
9
|
+
|
|
10
|
+
import { ModelAuthService, RegisteredPrincipal } from '../src/model';
|
|
11
|
+
|
|
12
|
+
export const TestModelSvcⲐ = Symbol.for('@trv:auth/test-model-svc');
|
|
13
|
+
|
|
14
|
+
@Model({ autoCreate: false })
|
|
15
|
+
class User implements RegisteredPrincipal {
|
|
16
|
+
id: string;
|
|
17
|
+
password?: string;
|
|
18
|
+
salt?: string;
|
|
19
|
+
hash?: string;
|
|
20
|
+
resetToken?: string;
|
|
21
|
+
resetExpires?: Date;
|
|
22
|
+
permissions?: string[];
|
|
23
|
+
details: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class TestConfig {
|
|
27
|
+
@InjectableFactory()
|
|
28
|
+
static getAuthService(@Inject(TestModelSvcⲐ) svc: ModelCrudSupport): ModelAuthService<User> {
|
|
29
|
+
const src = new ModelAuthService<User>(
|
|
30
|
+
svc,
|
|
31
|
+
User,
|
|
32
|
+
u => ({ ...u, details: u, source: 'model' }),
|
|
33
|
+
reg => User.from({ ...reg })
|
|
34
|
+
);
|
|
35
|
+
return src;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Suite()
|
|
40
|
+
@ModelSuite()
|
|
41
|
+
@InjectableSuite()
|
|
42
|
+
export abstract class AuthModelServiceSuite {
|
|
43
|
+
|
|
44
|
+
serviceClass: Class;
|
|
45
|
+
configClass: Class;
|
|
46
|
+
|
|
47
|
+
@Inject()
|
|
48
|
+
authService: ModelAuthService<User>;
|
|
49
|
+
|
|
50
|
+
@Inject(TestModelSvcⲐ)
|
|
51
|
+
svc: ModelCrudSupport;
|
|
52
|
+
|
|
53
|
+
@Test()
|
|
54
|
+
async register() {
|
|
55
|
+
const pre = User.from({
|
|
56
|
+
password: 'bob',
|
|
57
|
+
details: {}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const user = await this.authService.register(pre);
|
|
61
|
+
assert.ok(user.hash);
|
|
62
|
+
assert.ok(user.id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Test()
|
|
66
|
+
async authenticate() {
|
|
67
|
+
const pre = User.from({
|
|
68
|
+
id: this.svc.uuid(),
|
|
69
|
+
password: 'bob',
|
|
70
|
+
details: {}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await this.authService.authenticate(pre);
|
|
75
|
+
assert.fail('Should not have gotten here');
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err instanceof AppError && err.category === 'notfound') {
|
|
78
|
+
const user = await this.authService.register(pre);
|
|
79
|
+
assert.ok(user.hash);
|
|
80
|
+
assert.ok(user.id);
|
|
81
|
+
} else {
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await assert.doesNotReject(() => this.authService.authenticate(pre));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// @file-if @travetto/auth-rest
|
|
2
|
-
import { Request, Response } from '@travetto/rest';
|
|
3
|
-
import { ModelType } from '@travetto/model';
|
|
4
|
-
import { IdentitySource } from '@travetto/auth-rest';
|
|
5
|
-
import { Identity } from '@travetto/auth';
|
|
6
|
-
|
|
7
|
-
import { ModelPrincipalSource } from '../principal';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Provides an identity verification source in conjunction with the
|
|
11
|
-
* provided principal source.
|
|
12
|
-
*/
|
|
13
|
-
export class ModelIdentitySource<U extends ModelType> implements IdentitySource {
|
|
14
|
-
|
|
15
|
-
constructor(private source: ModelPrincipalSource<U>) { }
|
|
16
|
-
|
|
17
|
-
async authenticate(req: Request, res: Response): Promise<Identity | undefined> {
|
|
18
|
-
const ident = this.source.toIdentity(req.body);
|
|
19
|
-
return this.source.authenticate(ident.id!, ident.password!);
|
|
20
|
-
}
|
|
21
|
-
}
|
package/src/identity.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Identity } from '@travetto/auth';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* An identity that can be created/registered
|
|
5
|
-
*/
|
|
6
|
-
export interface RegisteredIdentity extends Identity {
|
|
7
|
-
/**
|
|
8
|
-
* Password hash
|
|
9
|
-
*/
|
|
10
|
-
hash: string;
|
|
11
|
-
/**
|
|
12
|
-
* Password salt
|
|
13
|
-
*/
|
|
14
|
-
salt: string;
|
|
15
|
-
/**
|
|
16
|
-
* Temporary Reset Token
|
|
17
|
-
*/
|
|
18
|
-
resetToken?: string;
|
|
19
|
-
/**
|
|
20
|
-
* End date for the reset token
|
|
21
|
-
*/
|
|
22
|
-
resetExpires?: Date;
|
|
23
|
-
/**
|
|
24
|
-
* The actual password, only used on password set/update
|
|
25
|
-
*/
|
|
26
|
-
password?: string;
|
|
27
|
-
}
|
package/src/principal.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { AppError, Util, Class } from '@travetto/base';
|
|
2
|
-
import { Inject } from '@travetto/di';
|
|
3
|
-
import { ModelCrudSupport, ModelType, NotFoundError } from '@travetto/model';
|
|
4
|
-
import { AuthContext, AuthUtil, Principal, PrincipalSource } from '@travetto/auth';
|
|
5
|
-
|
|
6
|
-
import { RegisteredIdentity } from './identity';
|
|
7
|
-
|
|
8
|
-
export const AuthModelSym = Symbol.for('@trv:auth-model/model');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* A model-based principal source
|
|
12
|
-
*/
|
|
13
|
-
export class ModelPrincipalSource<T extends ModelType> implements PrincipalSource {
|
|
14
|
-
|
|
15
|
-
@Inject(AuthModelSym)
|
|
16
|
-
private modelService: ModelCrudSupport;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Build a Model Principal Source
|
|
20
|
-
*
|
|
21
|
-
* @param cls Model class for the principal
|
|
22
|
-
* @param toIdentity Convert a model to an identity
|
|
23
|
-
* @param fromIdentity Convert an identity to the model
|
|
24
|
-
*/
|
|
25
|
-
constructor(
|
|
26
|
-
private cls: Class<T>,
|
|
27
|
-
public toIdentity: (t: T) => RegisteredIdentity,
|
|
28
|
-
public fromIdentity: (t: Partial<RegisteredIdentity>) => Partial<T>,
|
|
29
|
-
) { }
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Retrieve user by id
|
|
33
|
-
* @param userId The user id to retrieve
|
|
34
|
-
*/
|
|
35
|
-
async retrieve(userId: string) {
|
|
36
|
-
return await this.modelService.get<T>(this.cls, userId);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Convert identity to a principal
|
|
41
|
-
* @param ident The registered identity to resolve
|
|
42
|
-
*/
|
|
43
|
-
async resolvePrincipal(ident: RegisteredIdentity): Promise<Principal> {
|
|
44
|
-
const user = await this.retrieve(ident.id);
|
|
45
|
-
return this.toIdentity(user);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Authenticate password for model id
|
|
50
|
-
* @param userId The user id to authenticate against
|
|
51
|
-
* @param password The password to authenticate against
|
|
52
|
-
*/
|
|
53
|
-
async authenticate(userId: string, password: string) {
|
|
54
|
-
const user = await this.retrieve(userId);
|
|
55
|
-
const ident = await this.toIdentity(user);
|
|
56
|
-
|
|
57
|
-
const hash = await AuthUtil.generateHash(password, ident.salt);
|
|
58
|
-
if (hash !== ident.hash) {
|
|
59
|
-
throw new AppError('Invalid password', 'authentication');
|
|
60
|
-
} else {
|
|
61
|
-
delete ident.password;
|
|
62
|
-
return ident;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Register a user
|
|
68
|
-
* @param user The user to register
|
|
69
|
-
*/
|
|
70
|
-
async register(user: T) {
|
|
71
|
-
const ident = this.toIdentity(user);
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
if (ident.id) {
|
|
75
|
-
await this.retrieve(ident.id);
|
|
76
|
-
throw new AppError('That id is already taken.', 'data');
|
|
77
|
-
}
|
|
78
|
-
} catch (e) {
|
|
79
|
-
if (!(e instanceof NotFoundError)) {
|
|
80
|
-
throw e;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const fields = await AuthUtil.generatePassword(ident.password!);
|
|
85
|
-
|
|
86
|
-
ident.password = undefined; // Clear it out on set
|
|
87
|
-
|
|
88
|
-
Object.assign(user, this.fromIdentity(fields));
|
|
89
|
-
|
|
90
|
-
const res: T = await this.modelService.create(this.cls, user);
|
|
91
|
-
return res;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Change a password
|
|
96
|
-
* @param userId The user id to affet
|
|
97
|
-
* @param password The new password
|
|
98
|
-
* @param oldPassword The old password
|
|
99
|
-
*/
|
|
100
|
-
async changePassword(userId: string, password: string, oldPassword?: string) {
|
|
101
|
-
const user = await this.retrieve(userId);
|
|
102
|
-
const ident = this.toIdentity(user);
|
|
103
|
-
|
|
104
|
-
if (oldPassword !== undefined) {
|
|
105
|
-
if (oldPassword === ident.resetToken) {
|
|
106
|
-
if (ident.resetExpires && ident.resetExpires.getTime() < Date.now()) {
|
|
107
|
-
throw new AppError('Reset token has expired', 'data');
|
|
108
|
-
}
|
|
109
|
-
} else {
|
|
110
|
-
const pw = await AuthUtil.generateHash(oldPassword, ident.salt);
|
|
111
|
-
if (pw !== ident.hash) {
|
|
112
|
-
throw new AppError('Old password is required to change', 'authentication');
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const fields = await AuthUtil.generatePassword(password);
|
|
118
|
-
Object.assign(user, this.fromIdentity(fields));
|
|
119
|
-
|
|
120
|
-
return await this.modelService.update(this.cls, user);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Generate a reset token
|
|
125
|
-
* @param userId The user to reset for
|
|
126
|
-
*/
|
|
127
|
-
async generateResetToken(userId: string): Promise<RegisteredIdentity> {
|
|
128
|
-
const user = await this.retrieve(userId);
|
|
129
|
-
const ident = this.toIdentity(user);
|
|
130
|
-
const salt = await Util.uuid();
|
|
131
|
-
|
|
132
|
-
ident.resetToken = await AuthUtil.generateHash(`${new Date().getTime()}`, salt, 25000, 32);
|
|
133
|
-
ident.resetExpires = new Date(Date.now() + (60 * 60 * 1000 /* 1 hour */));
|
|
134
|
-
|
|
135
|
-
Object.assign(user, this.fromIdentity(ident));
|
|
136
|
-
|
|
137
|
-
await this.modelService.update(this.cls, user);
|
|
138
|
-
|
|
139
|
-
return ident;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async authorize(ident: RegisteredIdentity) {
|
|
143
|
-
return new AuthContext(ident, await this.resolvePrincipal(ident));
|
|
144
|
-
}
|
|
145
|
-
}
|
package/test-support/service.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import * as assert from 'assert';
|
|
2
|
-
|
|
3
|
-
import { AppError } from '@travetto/base';
|
|
4
|
-
import { Suite, Test } from '@travetto/test';
|
|
5
|
-
import { DependencyRegistry, InjectableFactory } from '@travetto/di';
|
|
6
|
-
import { BaseModelSuite } from '@travetto/model/test-support/base';
|
|
7
|
-
import { ModelCrudSupport, BaseModel, Model } from '@travetto/model';
|
|
8
|
-
|
|
9
|
-
import { ModelPrincipalSource, RegisteredIdentity } from '..';
|
|
10
|
-
import { AuthModelSym } from '../src/principal';
|
|
11
|
-
|
|
12
|
-
@Model({
|
|
13
|
-
for: AuthModelSym
|
|
14
|
-
})
|
|
15
|
-
class User extends BaseModel {
|
|
16
|
-
password?: string;
|
|
17
|
-
salt?: string;
|
|
18
|
-
hash?: string;
|
|
19
|
-
resetToken?: string;
|
|
20
|
-
resetExpires?: Date;
|
|
21
|
-
permissions?: string[];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
class TestConfig {
|
|
25
|
-
@InjectableFactory()
|
|
26
|
-
static getAuthService(): ModelPrincipalSource<User> {
|
|
27
|
-
return new ModelPrincipalSource<User>(
|
|
28
|
-
User,
|
|
29
|
-
(u) => ({
|
|
30
|
-
...(u as unknown as RegisteredIdentity),
|
|
31
|
-
details: u,
|
|
32
|
-
permissions: u.permissions ?? [],
|
|
33
|
-
source: 'model'
|
|
34
|
-
}),
|
|
35
|
-
(registered) => User.from({
|
|
36
|
-
...(registered as User)
|
|
37
|
-
})
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
@Suite()
|
|
43
|
-
export abstract class AuthModelServiceSuite extends BaseModelSuite<ModelCrudSupport> {
|
|
44
|
-
|
|
45
|
-
get principalSource() {
|
|
46
|
-
return DependencyRegistry.getInstance<ModelPrincipalSource<User>>(ModelPrincipalSource);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
@Test()
|
|
50
|
-
async register() {
|
|
51
|
-
const svc = await this.principalSource;
|
|
52
|
-
|
|
53
|
-
const pre = User.from({
|
|
54
|
-
password: 'bob'
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const user = await svc.register(pre);
|
|
58
|
-
assert.ok(user.hash);
|
|
59
|
-
assert.ok(user.id);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
@Test()
|
|
63
|
-
async authenticate() {
|
|
64
|
-
const svc = await this.principalSource;
|
|
65
|
-
|
|
66
|
-
const pre = User.from({
|
|
67
|
-
id: '5',
|
|
68
|
-
password: 'bob'
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
await svc.authenticate(pre.id!, pre.password!);
|
|
73
|
-
} catch (err) {
|
|
74
|
-
if (err instanceof AppError && err.category === 'notfound') {
|
|
75
|
-
const user = await svc.register(pre);
|
|
76
|
-
assert.ok(user.hash);
|
|
77
|
-
assert.ok(user.id);
|
|
78
|
-
} else {
|
|
79
|
-
throw err;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
await assert.doesNotReject(() => svc.authenticate(pre.id!, pre.password!));
|
|
84
|
-
}
|
|
85
|
-
}
|