@flink-app/oidc-plugin 1.0.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +846 -0
  4. package/dist/OidcInternalContext.d.ts +15 -0
  5. package/dist/OidcInternalContext.d.ts.map +1 -0
  6. package/dist/OidcInternalContext.js +2 -0
  7. package/dist/OidcPlugin.d.ts +77 -0
  8. package/dist/OidcPlugin.d.ts.map +1 -0
  9. package/dist/OidcPlugin.js +274 -0
  10. package/dist/OidcPluginContext.d.ts +73 -0
  11. package/dist/OidcPluginContext.d.ts.map +1 -0
  12. package/dist/OidcPluginContext.js +2 -0
  13. package/dist/OidcPluginOptions.d.ts +267 -0
  14. package/dist/OidcPluginOptions.d.ts.map +1 -0
  15. package/dist/OidcPluginOptions.js +2 -0
  16. package/dist/OidcProviderConfig.d.ts +77 -0
  17. package/dist/OidcProviderConfig.d.ts.map +1 -0
  18. package/dist/OidcProviderConfig.js +2 -0
  19. package/dist/handlers/CallbackOidc.d.ts +38 -0
  20. package/dist/handlers/CallbackOidc.d.ts.map +1 -0
  21. package/dist/handlers/CallbackOidc.js +219 -0
  22. package/dist/handlers/InitiateOidc.d.ts +35 -0
  23. package/dist/handlers/InitiateOidc.d.ts.map +1 -0
  24. package/dist/handlers/InitiateOidc.js +91 -0
  25. package/dist/index.d.ts +27 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +40 -0
  28. package/dist/providers/OidcProvider.d.ts +90 -0
  29. package/dist/providers/OidcProvider.d.ts.map +1 -0
  30. package/dist/providers/OidcProvider.js +208 -0
  31. package/dist/providers/ProviderRegistry.d.ts +55 -0
  32. package/dist/providers/ProviderRegistry.d.ts.map +1 -0
  33. package/dist/providers/ProviderRegistry.js +94 -0
  34. package/dist/repos/OidcConnectionRepo.d.ts +75 -0
  35. package/dist/repos/OidcConnectionRepo.d.ts.map +1 -0
  36. package/dist/repos/OidcConnectionRepo.js +122 -0
  37. package/dist/repos/OidcSessionRepo.d.ts +57 -0
  38. package/dist/repos/OidcSessionRepo.d.ts.map +1 -0
  39. package/dist/repos/OidcSessionRepo.js +91 -0
  40. package/dist/schemas/CallbackRequest.d.ts +37 -0
  41. package/dist/schemas/CallbackRequest.d.ts.map +1 -0
  42. package/dist/schemas/CallbackRequest.js +2 -0
  43. package/dist/schemas/InitiateRequest.d.ts +17 -0
  44. package/dist/schemas/InitiateRequest.d.ts.map +1 -0
  45. package/dist/schemas/InitiateRequest.js +2 -0
  46. package/dist/schemas/OidcConnection.d.ts +69 -0
  47. package/dist/schemas/OidcConnection.d.ts.map +1 -0
  48. package/dist/schemas/OidcConnection.js +2 -0
  49. package/dist/schemas/OidcProfile.d.ts +69 -0
  50. package/dist/schemas/OidcProfile.d.ts.map +1 -0
  51. package/dist/schemas/OidcProfile.js +2 -0
  52. package/dist/schemas/OidcSession.d.ts +46 -0
  53. package/dist/schemas/OidcSession.d.ts.map +1 -0
  54. package/dist/schemas/OidcSession.js +2 -0
  55. package/dist/schemas/OidcTokenSet.d.ts +42 -0
  56. package/dist/schemas/OidcTokenSet.d.ts.map +1 -0
  57. package/dist/schemas/OidcTokenSet.js +2 -0
  58. package/dist/utils/claims-mapper.d.ts +46 -0
  59. package/dist/utils/claims-mapper.d.ts.map +1 -0
  60. package/dist/utils/claims-mapper.js +104 -0
  61. package/dist/utils/encryption-utils.d.ts +32 -0
  62. package/dist/utils/encryption-utils.d.ts.map +1 -0
  63. package/dist/utils/encryption-utils.js +82 -0
  64. package/dist/utils/error-utils.d.ts +65 -0
  65. package/dist/utils/error-utils.d.ts.map +1 -0
  66. package/dist/utils/error-utils.js +150 -0
  67. package/dist/utils/response-utils.d.ts +18 -0
  68. package/dist/utils/response-utils.d.ts.map +1 -0
  69. package/dist/utils/response-utils.js +42 -0
  70. package/dist/utils/state-utils.d.ts +36 -0
  71. package/dist/utils/state-utils.d.ts.map +1 -0
  72. package/dist/utils/state-utils.js +66 -0
  73. package/examples/basic-oidc.ts +151 -0
  74. package/examples/multi-provider.ts +146 -0
  75. package/package.json +44 -0
  76. package/spec/handlers/InitiateOidc.spec.ts +62 -0
  77. package/spec/helpers/reporter.ts +34 -0
  78. package/spec/helpers/test-helpers.ts +108 -0
  79. package/spec/plugin/OidcPlugin.spec.ts +126 -0
  80. package/spec/providers/ProviderRegistry.spec.ts +197 -0
  81. package/spec/repos/OidcConnectionRepo.spec.ts +257 -0
  82. package/spec/repos/OidcSessionRepo.spec.ts +196 -0
  83. package/spec/support/jasmine.json +7 -0
  84. package/spec/utils/claims-mapper.spec.ts +257 -0
  85. package/spec/utils/encryption-utils.spec.ts +126 -0
  86. package/spec/utils/error-utils.spec.ts +107 -0
  87. package/spec/utils/state-utils.spec.ts +102 -0
  88. package/src/OidcInternalContext.ts +15 -0
  89. package/src/OidcPlugin.ts +290 -0
  90. package/src/OidcPluginContext.ts +76 -0
  91. package/src/OidcPluginOptions.ts +286 -0
  92. package/src/OidcProviderConfig.ts +87 -0
  93. package/src/handlers/CallbackOidc.ts +257 -0
  94. package/src/handlers/InitiateOidc.ts +110 -0
  95. package/src/index.ts +38 -0
  96. package/src/providers/OidcProvider.ts +237 -0
  97. package/src/providers/ProviderRegistry.ts +107 -0
  98. package/src/repos/OidcConnectionRepo.ts +132 -0
  99. package/src/repos/OidcSessionRepo.ts +99 -0
  100. package/src/schemas/CallbackRequest.ts +41 -0
  101. package/src/schemas/InitiateRequest.ts +17 -0
  102. package/src/schemas/OidcConnection.ts +80 -0
  103. package/src/schemas/OidcProfile.ts +79 -0
  104. package/src/schemas/OidcSession.ts +52 -0
  105. package/src/schemas/OidcTokenSet.ts +47 -0
  106. package/src/utils/claims-mapper.ts +114 -0
  107. package/src/utils/encryption-utils.ts +92 -0
  108. package/src/utils/error-utils.ts +167 -0
  109. package/src/utils/response-utils.ts +41 -0
  110. package/src/utils/state-utils.ts +66 -0
  111. package/tsconfig.dist.json +9 -0
  112. package/tsconfig.json +20 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Tests for ProviderRegistry
3
+ */
4
+
5
+ import { ProviderRegistry } from "../../src/providers/ProviderRegistry";
6
+ import { OidcProviderConfig } from "../../src/OidcProviderConfig";
7
+ import { createTestProviderConfig } from "../helpers/test-helpers";
8
+
9
+ describe("ProviderRegistry", () => {
10
+ let staticProviders: Record<string, OidcProviderConfig>;
11
+
12
+ beforeEach(() => {
13
+ staticProviders = {
14
+ google: createTestProviderConfig({
15
+ issuer: "https://accounts.google.com",
16
+ discoveryUrl: "https://accounts.google.com/.well-known/openid-configuration",
17
+ }),
18
+ microsoft: createTestProviderConfig({
19
+ issuer: "https://login.microsoftonline.com/common/v2.0",
20
+ discoveryUrl: "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration",
21
+ }),
22
+ };
23
+ });
24
+
25
+ describe("constructor", () => {
26
+ it("should create registry with static providers", () => {
27
+ const registry = new ProviderRegistry(staticProviders);
28
+ expect(registry).toBeDefined();
29
+ });
30
+
31
+ it("should create registry with provider loader", () => {
32
+ const loader = async (name: string) => null;
33
+ const registry = new ProviderRegistry(staticProviders, loader);
34
+ expect(registry).toBeDefined();
35
+ });
36
+ });
37
+
38
+ describe("hasProvider", () => {
39
+ it("should return true for configured static providers", () => {
40
+ const registry = new ProviderRegistry(staticProviders);
41
+ expect(registry.hasProvider("google")).toBe(true);
42
+ expect(registry.hasProvider("microsoft")).toBe(true);
43
+ });
44
+
45
+ it("should return false for non-configured providers", () => {
46
+ const registry = new ProviderRegistry(staticProviders);
47
+ expect(registry.hasProvider("okta")).toBe(false);
48
+ expect(registry.hasProvider("auth0")).toBe(false);
49
+ });
50
+
51
+ it("should return true if provider loader is configured", () => {
52
+ const loader = async (name: string) => null;
53
+ const registry = new ProviderRegistry(staticProviders, loader);
54
+ // hasProvider returns true if loader exists, even for unknown providers
55
+ expect(registry.hasProvider("unknown")).toBe(true);
56
+ });
57
+ });
58
+
59
+ describe("getProvider", () => {
60
+ it("should throw error for non-existent provider without loader", async () => {
61
+ const registry = new ProviderRegistry(staticProviders);
62
+
63
+ try {
64
+ await registry.getProvider("nonexistent");
65
+ fail("Should have thrown error");
66
+ } catch (error: any) {
67
+ expect(error.code).toBe("provider_not_configured");
68
+ expect(error.message).toContain("nonexistent");
69
+ }
70
+ });
71
+
72
+ it("should include available providers in error details", async () => {
73
+ const registry = new ProviderRegistry(staticProviders);
74
+
75
+ try {
76
+ await registry.getProvider("okta");
77
+ fail("Should have thrown error");
78
+ } catch (error: any) {
79
+ expect(error.details.availableProviders).toEqual(["google", "microsoft"]);
80
+ }
81
+ });
82
+
83
+ it("should use provider loader for non-static providers", async () => {
84
+ const dynamicConfig = createTestProviderConfig({
85
+ issuer: "https://okta.example.com",
86
+ discoveryUrl: "https://okta.example.com/.well-known/openid-configuration",
87
+ });
88
+
89
+ const loader = jasmine.createSpy("loader").and.returnValue(Promise.resolve(dynamicConfig));
90
+ const registry = new ProviderRegistry(staticProviders, loader);
91
+
92
+ // Note: This will attempt real OIDC discovery, which will fail in tests
93
+ // We're primarily testing that the loader is called
94
+ try {
95
+ await registry.getProvider("okta");
96
+ } catch (error) {
97
+ // Discovery will fail, but loader should have been called
98
+ expect(loader).toHaveBeenCalledWith("okta");
99
+ }
100
+ });
101
+
102
+ it("should prioritize static config over loader", async () => {
103
+ const loader = jasmine.createSpy("loader").and.returnValue(Promise.resolve(null));
104
+ const registry = new ProviderRegistry(staticProviders, loader);
105
+
106
+ // Note: This will attempt real OIDC discovery
107
+ try {
108
+ await registry.getProvider("google");
109
+ } catch (error) {
110
+ // Loader should NOT have been called for static provider
111
+ expect(loader).not.toHaveBeenCalled();
112
+ }
113
+ });
114
+
115
+ it("should throw error if loader returns null", async () => {
116
+ const loader = async (name: string) => null;
117
+ const registry = new ProviderRegistry({}, loader);
118
+
119
+ try {
120
+ await registry.getProvider("unknown");
121
+ fail("Should have thrown error");
122
+ } catch (error: any) {
123
+ expect(error.code).toBe("provider_not_configured");
124
+ }
125
+ });
126
+ });
127
+
128
+ describe("getProviderNames", () => {
129
+ it("should return list of static provider names", () => {
130
+ const registry = new ProviderRegistry(staticProviders);
131
+ const names = registry.getProviderNames();
132
+ expect(names).toEqual(["google", "microsoft"]);
133
+ });
134
+
135
+ it("should return empty array if no static providers", () => {
136
+ const registry = new ProviderRegistry({});
137
+ const names = registry.getProviderNames();
138
+ expect(names).toEqual([]);
139
+ });
140
+ });
141
+
142
+ describe("clearCache", () => {
143
+ it("should clear specific provider from cache", () => {
144
+ const registry = new ProviderRegistry(staticProviders);
145
+ registry.clearCache("google");
146
+ // Cache is cleared, next getProvider will re-initialize
147
+ expect(registry).toBeDefined(); // Basic assertion
148
+ });
149
+
150
+ it("should clear all providers from cache", () => {
151
+ const registry = new ProviderRegistry(staticProviders);
152
+ registry.clearCache();
153
+ expect(registry).toBeDefined();
154
+ });
155
+
156
+ it("should not throw error for non-existent provider", () => {
157
+ const registry = new ProviderRegistry(staticProviders);
158
+ expect(() => registry.clearCache("nonexistent")).not.toThrow();
159
+ });
160
+ });
161
+
162
+ describe("provider caching", () => {
163
+ it("should cache provider instances", async () => {
164
+ const loader = jasmine.createSpy("loader").and.returnValue(
165
+ Promise.resolve(
166
+ createTestProviderConfig({
167
+ issuer: "https://test.example.com",
168
+ discoveryUrl: "https://test.example.com/.well-known/openid-configuration",
169
+ })
170
+ )
171
+ );
172
+ const registry = new ProviderRegistry({}, loader);
173
+
174
+ // First call - will attempt to initialize (and fail on discovery)
175
+ try {
176
+ await registry.getProvider("test");
177
+ } catch (error) {
178
+ // Expected to fail on discovery
179
+ }
180
+
181
+ // Loader should have been called once
182
+ expect(loader).toHaveBeenCalledTimes(1);
183
+
184
+ // Clear cache and try again
185
+ registry.clearCache("test");
186
+
187
+ try {
188
+ await registry.getProvider("test");
189
+ } catch (error) {
190
+ // Expected to fail on discovery
191
+ }
192
+
193
+ // Loader should have been called again after cache clear
194
+ expect(loader).toHaveBeenCalledTimes(2);
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Tests for OidcConnectionRepo
3
+ */
4
+
5
+ import OidcConnectionRepo from "../../src/repos/OidcConnectionRepo";
6
+ import { createMockDb } from "../helpers/test-helpers";
7
+ import OidcConnection from "../../src/schemas/OidcConnection";
8
+
9
+ describe("OidcConnectionRepo", () => {
10
+ let repo: OidcConnectionRepo;
11
+ let db: any;
12
+ let cleanup: () => Promise<void>;
13
+
14
+ beforeEach(async () => {
15
+ const mockDb = await createMockDb();
16
+ db = mockDb.db;
17
+ cleanup = mockDb.cleanup;
18
+ repo = new OidcConnectionRepo("oidc_connections_test", db);
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await cleanup();
23
+ });
24
+
25
+ describe("create", () => {
26
+ it("should create a new connection", async () => {
27
+ const connection: Omit<OidcConnection, "_id"> = {
28
+ userId: "user-123",
29
+ provider: "google",
30
+ subject: "google-user-456",
31
+ issuer: "https://accounts.google.com",
32
+ accessToken: "encrypted-access-token",
33
+ idToken: "encrypted-id-token",
34
+ refreshToken: "encrypted-refresh-token",
35
+ expiresAt: new Date(Date.now() + 3600000),
36
+ scope: "openid email profile",
37
+ createdAt: new Date(),
38
+ updatedAt: new Date(),
39
+ };
40
+
41
+ const created = await repo.create(connection);
42
+
43
+ expect(created._id).toBeDefined();
44
+ expect(created.userId).toBe("user-123");
45
+ expect(created.provider).toBe("google");
46
+ expect(created.subject).toBe("google-user-456");
47
+ });
48
+ });
49
+
50
+ describe("findByUserAndProvider", () => {
51
+ it("should find connection by user ID and provider", async () => {
52
+ const connection: Omit<OidcConnection, "_id"> = {
53
+ userId: "user-456",
54
+ provider: "microsoft",
55
+ subject: "ms-user-789",
56
+ issuer: "https://login.microsoftonline.com",
57
+ accessToken: "encrypted-token",
58
+ idToken: "encrypted-id",
59
+ expiresAt: new Date(Date.now() + 3600000),
60
+ createdAt: new Date(),
61
+ updatedAt: new Date(),
62
+ };
63
+
64
+ await repo.create(connection);
65
+
66
+ const found = await repo.findByUserAndProvider("user-456", "microsoft");
67
+
68
+ expect(found).toBeDefined();
69
+ expect(found?.userId).toBe("user-456");
70
+ expect(found?.provider).toBe("microsoft");
71
+ expect(found?.subject).toBe("ms-user-789");
72
+ });
73
+
74
+ it("should return null if connection not found", async () => {
75
+ const found = await repo.findByUserAndProvider("nonexistent-user", "google");
76
+ expect(found).toBeNull();
77
+ });
78
+
79
+ it("should differentiate between providers for same user", async () => {
80
+ const googleConnection: Omit<OidcConnection, "_id"> = {
81
+ userId: "user-multi",
82
+ provider: "google",
83
+ subject: "google-sub",
84
+ issuer: "https://accounts.google.com",
85
+ accessToken: "google-token",
86
+ idToken: "google-id",
87
+ expiresAt: new Date(Date.now() + 3600000),
88
+ createdAt: new Date(),
89
+ updatedAt: new Date(),
90
+ };
91
+
92
+ const msConnection: Omit<OidcConnection, "_id"> = {
93
+ userId: "user-multi",
94
+ provider: "microsoft",
95
+ subject: "ms-sub",
96
+ issuer: "https://login.microsoftonline.com",
97
+ accessToken: "ms-token",
98
+ idToken: "ms-id",
99
+ expiresAt: new Date(Date.now() + 3600000),
100
+ createdAt: new Date(),
101
+ updatedAt: new Date(),
102
+ };
103
+
104
+ await repo.create(googleConnection);
105
+ await repo.create(msConnection);
106
+
107
+ const foundGoogle = await repo.findByUserAndProvider("user-multi", "google");
108
+ const foundMs = await repo.findByUserAndProvider("user-multi", "microsoft");
109
+
110
+ expect(foundGoogle?.subject).toBe("google-sub");
111
+ expect(foundMs?.subject).toBe("ms-sub");
112
+ });
113
+ });
114
+
115
+ describe("findByUserId", () => {
116
+ it("should find all connections for a user", async () => {
117
+ const connection1: Omit<OidcConnection, "_id"> = {
118
+ userId: "user-multi-conn",
119
+ provider: "google",
120
+ subject: "google-sub-1",
121
+ issuer: "https://accounts.google.com",
122
+ accessToken: "token-1",
123
+ idToken: "id-1",
124
+ expiresAt: new Date(Date.now() + 3600000),
125
+ createdAt: new Date(),
126
+ updatedAt: new Date(),
127
+ };
128
+
129
+ const connection2: Omit<OidcConnection, "_id"> = {
130
+ userId: "user-multi-conn",
131
+ provider: "okta",
132
+ subject: "okta-sub-2",
133
+ issuer: "https://okta.example.com",
134
+ accessToken: "token-2",
135
+ idToken: "id-2",
136
+ expiresAt: new Date(Date.now() + 3600000),
137
+ createdAt: new Date(),
138
+ updatedAt: new Date(),
139
+ };
140
+
141
+ await repo.create(connection1);
142
+ await repo.create(connection2);
143
+
144
+ const connections = await repo.findByUserId("user-multi-conn");
145
+
146
+ expect(connections.length).toBe(2);
147
+ expect(connections.map((c) => c.provider).sort()).toEqual(["google", "okta"]);
148
+ });
149
+
150
+ it("should return empty array if no connections found", async () => {
151
+ const connections = await repo.findByUserId("user-no-connections");
152
+ expect(connections).toEqual([]);
153
+ });
154
+ });
155
+
156
+ describe("findBySubjectAndIssuer", () => {
157
+ it("should find connection by subject and issuer", async () => {
158
+ const connection: Omit<OidcConnection, "_id"> = {
159
+ userId: "user-unique",
160
+ provider: "auth0",
161
+ subject: "auth0-subject-123",
162
+ issuer: "https://example.auth0.com",
163
+ accessToken: "token",
164
+ idToken: "id",
165
+ expiresAt: new Date(Date.now() + 3600000),
166
+ createdAt: new Date(),
167
+ updatedAt: new Date(),
168
+ };
169
+
170
+ await repo.create(connection);
171
+
172
+ const found = await repo.findBySubjectAndIssuer("auth0-subject-123", "https://example.auth0.com");
173
+
174
+ expect(found).toBeDefined();
175
+ expect(found?.userId).toBe("user-unique");
176
+ expect(found?.provider).toBe("auth0");
177
+ });
178
+
179
+ it("should return null if connection not found", async () => {
180
+ const found = await repo.findBySubjectAndIssuer("nonexistent-sub", "https://example.com");
181
+ expect(found).toBeNull();
182
+ });
183
+ });
184
+
185
+ // updateOne is tested via integration tests - ObjectId handling requires special setup
186
+
187
+ describe("deleteByUserAndProvider", () => {
188
+ it("should delete connection by user and provider", async () => {
189
+ const connection: Omit<OidcConnection, "_id"> = {
190
+ userId: "user-delete",
191
+ provider: "okta",
192
+ subject: "okta-sub",
193
+ issuer: "https://okta.example.com",
194
+ accessToken: "token",
195
+ idToken: "id",
196
+ expiresAt: new Date(Date.now() + 3600000),
197
+ createdAt: new Date(),
198
+ updatedAt: new Date(),
199
+ };
200
+
201
+ await repo.create(connection);
202
+
203
+ await repo.deleteByUserAndProvider("user-delete", "okta");
204
+
205
+ const found = await repo.findByUserAndProvider("user-delete", "okta");
206
+ expect(found).toBeNull();
207
+ });
208
+
209
+ it("should not throw if connection not found", async () => {
210
+ await repo.deleteByUserAndProvider("nonexistent-user", "google");
211
+ // Should complete without error
212
+ expect(true).toBe(true);
213
+ });
214
+ });
215
+
216
+ describe("deleteByUserId", () => {
217
+ it("should delete all connections for a user", async () => {
218
+ const connection1: Omit<OidcConnection, "_id"> = {
219
+ userId: "user-delete-all",
220
+ provider: "google",
221
+ subject: "google-sub",
222
+ issuer: "https://accounts.google.com",
223
+ accessToken: "token-1",
224
+ idToken: "id-1",
225
+ expiresAt: new Date(Date.now() + 3600000),
226
+ createdAt: new Date(),
227
+ updatedAt: new Date(),
228
+ };
229
+
230
+ const connection2: Omit<OidcConnection, "_id"> = {
231
+ userId: "user-delete-all",
232
+ provider: "microsoft",
233
+ subject: "ms-sub",
234
+ issuer: "https://login.microsoftonline.com",
235
+ accessToken: "token-2",
236
+ idToken: "id-2",
237
+ expiresAt: new Date(Date.now() + 3600000),
238
+ createdAt: new Date(),
239
+ updatedAt: new Date(),
240
+ };
241
+
242
+ await repo.create(connection1);
243
+ await repo.create(connection2);
244
+
245
+ const deletedCount = await repo.deleteByUserId("user-delete-all");
246
+ expect(deletedCount).toBe(2);
247
+
248
+ const connections = await repo.findByUserId("user-delete-all");
249
+ expect(connections.length).toBe(0);
250
+ });
251
+
252
+ it("should return 0 if no connections to delete", async () => {
253
+ const deletedCount = await repo.deleteByUserId("user-no-connections");
254
+ expect(deletedCount).toBe(0);
255
+ });
256
+ });
257
+ });
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Tests for OidcSessionRepo
3
+ */
4
+
5
+ import OidcSessionRepo from "../../src/repos/OidcSessionRepo";
6
+ import { createMockDb } from "../helpers/test-helpers";
7
+ import OidcSession from "../../src/schemas/OidcSession";
8
+
9
+ describe("OidcSessionRepo", () => {
10
+ let repo: OidcSessionRepo;
11
+ let db: any;
12
+ let cleanup: () => Promise<void>;
13
+
14
+ beforeEach(async () => {
15
+ const mockDb = await createMockDb();
16
+ db = mockDb.db;
17
+ cleanup = mockDb.cleanup;
18
+ repo = new OidcSessionRepo("oidc_sessions_test", db);
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await cleanup();
23
+ });
24
+
25
+ describe("create", () => {
26
+ it("should create a new session", async () => {
27
+ const session: Omit<OidcSession, "_id"> = {
28
+ sessionId: "session-123",
29
+ state: "state-abc",
30
+ codeVerifier: "verifier-xyz",
31
+ nonce: "nonce-789",
32
+ provider: "google",
33
+ redirectUri: "http://localhost:3000/callback",
34
+ createdAt: new Date(),
35
+ };
36
+
37
+ const created = await repo.create(session);
38
+
39
+ expect(created._id).toBeDefined();
40
+ expect(created.sessionId).toBe("session-123");
41
+ expect(created.state).toBe("state-abc");
42
+ expect(created.codeVerifier).toBe("verifier-xyz");
43
+ expect(created.nonce).toBe("nonce-789");
44
+ expect(created.provider).toBe("google");
45
+ });
46
+ });
47
+
48
+ describe("getByState", () => {
49
+ it("should find session by state", async () => {
50
+ const session: Omit<OidcSession, "_id"> = {
51
+ sessionId: "session-456",
52
+ state: "state-unique-123",
53
+ codeVerifier: "verifier-abc",
54
+ nonce: "nonce-xyz",
55
+ provider: "microsoft",
56
+ redirectUri: "http://localhost:3000/callback",
57
+ createdAt: new Date(),
58
+ };
59
+
60
+ await repo.create(session);
61
+
62
+ const found = await repo.getByState("state-unique-123");
63
+
64
+ expect(found).toBeDefined();
65
+ expect(found?.sessionId).toBe("session-456");
66
+ expect(found?.state).toBe("state-unique-123");
67
+ });
68
+
69
+ it("should return null if session not found", async () => {
70
+ const found = await repo.getByState("nonexistent-state");
71
+ expect(found).toBeNull();
72
+ });
73
+ });
74
+
75
+ describe("getOne({ sessionId", () => {
76
+ it("should find session by session ID", async () => {
77
+ const session: Omit<OidcSession, "_id"> = {
78
+ sessionId: "session-unique-789",
79
+ state: "state-def",
80
+ codeVerifier: "verifier-ghi",
81
+ nonce: "nonce-jkl",
82
+ provider: "okta",
83
+ redirectUri: "http://localhost:3000/callback",
84
+ createdAt: new Date(),
85
+ };
86
+
87
+ await repo.create(session);
88
+
89
+ const found = await repo.getOne({ sessionId: "session-unique-789" });
90
+
91
+ expect(found).toBeDefined();
92
+ expect(found?.sessionId).toBe("session-unique-789");
93
+ expect(found?.provider).toBe("okta");
94
+ });
95
+
96
+ it("should return null if session not found", async () => {
97
+ const found = await repo.getOne({ sessionId: "nonexistent-session" });
98
+ expect(found).toBeNull();
99
+ });
100
+ });
101
+
102
+ describe("deleteByState", () => {
103
+ it("should delete session by state", async () => {
104
+ const session: Omit<OidcSession, "_id"> = {
105
+ sessionId: "session-delete-1",
106
+ state: "state-delete-1",
107
+ codeVerifier: "verifier-delete",
108
+ nonce: "nonce-delete",
109
+ provider: "auth0",
110
+ redirectUri: "http://localhost:3000/callback",
111
+ createdAt: new Date(),
112
+ };
113
+
114
+ await repo.create(session);
115
+
116
+ await repo.deleteByState("state-delete-1");
117
+
118
+ const found = await repo.getByState("state-delete-1");
119
+ expect(found).toBeNull();
120
+ });
121
+
122
+ it("should not throw if session not found", async () => {
123
+ await repo.deleteByState("nonexistent-state");
124
+ // Should complete without error
125
+ expect(true).toBe(true);
126
+ });
127
+ });
128
+
129
+ describe("deleteBySessionId", () => {
130
+ it("should delete session by session ID", async () => {
131
+ const session: Omit<OidcSession, "_id"> = {
132
+ sessionId: "session-delete-2",
133
+ state: "state-delete-2",
134
+ codeVerifier: "verifier-delete-2",
135
+ nonce: "nonce-delete-2",
136
+ provider: "custom",
137
+ redirectUri: "http://localhost:3000/callback",
138
+ createdAt: new Date(),
139
+ };
140
+
141
+ await repo.create(session);
142
+
143
+ await repo.deleteBySessionId("session-delete-2");
144
+
145
+ const found = await repo.getOne({ sessionId: "session-delete-2" });
146
+ expect(found).toBeNull();
147
+ });
148
+
149
+ it("should not throw if session not found", async () => {
150
+ await repo.deleteBySessionId("nonexistent-session");
151
+ // Should complete without error
152
+ expect(true).toBe(true);
153
+ });
154
+ });
155
+
156
+ describe("multiple sessions", () => {
157
+ it("should handle multiple sessions independently", async () => {
158
+ const session1: Omit<OidcSession, "_id"> = {
159
+ sessionId: "session-multi-1",
160
+ state: "state-multi-1",
161
+ codeVerifier: "verifier-1",
162
+ nonce: "nonce-1",
163
+ provider: "google",
164
+ redirectUri: "http://localhost:3000/callback",
165
+ createdAt: new Date(),
166
+ };
167
+
168
+ const session2: Omit<OidcSession, "_id"> = {
169
+ sessionId: "session-multi-2",
170
+ state: "state-multi-2",
171
+ codeVerifier: "verifier-2",
172
+ nonce: "nonce-2",
173
+ provider: "microsoft",
174
+ redirectUri: "http://localhost:3000/callback",
175
+ createdAt: new Date(),
176
+ };
177
+
178
+ await repo.create(session1);
179
+ await repo.create(session2);
180
+
181
+ const found1 = await repo.getByState("state-multi-1");
182
+ const found2 = await repo.getByState("state-multi-2");
183
+
184
+ expect(found1?.sessionId).toBe("session-multi-1");
185
+ expect(found2?.sessionId).toBe("session-multi-2");
186
+
187
+ await repo.deleteByState("state-multi-1");
188
+
189
+ const stillExists = await repo.getByState("state-multi-2");
190
+ expect(stillExists).toBeDefined();
191
+
192
+ const deleted = await repo.getByState("state-multi-1");
193
+ expect(deleted).toBeNull();
194
+ });
195
+ });
196
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "spec_dir": "spec",
3
+ "spec_files": ["**/*[sS]pec.ts"],
4
+ "helpers": ["helpers/**/*.ts"],
5
+ "stopSpecOnExpectationFailure": false,
6
+ "random": false
7
+ }