auth-extensions.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. /**
  2. * OAuth provider extensions for specialized authentication flows.
  3. *
  4. * This module provides ready-to-use OAuthClientProvider implementations
  5. * for common machine-to-machine authentication scenarios.
  6. */
  7. /**
  8. * Helper to produce a private_key_jwt client authentication function.
  9. *
  10. * Usage:
  11. * const addClientAuth = createPrivateKeyJwtAuth({ issuer, subject, privateKey, alg, audience? });
  12. * // pass addClientAuth as provider.addClientAuthentication implementation
  13. */
  14. export function createPrivateKeyJwtAuth(options) {
  15. return async (_headers, params, url, metadata) => {
  16. // Lazy import to avoid heavy dependency unless used
  17. if (typeof globalThis.crypto === 'undefined') {
  18. throw new TypeError('crypto is not available, please ensure you add have Web Crypto API support for older Node.js versions (see https://github.com/modelcontextprotocol/typescript-sdk#nodejs-web-crypto-globalthiscrypto-compatibility)');
  19. }
  20. const jose = await import('jose');
  21. const audience = String(options.audience ?? metadata?.issuer ?? url);
  22. const lifetimeSeconds = options.lifetimeSeconds ?? 300;
  23. const now = Math.floor(Date.now() / 1000);
  24. const jti = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
  25. const baseClaims = {
  26. iss: options.issuer,
  27. sub: options.subject,
  28. aud: audience,
  29. exp: now + lifetimeSeconds,
  30. iat: now,
  31. jti
  32. };
  33. const claims = options.claims ? { ...baseClaims, ...options.claims } : baseClaims;
  34. // Import key for the requested algorithm
  35. const alg = options.alg;
  36. let key;
  37. if (typeof options.privateKey === 'string') {
  38. if (alg.startsWith('RS') || alg.startsWith('ES') || alg.startsWith('PS')) {
  39. key = await jose.importPKCS8(options.privateKey, alg);
  40. }
  41. else if (alg.startsWith('HS')) {
  42. key = new TextEncoder().encode(options.privateKey);
  43. }
  44. else {
  45. throw new Error(`Unsupported algorithm ${alg}`);
  46. }
  47. }
  48. else if (options.privateKey instanceof Uint8Array) {
  49. if (alg.startsWith('HS')) {
  50. key = options.privateKey;
  51. }
  52. else {
  53. // Assume PKCS#8 DER in Uint8Array for asymmetric algorithms
  54. key = await jose.importPKCS8(new TextDecoder().decode(options.privateKey), alg);
  55. }
  56. }
  57. else {
  58. // Treat as JWK
  59. key = await jose.importJWK(options.privateKey, alg);
  60. }
  61. // Sign JWT
  62. const assertion = await new jose.SignJWT(claims)
  63. .setProtectedHeader({ alg, typ: 'JWT' })
  64. .setIssuer(options.issuer)
  65. .setSubject(options.subject)
  66. .setAudience(audience)
  67. .setIssuedAt(now)
  68. .setExpirationTime(now + lifetimeSeconds)
  69. .setJti(jti)
  70. .sign(key);
  71. params.set('client_assertion', assertion);
  72. params.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
  73. };
  74. }
  75. /**
  76. * OAuth provider for client_credentials grant with client_secret_basic authentication.
  77. *
  78. * This provider is designed for machine-to-machine authentication where
  79. * the client authenticates using a client_id and client_secret.
  80. *
  81. * @example
  82. * const provider = new ClientCredentialsProvider({
  83. * clientId: 'my-client',
  84. * clientSecret: 'my-secret'
  85. * });
  86. *
  87. * const transport = new StreamableHTTPClientTransport(serverUrl, {
  88. * authProvider: provider
  89. * });
  90. */
  91. export class ClientCredentialsProvider {
  92. constructor(options) {
  93. this._clientInfo = {
  94. client_id: options.clientId,
  95. client_secret: options.clientSecret
  96. };
  97. this._clientMetadata = {
  98. client_name: options.clientName ?? 'client-credentials-client',
  99. redirect_uris: [],
  100. grant_types: ['client_credentials'],
  101. token_endpoint_auth_method: 'client_secret_basic',
  102. scope: options.scope
  103. };
  104. }
  105. get redirectUrl() {
  106. return undefined;
  107. }
  108. get clientMetadata() {
  109. return this._clientMetadata;
  110. }
  111. clientInformation() {
  112. return this._clientInfo;
  113. }
  114. saveClientInformation(info) {
  115. this._clientInfo = info;
  116. }
  117. tokens() {
  118. return this._tokens;
  119. }
  120. saveTokens(tokens) {
  121. this._tokens = tokens;
  122. }
  123. redirectToAuthorization() {
  124. throw new Error('redirectToAuthorization is not used for client_credentials flow');
  125. }
  126. saveCodeVerifier() {
  127. // Not used for client_credentials
  128. }
  129. codeVerifier() {
  130. throw new Error('codeVerifier is not used for client_credentials flow');
  131. }
  132. prepareTokenRequest(scope) {
  133. const params = new URLSearchParams({ grant_type: 'client_credentials' });
  134. if (scope)
  135. params.set('scope', scope);
  136. return params;
  137. }
  138. }
  139. /**
  140. * OAuth provider for client_credentials grant with private_key_jwt authentication.
  141. *
  142. * This provider is designed for machine-to-machine authentication where
  143. * the client authenticates using a signed JWT assertion (RFC 7523 Section 2.2).
  144. *
  145. * @example
  146. * const provider = new PrivateKeyJwtProvider({
  147. * clientId: 'my-client',
  148. * privateKey: pemEncodedPrivateKey,
  149. * algorithm: 'RS256'
  150. * });
  151. *
  152. * const transport = new StreamableHTTPClientTransport(serverUrl, {
  153. * authProvider: provider
  154. * });
  155. */
  156. export class PrivateKeyJwtProvider {
  157. constructor(options) {
  158. this._clientInfo = {
  159. client_id: options.clientId
  160. };
  161. this._clientMetadata = {
  162. client_name: options.clientName ?? 'private-key-jwt-client',
  163. redirect_uris: [],
  164. grant_types: ['client_credentials'],
  165. token_endpoint_auth_method: 'private_key_jwt',
  166. scope: options.scope
  167. };
  168. this.addClientAuthentication = createPrivateKeyJwtAuth({
  169. issuer: options.clientId,
  170. subject: options.clientId,
  171. privateKey: options.privateKey,
  172. alg: options.algorithm,
  173. lifetimeSeconds: options.jwtLifetimeSeconds
  174. });
  175. }
  176. get redirectUrl() {
  177. return undefined;
  178. }
  179. get clientMetadata() {
  180. return this._clientMetadata;
  181. }
  182. clientInformation() {
  183. return this._clientInfo;
  184. }
  185. saveClientInformation(info) {
  186. this._clientInfo = info;
  187. }
  188. tokens() {
  189. return this._tokens;
  190. }
  191. saveTokens(tokens) {
  192. this._tokens = tokens;
  193. }
  194. redirectToAuthorization() {
  195. throw new Error('redirectToAuthorization is not used for client_credentials flow');
  196. }
  197. saveCodeVerifier() {
  198. // Not used for client_credentials
  199. }
  200. codeVerifier() {
  201. throw new Error('codeVerifier is not used for client_credentials flow');
  202. }
  203. prepareTokenRequest(scope) {
  204. const params = new URLSearchParams({ grant_type: 'client_credentials' });
  205. if (scope)
  206. params.set('scope', scope);
  207. return params;
  208. }
  209. }
  210. /**
  211. * OAuth provider for client_credentials grant with a static private_key_jwt assertion.
  212. *
  213. * This provider mirrors {@link PrivateKeyJwtProvider} but instead of constructing and
  214. * signing a JWT on each request, it accepts a pre-built JWT assertion string and
  215. * uses it directly for authentication.
  216. */
  217. export class StaticPrivateKeyJwtProvider {
  218. constructor(options) {
  219. this._clientInfo = {
  220. client_id: options.clientId
  221. };
  222. this._clientMetadata = {
  223. client_name: options.clientName ?? 'static-private-key-jwt-client',
  224. redirect_uris: [],
  225. grant_types: ['client_credentials'],
  226. token_endpoint_auth_method: 'private_key_jwt',
  227. scope: options.scope
  228. };
  229. const assertion = options.jwtBearerAssertion;
  230. this.addClientAuthentication = async (_headers, params) => {
  231. params.set('client_assertion', assertion);
  232. params.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
  233. };
  234. }
  235. get redirectUrl() {
  236. return undefined;
  237. }
  238. get clientMetadata() {
  239. return this._clientMetadata;
  240. }
  241. clientInformation() {
  242. return this._clientInfo;
  243. }
  244. saveClientInformation(info) {
  245. this._clientInfo = info;
  246. }
  247. tokens() {
  248. return this._tokens;
  249. }
  250. saveTokens(tokens) {
  251. this._tokens = tokens;
  252. }
  253. redirectToAuthorization() {
  254. throw new Error('redirectToAuthorization is not used for client_credentials flow');
  255. }
  256. saveCodeVerifier() {
  257. // Not used for client_credentials
  258. }
  259. codeVerifier() {
  260. throw new Error('codeVerifier is not used for client_credentials flow');
  261. }
  262. prepareTokenRequest(scope) {
  263. const params = new URLSearchParams({ grant_type: 'client_credentials' });
  264. if (scope)
  265. params.set('scope', scope);
  266. return params;
  267. }
  268. }
  269. //# sourceMappingURL=auth-extensions.js.map