@travetto/auth 5.1.0 → 6.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 CHANGED
@@ -29,7 +29,11 @@ export interface Principal<D = AnyMap> {
29
29
  /**
30
30
  * Primary identifier for a user
31
31
  */
32
- id: string;
32
+ readonly id: string;
33
+ /**
34
+ * Unique identifier for the principal's lifecycle
35
+ */
36
+ readonly sessionId?: string;
33
37
  /**
34
38
  * Date of expiration
35
39
  */
@@ -38,22 +42,18 @@ export interface Principal<D = AnyMap> {
38
42
  * Date of issuance
39
43
  */
40
44
  issuedAt?: Date;
41
- /**
42
- * Max age in seconds a principal is valid
43
- */
44
- maxAge?: number;
45
45
  /**
46
46
  * The source of the issuance
47
47
  */
48
- issuer?: string;
48
+ readonly issuer?: string;
49
49
  /**
50
50
  * Supplemental details
51
51
  */
52
- details: D;
52
+ readonly details: D;
53
53
  /**
54
54
  * List of all provided permissions
55
55
  */
56
- permissions?: string[];
56
+ readonly permissions?: string[];
57
57
  }
58
58
  ```
59
59
 
@@ -82,9 +82,6 @@ export interface Authenticator<T = unknown, C = unknown, P extends Principal = P
82
82
 
83
83
  The [Authenticator Contract](https://github.com/travetto/travetto/tree/main/module/auth/src/types/authenticator.ts#L14) only requires one method to be defined, and that is `authenticate`. This method receives a generic payload, and a supplemental context as an input. The interface is responsible for converting that to an authenticated principal.
84
84
 
85
- ### Example
86
- The [JWT](https://github.com/travetto/travetto/tree/main/module/jwt#readme "JSON Web Token implementation") module is a good example of an authenticator. This is a common use case for simple internal auth.
87
-
88
85
  ## Authorization Contract
89
86
 
90
87
  **Code: Authorizer Contract**
@@ -126,15 +123,23 @@ export class AuthService {
126
123
  * @param authenticators List of valid authentication sources
127
124
  */
128
125
  async authenticate<T, C>(payload: T, context: C, authenticators: symbol[]): Promise<Principal | undefined>;
126
+ /**
127
+ * Manage expiry state, renewing if allowed
128
+ */
129
+ manageExpiry(p?: Principal): void;
130
+ /**
131
+ * Enforce expiry, invalidating the principal if expired
132
+ */
133
+ enforceExpiry(p?: Principal): Principal | undefined;
129
134
  }
130
135
  ```
131
136
 
132
- The [AuthService](https://github.com/travetto/travetto/tree/main/module/auth/src/service.ts#L12) operates as the owner of the current auth state for a given "request". "Request" here implies a set of operations over a period of time, with the http request/response model being an easy point of reference. This could also tie to a CLI operation, or any other invocation that requires some concept of authentication and authorization.
137
+ The [AuthService](https://github.com/travetto/travetto/tree/main/module/auth/src/service.ts#L14) operates as the owner of the current auth state for a given "request". "Request" here implies a set of operations over a period of time, with the http request/response model being an easy point of reference. This could also tie to a CLI operation, or any other invocation that requires some concept of authentication and authorization.
133
138
 
134
139
  The service allows for storing and retrieving the active [Principal Contract](https://github.com/travetto/travetto/tree/main/module/auth/src/types/principal.ts#L8), and/or the actively persisted auth token. This is extremely useful for other parts of the framework that may request authenticated information (if available). [Rest Auth](https://github.com/travetto/travetto/tree/main/module/auth-rest#readme "Rest authentication integration support for the Travetto framework") makes heavy use of this state for enforcing routes when authentication is required.
135
140
 
136
141
  ### Login
137
- "Logging in" can be thought of going through the action of finding a single source that can authenticate the identity for the request credentials. Some times there may be more than one valid source of authentication that you want to leverage, and the first one to authenticate wins. The [AuthService](https://github.com/travetto/travetto/tree/main/module/auth/src/service.ts#L12) operates in this fashion, in which a set of credentials and potential [Authenticator Contract](https://github.com/travetto/travetto/tree/main/module/auth/src/types/authenticator.ts#L14)s are submitted, and the service will attempt to authenticate.
142
+ "Logging in" can be thought of going through the action of finding a single source that can authenticate the identity for the request credentials. Some times there may be more than one valid source of authentication that you want to leverage, and the first one to authenticate wins. The [AuthService](https://github.com/travetto/travetto/tree/main/module/auth/src/service.ts#L14) operates in this fashion, in which a set of credentials and potential [Authenticator Contract](https://github.com/travetto/travetto/tree/main/module/auth/src/types/authenticator.ts#L14)s are submitted, and the service will attempt to authenticate.
138
143
 
139
144
  Upon successful authentication, an optional [Authorizer Contract](https://github.com/travetto/travetto/tree/main/module/auth/src/types/authorizer.ts#L8) may be invoked to authorize the authenticated user. The [Authenticator Contract](https://github.com/travetto/travetto/tree/main/module/auth/src/types/authenticator.ts#L14) is assumed to be only one within the system, and should be tied to the specific product you are building for. The [Authorizer Contract](https://github.com/travetto/travetto/tree/main/module/auth/src/types/authorizer.ts#L8) should be assumed to have multiple sources, and are generally specific to external third parties. All of these values are collected via the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module and will be auto-registered on startup.
140
145
 
@@ -145,18 +150,8 @@ When working with framework's authentication, the authenticated information is e
145
150
 
146
151
  **Code: Auth Context Outline**
147
152
  ```typescript
148
- type AuthContextShape = {
149
- principal?: Principal;
150
- authToken?: AuthToken;
151
- authenticatorState?: AuthenticatorState;
152
- };
153
- @Injectable()
154
153
  export class AuthContext {
155
154
  @Inject()
156
- /**
157
- * Get the principal, if set
158
- */
159
- get principal(): Principal | undefined;
160
155
  /**
161
156
  * Set principal
162
157
  */
package/__index__.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from './src/service';
2
2
  export * from './src/context';
3
+ export * from './src/config';
3
4
  export * from './src/types/authenticator';
4
5
  export * from './src/types/authorizer';
5
6
  export * from './src/types/principal';
6
- export * from './src/types/error';
7
+ export * from './src/types/error';
8
+ export * from './src/types/token';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/auth",
3
- "version": "5.1.0",
3
+ "version": "6.0.0-rc.1",
4
4
  "description": "Authentication scaffolding for the Travetto framework",
5
5
  "keywords": [
6
6
  "authentication",
@@ -23,7 +23,7 @@
23
23
  "directory": "module/auth"
24
24
  },
25
25
  "dependencies": {
26
- "@travetto/context": "^5.1.0"
26
+ "@travetto/context": "^6.0.0-rc.1"
27
27
  },
28
28
  "travetto": {
29
29
  "displayName": "Authentication"
package/src/config.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { Config } from '@travetto/config';
2
+ import { TimeSpan, TimeUtil } from '@travetto/runtime';
3
+ import { Ignore } from '@travetto/schema';
4
+
5
+ @Config('auth')
6
+ export class AuthConfig {
7
+
8
+ maxAge: TimeSpan | number = '1h';
9
+ rollingRenew: boolean = true;
10
+
11
+ @Ignore()
12
+ maxAgeMs: number;
13
+
14
+ postConstruct(): void {
15
+ this.maxAgeMs = TimeUtil.asMillis(this.maxAge);
16
+ }
17
+ }
package/src/context.ts CHANGED
@@ -2,72 +2,66 @@
2
2
  import { Inject, Injectable } from '@travetto/di';
3
3
  import { AsyncContext, AsyncContextValue } from '@travetto/context';
4
4
 
5
- import { AuthToken } from './internal/types';
5
+ import { AuthToken } from './types/token';
6
6
  import { Principal } from './types/principal';
7
7
  import { AuthenticatorState } from './types/authenticator';
8
8
 
9
- type AuthContextShape = {
10
- principal?: Principal;
11
- authToken?: AuthToken;
12
- authenticatorState?: AuthenticatorState;
13
- };
14
-
9
+ /**
10
+ * Provides the primary context for the authenticated state
11
+ *
12
+ *
13
+ * Will silently fail on reads, but will error on writes if the context is not established.
14
+ */
15
15
  @Injectable()
16
16
  export class AuthContext {
17
17
 
18
- #auth = new AsyncContextValue<AuthContextShape>(this);
18
+ #principal = new AsyncContextValue<Principal>(this, { failIfUnbound: { write: true } });
19
+ #authToken = new AsyncContextValue<AuthToken>(this, { failIfUnbound: { write: true } });
20
+ #authState = new AsyncContextValue<AuthenticatorState>(this, { failIfUnbound: { write: true } });
19
21
 
20
22
  @Inject()
21
23
  context: AsyncContext;
22
24
 
23
- /**
24
- * Initialize context
25
- * @private
26
- */
27
- init(): void {
28
- this.#auth.set({});
29
- }
30
-
31
25
  /**
32
26
  * Get the principal, if set
33
27
  */
34
28
  get principal(): Principal | undefined {
35
- return this.#auth.get()?.principal;
29
+ return this.#principal.get();
36
30
  }
37
31
 
38
32
  /**
39
33
  * Set principal
40
34
  */
41
35
  set principal(p: Principal | undefined) {
42
- this.#auth.get()!.principal = p;
36
+ this.#principal.set(p);
43
37
  }
44
38
 
45
39
  /**
46
40
  * Get the authentication token, if it exists
47
41
  */
48
42
  get authToken(): AuthToken | undefined {
49
- return this.#auth.get()?.authToken;
43
+ return this.#authToken.get();
50
44
  }
51
45
 
52
46
  /**
53
47
  * Set/overwrite the user's authentication token
54
48
  */
55
49
  set authToken(token: AuthToken | undefined) {
56
- this.#auth.get()!.authToken = token;
50
+ this.#authToken.set(token);
57
51
  }
58
52
 
59
53
  /**
60
54
  * Get the authenticator state, if it exists
61
55
  */
62
56
  get authenticatorState(): AuthenticatorState | undefined {
63
- return this.#auth.get()?.authenticatorState;
57
+ return this.#authState.get();
64
58
  }
65
59
 
66
60
  /**
67
61
  * Set/overwrite the authenticator state
68
62
  */
69
63
  set authenticatorState(state: AuthenticatorState | undefined) {
70
- this.#auth.get()!.authenticatorState = state;
64
+ this.#authState.set(state);
71
65
  }
72
66
 
73
67
  /**
@@ -75,6 +69,8 @@ export class AuthContext {
75
69
  * @private
76
70
  */
77
71
  async clear(): Promise<void> {
78
- this.#auth.set(undefined);
72
+ this.#principal.set(undefined);
73
+ this.#authToken.set(undefined);
74
+ this.#authState.set(undefined);
79
75
  }
80
76
  }
@@ -9,4 +9,3 @@ export class PrincipalTarget {
9
9
  }
10
10
  export class AuthorizerTarget { }
11
11
  export class AuthenticatorTarget { }
12
- export type AuthToken = { value: string, type: string };
package/src/service.ts CHANGED
@@ -1,5 +1,6 @@
1
1
 
2
2
  import { DependencyRegistry, Inject, Injectable } from '@travetto/di';
3
+ import { TimeUtil } from '@travetto/runtime';
3
4
 
4
5
  import { AuthenticatorTarget } from './internal/types';
5
6
  import { Principal } from './types/principal';
@@ -7,6 +8,7 @@ import { Authenticator } from './types/authenticator';
7
8
  import { Authorizer } from './types/authorizer';
8
9
  import { AuthenticationError } from './types/error';
9
10
  import { AuthContext } from './context';
11
+ import { AuthConfig } from './config';
10
12
 
11
13
  @Injectable()
12
14
  export class AuthService {
@@ -14,6 +16,9 @@ export class AuthService {
14
16
  @Inject()
15
17
  authContext: AuthContext;
16
18
 
19
+ @Inject()
20
+ config: AuthConfig;
21
+
17
22
  #authenticators = new Map<symbol, Promise<Authenticator>>();
18
23
 
19
24
  @Inject()
@@ -72,4 +77,38 @@ export class AuthService {
72
77
  // Take the last error and return
73
78
  throw new AuthenticationError('Unable to authenticate', { cause: lastError });
74
79
  }
80
+
81
+ /**
82
+ * Manage expiry state, renewing if allowed
83
+ */
84
+ manageExpiry(p?: Principal): void {
85
+ if (!p) {
86
+ return;
87
+ }
88
+
89
+ if (this.config.maxAgeMs) {
90
+ p.expiresAt ??= TimeUtil.fromNow(this.config.maxAgeMs);
91
+ }
92
+
93
+ p.issuedAt ??= new Date();
94
+
95
+ if (p.expiresAt && this.config.maxAgeMs && this.config.rollingRenew) { // Session behavior
96
+ const end = p.expiresAt.getTime();
97
+ const midPoint = end - this.config.maxAgeMs / 2;
98
+ if (Date.now() > midPoint) { // If we are past the half way mark, renew the token
99
+ p.issuedAt = new Date();
100
+ p.expiresAt = TimeUtil.fromNow(this.config.maxAgeMs); // This will trigger a re-send
101
+ }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Enforce expiry, invalidating the principal if expired
107
+ */
108
+ enforceExpiry(p?: Principal): Principal | undefined {
109
+ if (p && p.expiresAt && p.expiresAt.getTime() < Date.now()) {
110
+ return undefined;
111
+ }
112
+ return p;
113
+ }
75
114
  }
@@ -2,14 +2,18 @@ import { AnyMap } from '@travetto/runtime';
2
2
 
3
3
  /**
4
4
  * A user principal, including permissions and details
5
+ * @augments `@travetto/rest:ContextParam`
5
6
  * @concrete ../internal/types#PrincipalTarget
6
- * @augments `@travetto/rest:Context`
7
7
  */
8
8
  export interface Principal<D = AnyMap> {
9
9
  /**
10
10
  * Primary identifier for a user
11
11
  */
12
- id: string;
12
+ readonly id: string;
13
+ /**
14
+ * Unique identifier for the principal's lifecycle
15
+ */
16
+ readonly sessionId?: string;
13
17
  /**
14
18
  * Date of expiration
15
19
  */
@@ -18,20 +22,16 @@ export interface Principal<D = AnyMap> {
18
22
  * Date of issuance
19
23
  */
20
24
  issuedAt?: Date;
21
- /**
22
- * Max age in seconds a principal is valid
23
- */
24
- maxAge?: number;
25
25
  /**
26
26
  * The source of the issuance
27
27
  */
28
- issuer?: string;
28
+ readonly issuer?: string;
29
29
  /**
30
30
  * Supplemental details
31
31
  */
32
- details: D;
32
+ readonly details: D;
33
33
  /**
34
34
  * List of all provided permissions
35
35
  */
36
- permissions?: string[];
36
+ readonly permissions?: string[];
37
37
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Represents the authenticated token used, allows for reuse on subsequent calls
3
+ */
4
+ export interface AuthToken {
5
+ value: string;
6
+ type: string;
7
+ };