Skip to content

Commit 64fd0e4

Browse files
committed
feat(auth): Refactor authentication flow and integrate OAuth provider
- Simplified the authentication initialization process by removing the OAuthProvider from the `initializeAuth` function. - Enhanced the `createAuthenticationMiddleware` to conditionally use the OAuthProvider for full mode. - Introduced a new `createOAuthProviderAuthMiddleware` for handling authentication with the external OAuth provider. - Added a new `oauth-model.ts` file to manage OAuth-related data and operations, including token and authorization code handling. - Updated the `OAuthProvider` class to store external token information and manage authorization codes with PKCE support. - Implemented a new token exchange flow in the `createCallbackHandler` to handle authorization code exchanges with the external provider. - Enhanced error handling and logging throughout the OAuth flow. - Updated configuration validation to ensure required OAuth parameters are set for full mode. - Registered new OAuth routes and discovery endpoints in the main application setup.
1 parent aa5e5a5 commit 64fd0e4

File tree

8 files changed

+924
-242
lines changed

8 files changed

+924
-242
lines changed

src/auth/discovery.ts

Lines changed: 141 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import type { Request, Response } from "express";
2+
import OAuth2Server from '@node-oauth/oauth2-server';
23
import { getConfig } from "../config.ts";
34
import { logger } from "../logger.ts";
45

56
/**
67
* OAuth 2.0 Authorization Server Metadata endpoint
78
* RFC 8414: https://tools.ietf.org/html/rfc8414
9+
*
10+
* For AUTH_MODE=full, this describes our OAuth client proxy endpoints
811
*/
912
export function createAuthorizationServerMetadataHandler() {
1013
return (req: Request, res: Response) => {
@@ -19,14 +22,12 @@ export function createAuthorizationServerMetadataHandler() {
1922
response_types_supported: ["code"],
2023
grant_types_supported: ["authorization_code"],
2124
code_challenge_methods_supported: ["S256"],
22-
scopes_supported: ["openid", "profile", "email", "read", "write"],
23-
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
24-
revocation_endpoint: `${baseUrl}/oauth/revoke`,
25-
introspection_endpoint: `${baseUrl}/oauth/introspect`,
25+
scopes_supported: ["read", "write", "mcp"],
26+
token_endpoint_auth_methods_supported: ["none"]
2627
};
2728

2829
logger.info("OAuth authorization server metadata requested", {
29-
issuer: metadata.issuer
30+
issuer: metadata.issuer
3031
});
3132

3233
res.json(metadata);
@@ -45,6 +46,8 @@ export function createAuthorizationServerMetadataHandler() {
4546
/**
4647
* OAuth 2.0 Protected Resource Metadata endpoint
4748
* RFC 8705: https://tools.ietf.org/html/rfc8705
49+
*
50+
* For AUTH_MODE=full, this describes our resource server capabilities
4851
*/
4952
export function createProtectedResourceMetadataHandler() {
5053
return (req: Request, res: Response) => {
@@ -55,13 +58,13 @@ export function createProtectedResourceMetadataHandler() {
5558
const metadata = {
5659
resource: baseUrl,
5760
authorization_servers: [baseUrl],
58-
scopes_supported: ["read", "write"],
61+
scopes_supported: ["read", "write", "mcp"],
5962
bearer_methods_supported: ["header"],
60-
resource_documentation: `${baseUrl}/docs`,
63+
resource_documentation: `${baseUrl}/docs`
6164
};
6265

6366
logger.info("OAuth protected resource metadata requested", {
64-
resource: metadata.resource
67+
resource: metadata.resource
6568
});
6669

6770
res.json(metadata);
@@ -78,145 +81,189 @@ export function createProtectedResourceMetadataHandler() {
7881
}
7982

8083
/**
81-
* OAuth 2.1 token endpoint - proxies token requests to external OAuth provider
84+
* OAuth 2.0 Authorization endpoint
8285
*/
83-
export function createTokenHandler(oauthProvider: any) {
86+
export function createAuthorizeHandler(oauthServer: OAuth2Server) {
8487
return async (req: Request, res: Response) => {
8588
try {
86-
const config = getConfig();
87-
const { grant_type, code, redirect_uri, client_id, code_verifier } = req.body;
89+
logger.debug("Authorization request received", {
90+
query: req.query,
91+
method: req.method
92+
});
8893

89-
// Validate required parameters
90-
if (grant_type !== "authorization_code") {
91-
return res.status(400).json({
92-
error: "unsupported_grant_type",
93-
error_description: "Only 'authorization_code' grant type is supported"
94+
// Real OAuth implementation: Check for authenticated user
95+
// In a real implementation, this would:
96+
// 1. Check if user has valid session/cookie
97+
// 2. If not authenticated, redirect to login page
98+
// 3. After login, show consent page
99+
// 4. Only then proceed with authorization
100+
101+
// For now, this implementation requires external authentication
102+
// The user must be authenticated before reaching this endpoint
103+
const userId = req.headers['x-user-id'] as string;
104+
const username = req.headers['x-username'] as string;
105+
106+
if (!userId || !username) {
107+
logger.warn("Missing user authentication headers");
108+
return res.status(401).json({
109+
error: "access_denied",
110+
error_description: "User must be authenticated before authorization"
94111
});
95112
}
113+
114+
const user = {
115+
id: userId,
116+
username: username
117+
};
96118

97-
if (!code || !redirect_uri || !client_id || !code_verifier) {
98-
return res.status(400).json({
99-
error: "invalid_request",
100-
error_description: "Missing required parameters: code, redirect_uri, client_id, code_verifier"
101-
});
102-
}
119+
logger.debug("User authenticated, proceeding with authorization", { userId: user.id });
103120

104-
// Proxy token request to external OAuth provider
105-
const tokenParams = new URLSearchParams({
106-
grant_type: "authorization_code",
107-
code,
108-
redirect_uri: `${config.BASE_URL || "http://localhost:3000"}/oauth/callback`,
109-
client_id: config.OAUTH_CLIENT_ID!,
110-
client_secret: config.OAUTH_CLIENT_SECRET!,
111-
code_verifier
121+
// Use the OAuth2Server authorize method
122+
const request = new (OAuth2Server as any).Request(req);
123+
const response = new (OAuth2Server as any).Response(res);
124+
125+
const authorizationCode = await oauthServer.authorize(request, response, {
126+
authenticateHandler: {
127+
handle: async () => {
128+
logger.debug("Authenticate handler called");
129+
return user;
130+
}
131+
}
112132
});
113133

114-
const tokenResponse = await fetch(`${config.OAUTH_ISSUER}/oauth/token`, {
115-
method: "POST",
116-
headers: {
117-
"Content-Type": "application/x-www-form-urlencoded",
118-
},
119-
body: tokenParams
134+
logger.info("Authorization code granted", {
135+
clientId: authorizationCode.client.id,
136+
userId: user.id,
137+
code: authorizationCode.authorizationCode.substring(0, 8) + "..."
120138
});
121139

122-
if (!tokenResponse.ok) {
123-
logger.warn("External OAuth token exchange failed", {
124-
status: tokenResponse.status,
125-
statusText: tokenResponse.statusText
126-
});
127-
return res.status(400).json({
128-
error: "invalid_grant",
129-
error_description: "Authorization code exchange failed"
140+
// Redirect back to client with authorization code
141+
const redirectUri = req.query.redirect_uri as string;
142+
const state = req.query.state as string;
143+
144+
if (redirectUri) {
145+
const url = new URL(redirectUri);
146+
url.searchParams.set('code', authorizationCode.authorizationCode);
147+
if (state) url.searchParams.set('state', state);
148+
149+
logger.info("Redirecting to client", { redirectUrl: url.toString() });
150+
res.redirect(url.toString());
151+
} else {
152+
// Fallback - return as JSON
153+
res.json({
154+
authorization_code: authorizationCode.authorizationCode,
155+
state
130156
});
131157
}
132158

133-
const tokenData = await tokenResponse.json();
159+
} catch (error) {
160+
logger.error("Authorization endpoint error", {
161+
error: error instanceof Error ? error.message : error,
162+
stack: error instanceof Error ? error.stack : undefined
163+
});
164+
165+
res.status(400).json({
166+
error: "server_error",
167+
error_description: error instanceof Error ? error.message : "Failed to process authorization request"
168+
});
169+
}
170+
};
171+
}
172+
173+
/**
174+
* OAuth 2.0 Token endpoint
175+
*/
176+
export function createTokenHandler(oauthServer: OAuth2Server) {
177+
return async (req: Request, res: Response) => {
178+
try {
179+
const request = new (OAuth2Server as any).Request(req);
180+
const response = new (OAuth2Server as any).Response(res);
181+
182+
const token = await oauthServer.token(request, response);
134183

135-
logger.info("Token exchange successful via external provider", {
136-
client_id,
137-
scope: tokenData.scope
184+
logger.info("Access token granted", {
185+
clientId: token.client.id,
186+
userId: token.user?.id,
187+
scope: token.scope
138188
});
139189

140-
// Return tokens (optionally transform or wrap them)
141190
res.json({
142-
access_token: tokenData.access_token,
143-
token_type: tokenData.token_type || "Bearer",
144-
expires_in: tokenData.expires_in,
145-
scope: tokenData.scope,
146-
refresh_token: tokenData.refresh_token
191+
access_token: token.accessToken,
192+
token_type: "Bearer",
193+
expires_in: Math.floor((token.accessTokenExpiresAt!.getTime() - Date.now()) / 1000),
194+
scope: Array.isArray(token.scope) ? token.scope.join(' ') : token.scope,
195+
refresh_token: token.refreshToken
147196
});
148197

149198
} catch (error) {
150-
logger.error("Token endpoint proxy error", {
199+
logger.error("Token endpoint error", {
151200
error: error instanceof Error ? error.message : error
152201
});
153202

154-
res.status(500).json({
155-
error: "server_error",
156-
error_description: "Failed to process token request"
203+
res.status(400).json({
204+
error: "invalid_request",
205+
error_description: error instanceof Error ? error.message : "Token request failed"
157206
});
158207
}
159208
};
160209
}
161210

162211
/**
163-
* Token introspection endpoint - simplified for OAuth proxy pattern
212+
* Token introspection endpoint
164213
*/
165-
export function createIntrospectionHandler(oauthProvider?: any) {
214+
export function createIntrospectionHandler(oauthServer: OAuth2Server) {
166215
return async (req: Request, res: Response) => {
167216
try {
168-
const { token } = req.body;
169-
170-
if (!token) {
171-
return res.status(400).json({
172-
error: "invalid_request",
173-
error_description: "Missing token parameter"
174-
});
175-
}
176-
177-
logger.info("Token introspection requested", { token: token.substring(0, 10) + "..." });
217+
const request = new (OAuth2Server as any).Request(req);
218+
const response = new (OAuth2Server as any).Response(res);
178219

179-
// Return inactive for OAuth proxy pattern - external IdP handles actual validation
180-
res.json({ active: false });
220+
const token = await oauthServer.authenticate(request, response);
221+
222+
logger.info("Token introspection successful", {
223+
clientId: token.client.id,
224+
userId: token.user?.id,
225+
scope: token.scope
226+
});
227+
228+
res.json({
229+
active: true,
230+
scope: Array.isArray(token.scope) ? token.scope.join(' ') : token.scope,
231+
client_id: token.client.id,
232+
username: token.user?.username,
233+
sub: token.user?.id,
234+
exp: Math.floor((token.accessTokenExpiresAt?.getTime() || 0) / 1000)
235+
});
181236

182237
} catch (error) {
183-
logger.error("Token introspection error", {
238+
logger.debug("Token introspection failed", {
184239
error: error instanceof Error ? error.message : error
185240
});
186-
res.status(500).json({
187-
error: "server_error",
188-
error_description: "Failed to introspect token"
189-
});
241+
242+
res.json({ active: false });
190243
}
191244
};
192245
}
193246

194247
/**
195248
* Token revocation endpoint
196249
*/
197-
export function createRevocationHandler() {
250+
export function createRevocationHandler(oauthServer: OAuth2Server) {
198251
return async (req: Request, res: Response) => {
199252
try {
200-
const { token } = req.body;
201-
202-
if (!token) {
203-
return res.status(400).json({
204-
error: "invalid_request",
205-
error_description: "Missing token parameter"
206-
});
207-
}
208-
209-
// TODO: Implement actual token revocation
210-
logger.info("Token revocation requested", { token: token.substring(0, 10) + "..." });
211-
212-
res.status(200).send(); // Success response
253+
const request = new (OAuth2Server as any).Request(req);
254+
const response = new (OAuth2Server as any).Response(res);
255+
256+
await oauthServer.revoke(request, response);
257+
258+
logger.info("Token revoked successfully");
259+
res.status(200).send();
213260

214261
} catch (error) {
215262
logger.error("Token revocation error", {
216263
error: error instanceof Error ? error.message : error
217264
});
218-
res.status(500).json({
219-
error: "server_error",
265+
res.status(400).json({
266+
error: "invalid_request",
220267
error_description: "Failed to revoke token"
221268
});
222269
}

0 commit comments

Comments
 (0)