@travetto/auth-web 7.1.4 → 8.0.0-alpha.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 CHANGED
@@ -52,7 +52,7 @@ export interface Authenticator<T = unknown, C = unknown, P extends Principal = P
52
52
  *
53
53
  * @returns Valid principal if authenticated
54
54
  * @returns undefined if authentication is valid, but incomplete (multi-step)
55
- * @throws AppError if authentication fails
55
+ * @throws Error if authentication fails
56
56
  */
57
57
  authenticate(payload: T, context?: C): Promise<P | undefined> | P | undefined;
58
58
  }
@@ -110,7 +110,7 @@ export class AppConfig {
110
110
  The symbol `FB_AUTH` is what will be used to reference providers at runtime. This was chosen, over `class` references due to the fact that most providers will not be defined via a new class, but via an [@InjectableFactory](https://github.com/travetto/travetto/tree/main/module/di/src/decorator.ts#L48) method.
111
111
 
112
112
  ## Maintaining Auth Context
113
- The [AuthContextInterceptor](https://github.com/travetto/travetto/tree/main/module/auth-web/src/interceptors/context.ts#L20) acts as the bridge between the [Authentication](https://github.com/travetto/travetto/tree/main/module/auth#readme "Authentication scaffolding for the Travetto framework") and [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") modules. It serves to take an authenticated principal (via the [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11)/[WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3)) and integrate it into the [AuthContext](https://github.com/travetto/travetto/tree/main/module/auth/src/context.ts#L14). Leveraging [WebAuthConfig](https://github.com/travetto/travetto/tree/main/module/auth-web/src/config.ts#L8)'s configuration allows for basic control of how the principal is encoded and decoded, primarily with the choice between using a header or a cookie, and which header, or cookie value is specifically referenced. Additionally, the encoding process allows for auto-renewing of the token (on by default). The information is encoded into the [JWT](https://jwt.io/) appropriately, and when encoding using cookies, is also set as the expiry time for the cookie.
113
+ The [AuthContextInterceptor](https://github.com/travetto/travetto/tree/main/module/auth-web/src/interceptors/context.ts#L20) acts as the bridge between the [Authentication](https://github.com/travetto/travetto/tree/main/module/auth#readme "Authentication scaffolding for the Travetto framework") and [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") modules. It serves to take an authenticated principal (via the [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11)/[WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3)) and integrate it into the [AuthContext](https://github.com/travetto/travetto/tree/main/module/auth/src/context.ts#L14). Leveraging [WebAuthConfig](https://github.com/travetto/travetto/tree/main/module/auth-web/src/config.ts#L9)'s configuration allows for basic control of how the principal is encoded and decoded, primarily with the choice between using a header or a cookie, and which header, or cookie value is specifically referenced. Additionally, the encoding process allows for auto-renewing of the token (on by default). The information is encoded into the [JWT](https://jwt.io/) appropriately, and when encoding using cookies, is also set as the expiry time for the cookie.
114
114
 
115
115
  **Note for Cookie Use:** The automatic renewal, update, seamless receipt and transmission of the [Principal](https://github.com/travetto/travetto/tree/main/module/auth/src/types/principal.ts#L7) cookie act as a light-weight session. Generally the goal is to keep the token as small as possible, but for small amounts of data, this pattern proves to be fairly sufficient at maintaining a decentralized state.
116
116
 
@@ -121,9 +121,9 @@ The [PrincipalCodec](https://github.com/travetto/travetto/tree/main/module/auth-
121
121
  import type { Jwt, Verifier, SupportedAlgorithms } from 'njwt';
122
122
 
123
123
  import { type AuthContext, AuthenticationError, type AuthToken, type Principal } from '@travetto/auth';
124
- import { Injectable, Inject } from '@travetto/di';
124
+ import { Injectable, Inject, PostConstruct } from '@travetto/di';
125
125
  import { type WebResponse, type WebRequest, type WebAsyncContext, CookieJar } from '@travetto/web';
126
- import { AppError, castTo, TimeUtil } from '@travetto/runtime';
126
+ import { RuntimeError, castTo, TimeUtil } from '@travetto/runtime';
127
127
 
128
128
  import { CommonPrincipalCodecSymbol, type PrincipalCodec } from './types.ts';
129
129
  import type { WebAuthConfig } from './config.ts';
@@ -146,7 +146,8 @@ export class JWTPrincipalCodec implements PrincipalCodec {
146
146
  #verifier: Verifier;
147
147
  #algorithm: SupportedAlgorithms = 'HS256';
148
148
 
149
- async postConstruct(): Promise<void> {
149
+ @PostConstruct()
150
+ async finalizeVerifier(): Promise<void> {
150
151
  // Weird issue with their ES module support
151
152
  const { default: { createVerifier } } = await import('njwt');
152
153
  this.#verifier = createVerifier()
@@ -186,13 +187,13 @@ export class JWTPrincipalCodec implements PrincipalCodec {
186
187
  async create(value: Principal, keyId: string = 'default'): Promise<string> {
187
188
  const entry = this.config.keyMap[keyId];
188
189
  if (!entry) {
189
- throw new AppError('Requested unknown key for signing');
190
+ throw new RuntimeError('Requested unknown key for signing');
190
191
  }
191
192
  // Weird issue with their ES module support
192
193
  const { default: { create } } = await import('njwt');
193
194
  const jwt = create({}, '-')
194
195
  .setExpiration(value.expiresAt!)
195
- .setIssuedAt(TimeUtil.asSeconds(value.issuedAt!))
196
+ .setIssuedAt(TimeUtil.duration((value.issuedAt ?? new Date()).getTime(), 's'))
196
197
  .setClaim('core', castTo({ ...value }))
197
198
  .setIssuer(value.issuer!)
198
199
  .setJti(value.sessionId!)
@@ -225,7 +226,7 @@ A trivial/sample custom [PrincipalCodec](https://github.com/travetto/travetto/tr
225
226
  import type { Principal } from '@travetto/auth';
226
227
  import type { PrincipalCodec } from '@travetto/auth-web';
227
228
  import { Injectable } from '@travetto/di';
228
- import { BinaryUtil } from '@travetto/runtime';
229
+ import { BinaryMetadataUtil } from '@travetto/runtime';
229
230
  import type { WebResponse, WebRequest } from '@travetto/web';
230
231
 
231
232
  @Injectable()
@@ -234,7 +235,7 @@ export class CustomCodec implements PrincipalCodec {
234
235
 
235
236
  decode(request: WebRequest): Promise<Principal | undefined> | Principal | undefined {
236
237
  const [userId, sig] = request.headers.get('USER_ID')?.split(':') ?? [];
237
- if (userId && sig === BinaryUtil.hash(userId + this.secret)) {
238
+ if (userId && sig === BinaryMetadataUtil.hash(userId + this.secret)) {
238
239
  let principal: Principal | undefined;
239
240
  // Lookup user from db, remote system, etc.,
240
241
  return principal;
@@ -243,7 +244,7 @@ export class CustomCodec implements PrincipalCodec {
243
244
  }
244
245
  encode(response: WebResponse, data: Principal | undefined): WebResponse {
245
246
  if (data) {
246
- response.headers.set('USER_ID', `${data.id}:${BinaryUtil.hash(data.id + this.secret)}`);
247
+ response.headers.set('USER_ID', `${data.id}:${BinaryMetadataUtil.hash(data.id + this.secret)}`);
247
248
  }
248
249
  return response;
249
250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/auth-web",
3
- "version": "7.1.4",
3
+ "version": "8.0.0-alpha.1",
4
4
  "type": "module",
5
5
  "description": "Web authentication integration support for the Travetto framework",
6
6
  "keywords": [
@@ -27,13 +27,13 @@
27
27
  "directory": "module/auth-web"
28
28
  },
29
29
  "dependencies": {
30
- "@travetto/auth": "^7.1.4",
31
- "@travetto/config": "^7.1.4",
32
- "@travetto/web": "^7.1.4",
30
+ "@travetto/auth": "^8.0.0-alpha.1",
31
+ "@travetto/config": "^8.0.0-alpha.1",
32
+ "@travetto/web": "^8.0.0-alpha.1",
33
33
  "njwt": "^2.0.1"
34
34
  },
35
35
  "peerDependencies": {
36
- "@travetto/test": "^7.1.4"
36
+ "@travetto/test": "^8.0.0-alpha.1"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@travetto/test": {
package/src/codec.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { Jwt, Verifier, SupportedAlgorithms } from 'njwt';
2
2
 
3
3
  import { type AuthContext, AuthenticationError, type AuthToken, type Principal } from '@travetto/auth';
4
- import { Injectable, Inject } from '@travetto/di';
4
+ import { Injectable, Inject, PostConstruct } from '@travetto/di';
5
5
  import { type WebResponse, type WebRequest, type WebAsyncContext, CookieJar } from '@travetto/web';
6
- import { AppError, castTo, TimeUtil } from '@travetto/runtime';
6
+ import { RuntimeError, castTo, TimeUtil } from '@travetto/runtime';
7
7
 
8
8
  import { CommonPrincipalCodecSymbol, type PrincipalCodec } from './types.ts';
9
9
  import type { WebAuthConfig } from './config.ts';
@@ -26,7 +26,8 @@ export class JWTPrincipalCodec implements PrincipalCodec {
26
26
  #verifier: Verifier;
27
27
  #algorithm: SupportedAlgorithms = 'HS256';
28
28
 
29
- async postConstruct(): Promise<void> {
29
+ @PostConstruct()
30
+ async finalizeVerifier(): Promise<void> {
30
31
  // Weird issue with their ES module support
31
32
  const { default: { createVerifier } } = await import('njwt');
32
33
  this.#verifier = createVerifier()
@@ -66,13 +67,13 @@ export class JWTPrincipalCodec implements PrincipalCodec {
66
67
  async create(value: Principal, keyId: string = 'default'): Promise<string> {
67
68
  const entry = this.config.keyMap[keyId];
68
69
  if (!entry) {
69
- throw new AppError('Requested unknown key for signing');
70
+ throw new RuntimeError('Requested unknown key for signing');
70
71
  }
71
72
  // Weird issue with their ES module support
72
73
  const { default: { create } } = await import('njwt');
73
74
  const jwt = create({}, '-')
74
75
  .setExpiration(value.expiresAt!)
75
- .setIssuedAt(TimeUtil.asSeconds(value.issuedAt!))
76
+ .setIssuedAt(TimeUtil.duration((value.issuedAt ?? new Date()).getTime(), 's'))
76
77
  .setClaim('core', castTo({ ...value }))
77
78
  .setIssuer(value.issuer!)
78
79
  .setJti(value.sessionId!)
package/src/config.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Config } from '@travetto/config';
2
- import { Runtime, AppError, BinaryUtil } from '@travetto/runtime';
2
+ import { PostConstruct } from '@travetto/di';
3
+ import { Runtime, RuntimeError, BinaryMetadataUtil } from '@travetto/runtime';
3
4
  import { Ignore, Secret } from '@travetto/schema';
4
5
 
5
6
  type KeyEntry = { key: string, id: string };
@@ -17,13 +18,14 @@ export class WebAuthConfig {
17
18
  @Ignore()
18
19
  keyMap: Record<string, KeyEntry> & { default?: KeyEntry } = {};
19
20
 
20
- postConstruct(): void {
21
+ @PostConstruct()
22
+ finalize(): void {
21
23
  if (!this.signingKey && Runtime.production) {
22
- throw new AppError('The default signing key is only valid for development use, please specify a config value at web.auth.signingKey');
24
+ throw new RuntimeError('The default signing key is only valid for development use, please specify a config value at web.auth.signingKey');
23
25
  }
24
26
  this.signingKey ??= 'dummy';
25
27
 
26
- const all = [this.signingKey].flat().map(key => ({ key, id: BinaryUtil.hash(key, 8) }));
28
+ const all = [this.signingKey].flat().map(key => ({ key, id: BinaryMetadataUtil.hash(key, { length: 8 }) }));
27
29
  this.keyMap = Object.fromEntries(all.map(entry => [entry.id, entry]));
28
30
  this.keyMap.default = all[0];
29
31
  }
@@ -1,6 +1,6 @@
1
1
  import { toConcrete } from '@travetto/runtime';
2
2
  import type { WebInterceptor, WebAsyncContext, WebInterceptorCategory, WebChainedContext, WebResponse } from '@travetto/web';
3
- import { Injectable, Inject, DependencyRegistryIndex } from '@travetto/di';
3
+ import { Injectable, Inject, DependencyRegistryIndex, PostConstruct } from '@travetto/di';
4
4
  import type { AuthContext, AuthService, AuthToken, Principal } from '@travetto/auth';
5
5
  import { Required } from '@travetto/schema';
6
6
 
@@ -37,7 +37,8 @@ export class AuthContextInterceptor implements WebInterceptor {
37
37
  @Inject()
38
38
  webAsyncContext: WebAsyncContext;
39
39
 
40
- async postConstruct(): Promise<void> {
40
+ @PostConstruct()
41
+ async registerContextHandlers(): Promise<void> {
41
42
  this.codec ??= await DependencyRegistryIndex.getInstance(toConcrete<PrincipalCodec>(), CommonPrincipalCodecSymbol);
42
43
  this.webAsyncContext.registerSource(toConcrete<Principal>(), () => this.authContext.principal);
43
44
  this.webAsyncContext.registerSource(toConcrete<AuthToken>(), () => this.authContext.authToken);
@@ -1,4 +1,4 @@
1
- import { AppError, Util } from '@travetto/runtime';
1
+ import { RuntimeError, Util } from '@travetto/runtime';
2
2
  import type { WebInterceptor, WebInterceptorCategory, WebChainedContext, WebResponse, WebInterceptorContext } from '@travetto/web';
3
3
  import { Injectable, Inject } from '@travetto/di';
4
4
  import { Config } from '@travetto/config';
@@ -72,7 +72,7 @@ export class AuthVerifyInterceptor implements WebInterceptor<WebAuthVerifyConfig
72
72
  if (!principal) {
73
73
  throw new AuthenticationError('User is unauthenticated');
74
74
  } else if (!config.matcher(new Set(principal.permissions))) {
75
- throw new AppError('Access denied', { category: 'permissions' });
75
+ throw new RuntimeError('Access denied', { category: 'permissions' });
76
76
  }
77
77
  break;
78
78
  }
@@ -89,16 +89,20 @@ export abstract class AuthWebServerSuite extends BaseWebSuite {
89
89
  @Inject()
90
90
  config: WebAuthConfig;
91
91
 
92
- getCookie(headers: WebHeaders): Cookie | undefined {
93
- return new CookieJar().importSetCookieHeader(headers.getSetCookie()).getAll()[0];
92
+ async getCookie(headers: WebHeaders): Promise<Cookie | undefined> {
93
+ const jar = new CookieJar();
94
+ await jar.importSetCookieHeader(headers.getSetCookie());
95
+ return jar.getAll()[0];
94
96
  }
95
97
 
96
- getCookieHeader(headers: WebHeaders): string | undefined {
97
- return new CookieJar().importSetCookieHeader(headers.getSetCookie()).exportCookieHeader();
98
+ async getCookieHeader(headers: WebHeaders): Promise<string | undefined> {
99
+ const jar = new CookieJar();
100
+ await jar.importSetCookieHeader(headers.getSetCookie());
101
+ return jar.exportCookieHeader();
98
102
  }
99
103
 
100
- getCookieExpires(headers: WebHeaders): Date | undefined {
101
- const v = this.getCookie(headers)?.expires;
104
+ async getCookieExpires(headers: WebHeaders): Promise<Date | undefined> {
105
+ const v = (await this.getCookie(headers))?.expires;
102
106
  return v ? new Date(v) : undefined;
103
107
  }
104
108
 
@@ -151,7 +155,7 @@ export abstract class AuthWebServerSuite extends BaseWebSuite {
151
155
  }
152
156
  }, false);
153
157
  assert(statusCode === 201);
154
- const cookie = this.getCookieHeader(headers);
158
+ const cookie = await this.getCookieHeader(headers);
155
159
  assert(cookie);
156
160
 
157
161
  const { context: { httpStatusCode: lastStatus } } = await this.request({
@@ -222,7 +226,7 @@ export abstract class AuthWebServerSuite extends BaseWebSuite {
222
226
  }
223
227
  }, false);
224
228
  assert(statusCode === 201);
225
- const cookie = this.getCookieHeader(headers);
229
+ const cookie = await this.getCookieHeader(headers);
226
230
  assert(cookie);
227
231
 
228
232
  const { body, context: { httpStatusCode: lastStatus } } = await this.request({
@@ -250,17 +254,17 @@ export abstract class AuthWebServerSuite extends BaseWebSuite {
250
254
  assert(statusCode === 201);
251
255
 
252
256
  const start = Date.now();
253
- const cookie = this.getCookieHeader(headers);
257
+ const cookie = await this.getCookieHeader(headers);
254
258
  assert(cookie);
255
259
 
256
- const expires = this.getCookieExpires(headers);
260
+ const expires = await this.getCookieExpires(headers);
257
261
  assert(expires);
258
262
 
259
263
  const { headers: selfHeaders, context: { httpStatusCode: lastStatus } } = await this.request({
260
264
  context: { httpMethod: 'GET', path: '/test/auth/self' },
261
265
  headers: { cookie }
262
266
  }, false);
263
- assert(this.getCookie(selfHeaders) === undefined);
267
+ assert(await this.getCookie(selfHeaders) === undefined);
264
268
  assert(lastStatus === 200);
265
269
 
266
270
  const used = (Date.now() - start);
@@ -272,9 +276,9 @@ export abstract class AuthWebServerSuite extends BaseWebSuite {
272
276
  headers: { cookie }
273
277
  }, false);
274
278
  assert(lastStatus2 === 200);
275
- assert(this.getCookie(selfHeadersRenew));
279
+ assert(await this.getCookie(selfHeadersRenew));
276
280
 
277
- const expiresRenew = this.getCookieExpires(selfHeadersRenew);
281
+ const expiresRenew = await this.getCookieExpires(selfHeadersRenew);
278
282
  assert(expiresRenew);
279
283
 
280
284
  const delta = expiresRenew.getTime() - expires.getTime();