@travetto/auth-session 6.0.0-rc.0

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 ADDED
@@ -0,0 +1,65 @@
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-session/DOC.tsx and execute "npx trv doc" to rebuild -->
3
+ # Auth Session
4
+
5
+ ## Session provider for the travetto auth module.
6
+
7
+ **Install: @travetto/auth-session**
8
+ ```bash
9
+ npm install @travetto/auth-session
10
+
11
+ # or
12
+
13
+ yarn add @travetto/auth-session
14
+ ```
15
+
16
+ This is a module that adds session support to the [Authentication](https://github.com/travetto/travetto/tree/main/module/auth#readme "Authentication scaffolding for the Travetto framework") framework, via [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") storage. The concept here, is that the [Authentication](https://github.com/travetto/travetto/tree/main/module/auth#readme "Authentication scaffolding for the Travetto framework") module provides the solid foundation for ensuring authentication to the system, and transitively to the session data. The [Principal](https://github.com/travetto/travetto/tree/main/module/auth/src/types/principal.ts#L8) provides a session identifier, which refers to a unique authentication session. Each login will produce a novel session id. This id provides the contract between [Authentication](https://github.com/travetto/travetto/tree/main/module/auth#readme "Authentication scaffolding for the Travetto framework") and[Auth Session](https://github.com/travetto/travetto/tree/main/module/auth-session#readme "Session provider for the travetto auth module.").
17
+
18
+ This session identifier, is then used when retrieving data from [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") storage. This storage mechanism is not tied to a request/response model, but the [Rest Auth Session](https://github.com/travetto/travetto/tree/main/module/auth-rest-session#readme "Rest authentication session integration support for the Travetto framework") does provide a natural integration with the [RESTful API](https://github.com/travetto/travetto/tree/main/module/rest#readme "Declarative api for RESTful APIs with support for the dependency injection module.") module.
19
+
20
+ Within the framework the sessions are stored against any [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") implementation that provides [ModelExpirySupport](https://github.com/travetto/travetto/tree/main/module/model/src/service/expiry.ts), as the data needs to be able to be expired appropriately. The list of supported model providers are:
21
+ * [Redis Model Support](https://github.com/travetto/travetto/tree/main/module/model-redis#readme "Redis backing for the travetto model module.")
22
+ * [MongoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-mongo#readme "Mongo backing for the travetto model module.")
23
+ * [S3 Model Support](https://github.com/travetto/travetto/tree/main/module/model-s3#readme "S3 backing for the travetto model module.")
24
+ * [DynamoDB Model Support](https://github.com/travetto/travetto/tree/main/module/model-dynamodb#readme "DynamoDB backing for the travetto model module.")
25
+ * [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.")
26
+ * [File Model Support](https://github.com/travetto/travetto/tree/main/module/model-file#readme "File system backing for the travetto model module.")
27
+ * [Memory Model Support](https://github.com/travetto/travetto/tree/main/module/model-memory#readme "Memory backing for the travetto model module.")
28
+ While the expiry is not necessarily a hard requirement, the implementation without it can be quite messy. To that end, the ability to add [ModelExpirySupport](https://github.com/travetto/travetto/tree/main/module/model/src/service/expiry.ts) to the model provider would be the natural extension point if more expiry support is needed.
29
+
30
+ **Code: Sample usage of Session Service**
31
+ ```typescript
32
+ class RestSessionConfig implements ManagedInterceptorConfig { }
33
+
34
+ /**
35
+ * Loads session, and provides ability to create session as needed, persists when complete.
36
+ */
37
+ @Injectable()
38
+ export class AuthSessionInterceptor implements RestInterceptor {
39
+
40
+ dependsOn: Class<RestInterceptor>[] = [AuthContextInterceptor];
41
+ runsBefore: Class<RestInterceptor>[] = [];
42
+
43
+ @Inject()
44
+ service: SessionService;
45
+
46
+ @Inject()
47
+ config: RestSessionConfig;
48
+
49
+ async intercept(ctx: FilterContext, next: FilterNext): Promise<unknown> {
50
+ try {
51
+ await this.service.load();
52
+ Object.defineProperty(ctx.req, 'session', { get: () => this.service.getOrCreate() });
53
+ return await next();
54
+ } finally {
55
+ await this.service.persist();
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ The [SessionService](https://github.com/travetto/travetto/tree/main/module/auth-session/src/service.ts#L16) provides the basic integration with the [AuthContext](https://github.com/travetto/travetto/tree/main/module/auth/src/context.ts#L16) to authenticate and isolate session data. The usage is fairly simple, but the import pattern to follow is:
62
+ * load
63
+ * read/modify
64
+ * persist
65
+ And note, persist is intelligent enough to only update the data store if the expiration date has changed or if the data in the session has been modified.
package/__index__.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './src/service';
2
+ export * from './src/model';
3
+ export * from './src/session';
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@travetto/auth-session",
3
+ "version": "6.0.0-rc.0",
4
+ "description": "Session provider for the travetto auth module.",
5
+ "keywords": [
6
+ "auth",
7
+ "session",
8
+ "travetto",
9
+ "typescript"
10
+ ],
11
+ "homepage": "https://travetto.io",
12
+ "license": "MIT",
13
+ "author": {
14
+ "email": "travetto.framework@gmail.com",
15
+ "name": "Travetto Framework"
16
+ },
17
+ "files": [
18
+ "__index__.ts",
19
+ "src",
20
+ "support"
21
+ ],
22
+ "main": "__index__.ts",
23
+ "repository": {
24
+ "url": "git+https://github.com/travetto/travetto.git",
25
+ "directory": "module/auth-session"
26
+ },
27
+ "dependencies": {
28
+ "@travetto/auth": "^6.0.0-rc.0",
29
+ "@travetto/config": "^6.0.0-rc.0",
30
+ "@travetto/model": "^6.0.0-rc.0"
31
+ },
32
+ "peerDependencies": {
33
+ "@travetto/test": "^6.0.0-rc.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@travetto/test": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "travetto": {
41
+ "displayName": "Auth Session"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Session data, will basically be a key/value map
3
+ */
4
+ export class SessionDataTarget { }
package/src/model.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { Model, ExpiresAt } from '@travetto/model';
2
+ import { Text } from '@travetto/schema';
3
+
4
+ /**
5
+ * Session model service identifier
6
+ */
7
+ export const SessionModelSymbol = Symbol.for('@travetto/auth-session:model');
8
+
9
+ @Model({ autoCreate: false })
10
+ export class SessionEntry {
11
+ id: string;
12
+ @Text()
13
+ data: string;
14
+ @ExpiresAt()
15
+ expiresAt?: Date;
16
+ issuedAt: Date;
17
+ maxAge?: number;
18
+ }
package/src/service.ts ADDED
@@ -0,0 +1,157 @@
1
+ import { Injectable, Inject } from '@travetto/di';
2
+ import { isStorageSupported } from '@travetto/model/src/internal/service/common';
3
+ import { Runtime, Util } from '@travetto/runtime';
4
+ import { ModelExpirySupport, NotFoundError } from '@travetto/model';
5
+ import { AsyncContext, AsyncContextValue } from '@travetto/context';
6
+ import { AuthContext, AuthenticationError, AuthService } from '@travetto/auth';
7
+
8
+ import { Session } from './session';
9
+ import { SessionEntry, SessionModelSymbol } from './model';
10
+
11
+ /**
12
+ * Rest service for supporting the session and managing the session state
13
+ * during the normal lifecycle of requests.
14
+ */
15
+ @Injectable()
16
+ export class SessionService {
17
+
18
+ @Inject()
19
+ context: AsyncContext;
20
+
21
+ @Inject()
22
+ authContext: AuthContext;
23
+
24
+ @Inject()
25
+ authService: AuthService;
26
+
27
+ #modelService: ModelExpirySupport;
28
+
29
+ #session = new AsyncContextValue<Session>(this);
30
+
31
+ constructor(@Inject(SessionModelSymbol) service: ModelExpirySupport) {
32
+ this.#modelService = service;
33
+ }
34
+
35
+ /**
36
+ * Disconnect active session
37
+ */
38
+ clear(): void {
39
+ this.#session.set(undefined);
40
+ }
41
+
42
+ /**
43
+ * Initialize service if none defined
44
+ */
45
+ async postConstruct(): Promise<void> {
46
+ if (isStorageSupported(this.#modelService) && Runtime.dynamic) {
47
+ await this.#modelService.createModel?.(SessionEntry);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Load session by id
53
+ * @returns Session if valid
54
+ */
55
+ async #load(id: string): Promise<Session | undefined> {
56
+ try {
57
+ const record = await this.#modelService.get(SessionEntry, id);
58
+
59
+ const session = new Session({
60
+ ...record,
61
+ data: Util.decodeSafeJSON(record.data)
62
+ });
63
+
64
+ // Validate session
65
+ if (session.isExpired()) {
66
+ await this.#modelService.delete(SessionEntry, session.id).catch(() => { });
67
+ return new Session({ action: 'destroy' });
68
+ } else {
69
+ return session;
70
+ }
71
+ } catch (err) {
72
+ if (!(err instanceof NotFoundError)) {
73
+ throw err; // If not a not found error, throw
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Persist session
80
+ */
81
+ async persist(): Promise<void> {
82
+ const session = this.#session.get();
83
+
84
+ // If missing or new and no data
85
+ if (!session || (session.action === 'create' && session.isEmpty())) {
86
+ return;
87
+ }
88
+
89
+ // Ensure latest expiry information before persisting
90
+ await this.authService.manageExpiry(this.authContext.principal);
91
+
92
+ const p = this.authContext.principal;
93
+
94
+ // If not destroying, write to response, and store
95
+ if (p && session.action !== 'destroy') {
96
+ session.expiresAt = p.expiresAt;
97
+ session.issuedAt = p.issuedAt!;
98
+
99
+ // If expiration time has changed, send new session information
100
+ if (session.action === 'create' || session.isChanged()) {
101
+ await this.#modelService.upsert(SessionEntry, SessionEntry.from({
102
+ ...session,
103
+ data: Util.encodeSafeJSON(session.data)
104
+ }));
105
+ }
106
+ // If destroying
107
+ } else if (session.id) { // If destroy and id
108
+ await this.#modelService.delete(SessionEntry, session.id).catch(() => { });
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get or recreate session
114
+ */
115
+ getOrCreate(): Session {
116
+ const principal = this.authContext.principal;
117
+ if (!principal) {
118
+ throw new AuthenticationError('Unable to establish session without first authenticating');
119
+ }
120
+ const existing = this.#session.get();
121
+ const val = (existing?.action === 'destroy' ? undefined : existing) ??
122
+ new Session({
123
+ id: principal.sessionId,
124
+ expiresAt: principal.expiresAt,
125
+ issuedAt: principal.issuedAt,
126
+ action: 'create',
127
+ data: {},
128
+ });
129
+ this.#session.set(val);
130
+ return val;
131
+ }
132
+
133
+ /**
134
+ * Get session if defined
135
+ */
136
+ get(): Session | undefined {
137
+ return this.#session.get();
138
+ }
139
+
140
+ /**
141
+ * Load from request
142
+ */
143
+ async load(): Promise<Session | undefined> {
144
+ if (!this.#session.get()) {
145
+ const principal = this.authContext.principal;
146
+ if (principal?.sessionId) {
147
+ this.#session.set(await this.#load(principal.sessionId));
148
+ }
149
+ }
150
+ return this.#session.get();
151
+ }
152
+
153
+ destroy(): void {
154
+ this.get()?.destroy();
155
+ this.clear();
156
+ }
157
+ }
package/src/session.ts ADDED
@@ -0,0 +1,128 @@
1
+ import { AnyMap, castKey, castTo } from '@travetto/runtime';
2
+
3
+ /**
4
+ * @concrete ./internal/types#SessionDataTarget
5
+ * @augments `@travetto/rest:Context`
6
+ */
7
+ export interface SessionData extends AnyMap { }
8
+
9
+ /**
10
+ * Full session object, with metadata
11
+ * @augments `@travetto/rest:Context`
12
+ */
13
+ export class Session<T extends SessionData = SessionData> {
14
+ /**
15
+ * The expiry time when the session was loaded
16
+ */
17
+ #expiresAtLoaded: Date | undefined;
18
+ /**
19
+ * The payload of the session at load
20
+ */
21
+ #payload: string;
22
+ /**
23
+ * The session identifier
24
+ */
25
+ readonly id: string;
26
+ /**
27
+ * Session signature
28
+ */
29
+ readonly signature?: string;
30
+
31
+ /**
32
+ * Session initial issue timestamp
33
+ */
34
+ issuedAt: Date;
35
+ /**
36
+ * Expires at time
37
+ */
38
+ expiresAt: Date | undefined;
39
+ /**
40
+ * What action should be taken against the session
41
+ */
42
+ action?: 'create' | 'destroy' | 'modify';
43
+ /**
44
+ * The session data
45
+ */
46
+ data: T | undefined;
47
+
48
+ /**
49
+ * Create a new Session object given a partial version of itself
50
+ */
51
+ constructor(data: Partial<Session>) {
52
+ // Mark the issued at as now
53
+ this.issuedAt = new Date();
54
+
55
+ // Overwrite with data
56
+ Object.assign(this, data);
57
+
58
+ // Mark the expiry load time
59
+ this.#expiresAtLoaded = this.expiresAt ?? new Date();
60
+
61
+ // Hash the session as it stands
62
+ this.#payload = JSON.stringify(this);
63
+ }
64
+
65
+ /**
66
+ * Get session value
67
+ */
68
+ getValue<V>(key: string): V | undefined {
69
+ return this.data && key in this.data ? this.data[key] : undefined;
70
+ }
71
+
72
+ /**
73
+ * Set session value
74
+ */
75
+ setValue<V>(key: string, value: V): void {
76
+ const data = (this.data ??= castTo({}))!;
77
+ data[castKey<T>(key)] = castTo(value);
78
+ }
79
+
80
+ /**
81
+ * Determine if session has changed
82
+ */
83
+ isChanged(): boolean {
84
+ return this.isTimeChanged() || this.#payload !== JSON.stringify(this);
85
+ }
86
+
87
+ /**
88
+ * Determine if the expiry time has changed
89
+ */
90
+ isTimeChanged(): boolean {
91
+ return this.expiresAt !== undefined && this.expiresAt !== this.#expiresAtLoaded;
92
+ }
93
+
94
+ /**
95
+ * See if the session is truly expired
96
+ */
97
+ isExpired(): boolean {
98
+ return !!this.expiresAt && this.expiresAt.getTime() < Date.now();
99
+ }
100
+
101
+ /**
102
+ * See if session is empty, has any data been written
103
+ */
104
+ isEmpty(): boolean {
105
+ return !Object.keys(this.data ?? {}).length;
106
+ }
107
+
108
+ /**
109
+ * Mark the session for destruction, delete the data
110
+ */
111
+ destroy(): void {
112
+ this.action = 'destroy';
113
+ delete this.data;
114
+ }
115
+
116
+ /**
117
+ * Serialize the session
118
+ */
119
+ toJSON(): unknown {
120
+ return {
121
+ id: this.id,
122
+ signature: this.signature,
123
+ expiresAt: this.expiresAt?.getTime(),
124
+ issuedAt: this.issuedAt?.getTime(),
125
+ data: this.data
126
+ };
127
+ }
128
+ }
@@ -0,0 +1,72 @@
1
+ import assert from 'node:assert';
2
+
3
+ import { Suite, Test } from '@travetto/test';
4
+ import { Inject } from '@travetto/di';
5
+ import { SessionService } from '@travetto/auth-session';
6
+ import { AuthContext, AuthenticationError } from '@travetto/auth';
7
+ import { AsyncContext, WithAsyncContext } from '@travetto/context';
8
+ import { Util } from '@travetto/runtime';
9
+
10
+ import { InjectableSuite } from '@travetto/di/support/test/suite';
11
+ import { BaseRestSuite } from '@travetto/rest/support/test/base';
12
+
13
+ @Suite()
14
+ @InjectableSuite()
15
+ export abstract class AuthSessionServerSuite extends BaseRestSuite {
16
+
17
+ timeScale = 1;
18
+
19
+ @Inject()
20
+ auth: AuthContext;
21
+
22
+ @Inject()
23
+ session: SessionService;
24
+
25
+ @Inject()
26
+ context: AsyncContext;
27
+
28
+ @WithAsyncContext()
29
+ @Test()
30
+ async testSessionEstablishment() {
31
+ await this.auth.init();
32
+
33
+ this.auth.principal = {
34
+ id: 'orange',
35
+ details: {},
36
+ sessionId: Util.uuid()
37
+ };
38
+
39
+ assert(this.session.get() === undefined);
40
+ assert(await this.session.load() === undefined);
41
+
42
+ const sess = this.session.getOrCreate();
43
+ assert(sess.id === this.auth.principal.sessionId);
44
+ sess.data = { name: 'bob' };
45
+ await this.session.persist();
46
+
47
+ this.session.clear(); // Disconnect
48
+
49
+ assert(await this.session.load() !== undefined);
50
+ const sess2 = this.session.getOrCreate();
51
+ assert(sess2.data?.name === 'bob');
52
+
53
+ this.auth.principal = {
54
+ id: 'orange',
55
+ details: {},
56
+ sessionId: Util.uuid()
57
+ };
58
+
59
+ this.session.clear(); // Disconnect
60
+
61
+ assert(await this.session.load() === undefined);
62
+ const sess3 = this.session.getOrCreate();
63
+ assert.deepStrictEqual(sess3.data, {});
64
+ }
65
+
66
+ @WithAsyncContext()
67
+ @Test()
68
+ async testUnauthenticatedSession() {
69
+ await assert.throws(() => this.session.getOrCreate(), AuthenticationError);
70
+
71
+ }
72
+ }