@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,257 @@
1
+ /**
2
+ * Tests for claims mapping utilities
3
+ */
4
+
5
+ import { mapClaimsToProfile, extractCustomClaims } from "../../src/utils/claims-mapper";
6
+
7
+ describe("claims-mapper", () => {
8
+ describe("mapClaimsToProfile", () => {
9
+ it("should map standard OIDC claims to profile", () => {
10
+ const claims = {
11
+ sub: "user-123",
12
+ email: "test@example.com",
13
+ email_verified: true,
14
+ name: "Test User",
15
+ given_name: "Test",
16
+ family_name: "User",
17
+ preferred_username: "testuser",
18
+ picture: "https://example.com/photo.jpg",
19
+ phone_number: "+1234567890",
20
+ };
21
+
22
+ const profile = mapClaimsToProfile(claims);
23
+
24
+ expect(profile.id).toBe("user-123");
25
+ expect(profile.email).toBe("test@example.com");
26
+ expect(profile.emailVerified).toBe(true);
27
+ expect(profile.name).toBe("Test User");
28
+ expect(profile.givenName).toBe("Test");
29
+ expect(profile.familyName).toBe("User");
30
+ expect(profile.username).toBe("testuser");
31
+ expect(profile.picture).toBe("https://example.com/photo.jpg");
32
+ expect(profile.phoneNumber).toBe("+1234567890");
33
+ });
34
+
35
+ it("should handle minimal claims (only sub and email)", () => {
36
+ const claims = {
37
+ sub: "user-456",
38
+ email: "minimal@example.com",
39
+ };
40
+
41
+ const profile = mapClaimsToProfile(claims);
42
+
43
+ expect(profile.id).toBe("user-456");
44
+ expect(profile.email).toBe("minimal@example.com");
45
+ expect(profile.emailVerified).toBeUndefined();
46
+ expect(profile.name).toBeUndefined();
47
+ expect(profile.givenName).toBeUndefined();
48
+ expect(profile.familyName).toBeUndefined();
49
+ });
50
+
51
+ it("should include all raw claims", () => {
52
+ const claims = {
53
+ sub: "user-789",
54
+ email: "test@example.com",
55
+ custom_field: "custom_value",
56
+ another_field: 123,
57
+ };
58
+
59
+ const profile = mapClaimsToProfile(claims);
60
+
61
+ expect(profile.raw).toEqual(claims);
62
+ expect(profile.raw.custom_field).toBe("custom_value");
63
+ expect(profile.raw.another_field).toBe(123);
64
+ });
65
+
66
+ it("should handle missing email", () => {
67
+ const claims = {
68
+ sub: "user-000",
69
+ };
70
+
71
+ const profile = mapClaimsToProfile(claims);
72
+
73
+ expect(profile.id).toBe("user-000");
74
+ expect(profile.email).toBeUndefined();
75
+ });
76
+
77
+ it("should pass through email_verified as-is", () => {
78
+ const claims1 = {
79
+ sub: "user-111",
80
+ email: "test@example.com",
81
+ email_verified: "true" as any,
82
+ };
83
+
84
+ const profile1 = mapClaimsToProfile(claims1);
85
+ expect(profile1.emailVerified).toBe("true" as any);
86
+
87
+ const claims2 = {
88
+ sub: "user-222",
89
+ email: "test@example.com",
90
+ email_verified: true,
91
+ };
92
+
93
+ const profile2 = mapClaimsToProfile(claims2);
94
+ expect(profile2.emailVerified).toBe(true);
95
+ });
96
+
97
+ it("should use email as fallback for missing preferred_username", () => {
98
+ const claims = {
99
+ sub: "user-333",
100
+ email: "username@example.com",
101
+ };
102
+
103
+ const profile = mapClaimsToProfile(claims);
104
+
105
+ // Should not set username if preferred_username is missing
106
+ expect(profile.username).toBeUndefined();
107
+ });
108
+ });
109
+
110
+ describe("extractCustomClaims", () => {
111
+ it("should extract custom claims using mapping", () => {
112
+ const claims = {
113
+ sub: "user-123",
114
+ email: "test@example.com",
115
+ organization_id: "org-456",
116
+ department: "Engineering",
117
+ employee_id: "EMP-789",
118
+ };
119
+
120
+ const claimMapping = {
121
+ organizationId: "organization_id",
122
+ department: "department",
123
+ employeeId: "employee_id",
124
+ };
125
+
126
+ const customClaims = extractCustomClaims(claims, claimMapping);
127
+
128
+ expect(customClaims.organizationId).toBe("org-456");
129
+ expect(customClaims.department).toBe("Engineering");
130
+ expect(customClaims.employeeId).toBe("EMP-789");
131
+ });
132
+
133
+ it("should handle nested claim paths with dot notation", () => {
134
+ const claims = {
135
+ sub: "user-123",
136
+ email: "test@example.com",
137
+ "custom:org": {
138
+ id: "org-456",
139
+ name: "ACME Corp",
140
+ },
141
+ roles: ["admin", "user"],
142
+ };
143
+
144
+ const claimMapping = {
145
+ organizationId: "custom:org.id",
146
+ organizationName: "custom:org.name",
147
+ roles: "roles",
148
+ };
149
+
150
+ const customClaims = extractCustomClaims(claims, claimMapping);
151
+
152
+ expect(customClaims.organizationId).toBe("org-456");
153
+ expect(customClaims.organizationName).toBe("ACME Corp");
154
+ expect(customClaims.roles).toEqual(["admin", "user"]);
155
+ });
156
+
157
+ it("should return undefined for missing claims", () => {
158
+ const claims = {
159
+ sub: "user-123",
160
+ email: "test@example.com",
161
+ };
162
+
163
+ const claimMapping = {
164
+ organizationId: "organization_id",
165
+ missingField: "does_not_exist",
166
+ };
167
+
168
+ const customClaims = extractCustomClaims(claims, claimMapping);
169
+
170
+ expect(customClaims.organizationId).toBeUndefined();
171
+ expect(customClaims.missingField).toBeUndefined();
172
+ });
173
+
174
+ it("should handle empty claim mapping", () => {
175
+ const claims = {
176
+ sub: "user-123",
177
+ email: "test@example.com",
178
+ };
179
+
180
+ const customClaims = extractCustomClaims(claims, {});
181
+
182
+ expect(customClaims).toEqual({});
183
+ });
184
+
185
+ it("should preserve data types", () => {
186
+ const claims = {
187
+ sub: "user-123",
188
+ email: "test@example.com",
189
+ age: 30,
190
+ active: true,
191
+ roles: ["admin", "user"],
192
+ metadata: { key: "value" },
193
+ };
194
+
195
+ const claimMapping = {
196
+ age: "age",
197
+ active: "active",
198
+ roles: "roles",
199
+ metadata: "metadata",
200
+ };
201
+
202
+ const customClaims = extractCustomClaims(claims, claimMapping);
203
+
204
+ expect(customClaims.age).toBe(30);
205
+ expect(typeof customClaims.age).toBe("number");
206
+ expect(customClaims.active).toBe(true);
207
+ expect(typeof customClaims.active).toBe("boolean");
208
+ expect(Array.isArray(customClaims.roles)).toBe(true);
209
+ expect(typeof customClaims.metadata).toBe("object");
210
+ });
211
+
212
+ it("should handle array access in paths", () => {
213
+ const claims = {
214
+ sub: "user-123",
215
+ email: "test@example.com",
216
+ groups: [{ id: "group-1", name: "Admins" }, { id: "group-2", name: "Users" }],
217
+ };
218
+
219
+ const claimMapping = {
220
+ firstGroupId: "groups.0.id",
221
+ firstGroupName: "groups.0.name",
222
+ };
223
+
224
+ const customClaims = extractCustomClaims(claims, claimMapping);
225
+
226
+ expect(customClaims.firstGroupId).toBe("group-1");
227
+ expect(customClaims.firstGroupName).toBe("Admins");
228
+ });
229
+ });
230
+
231
+ describe("integration: mapClaimsToProfile with extractCustomClaims", () => {
232
+ it("should work together to create enriched profile", () => {
233
+ const claims = {
234
+ sub: "user-123",
235
+ email: "test@example.com",
236
+ name: "Test User",
237
+ organization_id: "org-456",
238
+ department: "Engineering",
239
+ };
240
+
241
+ const claimMapping = {
242
+ organizationId: "organization_id",
243
+ department: "department",
244
+ };
245
+
246
+ const customClaims = extractCustomClaims(claims, claimMapping);
247
+ const enrichedClaims = { ...claims, ...customClaims };
248
+ const profile = mapClaimsToProfile(enrichedClaims);
249
+
250
+ expect(profile.id).toBe("user-123");
251
+ expect(profile.email).toBe("test@example.com");
252
+ expect(profile.name).toBe("Test User");
253
+ expect(profile.raw.organizationId).toBe("org-456");
254
+ expect(profile.raw.department).toBe("Engineering");
255
+ });
256
+ });
257
+ });
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Tests for encryption utility functions
3
+ */
4
+
5
+ import { encryptToken, decryptToken, validateEncryptionSecret } from "../../src/utils/encryption-utils";
6
+
7
+ describe("encryption-utils", () => {
8
+ const validSecret = "this-is-a-very-secure-secret-key-at-least-32-chars-long";
9
+ const testToken = "test-access-token-value";
10
+
11
+ describe("validateEncryptionSecret", () => {
12
+ it("should accept valid secrets (32+ chars)", () => {
13
+ expect(() => validateEncryptionSecret(validSecret)).not.toThrow();
14
+ });
15
+
16
+ it("should reject short secrets", () => {
17
+ expect(() => validateEncryptionSecret("short")).toThrowError(/at least 32 characters/);
18
+ });
19
+
20
+ it("should reject empty secrets", () => {
21
+ expect(() => validateEncryptionSecret("")).toThrow();
22
+ });
23
+
24
+ it("should accept exactly 32 character secrets", () => {
25
+ const exactSecret = "a".repeat(32);
26
+ expect(() => validateEncryptionSecret(exactSecret)).not.toThrow();
27
+ });
28
+ });
29
+
30
+ describe("encryptToken", () => {
31
+ it("should encrypt a token", () => {
32
+ const encrypted = encryptToken(testToken, validSecret);
33
+ expect(encrypted).toBeDefined();
34
+ expect(typeof encrypted).toBe("string");
35
+ expect(encrypted).not.toBe(testToken);
36
+ });
37
+
38
+ it("should produce different ciphertext each time (unique IV)", () => {
39
+ const encrypted1 = encryptToken(testToken, validSecret);
40
+ const encrypted2 = encryptToken(testToken, validSecret);
41
+ expect(encrypted1).not.toBe(encrypted2);
42
+ });
43
+
44
+ it("should produce valid hex output with colons", () => {
45
+ const encrypted = encryptToken(testToken, validSecret);
46
+ // Format: iv:authTag:encryptedData (all hex-encoded)
47
+ expect(encrypted).toMatch(/^[a-f0-9]+:[a-f0-9]+:[a-f0-9]+$/);
48
+ });
49
+
50
+ it("should handle empty tokens", () => {
51
+ const encrypted = encryptToken("", validSecret);
52
+ expect(encrypted).toBeDefined();
53
+ expect(typeof encrypted).toBe("string");
54
+ });
55
+ });
56
+
57
+ describe("decryptToken", () => {
58
+ it("should decrypt an encrypted token", () => {
59
+ const encrypted = encryptToken(testToken, validSecret);
60
+ const decrypted = decryptToken(encrypted, validSecret);
61
+ expect(decrypted).toBe(testToken);
62
+ });
63
+
64
+ it("should handle empty tokens", () => {
65
+ const encrypted = encryptToken("", validSecret);
66
+ const decrypted = decryptToken(encrypted, validSecret);
67
+ expect(decrypted).toBe("");
68
+ });
69
+
70
+ it("should handle long tokens", () => {
71
+ const longToken = "a".repeat(1000);
72
+ const encrypted = encryptToken(longToken, validSecret);
73
+ const decrypted = decryptToken(encrypted, validSecret);
74
+ expect(decrypted).toBe(longToken);
75
+ });
76
+
77
+ it("should throw error for invalid ciphertext", () => {
78
+ expect(() => decryptToken("invalid-base64", validSecret)).toThrow();
79
+ });
80
+
81
+ it("should throw error for tampered ciphertext", () => {
82
+ const encrypted = encryptToken(testToken, validSecret);
83
+ const tampered = encrypted.substring(0, encrypted.length - 5) + "xxxxx";
84
+ expect(() => decryptToken(tampered, validSecret)).toThrow();
85
+ });
86
+
87
+ it("should throw error for wrong secret", () => {
88
+ const encrypted = encryptToken(testToken, validSecret);
89
+ const wrongSecret = "different-secret-key-at-least-32-chars-long-enough";
90
+ expect(() => decryptToken(encrypted, wrongSecret)).toThrow();
91
+ });
92
+ });
93
+
94
+ describe("encrypt/decrypt round-trip", () => {
95
+ it("should successfully round-trip various token formats", () => {
96
+ const testCases = [
97
+ "simple-token",
98
+ "token.with.dots",
99
+ "token-with-dashes",
100
+ "token_with_underscores",
101
+ "VeryLongTokenWithMixedCaseAndNumbers123456789",
102
+ "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig",
103
+ ];
104
+
105
+ testCases.forEach((token) => {
106
+ const encrypted = encryptToken(token, validSecret);
107
+ const decrypted = decryptToken(encrypted, validSecret);
108
+ expect(decrypted).toBe(token);
109
+ });
110
+ });
111
+
112
+ it("should handle special characters", () => {
113
+ const specialToken = "token!@#$%^&*()+=[]{}|;:',.<>?/~`";
114
+ const encrypted = encryptToken(specialToken, validSecret);
115
+ const decrypted = decryptToken(encrypted, validSecret);
116
+ expect(decrypted).toBe(specialToken);
117
+ });
118
+
119
+ it("should handle unicode characters", () => {
120
+ const unicodeToken = "token-with-emoji-😀-and-unicode-™-©";
121
+ const encrypted = encryptToken(unicodeToken, validSecret);
122
+ const decrypted = decryptToken(encrypted, validSecret);
123
+ expect(decrypted).toBe(unicodeToken);
124
+ });
125
+ });
126
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Tests for error utility functions
3
+ */
4
+
5
+ import { createOidcError, validateProvider, OidcErrorCodes } from "../../src/utils/error-utils";
6
+
7
+ describe("error-utils", () => {
8
+ describe("OidcErrorCodes", () => {
9
+ it("should define all expected error codes", () => {
10
+ expect(OidcErrorCodes.INVALID_STATE).toBe("invalid_state");
11
+ expect(OidcErrorCodes.SESSION_NOT_FOUND).toBe("session_not_found");
12
+ expect(OidcErrorCodes.SESSION_EXPIRED).toBe("session_expired");
13
+ expect(OidcErrorCodes.PROVIDER_NOT_CONFIGURED).toBe("provider_not_configured");
14
+ expect(OidcErrorCodes.DISCOVERY_FAILED).toBe("discovery_failed");
15
+ expect(OidcErrorCodes.TOKEN_EXCHANGE_FAILED).toBe("token_exchange_failed");
16
+ expect(OidcErrorCodes.USERINFO_FAILED).toBe("userinfo_failed");
17
+ expect(OidcErrorCodes.INVALID_PROVIDER).toBe("invalid_provider");
18
+ expect(OidcErrorCodes.MISSING_CODE).toBe("missing_code");
19
+ expect(OidcErrorCodes.MISSING_STATE).toBe("missing_state");
20
+ });
21
+ });
22
+
23
+ describe("createOidcError", () => {
24
+ it("should create an error with code and message", () => {
25
+ const error = createOidcError(OidcErrorCodes.INVALID_STATE, "State mismatch");
26
+ expect(error).toBeInstanceOf(Error);
27
+ expect(error.message).toBe("State mismatch");
28
+ expect(error.code).toBe("invalid_state");
29
+ });
30
+
31
+ it("should include details if provided", () => {
32
+ const details = { provider: "test", sessionId: "123" };
33
+ const error = createOidcError(OidcErrorCodes.SESSION_NOT_FOUND, "Session not found", details);
34
+ expect(error.details).toEqual(details);
35
+ });
36
+
37
+ it("should work without details", () => {
38
+ const error = createOidcError(OidcErrorCodes.INVALID_STATE, "State mismatch");
39
+ expect(error.details).toBeUndefined();
40
+ });
41
+
42
+ it("should be throwable", () => {
43
+ expect(() => {
44
+ throw createOidcError(OidcErrorCodes.SESSION_EXPIRED, "Session expired");
45
+ }).toThrowError("Session expired");
46
+ });
47
+ });
48
+
49
+
50
+ describe("validateProvider", () => {
51
+ it("should accept valid provider names", () => {
52
+ expect(() => validateProvider("google")).not.toThrow();
53
+ expect(() => validateProvider("microsoft")).not.toThrow();
54
+ expect(() => validateProvider("okta")).not.toThrow();
55
+ expect(() => validateProvider("auth0")).not.toThrow();
56
+ expect(() => validateProvider("custom-provider")).not.toThrow();
57
+ expect(() => validateProvider("provider_123")).not.toThrow();
58
+ });
59
+
60
+ it("should reject invalid provider names", () => {
61
+ expect(() => validateProvider("")).toThrow();
62
+ expect(() => validateProvider("provider with spaces")).toThrow();
63
+ expect(() => validateProvider("provider/slash")).toThrow();
64
+ expect(() => validateProvider("provider\\backslash")).toThrow();
65
+ expect(() => validateProvider("provider.dot")).toThrow();
66
+ });
67
+
68
+ it("should reject provider names with special characters", () => {
69
+ const invalidChars = ["@", "#", "$", "%", "^", "&", "*", "(", ")", "+", "=", "[", "]", "{", "}", "|", ";", ":", "'", '"', "<", ">", "?", "/", "\\"];
70
+ invalidChars.forEach((char) => {
71
+ expect(() => validateProvider(`provider${char}name`)).toThrow();
72
+ });
73
+ });
74
+
75
+ it("should accept alphanumeric with dash and underscore", () => {
76
+ expect(() => validateProvider("provider-name-123")).not.toThrow();
77
+ expect(() => validateProvider("provider_name_456")).not.toThrow();
78
+ expect(() => validateProvider("ProviderName789")).not.toThrow();
79
+ });
80
+ });
81
+
82
+ describe("error handling in try-catch", () => {
83
+ it("should be catchable and inspectable", () => {
84
+ try {
85
+ throw createOidcError(OidcErrorCodes.TOKEN_EXCHANGE_FAILED, "Token exchange failed", {
86
+ originalError: "Network timeout",
87
+ });
88
+ } catch (error: any) {
89
+ expect(error.code).toBe("token_exchange_failed");
90
+ expect(error.message).toBe("Token exchange failed");
91
+ expect(error.details.originalError).toBe("Network timeout");
92
+ }
93
+ });
94
+
95
+ it("should allow error code checking", () => {
96
+ try {
97
+ throw createOidcError(OidcErrorCodes.PROVIDER_NOT_CONFIGURED, "Provider not found");
98
+ } catch (error: any) {
99
+ if (error.code === "provider_not_configured") {
100
+ expect(error.message).toBe("Provider not found");
101
+ } else {
102
+ fail("Should have caught provider_not_configured error");
103
+ }
104
+ }
105
+ });
106
+ });
107
+ });
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Tests for state utility functions
3
+ */
4
+
5
+ import { generateState, generateSessionId, generateNonce, validateState } from "../../src/utils/state-utils";
6
+
7
+ describe("state-utils", () => {
8
+ describe("generateState", () => {
9
+ it("should generate a random state string", () => {
10
+ const state = generateState();
11
+ expect(state).toBeDefined();
12
+ expect(typeof state).toBe("string");
13
+ expect(state.length).toBeGreaterThan(0);
14
+ });
15
+
16
+ it("should generate unique state values", () => {
17
+ const state1 = generateState();
18
+ const state2 = generateState();
19
+ expect(state1).not.toBe(state2);
20
+ });
21
+
22
+ it("should generate state with sufficient entropy (64 hex chars = 32 bytes)", () => {
23
+ const state = generateState();
24
+ expect(state.length).toBe(64);
25
+ expect(state).toMatch(/^[a-f0-9]{64}$/);
26
+ });
27
+ });
28
+
29
+ describe("generateSessionId", () => {
30
+ it("should generate a random session ID", () => {
31
+ const sessionId = generateSessionId();
32
+ expect(sessionId).toBeDefined();
33
+ expect(typeof sessionId).toBe("string");
34
+ expect(sessionId.length).toBeGreaterThan(0);
35
+ });
36
+
37
+ it("should generate unique session IDs", () => {
38
+ const id1 = generateSessionId();
39
+ const id2 = generateSessionId();
40
+ expect(id1).not.toBe(id2);
41
+ });
42
+
43
+ it("should generate session ID with sufficient entropy", () => {
44
+ const sessionId = generateSessionId();
45
+ expect(sessionId.length).toBe(32);
46
+ expect(sessionId).toMatch(/^[a-f0-9]{32}$/);
47
+ });
48
+ });
49
+
50
+ describe("generateNonce", () => {
51
+ it("should generate a random nonce", () => {
52
+ const nonce = generateNonce();
53
+ expect(nonce).toBeDefined();
54
+ expect(typeof nonce).toBe("string");
55
+ expect(nonce.length).toBeGreaterThan(0);
56
+ });
57
+
58
+ it("should generate unique nonces", () => {
59
+ const nonce1 = generateNonce();
60
+ const nonce2 = generateNonce();
61
+ expect(nonce1).not.toBe(nonce2);
62
+ });
63
+
64
+ it("should generate nonce with sufficient entropy", () => {
65
+ const nonce = generateNonce();
66
+ expect(nonce.length).toBe(32);
67
+ expect(nonce).toMatch(/^[a-f0-9]{32}$/);
68
+ });
69
+ });
70
+
71
+ describe("validateState", () => {
72
+ it("should return true for matching states", () => {
73
+ const state = generateState();
74
+ const result = validateState(state, state);
75
+ expect(result).toBe(true);
76
+ });
77
+
78
+ it("should return false for non-matching states", () => {
79
+ const state1 = generateState();
80
+ const state2 = generateState();
81
+ const result = validateState(state1, state2);
82
+ expect(result).toBe(false);
83
+ });
84
+
85
+ it("should use constant-time comparison to prevent timing attacks", () => {
86
+ const state = "a".repeat(64);
87
+ const wrongState1 = "b".repeat(64);
88
+ const wrongState2 = "a".repeat(63) + "b";
89
+
90
+ // Both should return false
91
+ expect(validateState(wrongState1, state)).toBe(false);
92
+ expect(validateState(wrongState2, state)).toBe(false);
93
+ });
94
+
95
+ it("should handle empty strings", () => {
96
+ expect(validateState("", "")).toBe(false);
97
+ expect(validateState("abc", "")).toBe(false);
98
+ expect(validateState("", "abc")).toBe(false);
99
+ });
100
+ });
101
+
102
+ });
@@ -0,0 +1,15 @@
1
+ import { FlinkContext } from "@flink-app/flink";
2
+ import { OidcPluginContext } from "./OidcPluginContext";
3
+ import OidcSessionRepo from "./repos/OidcSessionRepo";
4
+ import OidcConnectionRepo from "./repos/OidcConnectionRepo";
5
+
6
+ /**
7
+ * Internal context type for OIDC plugin
8
+ * Extends the app's context with OIDC-specific repositories
9
+ */
10
+ export interface OidcInternalContext extends FlinkContext, OidcPluginContext {
11
+ repos: FlinkContext["repos"] & {
12
+ oidcSessionRepo: OidcSessionRepo;
13
+ oidcConnectionRepo: OidcConnectionRepo;
14
+ };
15
+ }