Skip to content

Commit 4cc37e5

Browse files
committed
feat(auth): integrate oauth2-server for OAuth 2.1 support with PKCE and token validation
1 parent 693333e commit 4cc37e5

File tree

8 files changed

+523
-194
lines changed

8 files changed

+523
-194
lines changed

package-lock.json

Lines changed: 173 additions & 1 deletion
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
@@ -41,8 +41,12 @@
4141
"dependencies": {
4242
"@modelcontextprotocol/sdk": "^1.17.0",
4343
"@types/express": "^5.0.3",
44+
"@types/oauth2-server": "^3.0.18",
4445
"express": "^5.1.0",
46+
"jose": "^6.0.12",
47+
"oauth2-server": "^3.1.1",
4548
"pino": "^9.0.0",
46-
"pino-pretty": "^11.0.0"
49+
"pino-pretty": "^11.0.0",
50+
"pkce-challenge": "^5.0.0"
4751
}
4852
}

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", {

0 commit comments

Comments
 (0)