Skip to content

Commit 05f9765

Browse files
committed
feat(auth): integrate oauth2-server for OAuth 2.1 support with PKCE and token validation
1 parent 85c050b commit 05f9765

File tree

8 files changed

+538
-475
lines changed

8 files changed

+538
-475
lines changed

package-lock.json

Lines changed: 188 additions & 282 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@
4646
"dependencies": {
4747
"@modelcontextprotocol/sdk": "^1.19.1",
4848
"@types/express": "^5.0.3",
49+
"@types/oauth2-server": "^3.0.18",
4950
"express": "^5.1.0",
51+
"jose": "^6.0.12",
52+
"oauth2-server": "^3.1.1",
5053
"pino": "^9.0.0",
51-
"pino-pretty": "^13.1.1"
54+
"pino-pretty": "^13.1.1",
55+
"pkce-challenge": "^5.0.0"
5256
}
5357
}

src/auth/discovery.ts

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -78,65 +78,57 @@ export function createProtectedResourceMetadataHandler() {
7878
}
7979

8080
/**
81-
* OAuth 2.1 token endpoint with PKCE support
81+
* OAuth 2.1 token endpoint with PKCE support using oauth2-server
8282
*/
83-
export function createTokenHandler(oauthProvider: any) {
83+
export function createTokenHandler(oauthServer: any) {
8484
return async (req: Request, res: Response) => {
8585
try {
86-
const { grant_type, code, redirect_uri, code_verifier, client_id } = req.body;
87-
88-
if (grant_type !== "authorization_code") {
89-
return res.status(400).json({
90-
error: "unsupported_grant_type",
91-
error_description: "Only authorization_code grant type is supported"
92-
});
93-
}
94-
95-
if (!code || !redirect_uri || !code_verifier || !client_id) {
96-
return res.status(400).json({
97-
error: "invalid_request",
98-
error_description: "Missing required parameters: code, redirect_uri, code_verifier, client_id"
99-
});
100-
}
101-
const tokenResult = await oauthProvider.exchangeAuthorizationCode(
102-
code,
103-
code_verifier,
104-
client_id,
105-
redirect_uri
106-
);
107-
108-
if (!tokenResult) {
109-
return res.status(400).json({
110-
error: "invalid_grant",
111-
error_description: "Invalid authorization code or PKCE verification failed"
112-
});
113-
}
114-
115-
logger.info("Token exchange successful", { client_id, scope: tokenResult.scope });
86+
const request = new oauthServer.server.Request(req);
87+
const response = new oauthServer.server.Response(res);
88+
89+
const token = await oauthServer.server.token(request, response);
90+
91+
logger.info("Token exchange successful", {
92+
client_id: token.client.id,
93+
scope: token.scope
94+
});
11695

11796
res.json({
118-
access_token: tokenResult.accessToken,
97+
access_token: token.accessToken,
11998
token_type: "Bearer",
120-
expires_in: tokenResult.expiresIn,
121-
scope: tokenResult.scope
99+
expires_in: Math.floor((token.accessTokenExpiresAt.getTime() - Date.now()) / 1000),
100+
scope: token.scope
122101
});
123102

124103
} catch (error) {
125104
logger.error("Token endpoint error", {
126105
error: error instanceof Error ? error.message : error
127106
});
128-
res.status(500).json({
129-
error: "server_error",
130-
error_description: "Failed to process token request"
131-
});
107+
108+
if (error.name === 'InvalidGrantError') {
109+
res.status(400).json({
110+
error: "invalid_grant",
111+
error_description: error.message
112+
});
113+
} else if (error.name === 'InvalidRequestError') {
114+
res.status(400).json({
115+
error: "invalid_request",
116+
error_description: error.message
117+
});
118+
} else {
119+
res.status(500).json({
120+
error: "server_error",
121+
error_description: "Failed to process token request"
122+
});
123+
}
132124
}
133125
};
134126
}
135127

136128
/**
137-
* Token introspection endpoint
129+
* Token introspection endpoint using oauth2-server
138130
*/
139-
export function createIntrospectionHandler() {
131+
export function createIntrospectionHandler(oauthServer?: any) {
140132
return async (req: Request, res: Response) => {
141133
try {
142134
const { token } = req.body;
@@ -148,16 +140,38 @@ export function createIntrospectionHandler() {
148140
});
149141
}
150142

151-
// TODO: Implement actual token introspection
152-
// For now, return active=true for any token
153-
logger.info("Token introspection requested", { token: token.substring(0, 10) + "..." });
154-
155-
res.json({
156-
active: true,
157-
scope: "read",
158-
client_id: "mcp-client",
159-
exp: Math.floor(Date.now() / 1000) + 3600
160-
});
143+
if (oauthServer) {
144+
try {
145+
const accessToken = await oauthServer.server.model.getAccessToken(token);
146+
147+
if (!accessToken || accessToken.accessTokenExpiresAt < new Date()) {
148+
return res.json({ active: false });
149+
}
150+
151+
logger.info("Token introspection requested", {
152+
token: token.substring(0, 10) + "...",
153+
client_id: accessToken.client.id
154+
});
155+
156+
res.json({
157+
active: true,
158+
scope: accessToken.scope,
159+
client_id: accessToken.client.id,
160+
exp: Math.floor(accessToken.accessTokenExpiresAt.getTime() / 1000)
161+
});
162+
} catch (error) {
163+
res.json({ active: false });
164+
}
165+
} else {
166+
// Fallback for gateway mode
167+
logger.info("Token introspection requested", { token: token.substring(0, 10) + "..." });
168+
res.json({
169+
active: true,
170+
scope: "read",
171+
client_id: "mcp-client",
172+
exp: Math.floor(Date.now() / 1000) + 3600
173+
});
174+
}
161175

162176
} catch (error) {
163177
logger.error("Token introspection error", {

src/auth/index.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { OAuthProvider, type OAuthConfig } from "./oauth-provider.ts";
2-
import { GatewayTokenValidator } from "./token-validator.ts";
1+
import { ManagedOAuthServer } from "./oauth-server.ts";
2+
import { GatewayTokenValidator, BuiltinTokenValidator } from "./token-validator.ts";
33
import { createAuthMiddleware } from "./middleware.ts";
44
import { getConfig } from "../config.ts";
55
import { logger } from "../logger.ts";
@@ -12,7 +12,7 @@ export function initializeAuth() {
1212

1313
if (!config.ENABLE_AUTH) {
1414
logger.info("Authentication is disabled");
15-
return { tokenValidator: null, oauthProvider: null };
15+
return { tokenValidator: null, oauthServer: null };
1616
}
1717

1818
if (config.AUTH_MODE === "gateway") {
@@ -21,22 +21,14 @@ export function initializeAuth() {
2121
config.OAUTH_ISSUER!,
2222
config.OAUTH_AUDIENCE
2323
);
24-
return { tokenValidator, oauthProvider: null };
24+
return { tokenValidator, oauthServer: null };
2525
}
2626

2727
if (config.AUTH_MODE === "builtin") {
28-
logger.info("Initializing built-in auth mode (OAuth client + resource server)");
29-
const oauthConfig: OAuthConfig = {
30-
clientId: config.OAUTH_CLIENT_ID!,
31-
clientSecret: config.OAUTH_CLIENT_SECRET!,
32-
authorizationEndpoint: config.OAUTH_AUTH_ENDPOINT!,
33-
tokenEndpoint: config.OAUTH_TOKEN_ENDPOINT!,
34-
scope: config.OAUTH_SCOPE || "read",
35-
redirectUri: config.OAUTH_REDIRECT_URI!,
36-
};
37-
38-
const oauthProvider = new OAuthProvider(oauthConfig);
39-
return { tokenValidator: oauthProvider.tokenValidator, oauthProvider };
28+
logger.info("Initializing built-in auth mode (OAuth authorization server)");
29+
const oauthServer = new ManagedOAuthServer();
30+
const tokenValidator = new BuiltinTokenValidator(oauthServer);
31+
return { tokenValidator, oauthServer };
4032
}
4133

4234
throw new Error(`Unknown auth mode: ${config.AUTH_MODE}`);

0 commit comments

Comments
 (0)