@flink-app/oauth-plugin 0.12.1-alpha.35 → 0.12.1-alpha.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/OAuthPlugin.js +31 -4
- package/dist/OAuthPluginOptions.d.ts +6 -0
- package/dist/handlers/CallbackOAuth.d.ts +37 -0
- package/dist/handlers/CallbackOAuth.js +200 -0
- package/dist/handlers/InitiateOAuth.d.ts +35 -0
- package/dist/handlers/InitiateOAuth.js +82 -0
- package/dist/schemas/CallbackRequest.d.ts +53 -0
- package/dist/schemas/CallbackRequest.js +2 -0
- package/dist/schemas/InitiateRequest.d.ts +10 -0
- package/dist/schemas/InitiateRequest.js +2 -0
- package/dist/utils/token-response-utils.d.ts +14 -0
- package/dist/utils/token-response-utils.js +50 -0
- package/package.json +2 -2
- package/src/OAuthPlugin.ts +8 -4
- package/src/OAuthPluginOptions.ts +7 -0
package/dist/OAuthPlugin.js
CHANGED
|
@@ -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
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
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,200 @@
|
|
|
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
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* OAuth Callback Handler
|
|
33
|
+
*
|
|
34
|
+
* Completes the OAuth flow by exchanging the authorization code for tokens,
|
|
35
|
+
* fetching user profile, calling the app's onAuthSuccess callback to generate
|
|
36
|
+
* JWT token, and returning the token to the client.
|
|
37
|
+
*/
|
|
38
|
+
const CallbackOAuth = async ({ ctx, req }) => {
|
|
39
|
+
const { provider } = req.params;
|
|
40
|
+
const { code, state, error: oauthError, response_type } = req.query;
|
|
41
|
+
try {
|
|
42
|
+
// Validate provider and response_type
|
|
43
|
+
(0, error_utils_1.validateProvider)(provider);
|
|
44
|
+
(0, error_utils_1.validateResponseType)(response_type);
|
|
45
|
+
// Check for OAuth provider errors (e.g., user denied access)
|
|
46
|
+
if (oauthError) {
|
|
47
|
+
const error = (0, error_utils_1.handleProviderError)({ error: oauthError });
|
|
48
|
+
// Call onAuthError callback if provided
|
|
49
|
+
const { options } = ctx.plugins.oauth;
|
|
50
|
+
if (options.onAuthError) {
|
|
51
|
+
const errorResult = await options.onAuthError({
|
|
52
|
+
error,
|
|
53
|
+
provider: provider,
|
|
54
|
+
});
|
|
55
|
+
if (errorResult.redirectUrl) {
|
|
56
|
+
return {
|
|
57
|
+
status: 302,
|
|
58
|
+
headers: { Location: errorResult.redirectUrl },
|
|
59
|
+
data: {},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return (0, flink_1.badRequest)(error.message);
|
|
64
|
+
}
|
|
65
|
+
// Validate required parameters
|
|
66
|
+
if (!code || !state) {
|
|
67
|
+
throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.MISSING_CODE, "Missing authorization code or state parameter", { hasCode: !!code, hasState: !!state });
|
|
68
|
+
}
|
|
69
|
+
// Find OAuth session by state
|
|
70
|
+
const session = await ctx.repos.oauthSessionRepo.getOne({ state });
|
|
71
|
+
if (!session) {
|
|
72
|
+
throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.SESSION_EXPIRED, "OAuth session not found or expired. Please try logging in again.", { state });
|
|
73
|
+
}
|
|
74
|
+
// Validate state parameter (CSRF protection)
|
|
75
|
+
if (!(0, state_utils_1.validateState)(state, session.state)) {
|
|
76
|
+
throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.INVALID_STATE, "Invalid state parameter. Possible CSRF attack detected.", {
|
|
77
|
+
providedState: state.substring(0, 10) + "...",
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Delete session immediately after validation (one-time use)
|
|
81
|
+
await ctx.repos.oauthSessionRepo.deleteBySessionId(session.sessionId);
|
|
82
|
+
// Get plugin options and provider config
|
|
83
|
+
const { options } = ctx.plugins.oauth;
|
|
84
|
+
const providerConfig = options.providers[provider];
|
|
85
|
+
if (!providerConfig) {
|
|
86
|
+
throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.INVALID_PROVIDER, `Provider "${provider}" is not configured`, { provider });
|
|
87
|
+
}
|
|
88
|
+
// Exchange authorization code for access token
|
|
89
|
+
const oauthProvider = (0, ProviderRegistry_1.getProvider)(provider, providerConfig);
|
|
90
|
+
const tokens = await oauthProvider.exchangeCodeForToken({
|
|
91
|
+
code,
|
|
92
|
+
redirectUri: providerConfig.callbackUrl,
|
|
93
|
+
});
|
|
94
|
+
// Fetch user profile from provider
|
|
95
|
+
const profile = await oauthProvider.getUserProfile(tokens.accessToken);
|
|
96
|
+
// Call onAuthSuccess callback to create/link user and generate JWT token
|
|
97
|
+
const authSuccessParams = {
|
|
98
|
+
profile,
|
|
99
|
+
provider: provider,
|
|
100
|
+
...(options.storeTokens ? { tokens } : {}),
|
|
101
|
+
};
|
|
102
|
+
let authResult;
|
|
103
|
+
try {
|
|
104
|
+
authResult = await options.onAuthSuccess(authSuccessParams, ctx);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Handle JWT generation or user creation errors
|
|
108
|
+
flink_1.log.error("OAuth onAuthSuccess callback failed:", error);
|
|
109
|
+
const oauthError = (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.JWT_GENERATION_FAILED, "Failed to complete authentication. Please try again.", {
|
|
110
|
+
originalError: error.message,
|
|
111
|
+
});
|
|
112
|
+
// Call onAuthError callback if provided
|
|
113
|
+
if (options.onAuthError) {
|
|
114
|
+
const errorResult = await options.onAuthError({
|
|
115
|
+
error: oauthError,
|
|
116
|
+
provider: provider,
|
|
117
|
+
});
|
|
118
|
+
if (errorResult.redirectUrl) {
|
|
119
|
+
return {
|
|
120
|
+
status: 302,
|
|
121
|
+
headers: { Location: errorResult.redirectUrl },
|
|
122
|
+
data: {},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return (0, flink_1.internalServerError)("Authentication failed. Please try again.");
|
|
127
|
+
}
|
|
128
|
+
// Extract user and JWT token from callback result
|
|
129
|
+
const { user, token, redirectUrl } = authResult;
|
|
130
|
+
if (!token) {
|
|
131
|
+
throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.JWT_GENERATION_FAILED, "No authentication token returned from callback", { hasUser: !!user });
|
|
132
|
+
}
|
|
133
|
+
// Store OAuth connection if token storage is enabled
|
|
134
|
+
if (options.storeTokens && user && user._id) {
|
|
135
|
+
const encryptionSecret = providerConfig.clientSecret;
|
|
136
|
+
// Encrypt tokens before storing
|
|
137
|
+
const encryptedAccessToken = (0, encryption_utils_1.encryptToken)(tokens.accessToken, encryptionSecret);
|
|
138
|
+
const encryptedRefreshToken = tokens.refreshToken ? (0, encryption_utils_1.encryptToken)(tokens.refreshToken, encryptionSecret) : undefined;
|
|
139
|
+
// Calculate token expiration
|
|
140
|
+
const expiresAt = tokens.expiresIn ? new Date(Date.now() + tokens.expiresIn * 1000) : undefined;
|
|
141
|
+
// Create or update OAuth connection
|
|
142
|
+
const existingConnection = await ctx.repos.oauthConnectionRepo.findByUserAndProvider(user._id, provider);
|
|
143
|
+
if (existingConnection) {
|
|
144
|
+
await ctx.repos.oauthConnectionRepo.updateOne(existingConnection._id, {
|
|
145
|
+
accessToken: encryptedAccessToken,
|
|
146
|
+
refreshToken: encryptedRefreshToken,
|
|
147
|
+
scope: tokens.scope || "",
|
|
148
|
+
expiresAt,
|
|
149
|
+
updatedAt: new Date(),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
await ctx.repos.oauthConnectionRepo.create({
|
|
154
|
+
userId: user._id,
|
|
155
|
+
provider: provider,
|
|
156
|
+
providerId: profile.id,
|
|
157
|
+
accessToken: encryptedAccessToken,
|
|
158
|
+
refreshToken: encryptedRefreshToken,
|
|
159
|
+
scope: tokens.scope || "",
|
|
160
|
+
expiresAt,
|
|
161
|
+
createdAt: new Date(),
|
|
162
|
+
updatedAt: new Date(),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Return JWT token in requested format
|
|
167
|
+
return (0, token_response_utils_1.formatTokenResponse)(token, user, redirectUrl || session.redirectUri, response_type);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
flink_1.log.error("OAuth callback error:", error);
|
|
171
|
+
// Handle OAuth-specific errors
|
|
172
|
+
if (error.code && Object.values(error_utils_1.OAuthErrorCodes).includes(error.code)) {
|
|
173
|
+
// Call onAuthError callback if provided
|
|
174
|
+
const { options } = ctx.plugins.oauth;
|
|
175
|
+
if (options.onAuthError) {
|
|
176
|
+
try {
|
|
177
|
+
const errorResult = await options.onAuthError({
|
|
178
|
+
error,
|
|
179
|
+
provider: provider,
|
|
180
|
+
});
|
|
181
|
+
if (errorResult.redirectUrl) {
|
|
182
|
+
return {
|
|
183
|
+
status: 302,
|
|
184
|
+
headers: { Location: errorResult.redirectUrl },
|
|
185
|
+
data: {},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (callbackError) {
|
|
190
|
+
flink_1.log.error("onAuthError callback failed:", callbackError);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return (0, flink_1.badRequest)(error.message);
|
|
194
|
+
}
|
|
195
|
+
// Handle provider errors
|
|
196
|
+
const mappedError = (0, error_utils_1.handleProviderError)(error);
|
|
197
|
+
return (0, flink_1.internalServerError)(mappedError.message);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
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,82 @@
|
|
|
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
|
+
};
|
|
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
|
+
const InitiateOAuth = async ({ ctx, req }) => {
|
|
35
|
+
const { provider } = req.params;
|
|
36
|
+
const { redirectUri } = req.query;
|
|
37
|
+
try {
|
|
38
|
+
// Validate provider is supported
|
|
39
|
+
(0, error_utils_1.validateProvider)(provider);
|
|
40
|
+
// Get plugin options and provider config
|
|
41
|
+
const { options } = ctx.plugins.oauth;
|
|
42
|
+
const providerConfig = options.providers[provider];
|
|
43
|
+
if (!providerConfig) {
|
|
44
|
+
throw (0, error_utils_1.createOAuthError)(error_utils_1.OAuthErrorCodes.INVALID_PROVIDER, `Provider "${provider}" is not configured`, { provider });
|
|
45
|
+
}
|
|
46
|
+
// Generate cryptographically secure state and session ID
|
|
47
|
+
const state = (0, state_utils_1.generateState)();
|
|
48
|
+
const sessionId = (0, state_utils_1.generateSessionId)();
|
|
49
|
+
// Store session for state validation in callback
|
|
50
|
+
await ctx.repos.oauthSessionRepo.create({
|
|
51
|
+
sessionId,
|
|
52
|
+
state,
|
|
53
|
+
provider: provider,
|
|
54
|
+
redirectUri: redirectUri || providerConfig.callbackUrl,
|
|
55
|
+
createdAt: new Date(),
|
|
56
|
+
});
|
|
57
|
+
// Get provider instance and build authorization URL
|
|
58
|
+
const oauthProvider = (0, ProviderRegistry_1.getProvider)(provider, providerConfig);
|
|
59
|
+
const authorizationUrl = oauthProvider.getAuthorizationUrl({
|
|
60
|
+
state,
|
|
61
|
+
redirectUri: providerConfig.callbackUrl,
|
|
62
|
+
scope: providerConfig.scope || [],
|
|
63
|
+
});
|
|
64
|
+
// Redirect user to provider's authorization page
|
|
65
|
+
return {
|
|
66
|
+
status: 302,
|
|
67
|
+
headers: {
|
|
68
|
+
Location: authorizationUrl,
|
|
69
|
+
},
|
|
70
|
+
data: {},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// Handle validation errors
|
|
75
|
+
if (error.code && Object.values(error_utils_1.OAuthErrorCodes).includes(error.code)) {
|
|
76
|
+
return (0, flink_1.badRequest)(error.message);
|
|
77
|
+
}
|
|
78
|
+
// Handle unexpected errors
|
|
79
|
+
return (0, flink_1.internalServerError)(error.message || "Failed to initiate OAuth flow");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
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,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.
|
|
3
|
+
"version": "0.12.1-alpha.36",
|
|
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": "
|
|
37
|
+
"gitHead": "3becd47c43eb095f19f8b1ad2fa5ecbecfbd5ab6"
|
|
38
38
|
}
|
package/src/OAuthPlugin.ts
CHANGED
|
@@ -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
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
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
|
}
|