@travetto/auth-model 2.0.0-alpha.3 → 3.0.0-rc.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/README.md CHANGED
@@ -1,47 +1,49 @@
1
- <!-- This file was generated by the framweork and should not be modified directly -->
2
- <!-- Please modify https://github.com/travetto/travetto/tree/master/module/auth-model/doc.ts and execute "npm run docs" to rebuild -->
3
- # Model Auth Source
4
- ## Model-based authentication and registration support for the travetto framework
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 provides the integration between the [Authentication](https://github.com/travetto/travetto/tree/master/module/auth#readme "Authentication scaffolding for the travetto framework") module and the [Data Modeling Support](https://github.com/travetto/travetto/tree/master/module/model#readme "Datastore abstraction for core operations.").
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 an [CRUD](https://github.com/travetto/travetto/tree/master/module/model/src/service/crud.ts#L10) 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/master/module/model/src/service/crud.ts#L10), or you can roll your own.
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/master/module/model/src/service/crud.ts#L10):
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/master/module/model-dynamodb#readme "DynamoDB backing for the travetto model module.") - @travetto/model-dynamodb
23
- * [Elasticsearch Model Source](https://github.com/travetto/travetto/tree/master/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/master/module/model-firestore#readme "Firestore backing for the travetto model module.") @travetto/model-firestore
25
- * [MongoDB Model Support](https://github.com/travetto/travetto/tree/master/module/model-mongo#readme "Mongo backing for the travetto model module.") @travetto/model-mongo
26
- * [Redis Model Support](https://github.com/travetto/travetto/tree/master/module/model-redis#readme "Redis backing for the travetto model module.") @travetto/model-redis
27
- * [S3 Model Support](https://github.com/travetto/travetto/tree/master/module/model-s3#readme "S3 backing for the travetto model module.") @travetto/model-s3
28
- * [SQL Model Service](https://github.com/travetto/travetto/tree/master/module/model-sql#readme "SQL backing for the travetto model module, with real-time modeling support for SQL schemas.") @travetto/model-sql
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 [RegisteredIdentity](https://github.com/travetto/travetto/tree/master/module/auth-model/src/identity.ts#L6)
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 identity extends the base concept of an identity, by adding in additional fields needed for local registration, specifically password management information.
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 Identity**
36
+ **Code: Registered Principal**
35
37
  ```typescript
36
- export interface RegisteredIdentity extends Identity {
38
+ export interface RegisteredPrincipal extends Principal {
37
39
  /**
38
40
  * Password hash
39
41
  */
40
- hash: string;
42
+ hash?: string;
41
43
  /**
42
44
  * Password salt
43
45
  */
44
- salt: string;
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, BaseModel } from '@travetto/model';
63
- import { RegisteredIdentity } from '@travetto/auth-model';
64
+ import { Model } from '@travetto/model';
65
+ import { RegisteredPrincipal } from '@travetto/auth-model';
64
66
 
65
67
  @Model()
66
- export class User extends BaseModel implements RegisteredIdentity {
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 homogeonized 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.
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 { ModelPrincipalSource, RegisteredIdentity } from '@travetto/auth-model';
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 getModelPrincipalSource() {
93
- return new ModelPrincipalSource(
95
+ static getModelAuthService(svc: ModelCrudSupport) {
96
+ return new ModelAuthService(
97
+ svc,
94
98
  User,
95
- (u: User) => ({ // This converts User to a RegisteredIdentity
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<RegisteredIdentity>) => User.from(({ // This converts a RegisteredIdentity to a User
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 { ModelPrincipalSource } from '@travetto/auth-model';
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: ModelPrincipalSource<User>;
137
+ private auth: ModelAuthService<User>;
134
138
 
135
139
  async authenticate(identity: User) {
136
140
  try {
137
- return await this.auth.authenticate(identity.id!, identity.password!);
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
@@ -1,4 +1 @@
1
- export * from './src/identity';
2
- export * from './src/principal';
3
- // Named export needed for proxying
4
- export { ModelIdentitySource } from './src/extension/auth-rest';
1
+ export * from './src/model';
package/package.json CHANGED
@@ -1,39 +1,36 @@
1
1
  {
2
- "author": {
3
- "email": "travetto.framework@gmail.com",
4
- "name": "Travetto Framework"
5
- },
6
- "publishConfig": {
7
- "access": "public"
8
- },
9
- "dependencies": {
10
- "@travetto/auth": "^2.0.0-alpha.2",
11
- "@travetto/model": "^2.0.0-alpha.3"
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.2",
5
+ "description": "Authentication model support for the travetto framework",
19
6
  "keywords": [
20
7
  "authentication",
21
8
  "model",
22
9
  "travetto",
23
- "registration"
10
+ "typescript"
24
11
  ],
12
+ "homepage": "https://travetto.io",
25
13
  "license": "MIT",
26
- "main": "index.ts",
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
- "name": "@travetto/auth-model",
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
- "version": "2.0.0-alpha.3",
38
- "gitHead": "2f2ca49b7cb4cb89ba7f565b8cf2bd5d63fb0696"
28
+ "dependencies": {
29
+ "@travetto/auth": "^3.0.0-rc.0",
30
+ "@travetto/model": "^3.0.0-rc.2"
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
- }
@@ -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
- }