@flink-app/oidc-plugin 0.13.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,151 @@
1
+ /**
2
+ * Basic OIDC Authentication Example
3
+ *
4
+ * Demonstrates:
5
+ * - Single OIDC provider configuration
6
+ * - OIDC Discovery
7
+ * - JIT (Just-In-Time) user provisioning
8
+ * - JWT token generation
9
+ */
10
+
11
+ import { FlinkApp } from "@flink-app/flink";
12
+ import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
13
+ import { oidcPlugin } from "@flink-app/oidc-plugin";
14
+
15
+ interface User {
16
+ _id?: string;
17
+ email: string;
18
+ name: string;
19
+ oidcConnections: Array<{
20
+ issuer: string;
21
+ subject: string;
22
+ provider: string;
23
+ }>;
24
+ createdAt: Date;
25
+ }
26
+
27
+ async function start() {
28
+ const app = new FlinkApp({
29
+ name: "Basic OIDC Example",
30
+
31
+ // Step 1: Configure JWT Auth Plugin
32
+ auth: jwtAuthPlugin({
33
+ secret: process.env.JWT_SECRET || "your-jwt-secret-min-32-chars-long",
34
+ getUser: async (tokenData) => {
35
+ // Fetch user from database using token data
36
+ return await app.ctx.repos.userRepo.getById(tokenData.userId);
37
+ },
38
+ rolePermissions: {
39
+ user: ["read", "write"],
40
+ admin: ["read", "write", "delete"],
41
+ },
42
+ }),
43
+
44
+ db: {
45
+ uri: process.env.MONGODB_URI || "mongodb://localhost:27017/oidc-example",
46
+ },
47
+
48
+ // Step 2: Configure OIDC Plugin
49
+ plugins: [
50
+ oidcPlugin({
51
+ providers: {
52
+ // Provider name (used in URLs: /oidc/acme/initiate)
53
+ acme: {
54
+ // OIDC Issuer URL
55
+ issuer: process.env.OIDC_ISSUER || "https://idp.acme.com",
56
+
57
+ // Client credentials from IdP
58
+ clientId: process.env.OIDC_CLIENT_ID || "your-client-id",
59
+ clientSecret: process.env.OIDC_CLIENT_SECRET || "your-client-secret",
60
+
61
+ // Callback URL (must match IdP configuration)
62
+ callbackUrl: process.env.OIDC_CALLBACK_URL || "http://localhost:3000/oidc/acme/callback",
63
+
64
+ // OIDC Discovery URL (automatic endpoint configuration)
65
+ discoveryUrl: `${process.env.OIDC_ISSUER || "https://idp.acme.com"}/.well-known/openid-configuration`,
66
+
67
+ // Scopes to request
68
+ scope: ["openid", "email", "profile"],
69
+ },
70
+ },
71
+
72
+ // Step 3: Implement JIT user provisioning
73
+ onAuthSuccess: async ({ profile, claims, provider }, ctx) => {
74
+ console.log("OIDC authentication successful");
75
+ console.log("Provider:", provider);
76
+ console.log("Profile:", profile);
77
+ console.log("Claims:", claims);
78
+
79
+ // Find user by OIDC subject + issuer
80
+ let user = await ctx.repos.userRepo.getOne({
81
+ "oidcConnections.subject": claims.sub,
82
+ "oidcConnections.issuer": claims.iss,
83
+ });
84
+
85
+ if (!user) {
86
+ // JIT provisioning - create new user
87
+ console.log("Creating new user via JIT provisioning");
88
+
89
+ user = await ctx.repos.userRepo.create({
90
+ email: claims.email,
91
+ name: claims.name || profile.name || "Unknown User",
92
+ oidcConnections: [
93
+ {
94
+ issuer: claims.iss,
95
+ subject: claims.sub,
96
+ provider,
97
+ },
98
+ ],
99
+ createdAt: new Date(),
100
+ });
101
+
102
+ console.log("User created:", user._id);
103
+ } else {
104
+ console.log("Existing user found:", user._id);
105
+ }
106
+
107
+ // Generate JWT token for the application
108
+ const token = await ctx.plugins.jwtAuth.createToken(
109
+ {
110
+ userId: user._id,
111
+ email: user.email,
112
+ },
113
+ ["user"]
114
+ );
115
+
116
+ console.log("JWT token generated");
117
+
118
+ return {
119
+ user,
120
+ token,
121
+ redirectUrl: "/dashboard",
122
+ };
123
+ },
124
+
125
+ // Step 4: Handle authentication errors
126
+ onAuthError: async ({ error, provider }) => {
127
+ console.error(`OIDC error for ${provider}:`, error);
128
+
129
+ if (error.code === "access_denied") {
130
+ return {
131
+ redirectUrl: "/login?error=user_cancelled",
132
+ };
133
+ }
134
+
135
+ return {
136
+ redirectUrl: "/login?error=authentication_failed",
137
+ };
138
+ },
139
+ }),
140
+ ],
141
+ });
142
+
143
+ await app.start();
144
+ console.log("App started on port 3000");
145
+ console.log("Visit: http://localhost:3000/oidc/acme/initiate to start OIDC flow");
146
+ }
147
+
148
+ start().catch((error) => {
149
+ console.error("Failed to start app:", error);
150
+ process.exit(1);
151
+ });
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Multiple OIDC Providers Example
3
+ *
4
+ * Demonstrates:
5
+ * - Multiple OIDC providers (Azure AD, Okta)
6
+ * - Provider-specific configuration
7
+ * - User linking to multiple IdPs
8
+ */
9
+
10
+ import { FlinkApp } from "@flink-app/flink";
11
+ import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
12
+ import { oidcPlugin } from "@flink-app/oidc-plugin";
13
+
14
+ async function start() {
15
+ const app = new FlinkApp({
16
+ name: "Multi-Provider OIDC Example",
17
+
18
+ auth: jwtAuthPlugin({
19
+ secret: process.env.JWT_SECRET!,
20
+ getUser: async (tokenData) => {
21
+ return await app.ctx.repos.userRepo.getById(tokenData.userId);
22
+ },
23
+ rolePermissions: {
24
+ user: ["read", "write"],
25
+ admin: ["read", "write", "delete"],
26
+ },
27
+ }),
28
+
29
+ db: {
30
+ uri: process.env.MONGODB_URI!,
31
+ },
32
+
33
+ plugins: [
34
+ oidcPlugin({
35
+ providers: {
36
+ // Azure AD / Entra ID
37
+ azure: {
38
+ issuer: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/v2.0`,
39
+ clientId: process.env.AZURE_CLIENT_ID!,
40
+ clientSecret: process.env.AZURE_CLIENT_SECRET!,
41
+ callbackUrl: "https://myapp.com/oidc/azure/callback",
42
+ discoveryUrl: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/v2.0/.well-known/openid-configuration`,
43
+ scope: ["openid", "email", "profile"],
44
+ },
45
+
46
+ // Okta
47
+ okta: {
48
+ issuer: `https://${process.env.OKTA_DOMAIN}`,
49
+ clientId: process.env.OKTA_CLIENT_ID!,
50
+ clientSecret: process.env.OKTA_CLIENT_SECRET!,
51
+ callbackUrl: "https://myapp.com/oidc/okta/callback",
52
+ discoveryUrl: `https://${process.env.OKTA_DOMAIN}/.well-known/openid-configuration`,
53
+ scope: ["openid", "email", "profile"],
54
+ },
55
+
56
+ // Auth0
57
+ auth0: {
58
+ issuer: `https://${process.env.AUTH0_DOMAIN}`,
59
+ clientId: process.env.AUTH0_CLIENT_ID!,
60
+ clientSecret: process.env.AUTH0_CLIENT_SECRET!,
61
+ callbackUrl: "https://myapp.com/oidc/auth0/callback",
62
+ discoveryUrl: `https://${process.env.AUTH0_DOMAIN}/.well-known/openid-configuration`,
63
+ scope: ["openid", "email", "profile"],
64
+ },
65
+ },
66
+
67
+ onAuthSuccess: async ({ profile, claims, provider }, ctx) => {
68
+ // Try to find existing user by OIDC connection
69
+ let user = await ctx.repos.userRepo.getOne({
70
+ "oidcConnections.subject": claims.sub,
71
+ "oidcConnections.issuer": claims.iss,
72
+ });
73
+
74
+ if (!user) {
75
+ // Try to find by email (link existing account)
76
+ user = await ctx.repos.userRepo.getOne({ email: claims.email });
77
+
78
+ if (user) {
79
+ // Link new provider to existing user
80
+ console.log(`Linking ${provider} to existing user ${user._id}`);
81
+
82
+ user.oidcConnections = user.oidcConnections || [];
83
+ user.oidcConnections.push({
84
+ issuer: claims.iss,
85
+ subject: claims.sub,
86
+ provider,
87
+ });
88
+
89
+ await ctx.repos.userRepo.updateOne(user._id, {
90
+ oidcConnections: user.oidcConnections,
91
+ });
92
+ } else {
93
+ // Create new user
94
+ console.log(`Creating new user via ${provider}`);
95
+
96
+ user = await ctx.repos.userRepo.create({
97
+ email: claims.email,
98
+ name: claims.name || profile.name || "Unknown User",
99
+ oidcConnections: [
100
+ {
101
+ issuer: claims.iss,
102
+ subject: claims.sub,
103
+ provider,
104
+ },
105
+ ],
106
+ createdAt: new Date(),
107
+ });
108
+ }
109
+ }
110
+
111
+ const token = await ctx.plugins.jwtAuth.createToken(
112
+ {
113
+ userId: user._id,
114
+ email: user.email,
115
+ },
116
+ ["user"]
117
+ );
118
+
119
+ return {
120
+ user,
121
+ token,
122
+ redirectUrl: "/dashboard",
123
+ };
124
+ },
125
+
126
+ onAuthError: async ({ error, provider }) => {
127
+ console.error(`OIDC error for ${provider}:`, error);
128
+ return {
129
+ redirectUrl: `/login?error=${error.code}&provider=${provider}`,
130
+ };
131
+ },
132
+ }),
133
+ ],
134
+ });
135
+
136
+ await app.start();
137
+ console.log("App started with multiple OIDC providers:");
138
+ console.log("- Azure AD: /oidc/azure/initiate");
139
+ console.log("- Okta: /oidc/okta/initiate");
140
+ console.log("- Auth0: /oidc/auth0/initiate");
141
+ }
142
+
143
+ start().catch((error) => {
144
+ console.error("Failed to start app:", error);
145
+ process.exit(1);
146
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@flink-app/oidc-plugin",
3
+ "version": "0.13.0",
4
+ "description": "Flink plugin for OIDC authentication with generic IdP support",
5
+ "author": "joel@frost.se",
6
+ "license": "MIT",
7
+ "types": "dist/index.d.ts",
8
+ "main": "dist/index.js",
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "dependencies": {
13
+ "openid-client": "^5.7.0"
14
+ },
15
+ "peerDependencies": {
16
+ "mongodb": "^6.15.0",
17
+ "@flink-app/flink": "0.13.0",
18
+ "@flink-app/jwt-auth-plugin": "0.13.0"
19
+ },
20
+ "peerDependenciesMeta": {
21
+ "@flink-app/flink": {
22
+ "optional": false
23
+ },
24
+ "@flink-app/jwt-auth-plugin": {
25
+ "optional": false
26
+ }
27
+ },
28
+ "devDependencies": {
29
+ "@types/jasmine": "^3.7.1",
30
+ "@types/node": "22.13.10",
31
+ "ts-node": "^10.9.2",
32
+ "tsc-watch": "^4.2.9",
33
+ "@flink-app/flink": "0.13.0",
34
+ "@flink-app/jwt-auth-plugin": "0.13.0",
35
+ "@flink-app/test-utils": "0.13.0"
36
+ },
37
+ "scripts": {
38
+ "test": "jasmine-ts --config=./spec/support/jasmine.json",
39
+ "test:watch": "nodemon --ext ts --exec 'jasmine-ts --config=./spec/support/jasmine.json'",
40
+ "build": "tsc --project tsconfig.dist.json",
41
+ "watch": "tsc-watch --project tsconfig.dist.json",
42
+ "clean": "rimraf dist .flink"
43
+ }
44
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Tests for InitiateOidc handler
3
+ *
4
+ * Note: These are unit tests for handler logic.
5
+ * For full integration tests, use the test-utils package in a demo app.
6
+ */
7
+
8
+ import InitiateOidc from "../../src/handlers/InitiateOidc";
9
+ import { validateProvider } from "../../src/utils/error-utils";
10
+ import { generateState, generateSessionId, generateNonce } from "../../src/utils/state-utils";
11
+
12
+ describe("InitiateOidc handler", () => {
13
+ describe("Route configuration", () => {
14
+ it("should export correct route configuration", () => {
15
+ const { Route } = require("../../src/handlers/InitiateOidc");
16
+ expect(Route.path).toBe("/oidc/:provider/initiate");
17
+ expect(Route.method).toBe("get");
18
+ });
19
+ });
20
+
21
+ describe("Provider validation", () => {
22
+ it("should validate provider name format", () => {
23
+ expect(() => validateProvider("google")).not.toThrow();
24
+ expect(() => validateProvider("microsoft")).not.toThrow();
25
+ expect(() => validateProvider("custom-provider")).not.toThrow();
26
+ });
27
+
28
+ it("should reject invalid provider names", () => {
29
+ expect(() => validateProvider("")).toThrow();
30
+ expect(() => validateProvider("provider/slash")).toThrow();
31
+ expect(() => validateProvider("provider with spaces")).toThrow();
32
+ });
33
+ });
34
+
35
+ describe("Security parameter generation", () => {
36
+ it("should generate unique state", () => {
37
+ const state1 = generateState();
38
+ const state2 = generateState();
39
+
40
+ expect(state1).toBeDefined();
41
+ expect(state2).toBeDefined();
42
+ expect(state1).not.toBe(state2);
43
+ expect(state1.length).toBe(64);
44
+ });
45
+
46
+ it("should generate unique session ID", () => {
47
+ const id1 = generateSessionId();
48
+ const id2 = generateSessionId();
49
+
50
+ expect(id1).not.toBe(id2);
51
+ expect(id1.length).toBe(32);
52
+ });
53
+
54
+ it("should generate unique nonce", () => {
55
+ const nonce1 = generateNonce();
56
+ const nonce2 = generateNonce();
57
+
58
+ expect(nonce1).not.toBe(nonce2);
59
+ expect(nonce1.length).toBe(32);
60
+ });
61
+ });
62
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Configure Jasmine spec reporter for verbose output
3
+ */
4
+
5
+ import { SpecReporter } from "jasmine-spec-reporter";
6
+
7
+ jasmine.getEnv().clearReporters();
8
+ jasmine.getEnv().addReporter(
9
+ new SpecReporter({
10
+ spec: {
11
+ displaySuccessful: true,
12
+ displayFailed: true,
13
+ displayPending: true,
14
+ displayDuration: true,
15
+ },
16
+ summary: {
17
+ displaySuccessful: true,
18
+ displayFailed: true,
19
+ displayPending: true,
20
+ displayDuration: true,
21
+ },
22
+ colors: {
23
+ enabled: true,
24
+ successful: "green",
25
+ failed: "red",
26
+ pending: "yellow",
27
+ },
28
+ prefixes: {
29
+ successful: "✓ ",
30
+ failed: "✗ ",
31
+ pending: "- ",
32
+ },
33
+ })
34
+ );
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Test helpers for OIDC plugin tests
3
+ */
4
+
5
+ import { Db, MongoClient } from "mongodb";
6
+ import { OidcProviderConfig } from "../../src/OidcProviderConfig";
7
+
8
+ /**
9
+ * Create a mock MongoDB database for testing
10
+ */
11
+ export async function createMockDb(): Promise<{ db: Db; client: MongoClient; cleanup: () => Promise<void> }> {
12
+ const client = new MongoClient("mongodb://localhost:27017", {
13
+ serverSelectionTimeoutMS: 5000,
14
+ });
15
+
16
+ await client.connect();
17
+ const db = client.db("oidc_plugin_test_" + Date.now());
18
+
19
+ const cleanup = async () => {
20
+ await db.dropDatabase();
21
+ await client.close();
22
+ };
23
+
24
+ return { db, client, cleanup };
25
+ }
26
+
27
+ /**
28
+ * Create a test OIDC provider configuration
29
+ */
30
+ export function createTestProviderConfig(overrides?: Partial<OidcProviderConfig>): OidcProviderConfig {
31
+ return {
32
+ issuer: "https://test-idp.example.com",
33
+ clientId: "test-client-id",
34
+ clientSecret: "test-client-secret-at-least-32-chars-long",
35
+ callbackUrl: "http://localhost:3000/oidc/test/callback",
36
+ discoveryUrl: "https://test-idp.example.com/.well-known/openid-configuration",
37
+ scope: ["openid", "email", "profile"],
38
+ ...overrides,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Create a mock OIDC token set
44
+ */
45
+ export function createMockTokenSet() {
46
+ return {
47
+ accessToken: "mock-access-token",
48
+ idToken: "mock-id-token",
49
+ refreshToken: "mock-refresh-token",
50
+ tokenType: "Bearer",
51
+ expiresIn: 3600,
52
+ scope: "openid email profile",
53
+ claims: {
54
+ sub: "user-123",
55
+ iss: "https://test-idp.example.com",
56
+ aud: "test-client-id",
57
+ exp: Math.floor(Date.now() / 1000) + 3600,
58
+ iat: Math.floor(Date.now() / 1000),
59
+ email: "test@example.com",
60
+ email_verified: true,
61
+ name: "Test User",
62
+ given_name: "Test",
63
+ family_name: "User",
64
+ },
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Create a mock OIDC profile
70
+ */
71
+ export function createMockProfile() {
72
+ return {
73
+ id: "user-123",
74
+ email: "test@example.com",
75
+ emailVerified: true,
76
+ name: "Test User",
77
+ givenName: "Test",
78
+ familyName: "User",
79
+ raw: {
80
+ sub: "user-123",
81
+ email: "test@example.com",
82
+ email_verified: true,
83
+ name: "Test User",
84
+ given_name: "Test",
85
+ family_name: "User",
86
+ },
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Wait for a condition to be true
92
+ */
93
+ export async function waitFor(condition: () => boolean | Promise<boolean>, timeout: number = 5000): Promise<void> {
94
+ const startTime = Date.now();
95
+ while (!(await condition())) {
96
+ if (Date.now() - startTime > timeout) {
97
+ throw new Error("Timeout waiting for condition");
98
+ }
99
+ await new Promise((resolve) => setTimeout(resolve, 100));
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Sleep for a given number of milliseconds
105
+ */
106
+ export function sleep(ms: number): Promise<void> {
107
+ return new Promise((resolve) => setTimeout(resolve, ms));
108
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Tests for OidcPlugin factory
3
+ *
4
+ * Note: These are unit tests for plugin configuration and initialization logic.
5
+ * For full integration tests, use the test-utils package in a demo app.
6
+ */
7
+
8
+ import { oidcPlugin } from "../../src/OidcPlugin";
9
+ import { createTestProviderConfig } from "../helpers/test-helpers";
10
+
11
+ describe("OidcPlugin", () => {
12
+
13
+ describe("configuration validation", () => {
14
+ it("should throw error if no providers configured", () => {
15
+ expect(() => {
16
+ oidcPlugin({
17
+ providers: {},
18
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
19
+ });
20
+ }).toThrowError(/At least one provider must be configured/);
21
+ });
22
+
23
+ it("should throw error if onAuthSuccess is missing", () => {
24
+ expect(() => {
25
+ oidcPlugin({
26
+ providers: {
27
+ test: createTestProviderConfig(),
28
+ },
29
+ onAuthSuccess: undefined as any,
30
+ });
31
+ }).toThrowError(/onAuthSuccess callback is required/);
32
+ });
33
+
34
+ it("should throw error if provider missing required fields", () => {
35
+ expect(() => {
36
+ oidcPlugin({
37
+ providers: {
38
+ test: {
39
+ issuer: "https://test.example.com",
40
+ clientId: "",
41
+ clientSecret: "",
42
+ callbackUrl: "",
43
+ } as any,
44
+ },
45
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
46
+ });
47
+ }).toThrowError(/clientId is required/);
48
+ });
49
+
50
+ it("should throw error if encryption key too short", () => {
51
+ expect(() => {
52
+ oidcPlugin({
53
+ providers: {
54
+ test: createTestProviderConfig(),
55
+ },
56
+ encryptionKey: "short",
57
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
58
+ });
59
+ }).toThrowError(/at least 32 characters/);
60
+ });
61
+
62
+ it("should accept valid configuration", () => {
63
+ expect(() => {
64
+ oidcPlugin({
65
+ providers: {
66
+ test: createTestProviderConfig(),
67
+ },
68
+ encryptionKey: "valid-encryption-key-at-least-32-chars-long",
69
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
70
+ });
71
+ }).not.toThrow();
72
+ });
73
+ });
74
+
75
+ describe("plugin structure", () => {
76
+ it("should return plugin with correct ID", () => {
77
+ const plugin = oidcPlugin({
78
+ providers: {
79
+ test: createTestProviderConfig(),
80
+ },
81
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
82
+ });
83
+
84
+ expect(plugin.id).toBe("oidc");
85
+ });
86
+
87
+ it("should configure database usage", () => {
88
+ const plugin = oidcPlugin({
89
+ providers: {
90
+ test: createTestProviderConfig(),
91
+ },
92
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
93
+ });
94
+
95
+ expect(plugin.db).toBeDefined();
96
+ expect(plugin.db?.useHostDb).toBe(true);
97
+ });
98
+
99
+ it("should provide plugin context with API methods", () => {
100
+ const plugin = oidcPlugin({
101
+ providers: {
102
+ test: createTestProviderConfig(),
103
+ },
104
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
105
+ });
106
+
107
+ expect(plugin.ctx).toBeDefined();
108
+ expect(plugin.ctx.getConnection).toBeDefined();
109
+ expect(plugin.ctx.getConnections).toBeDefined();
110
+ expect(plugin.ctx.deleteConnection).toBeDefined();
111
+ expect(plugin.ctx.options).toBeDefined();
112
+ });
113
+
114
+ it("should provide init function", () => {
115
+ const plugin = oidcPlugin({
116
+ providers: {
117
+ test: createTestProviderConfig(),
118
+ },
119
+ onAuthSuccess: async () => ({ user: {}, token: "", redirectUrl: "" }),
120
+ });
121
+
122
+ expect(plugin.init).toBeDefined();
123
+ expect(typeof plugin.init).toBe("function");
124
+ });
125
+ });
126
+ });