Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | 69x 69x 69x 69x 69x 69x 19x 19x 1x 1x 18x 18x 3x 3x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 15x 1x 1x 14x 1x 1x 13x 13x 1x 1x 12x 1x 1x 1x 11x 11x 8x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 6x 7x 7x 4x 4x 4x 4x 4x 4x 4x 4x | /**
* OAuth Callback Endpoint
*
* Handles the callback from GitLab after user authorization in Authorization Code Flow.
* This endpoint receives the GitLab authorization code, exchanges it for tokens,
* creates a session, and redirects back to the client with an MCP authorization code.
*
* Flow:
* 1. User completes GitLab authorization
* 2. GitLab redirects to /oauth/callback with code and state
* 3. We exchange GitLab code for GitLab tokens
* 4. We create a session with GitLab tokens
* 5. We generate an MCP authorization code
* 6. We redirect to client's redirect_uri with MCP code
*/
import { Request, Response } from "express";
import { loadOAuthConfig } from "../config";
import { sessionStore } from "../session-store";
import { exchangeGitLabAuthCode, getGitLabUser } from "../gitlab-device-flow";
import { generateSessionId, generateAuthorizationCode, calculateTokenExpiry } from "../token-utils";
import { logger } from "../../logger";
/**
* OAuth callback handler
*
* Handles GET /oauth/callback from GitLab after user authorization.
*
* Query parameters (from GitLab):
* - code: GitLab authorization code
* - state: Internal state we sent to GitLab (maps to AuthCodeFlowState)
*
* On success, redirects to client's redirect_uri with:
* - code: MCP authorization code (for /token exchange)
* - state: Original client state (for CSRF verification)
*/
export async function callbackHandler(req: Request, res: Response): Promise<void> {
const config = loadOAuthConfig();
if (!config) {
res.status(500).json({
error: "server_error",
error_description: "OAuth not configured",
});
return;
}
const { code, state, error, error_description } = req.query as Record<string, string | undefined>;
// Handle GitLab error responses
if (error) {
logger.warn({ error, error_description }, "GitLab authorization error");
// Redirect to client with error if we can find the flow state
if (state) {
const flow = sessionStore.getAuthCodeFlow(state);
if (flow) {
sessionStore.deleteAuthCodeFlow(state);
const redirectUrl = new URL(flow.clientRedirectUri);
redirectUrl.searchParams.set("error", error);
Eif (error_description) {
redirectUrl.searchParams.set("error_description", error_description);
}
Eif (flow.clientState) {
redirectUrl.searchParams.set("state", flow.clientState);
}
res.redirect(redirectUrl.toString());
return;
}
}
res.status(400).json({
error: error,
error_description: error_description ?? "GitLab authorization failed",
});
return;
}
// Validate required parameters
if (!code) {
res.status(400).json({
error: "invalid_request",
error_description: "Missing authorization code from GitLab",
});
return;
}
if (!state) {
res.status(400).json({
error: "invalid_request",
error_description: "Missing state parameter",
});
return;
}
// Look up the auth code flow state
const flow = sessionStore.getAuthCodeFlow(state);
if (!flow) {
res.status(400).json({
error: "invalid_request",
error_description: "Invalid or expired state. Please start authorization again.",
});
return;
}
// Check if flow has expired
if (Date.now() > flow.expiresAt) {
sessionStore.deleteAuthCodeFlow(state);
res.status(400).json({
error: "invalid_request",
error_description: "Authorization flow expired. Please start again.",
});
return;
}
try {
// Exchange GitLab authorization code for tokens
const gitlabTokens = await exchangeGitLabAuthCode(code, flow.callbackUri, config);
// Get GitLab user info
const userInfo = await getGitLabUser(gitlabTokens.access_token);
// Create session
const sessionId = generateSessionId();
const now = Date.now();
// Generate MCP authorization code for the client
const mcpAuthCode = generateAuthorizationCode();
// Store MCP authorization code (single-use, expires in 10 minutes)
sessionStore.storeAuthCode({
code: mcpAuthCode,
sessionId,
clientId: flow.clientId,
codeChallenge: flow.codeChallenge,
codeChallengeMethod: flow.codeChallengeMethod,
redirectUri: flow.clientRedirectUri,
expiresAt: now + 10 * 60 * 1000, // 10 minutes
});
// Create session with GitLab tokens
// MCP tokens will be set when the authorization code is exchanged via /token
sessionStore.createSession({
id: sessionId,
mcpAccessToken: "", // Set on /token
mcpRefreshToken: "", // Set on /token
mcpTokenExpiry: 0, // Set on /token
gitlabAccessToken: gitlabTokens.access_token,
gitlabRefreshToken: gitlabTokens.refresh_token,
gitlabTokenExpiry: calculateTokenExpiry(gitlabTokens.expires_in),
gitlabUserId: userInfo.id,
gitlabUsername: userInfo.username,
clientId: flow.clientId,
scopes: ["mcp:tools", "mcp:resources"],
createdAt: now,
updatedAt: now,
});
// Clean up the auth code flow state
sessionStore.deleteAuthCodeFlow(state);
logger.info(
{
sessionId: sessionId.substring(0, 8) + "...",
userId: userInfo.id,
username: userInfo.username,
},
"Authorization Code Flow completed successfully"
);
// Redirect to client with MCP authorization code
const redirectUrl = new URL(flow.clientRedirectUri);
redirectUrl.searchParams.set("code", mcpAuthCode);
if (flow.clientState) {
redirectUrl.searchParams.set("state", flow.clientState);
}
logger.debug(
{ redirectUri: flow.clientRedirectUri },
"Redirecting to client with authorization code"
);
res.redirect(redirectUrl.toString());
} catch (error: unknown) {
logger.error({ err: error as Error }, "Failed to complete authorization code flow");
// Clean up the flow state on error
sessionStore.deleteAuthCodeFlow(state);
// Try to redirect to client with error
const redirectUrl = new URL(flow.clientRedirectUri);
redirectUrl.searchParams.set("error", "server_error");
redirectUrl.searchParams.set(
"error_description",
error instanceof Error ? error.message : "Failed to complete authorization"
);
Eif (flow.clientState) {
redirectUrl.searchParams.set("state", flow.clientState);
}
res.redirect(redirectUrl.toString());
}
}
|