@flink-app/oauth-plugin 0.12.1-alpha.35 → 0.12.1-alpha.37

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.
@@ -1,4 +1,27 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
27
  };
@@ -8,6 +31,8 @@ const flink_1 = require("@flink-app/flink");
8
31
  const OAuthSessionRepo_1 = __importDefault(require("./repos/OAuthSessionRepo"));
9
32
  const OAuthConnectionRepo_1 = __importDefault(require("./repos/OAuthConnectionRepo"));
10
33
  const encryption_utils_1 = require("./utils/encryption-utils");
34
+ const InitiateOAuth = __importStar(require("./handlers/InitiateOAuth"));
35
+ const CallbackOAuth = __importStar(require("./handlers/CallbackOAuth"));
11
36
  /**
12
37
  * OAuth Plugin Factory Function
13
38
  *
@@ -139,10 +164,12 @@ function oauthPlugin(options) {
139
164
  const sessionTTL = options.sessionTTL || 600; // Default 10 minutes
140
165
  await db.collection(sessionsCollectionName).createIndex({ createdAt: 1 }, { expireAfterSeconds: sessionTTL });
141
166
  flink_1.log.info(`OAuth Plugin: Created TTL index on ${sessionsCollectionName} with ${sessionTTL}s expiration`);
142
- // Register handlers for each configured provider
143
- // Note: Handlers will be registered dynamically
144
- // This requires handlers to be imported, but we'll handle that in the handler files
145
- // For now, we'll skip handler registration and implement it when handlers are ready
167
+ // Register OAuth handlers
168
+ // Only register handlers if registerRoutes is enabled (default: true)
169
+ if (options.registerRoutes !== false) {
170
+ flinkApp.addHandler(InitiateOAuth);
171
+ flinkApp.addHandler(CallbackOAuth);
172
+ }
146
173
  flink_1.log.info(`OAuth Plugin initialized with providers: ${configuredProviders.join(", ")}`);
147
174
  }
148
175
  catch (error) {
@@ -108,4 +108,10 @@ export interface OAuthPluginOptions {
108
108
  * Recommended: Use a dedicated encryption key from environment variables
109
109
  */
110
110
  encryptionKey?: string;
111
+ /**
112
+ * Whether to register OAuth routes automatically
113
+ * If false, you must manually handle OAuth flow
114
+ * Default: true
115
+ */
116
+ registerRoutes?: boolean;
111
117
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * OAuth Callback Handler
3
+ *
4
+ * Handles the OAuth 2.0 callback from the provider by:
5
+ * 1. Validating the state parameter to prevent CSRF attacks
6
+ * 2. Exchanging the authorization code for an access token
7
+ * 3. Fetching the user profile from the provider
8
+ * 4. Calling the onAuthSuccess callback to create/link user and generate JWT token
9
+ * 5. Optionally storing the OAuth connection (if storeTokens enabled)
10
+ * 6. Returning the JWT token to the client (via JSON or redirect)
11
+ *
12
+ * Route: GET /oauth/:provider/callback?code=...&state=...&response_type=json
13
+ */
14
+ import { GetHandler, RouteProps } from "@flink-app/flink";
15
+ import CallbackRequest from "../schemas/CallbackRequest";
16
+ /**
17
+ * Path parameters for the handler
18
+ */
19
+ interface PathParams {
20
+ provider: string;
21
+ [key: string]: string;
22
+ }
23
+ /**
24
+ * Route configuration
25
+ * Note: This handler is registered programmatically by the plugin
26
+ * with dynamic provider parameter support
27
+ */
28
+ export declare const Route: RouteProps;
29
+ /**
30
+ * OAuth Callback Handler
31
+ *
32
+ * Completes the OAuth flow by exchanging the authorization code for tokens,
33
+ * fetching user profile, calling the app's onAuthSuccess callback to generate
34
+ * JWT token, and returning the token to the client.
35
+ */
36
+ declare const CallbackOAuth: GetHandler<any, any, PathParams, CallbackRequest>;
37
+ export default CallbackOAuth;
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ /**
3
+ * OAuth Callback Handler
4
+ *
5
+ * Handles the OAuth 2.0 callback from the provider by:
6
+ * 1. Validating the state parameter to prevent CSRF attacks
7
+ * 2. Exchanging the authorization code for an access token
8
+ * 3. Fetching the user profile from the provider
9
+ * 4. Calling the onAuthSuccess callback to create/link user and generate JWT token
10
+ * 5. Optionally storing the OAuth connection (if storeTokens enabled)
11
+ * 6. Returning the JWT token to the client (via JSON or redirect)
12
+ *
13
+ * Route: GET /oauth/:provider/callback?code=...&state=...&response_type=json
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.Route = void 0;
17
+ const flink_1 = require("@flink-app/flink");
18
+ const state_utils_1 = require("../utils/state-utils");
19
+ const ProviderRegistry_1 = require("../providers/ProviderRegistry");
20
+ const token_response_utils_1 = require("../utils/token-response-utils");
21
+ const encryption_utils_1 = require("../utils/encryption-utils");
22
+ const error_utils_1 = require("../utils/error-utils");
23
+ /**
24
+ * Route configuration
25
+ * Note: This handler is registered programmatically by the plugin
26
+ * with dynamic provider parameter support
27
+ */
28
+ exports.Route = {
29
+ path: "/oauth/:provider/callback",
30
+ method: flink_1.HttpMethod.get,
31
+ };
32
+ /**
33
+ * OAuth Callback Handler
34
+ *
35
+ * Completes the OAuth flow by exchanging the authorization code for tokens,
36
+ * fetching user profile, calling the app's onAuthSuccess callback to generate
37
+ * JWT token, and returning the token to the client.
38
+ */
39
+ const CallbackOAuth = async ({ ctx, req }) => {
40
+ const { provider } = req.params;
41
+ const { code, state, error: oauthError, response_type } = req.query;
42
+ try {
43
+ // Validate provider and response_type
44
+ (0, error_utils_1.validateProvider)(provider);
45
+ (0, error_utils_1.validateResponseType)(response_type);
46
+ // Check for OAuth provider errors (e.g., user denied access)
47
+ if (oauthError) {
48
+ const error = (0, error_utils_1.handleProviderError)({ error: oauthError });
49
+ // Call onAuthError callback if provided
50
+ const { options } = ctx.plugins.oauth;
51
+ if (options.onAuthError) {
52
+ const errorResult = await options.onAuthError({
53
+ error,
54
+ provider: provider,
55
+ });
56
+ if (errorResult.redirectUrl) {
57
+ return {
58
+ status: 302,
59
+ headers: { Location: errorResult.redirectUrl },
60
+ data: {},
61
+ };
62
+ }
63
+ }
64
+ return (0, flink_1.badRequest)(error.message);
65
+ }
66
+ // Validate required parameters
67
+ if (!code || !state) {
68
+ throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.MISSING_CODE, "Missing authorization code or state parameter", { hasCode: !!code, hasState: !!state });
69
+ }
70
+ // Find OAuth session by state
71
+ const session = await ctx.repos.oauthSessionRepo.getOne({ state });
72
+ if (!session) {
73
+ throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.SESSION_EXPIRED, "OAuth session not found or expired. Please try logging in again.", { state });
74
+ }
75
+ // Validate state parameter (CSRF protection)
76
+ if (!(0, state_utils_1.validateState)(state, session.state)) {
77
+ throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
78
+ providedState: state.substring(0, 10) + "...",
79
+ });
80
+ }
81
+ // Delete session immediately after validation (one-time use)
82
+ await ctx.repos.oauthSessionRepo.deleteBySessionId(session.sessionId);
83
+ // Get plugin options and provider config
84
+ const { options } = ctx.plugins.oauth;
85
+ const providerConfig = options.providers[provider];
86
+ if (!providerConfig) {
87
+ throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.INVALID_PROVIDER, `Provider "${provider}" is not configured`, { provider });
88
+ }
89
+ // Exchange authorization code for access token
90
+ const oauthProvider = (0, ProviderRegistry_1.getProvider)(provider, providerConfig);
91
+ const tokens = await oauthProvider.exchangeCodeForToken({
92
+ code,
93
+ redirectUri: providerConfig.callbackUrl,
94
+ });
95
+ // Fetch user profile from provider
96
+ const profile = await oauthProvider.getUserProfile(tokens.accessToken);
97
+ // Call onAuthSuccess callback to create/link user and generate JWT token
98
+ const authSuccessParams = {
99
+ profile,
100
+ provider: provider,
101
+ ...(options.storeTokens ? { tokens } : {}),
102
+ };
103
+ let authResult;
104
+ try {
105
+ authResult = await options.onAuthSuccess(authSuccessParams, ctx);
106
+ }
107
+ catch (error) {
108
+ // Handle JWT generation or user creation errors
109
+ flink_1.log.error("OAuth onAuthSuccess callback failed:", error);
110
+ const oauthError = (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.JWT_GENERATION_FAILED, "Failed to complete authentication. Please try again.", {
111
+ originalError: error.message,
112
+ });
113
+ // Call onAuthError callback if provided
114
+ if (options.onAuthError) {
115
+ const errorResult = await options.onAuthError({
116
+ error: oauthError,
117
+ provider: provider,
118
+ });
119
+ if (errorResult.redirectUrl) {
120
+ return {
121
+ status: 302,
122
+ headers: { Location: errorResult.redirectUrl },
123
+ data: {},
124
+ };
125
+ }
126
+ }
127
+ return (0, flink_1.internalServerError)("Authentication failed. Please try again.");
128
+ }
129
+ // Extract user and JWT token from callback result
130
+ const { user, token, redirectUrl } = authResult;
131
+ if (!token) {
132
+ throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.JWT_GENERATION_FAILED, "No authentication token returned from callback", { hasUser: !!user });
133
+ }
134
+ // Store OAuth connection if token storage is enabled
135
+ if (options.storeTokens && user && user._id) {
136
+ const encryptionSecret = providerConfig.clientSecret;
137
+ // Encrypt tokens before storing
138
+ const encryptedAccessToken = (0, encryption_utils_1.encryptToken)(tokens.accessToken, encryptionSecret);
139
+ const encryptedRefreshToken = tokens.refreshToken ? (0, encryption_utils_1.encryptToken)(tokens.refreshToken, encryptionSecret) : undefined;
140
+ // Calculate token expiration
141
+ const expiresAt = tokens.expiresIn ? new Date(Date.now() + tokens.expiresIn * 1000) : undefined;
142
+ // Create or update OAuth connection
143
+ const existingConnection = await ctx.repos.oauthConnectionRepo.findByUserAndProvider(user._id, provider);
144
+ if (existingConnection) {
145
+ await ctx.repos.oauthConnectionRepo.updateOne(existingConnection._id, {
146
+ accessToken: encryptedAccessToken,
147
+ refreshToken: encryptedRefreshToken,
148
+ scope: tokens.scope || "",
149
+ expiresAt,
150
+ updatedAt: new Date(),
151
+ });
152
+ }
153
+ else {
154
+ await ctx.repos.oauthConnectionRepo.create({
155
+ userId: user._id,
156
+ provider: provider,
157
+ providerId: profile.id,
158
+ accessToken: encryptedAccessToken,
159
+ refreshToken: encryptedRefreshToken,
160
+ scope: tokens.scope || "",
161
+ expiresAt,
162
+ createdAt: new Date(),
163
+ updatedAt: new Date(),
164
+ });
165
+ }
166
+ }
167
+ // Return JWT token in requested format
168
+ return (0, token_response_utils_1.formatTokenResponse)(token, user, redirectUrl || session.redirectUri, response_type);
169
+ }
170
+ catch (error) {
171
+ flink_1.log.error("OAuth callback error:", error);
172
+ // Handle OAuth-specific errors
173
+ if (error.code && Object.values(error_utils_1.OAuthErrorCodes).includes(error.code)) {
174
+ // Call onAuthError callback if provided
175
+ const { options } = ctx.plugins.oauth;
176
+ if (options.onAuthError) {
177
+ try {
178
+ const errorResult = await options.onAuthError({
179
+ error,
180
+ provider: provider,
181
+ });
182
+ if (errorResult.redirectUrl) {
183
+ return {
184
+ status: 302,
185
+ headers: { Location: errorResult.redirectUrl },
186
+ data: {},
187
+ };
188
+ }
189
+ }
190
+ catch (callbackError) {
191
+ flink_1.log.error("onAuthError callback failed:", callbackError);
192
+ }
193
+ }
194
+ return (0, flink_1.badRequest)(error.message);
195
+ }
196
+ // Handle provider errors
197
+ const mappedError = (0, error_utils_1.handleProviderError)(error);
198
+ return (0, flink_1.internalServerError)(mappedError.message);
199
+ }
200
+ };
201
+ exports.default = CallbackOAuth;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * OAuth Initiate Handler
3
+ *
4
+ * Initiates the OAuth 2.0 authorization code flow by:
5
+ * 1. Validating the provider is supported and configured
6
+ * 2. Generating a cryptographically secure state parameter for CSRF protection
7
+ * 3. Creating an OAuth session to track the flow
8
+ * 4. Building the provider's authorization URL
9
+ * 5. Redirecting the user to the provider for authorization
10
+ *
11
+ * Route: GET /oauth/:provider/initiate?redirectUri={optional_redirect_url}
12
+ */
13
+ import { GetHandler, RouteProps } from "@flink-app/flink";
14
+ import InitiateRequest from "../schemas/InitiateRequest";
15
+ /**
16
+ * Path parameters for the handler
17
+ */
18
+ interface PathParams {
19
+ provider: string;
20
+ [key: string]: string;
21
+ }
22
+ /**
23
+ * Route configuration
24
+ * Note: This handler is registered programmatically by the plugin
25
+ * with dynamic provider parameter support
26
+ */
27
+ export declare const Route: RouteProps;
28
+ /**
29
+ * OAuth Initiate Handler
30
+ *
31
+ * Starts the OAuth flow by generating state, creating a session,
32
+ * and redirecting to the OAuth provider's authorization URL.
33
+ */
34
+ declare const InitiateOAuth: GetHandler<any, any, PathParams, InitiateRequest>;
35
+ export default InitiateOAuth;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ /**
3
+ * OAuth Initiate Handler
4
+ *
5
+ * Initiates the OAuth 2.0 authorization code flow by:
6
+ * 1. Validating the provider is supported and configured
7
+ * 2. Generating a cryptographically secure state parameter for CSRF protection
8
+ * 3. Creating an OAuth session to track the flow
9
+ * 4. Building the provider's authorization URL
10
+ * 5. Redirecting the user to the provider for authorization
11
+ *
12
+ * Route: GET /oauth/:provider/initiate?redirectUri={optional_redirect_url}
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.Route = void 0;
16
+ const flink_1 = require("@flink-app/flink");
17
+ const state_utils_1 = require("../utils/state-utils");
18
+ const ProviderRegistry_1 = require("../providers/ProviderRegistry");
19
+ const error_utils_1 = require("../utils/error-utils");
20
+ /**
21
+ * Route configuration
22
+ * Note: This handler is registered programmatically by the plugin
23
+ * with dynamic provider parameter support
24
+ */
25
+ exports.Route = {
26
+ path: "/oauth/:provider/initiate",
27
+ method: flink_1.HttpMethod.get,
28
+ };
29
+ /**
30
+ * OAuth Initiate Handler
31
+ *
32
+ * Starts the OAuth flow by generating state, creating a session,
33
+ * and redirecting to the OAuth provider's authorization URL.
34
+ */
35
+ const InitiateOAuth = async ({ ctx, req }) => {
36
+ const { provider } = req.params;
37
+ const { redirectUri } = req.query;
38
+ try {
39
+ // Validate provider is supported
40
+ (0, error_utils_1.validateProvider)(provider);
41
+ // Get plugin options and provider config
42
+ const { options } = ctx.plugins.oauth;
43
+ const providerConfig = options.providers[provider];
44
+ if (!providerConfig) {
45
+ throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.INVALID_PROVIDER, `Provider "${provider}" is not configured`, { provider });
46
+ }
47
+ // Generate cryptographically secure state and session ID
48
+ const state = (0, state_utils_1.generateState)();
49
+ const sessionId = (0, state_utils_1.generateSessionId)();
50
+ // Store session for state validation in callback
51
+ await ctx.repos.oauthSessionRepo.create({
52
+ sessionId,
53
+ state,
54
+ provider: provider,
55
+ redirectUri: redirectUri || providerConfig.callbackUrl,
56
+ createdAt: new Date(),
57
+ });
58
+ // Get provider instance and build authorization URL
59
+ const oauthProvider = (0, ProviderRegistry_1.getProvider)(provider, providerConfig);
60
+ const authorizationUrl = oauthProvider.getAuthorizationUrl({
61
+ state,
62
+ redirectUri: providerConfig.callbackUrl,
63
+ scope: providerConfig.scope || [],
64
+ });
65
+ // Redirect user to provider's authorization page
66
+ return {
67
+ status: 302,
68
+ headers: {
69
+ Location: authorizationUrl,
70
+ },
71
+ data: {},
72
+ };
73
+ }
74
+ catch (error) {
75
+ // Handle validation errors
76
+ if (error.code && Object.values(error_utils_1.OAuthErrorCodes).includes(error.code)) {
77
+ return (0, flink_1.badRequest)(error.message);
78
+ }
79
+ // Handle unexpected errors
80
+ return (0, flink_1.internalServerError)(error.message || "Failed to initiate OAuth flow");
81
+ }
82
+ };
83
+ exports.default = InitiateOAuth;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Query parameters for OAuth callback request
3
+ */
4
+ export default interface CallbackRequest {
5
+ /**
6
+ * Authorization code from OAuth provider
7
+ */
8
+ code: string;
9
+ /**
10
+ * CSRF protection state parameter
11
+ */
12
+ state: string;
13
+ /**
14
+ * Optional error from OAuth provider
15
+ * Common values: access_denied, invalid_request, unauthorized_client, etc.
16
+ */
17
+ error?: string;
18
+ /**
19
+ * Human-readable error description (OAuth 2.0 standard)
20
+ */
21
+ error_description?: string;
22
+ /**
23
+ * URI with error information (OAuth 2.0 standard)
24
+ */
25
+ error_uri?: string;
26
+ /**
27
+ * Response type - 'json' returns JSON instead of redirect
28
+ */
29
+ response_type?: "json";
30
+ /**
31
+ * Granted scopes (provider-specific)
32
+ * May be sent by GitHub, Google, and other providers
33
+ */
34
+ scope?: string;
35
+ /**
36
+ * Google-specific: Index of the account selected by the user
37
+ */
38
+ authuser?: string;
39
+ /**
40
+ * Google-specific: Indicates which prompt was shown to the user
41
+ * Values: none, consent, select_account
42
+ */
43
+ prompt?: string;
44
+ /**
45
+ * Google-specific: Hosted domain of the user
46
+ */
47
+ hd?: string;
48
+ /**
49
+ * Session state or other provider-specific parameters
50
+ */
51
+ session_state?: string;
52
+ [key: string]: string | undefined;
53
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Query parameters for OAuth initiate request
3
+ */
4
+ export default interface InitiateRequest {
5
+ /**
6
+ * Optional redirect URI to return to after OAuth flow completes
7
+ */
8
+ redirectUri?: string;
9
+ [key: string]: string | undefined;
10
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Formats the OAuth callback response with JWT token.
3
+ * Supports multiple response formats:
4
+ * - JSON response with user and token
5
+ * - URL fragment redirect with token
6
+ * - Query parameter redirect with token
7
+ *
8
+ * @param token - JWT token to return
9
+ * @param user - User object to return
10
+ * @param redirectUrl - Optional redirect URL
11
+ * @param responseType - Response format ('json' or undefined for redirect)
12
+ * @returns Response object for Flink handler
13
+ */
14
+ export declare function formatTokenResponse(token: string, user: any, redirectUrl?: string, responseType?: string): any;
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatTokenResponse = void 0;
4
+ /**
5
+ * Formats the OAuth callback response with JWT token.
6
+ * Supports multiple response formats:
7
+ * - JSON response with user and token
8
+ * - URL fragment redirect with token
9
+ * - Query parameter redirect with token
10
+ *
11
+ * @param token - JWT token to return
12
+ * @param user - User object to return
13
+ * @param redirectUrl - Optional redirect URL
14
+ * @param responseType - Response format ('json' or undefined for redirect)
15
+ * @returns Response object for Flink handler
16
+ */
17
+ function formatTokenResponse(token, user, redirectUrl, responseType) {
18
+ // JSON response format
19
+ if (responseType === "json") {
20
+ return {
21
+ status: 200,
22
+ data: {
23
+ user,
24
+ token,
25
+ },
26
+ };
27
+ }
28
+ // Redirect format
29
+ if (redirectUrl) {
30
+ // Use URL fragment for better security (token not sent to server)
31
+ const separator = redirectUrl.includes("#") ? "&" : "#";
32
+ const fullRedirectUrl = `${redirectUrl}${separator}token=${encodeURIComponent(token)}`;
33
+ return {
34
+ status: 302,
35
+ headers: {
36
+ Location: fullRedirectUrl,
37
+ },
38
+ data: {},
39
+ };
40
+ }
41
+ // Default: return JSON if no redirect URL provided
42
+ return {
43
+ status: 200,
44
+ data: {
45
+ user,
46
+ token,
47
+ },
48
+ };
49
+ }
50
+ exports.formatTokenResponse = formatTokenResponse;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flink-app/oauth-plugin",
3
- "version": "0.12.1-alpha.35",
3
+ "version": "0.12.1-alpha.37",
4
4
  "description": "Flink plugin for OAuth 2.0 authentication with GitHub and Google providers",
5
5
  "scripts": {
6
6
  "test": "node --preserve-symlinks -r ts-node/register -- node_modules/jasmine/bin/jasmine --config=./spec/support/jasmine.json",
@@ -34,5 +34,5 @@
34
34
  "tsc-watch": "^4.2.9",
35
35
  "typescript": "5.4.5"
36
36
  },
37
- "gitHead": "f8e8c6565a9ca1dd3e5fdb4c2a791c99ae3ba51a"
37
+ "gitHead": "58f5ef1c6307ebda4b3ad6e75c29fd56345a2a54"
38
38
  }
@@ -12,135 +12,134 @@
12
12
  * critical OAuth flow scenarios.
13
13
  */
14
14
 
15
- import { FlinkApp } from '@flink-app/flink';
16
- import * as http from '@flink-app/test-utils';
17
-
18
- describe('OAuth Handlers', () => {
19
- let app: FlinkApp<any>;
20
- let testSessionId: string;
21
- let testState: string;
22
-
23
- beforeAll(async () => {
24
- // Note: This is a placeholder test structure
25
- // The actual FlinkApp setup with oauth plugin will be completed
26
- // in Task Group 5 when the plugin is fully implemented
27
-
28
- // For now, we're creating the test structure to validate
29
- // the handler logic once the plugin integration is complete
30
- });
31
-
32
- afterAll(async () => {
33
- if (app) {
34
- await app.stop();
35
- }
36
- });
37
-
38
- describe('InitiateOAuth Handler', () => {
39
- it('should generate state, create session, and redirect to provider', async () => {
40
- // Test that initiate handler:
41
- // 1. Validates provider is configured
42
- // 2. Generates cryptographically secure state parameter
43
- // 3. Creates OAuth session with state
44
- // 4. Returns 302 redirect to provider authorization URL
45
-
46
- // This test will be implemented once plugin setup is complete
47
- expect(true).toBe(true); // Placeholder
15
+ import { FlinkApp } from "@flink-app/flink";
16
+ import * as http from "@flink-app/test-utils";
17
+
18
+ describe("OAuth Handlers", () => {
19
+ let app: FlinkApp<any>;
20
+ let testSessionId: string;
21
+ let testState: string;
22
+
23
+ beforeAll(async () => {
24
+ // Note: This is a placeholder test structure
25
+ // The actual FlinkApp setup with oauth plugin will be completed
26
+ // in Task Group 5 when the plugin is fully implemented
27
+ // For now, we're creating the test structure to validate
28
+ // the handler logic once the plugin integration is complete
48
29
  });
49
30
 
50
- it('should reject unsupported provider', async () => {
51
- // Test that initiate handler returns 400 for invalid provider
52
-
53
- // This test will be implemented once plugin setup is complete
54
- expect(true).toBe(true); // Placeholder
55
- });
56
- });
57
-
58
- describe('CallbackOAuth Handler', () => {
59
- it('should validate state, exchange code, and call onAuthSuccess with context', async () => {
60
- // Test that callback handler:
61
- // 1. Validates state parameter matches session
62
- // 2. Exchanges authorization code for access token
63
- // 3. Fetches user profile from provider
64
- // 4. Calls onAuthSuccess callback with profile and context
65
- // 5. Receives JWT token from callback
66
-
67
- // This test will be implemented once plugin setup is complete
68
- expect(true).toBe(true); // Placeholder
69
- });
70
-
71
- it('should receive JWT token from callback and return to client', async () => {
72
- // Test that callback handler:
73
- // 1. Receives JWT token from onAuthSuccess callback
74
- // 2. Returns token in appropriate format (redirect by default)
75
-
76
- // This test will be implemented once plugin setup is complete
77
- expect(true).toBe(true); // Placeholder
31
+ afterAll(async () => {
32
+ if (app) {
33
+ await app.stop();
34
+ }
78
35
  });
79
36
 
80
- it('should support response_type=json query parameter', async () => {
81
- // Test that callback handler:
82
- // 1. Detects response_type=json in query parameters
83
- // 2. Returns JSON response with user and token
84
- // 3. Does not redirect when response_type=json
37
+ describe("InitiateOAuth Handler", () => {
38
+ it("should generate state, create session, and redirect to provider", async () => {
39
+ // Test that initiate handler:
40
+ // 1. Validates provider is configured
41
+ // 2. Generates cryptographically secure state parameter
42
+ // 3. Creates OAuth session with state
43
+ // 4. Returns 302 redirect to provider authorization URL
85
44
 
86
- // This test will be implemented once plugin setup is complete
87
- expect(true).toBe(true); // Placeholder
88
- });
45
+ // This test will be implemented once plugin setup is complete
46
+ expect(true).toBe(true); // Placeholder
47
+ });
89
48
 
90
- it('should reject callback with invalid state parameter', async () => {
91
- // Test that callback handler:
92
- // 1. Detects state mismatch
93
- // 2. Returns 400 error
94
- // 3. Prevents CSRF attacks
49
+ it("should reject unsupported provider", async () => {
50
+ // Test that initiate handler returns 400 for invalid provider
95
51
 
96
- // This test will be implemented once plugin setup is complete
97
- expect(true).toBe(true); // Placeholder
52
+ // This test will be implemented once plugin setup is complete
53
+ expect(true).toBe(true); // Placeholder
54
+ });
98
55
  });
99
56
 
100
- it('should reject callback with missing code parameter', async () => {
101
- // Test that callback handler:
102
- // 1. Detects missing authorization code
103
- // 2. Returns 400 error with clear message
104
-
105
- // This test will be implemented once plugin setup is complete
106
- expect(true).toBe(true); // Placeholder
57
+ describe("CallbackOAuth Handler", () => {
58
+ it("should validate state, exchange code, and call onAuthSuccess with context", async () => {
59
+ // Test that callback handler:
60
+ // 1. Validates state parameter matches session
61
+ // 2. Exchanges authorization code for access token
62
+ // 3. Fetches user profile from provider
63
+ // 4. Calls onAuthSuccess callback with profile and context
64
+ // 5. Receives JWT token from callback
65
+
66
+ // This test will be implemented once plugin setup is complete
67
+ expect(true).toBe(true); // Placeholder
68
+ });
69
+
70
+ it("should receive JWT token from callback and return to client", async () => {
71
+ // Test that callback handler:
72
+ // 1. Receives JWT token from onAuthSuccess callback
73
+ // 2. Returns token in appropriate format (redirect by default)
74
+
75
+ // This test will be implemented once plugin setup is complete
76
+ expect(true).toBe(true); // Placeholder
77
+ });
78
+
79
+ it("should support response_type=json query parameter", async () => {
80
+ // Test that callback handler:
81
+ // 1. Detects response_type=json in query parameters
82
+ // 2. Returns JSON response with user and token
83
+ // 3. Does not redirect when response_type=json
84
+
85
+ // This test will be implemented once plugin setup is complete
86
+ expect(true).toBe(true); // Placeholder
87
+ });
88
+
89
+ it("should reject callback with invalid state parameter", async () => {
90
+ // Test that callback handler:
91
+ // 1. Detects state mismatch
92
+ // 2. Returns 400 error
93
+ // 3. Prevents CSRF attacks
94
+
95
+ // This test will be implemented once plugin setup is complete
96
+ expect(true).toBe(true); // Placeholder
97
+ });
98
+
99
+ it("should reject callback with missing code parameter", async () => {
100
+ // Test that callback handler:
101
+ // 1. Detects missing authorization code
102
+ // 2. Returns 400 error with clear message
103
+
104
+ // This test will be implemented once plugin setup is complete
105
+ expect(true).toBe(true); // Placeholder
106
+ });
107
+
108
+ it("should handle OAuth provider error (access_denied)", async () => {
109
+ // Test that callback handler:
110
+ // 1. Detects error parameter from provider
111
+ // 2. Maps error to user-friendly message
112
+ // 3. Calls onAuthError callback if provided
113
+
114
+ // This test will be implemented once plugin setup is complete
115
+ expect(true).toBe(true); // Placeholder
116
+ });
117
+
118
+ it("should handle expired or missing session", async () => {
119
+ // Test that callback handler:
120
+ // 1. Detects missing session for state
121
+ // 2. Returns appropriate error message
122
+ // 3. Suggests user try logging in again
123
+
124
+ // This test will be implemented once plugin setup is complete
125
+ expect(true).toBe(true); // Placeholder
126
+ });
127
+
128
+ it("should handle JWT generation failure gracefully", async () => {
129
+ // Test that callback handler:
130
+ // 1. Catches errors from onAuthSuccess callback
131
+ // 2. Logs error securely
132
+ // 3. Calls onAuthError callback if provided
133
+ // 4. Returns user-friendly error message
134
+
135
+ // This test will be implemented once plugin setup is complete
136
+ expect(true).toBe(true); // Placeholder
137
+ });
107
138
  });
108
139
 
109
- it('should handle OAuth provider error (access_denied)', async () => {
110
- // Test that callback handler:
111
- // 1. Detects error parameter from provider
112
- // 2. Maps error to user-friendly message
113
- // 3. Calls onAuthError callback if provided
114
-
115
- // This test will be implemented once plugin setup is complete
116
- expect(true).toBe(true); // Placeholder
140
+ describe("OAuth Flow End-to-End", () => {
141
+ // Note: Full end-to-end tests with mock OAuth provider responses
142
+ // will be added in Task Group 7 by the testing-engineer
143
+ // These placeholder tests establish the test structure
117
144
  });
118
-
119
- it('should handle expired or missing session', async () => {
120
- // Test that callback handler:
121
- // 1. Detects missing session for state
122
- // 2. Returns appropriate error message
123
- // 3. Suggests user try logging in again
124
-
125
- // This test will be implemented once plugin setup is complete
126
- expect(true).toBe(true); // Placeholder
127
- });
128
-
129
- it('should handle JWT generation failure gracefully', async () => {
130
- // Test that callback handler:
131
- // 1. Catches errors from onAuthSuccess callback
132
- // 2. Logs error securely
133
- // 3. Calls onAuthError callback if provided
134
- // 4. Returns user-friendly error message
135
-
136
- // This test will be implemented once plugin setup is complete
137
- expect(true).toBe(true); // Placeholder
138
- });
139
- });
140
-
141
- describe('OAuth Flow End-to-End', () => {
142
- // Note: Full end-to-end tests with mock OAuth provider responses
143
- // will be added in Task Group 7 by the testing-engineer
144
- // These placeholder tests establish the test structure
145
- });
146
145
  });
@@ -7,6 +7,8 @@ import OAuthSessionRepo from "./repos/OAuthSessionRepo";
7
7
  import OAuthConnectionRepo from "./repos/OAuthConnectionRepo";
8
8
  import { encryptToken, decryptToken, validateEncryptionSecret } from "./utils/encryption-utils";
9
9
  import OAuthConnection from "./schemas/OAuthConnection";
10
+ import * as InitiateOAuth from "./handlers/InitiateOAuth";
11
+ import * as CallbackOAuth from "./handlers/CallbackOAuth";
10
12
 
11
13
  /**
12
14
  * OAuth Plugin Factory Function
@@ -156,10 +158,12 @@ export function oauthPlugin(options: OAuthPluginOptions): FlinkPlugin {
156
158
 
157
159
  log.info(`OAuth Plugin: Created TTL index on ${sessionsCollectionName} with ${sessionTTL}s expiration`);
158
160
 
159
- // Register handlers for each configured provider
160
- // Note: Handlers will be registered dynamically
161
- // This requires handlers to be imported, but we'll handle that in the handler files
162
- // For now, we'll skip handler registration and implement it when handlers are ready
161
+ // Register OAuth handlers
162
+ // Only register handlers if registerRoutes is enabled (default: true)
163
+ if (options.registerRoutes !== false) {
164
+ flinkApp.addHandler(InitiateOAuth);
165
+ flinkApp.addHandler(CallbackOAuth);
166
+ }
163
167
 
164
168
  log.info(`OAuth Plugin initialized with providers: ${configuredProviders.join(", ")}`);
165
169
  } catch (error) {
@@ -119,4 +119,11 @@ export interface OAuthPluginOptions {
119
119
  * Recommended: Use a dedicated encryption key from environment variables
120
120
  */
121
121
  encryptionKey?: string;
122
+
123
+ /**
124
+ * Whether to register OAuth routes automatically
125
+ * If false, you must manually handle OAuth flow
126
+ * Default: true
127
+ */
128
+ registerRoutes?: boolean;
122
129
  }
@@ -12,7 +12,7 @@
12
12
  * Route: GET /oauth/:provider/callback?code=...&state=...&response_type=json
13
13
  */
14
14
 
15
- import { GetHandler, RouteProps, badRequest, internalServerError, log } from "@flink-app/flink";
15
+ import { GetHandler, HttpMethod, RouteProps, badRequest, internalServerError, log } from "@flink-app/flink";
16
16
  import CallbackRequest from "../schemas/CallbackRequest";
17
17
  import { validateState } from "../utils/state-utils";
18
18
  import { getProvider } from "../providers/ProviderRegistry";
@@ -35,6 +35,7 @@ interface PathParams {
35
35
  */
36
36
  export const Route: RouteProps = {
37
37
  path: "/oauth/:provider/callback",
38
+ method: HttpMethod.get,
38
39
  };
39
40
 
40
41
  /**
@@ -11,7 +11,7 @@
11
11
  * Route: GET /oauth/:provider/initiate?redirectUri={optional_redirect_url}
12
12
  */
13
13
 
14
- import { GetHandler, RouteProps, badRequest, internalServerError } from "@flink-app/flink";
14
+ import { GetHandler, HttpMethod, RouteProps, badRequest, internalServerError } from "@flink-app/flink";
15
15
  import InitiateRequest from "../schemas/InitiateRequest";
16
16
  import { generateState, generateSessionId } from "../utils/state-utils";
17
17
  import { getProvider } from "../providers/ProviderRegistry";
@@ -32,6 +32,7 @@ interface PathParams {
32
32
  */
33
33
  export const Route: RouteProps = {
34
34
  path: "/oauth/:provider/initiate",
35
+ method: HttpMethod.get,
35
36
  };
36
37
 
37
38
  /**