1- import { createHmac , timingSafeEqual } from 'node:crypto ' ;
1+ import { SignJWT , jwtVerify , type JWTPayload } from 'jose ' ;
22import type {
33 OAuthServerProvider ,
44 AuthorizationParams ,
@@ -14,40 +14,35 @@ import { InvalidGrantError } from '@modelcontextprotocol/sdk/server/auth/errors.
1414import { ClientsStore , CodeStore , RefreshStore } from './store.js' ;
1515import { logger } from '../logger.js' ;
1616
17- // --- JWT helpers (HMAC-SHA256 via node:crypto — no extra deps ) ---
17+ // --- JWT helpers (HMAC-SHA256 via jose ) ---
1818
19- function base64url ( buf : Buffer ) : string {
20- return buf . toString ( 'base64' ) . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = / g, '' ) ;
21- }
22-
23- function base64urlStr ( s : string ) : string {
24- return base64url ( Buffer . from ( s , 'utf8' ) ) ;
25- }
26-
27- function signJwt ( payload : Record < string , unknown > , secret : string , expiresInSecs : number ) : string {
19+ async function signJwt (
20+ payload : Record < string , unknown > ,
21+ secret : string ,
22+ expiresInSecs : number
23+ ) : Promise < string > {
24+ const key = new TextEncoder ( ) . encode ( secret ) ;
2825 const now = Math . floor ( Date . now ( ) / 1000 ) ;
29- const claims = { ...payload , iat : now , exp : now + expiresInSecs } ;
30- const header = base64urlStr ( JSON . stringify ( { alg : 'HS256' , typ : 'JWT' } ) ) ;
31- const body = base64urlStr ( JSON . stringify ( claims ) ) ;
32- const signing = `${ header } .${ body } ` ;
33- const sig = base64url ( createHmac ( 'sha256' , secret ) . update ( signing ) . digest ( ) ) ;
34- return `${ signing } .${ sig } ` ;
26+ return new SignJWT ( payload )
27+ . setProtectedHeader ( { alg : 'HS256' } )
28+ . setIssuedAt ( now )
29+ . setExpirationTime ( now + expiresInSecs )
30+ . sign ( key ) ;
3531}
3632
37- function verifyJwt ( token : string , secret : string ) : Record < string , unknown > | null {
38- const parts = token . split ( '.' ) ;
39- if ( parts . length !== 3 ) return null ;
40- const [ header , body , sig ] = parts ;
41- const expected = base64url ( createHmac ( 'sha256' , secret ) . update ( `${ header } .${ body } ` ) . digest ( ) ) ;
42- const sigBuf = Buffer . from ( sig ) ;
43- const expBuf = Buffer . from ( expected ) ;
44- // HMAC-SHA256 base64url is always 43 chars — same length guaranteed
45- if ( sigBuf . length !== expBuf . length ) return null ;
46- if ( ! timingSafeEqual ( sigBuf , expBuf ) ) return null ;
33+ async function verifyJwt (
34+ token : string ,
35+ secret : string ,
36+ opts ?: { issuer ?: string ; audience ?: string }
37+ ) : Promise < JWTPayload | null > {
4738 try {
48- const claims = JSON . parse ( Buffer . from ( body , 'base64url' ) . toString ( 'utf8' ) ) ;
49- if ( typeof claims . exp === 'number' && Date . now ( ) / 1000 > claims . exp ) return null ;
50- return claims as Record < string , unknown > ;
39+ const key = new TextEncoder ( ) . encode ( secret ) ;
40+ const { payload } = await jwtVerify ( token , key , {
41+ algorithms : [ 'HS256' ] ,
42+ ...( opts ?. issuer && { issuer : opts . issuer } ) ,
43+ ...( opts ?. audience && { audience : opts . audience } ) ,
44+ } ) ;
45+ return payload ;
5146 } catch {
5247 return null ;
5348 }
@@ -195,8 +190,14 @@ export class NextcloudOAuthProvider implements OAuthServerProvider {
195190 }
196191 this . codeStore . delete ( authorizationCode ) ;
197192
198- const accessToken = signJwt (
199- { sub : entry . userId , client_id : entry . clientId , scopes : entry . scopes } ,
193+ const accessToken = await signJwt (
194+ {
195+ sub : entry . userId ,
196+ client_id : entry . clientId ,
197+ scopes : entry . scopes ,
198+ iss : this . getIssuer ( ) ,
199+ aud : this . getResourceUrl ( ) ,
200+ } ,
200201 this . getSecret ( ) ,
201202 ACCESS_TOKEN_TTL_SECS
202203 ) ;
@@ -236,8 +237,14 @@ export class NextcloudOAuthProvider implements OAuthServerProvider {
236237 const effectiveScopes = scopes ?? entry . scopes ;
237238 this . refreshStore . delete ( refreshToken ) ;
238239
239- const accessToken = signJwt (
240- { sub : entry . userId , client_id : entry . clientId , scopes : effectiveScopes } ,
240+ const accessToken = await signJwt (
241+ {
242+ sub : entry . userId ,
243+ client_id : entry . clientId ,
244+ scopes : effectiveScopes ,
245+ iss : this . getIssuer ( ) ,
246+ aud : this . getResourceUrl ( ) ,
247+ } ,
241248 this . getSecret ( ) ,
242249 ACCESS_TOKEN_TTL_SECS
243250 ) ;
@@ -258,7 +265,12 @@ export class NextcloudOAuthProvider implements OAuthServerProvider {
258265 }
259266
260267 async verifyAccessToken ( token : string ) : Promise < AuthInfo > {
261- const claims = verifyJwt ( token , this . getSecret ( ) ) ;
268+ const issuer = process . env . MCP_AUTH_ISSUER ;
269+ const claims = await verifyJwt (
270+ token ,
271+ this . getSecret ( ) ,
272+ issuer ? { issuer, audience : new URL ( '/mcp' , issuer ) . toString ( ) } : undefined
273+ ) ;
262274 if ( ! claims ) {
263275 logger . warn ( '[auth] Access token verification failed: invalid or expired token' ) ;
264276 throw new Error ( 'Invalid or expired access token' ) ;
@@ -295,4 +307,14 @@ export class NextcloudOAuthProvider implements OAuthServerProvider {
295307 if ( ! secret ) throw new Error ( 'MCP_AUTH_SECRET is not set' ) ;
296308 return secret ;
297309 }
310+
311+ private getIssuer ( ) : string {
312+ const issuer = process . env . MCP_AUTH_ISSUER ;
313+ if ( ! issuer ) throw new Error ( 'MCP_AUTH_ISSUER is not set' ) ;
314+ return issuer ;
315+ }
316+
317+ private getResourceUrl ( ) : string {
318+ return new URL ( '/mcp' , this . getIssuer ( ) ) . toString ( ) ;
319+ }
298320}
0 commit comments