Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Skipped
290 lines
10 KiB
Plaintext
290 lines
10 KiB
Plaintext
/**
|
|
* OAuth provider extensions for specialized authentication flows.
|
|
*
|
|
* This module provides ready-to-use OAuthClientProvider implementations
|
|
* for common machine-to-machine authentication scenarios.
|
|
*/
|
|
/**
|
|
* Helper to produce a private_key_jwt client authentication function.
|
|
*
|
|
* Usage:
|
|
* const addClientAuth = createPrivateKeyJwtAuth({ issuer, subject, privateKey, alg, audience? });
|
|
* // pass addClientAuth as provider.addClientAuthentication implementation
|
|
*/
|
|
export function createPrivateKeyJwtAuth(options) {
|
|
return async (_headers, params, url, metadata) => {
|
|
// Lazy import to avoid heavy dependency unless used
|
|
if (typeof globalThis.crypto === 'undefined') {
|
|
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)');
|
|
}
|
|
const jose = await import('jose');
|
|
const audience = String(options.audience ?? metadata?.issuer ?? url);
|
|
const lifetimeSeconds = options.lifetimeSeconds ?? 300;
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const jti = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
const baseClaims = {
|
|
iss: options.issuer,
|
|
sub: options.subject,
|
|
aud: audience,
|
|
exp: now + lifetimeSeconds,
|
|
iat: now,
|
|
jti
|
|
};
|
|
const claims = options.claims ? { ...baseClaims, ...options.claims } : baseClaims;
|
|
// Import key for the requested algorithm
|
|
const alg = options.alg;
|
|
let key;
|
|
if (typeof options.privateKey === 'string') {
|
|
if (alg.startsWith('RS') || alg.startsWith('ES') || alg.startsWith('PS')) {
|
|
key = await jose.importPKCS8(options.privateKey, alg);
|
|
}
|
|
else if (alg.startsWith('HS')) {
|
|
key = new TextEncoder().encode(options.privateKey);
|
|
}
|
|
else {
|
|
throw new Error(`Unsupported algorithm ${alg}`);
|
|
}
|
|
}
|
|
else if (options.privateKey instanceof Uint8Array) {
|
|
if (alg.startsWith('HS')) {
|
|
key = options.privateKey;
|
|
}
|
|
else {
|
|
// Assume PKCS#8 DER in Uint8Array for asymmetric algorithms
|
|
key = await jose.importPKCS8(new TextDecoder().decode(options.privateKey), alg);
|
|
}
|
|
}
|
|
else {
|
|
// Treat as JWK
|
|
key = await jose.importJWK(options.privateKey, alg);
|
|
}
|
|
// Sign JWT
|
|
const assertion = await new jose.SignJWT(claims)
|
|
.setProtectedHeader({ alg, typ: 'JWT' })
|
|
.setIssuer(options.issuer)
|
|
.setSubject(options.subject)
|
|
.setAudience(audience)
|
|
.setIssuedAt(now)
|
|
.setExpirationTime(now + lifetimeSeconds)
|
|
.setJti(jti)
|
|
.sign(key);
|
|
params.set('client_assertion', assertion);
|
|
params.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
|
|
};
|
|
}
|
|
/**
|
|
* The `issuer` stamp for constructor-supplied client information. Omitting `expectedIssuer` is
|
|
* deprecated: nothing else says which authorization server such credentials belong to.
|
|
*/
|
|
function checkedExpectedIssuer(expectedIssuer) {
|
|
if (expectedIssuer === undefined) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn('[mcp-sdk] Omitting `expectedIssuer` is deprecated. Without it, the MCP server decides which authorization ' +
|
|
"server receives this client's credentials; pass your authorization server's issuer URL as " +
|
|
'`expectedIssuer` so they are only sent there.');
|
|
}
|
|
else if (typeof expectedIssuer !== 'string' || !expectedIssuer) {
|
|
throw new Error("expectedIssuer must be the authorization server's issuer URL");
|
|
}
|
|
return expectedIssuer;
|
|
}
|
|
/**
|
|
* OAuth provider for client_credentials grant with client_secret_basic authentication.
|
|
*
|
|
* This provider is designed for machine-to-machine authentication where
|
|
* the client authenticates using a client_id and client_secret.
|
|
*
|
|
* @example
|
|
* const provider = new ClientCredentialsProvider({
|
|
* clientId: 'my-client',
|
|
* clientSecret: 'my-secret',
|
|
* expectedIssuer: 'https://auth.example.com'
|
|
* });
|
|
*
|
|
* const transport = new StreamableHTTPClientTransport(serverUrl, {
|
|
* authProvider: provider
|
|
* });
|
|
*/
|
|
export class ClientCredentialsProvider {
|
|
constructor(options) {
|
|
this._clientInfo = {
|
|
client_id: options.clientId,
|
|
client_secret: options.clientSecret,
|
|
issuer: checkedExpectedIssuer(options.expectedIssuer)
|
|
};
|
|
this._clientMetadata = {
|
|
client_name: options.clientName ?? 'client-credentials-client',
|
|
redirect_uris: [],
|
|
grant_types: ['client_credentials'],
|
|
token_endpoint_auth_method: 'client_secret_basic',
|
|
scope: options.scope
|
|
};
|
|
}
|
|
get redirectUrl() {
|
|
return undefined;
|
|
}
|
|
get clientMetadata() {
|
|
return this._clientMetadata;
|
|
}
|
|
clientInformation() {
|
|
return this._clientInfo;
|
|
}
|
|
saveClientInformation(info) {
|
|
this._clientInfo = info;
|
|
}
|
|
tokens() {
|
|
return this._tokens;
|
|
}
|
|
saveTokens(tokens) {
|
|
this._tokens = tokens;
|
|
}
|
|
redirectToAuthorization() {
|
|
throw new Error('redirectToAuthorization is not used for client_credentials flow');
|
|
}
|
|
saveCodeVerifier() {
|
|
// Not used for client_credentials
|
|
}
|
|
codeVerifier() {
|
|
throw new Error('codeVerifier is not used for client_credentials flow');
|
|
}
|
|
prepareTokenRequest(scope) {
|
|
const params = new URLSearchParams({ grant_type: 'client_credentials' });
|
|
if (scope)
|
|
params.set('scope', scope);
|
|
return params;
|
|
}
|
|
}
|
|
/**
|
|
* OAuth provider for client_credentials grant with private_key_jwt authentication.
|
|
*
|
|
* This provider is designed for machine-to-machine authentication where
|
|
* the client authenticates using a signed JWT assertion (RFC 7523 Section 2.2).
|
|
*
|
|
* @example
|
|
* const provider = new PrivateKeyJwtProvider({
|
|
* clientId: 'my-client',
|
|
* privateKey: pemEncodedPrivateKey,
|
|
* algorithm: 'RS256',
|
|
* expectedIssuer: 'https://auth.example.com'
|
|
* });
|
|
*
|
|
* const transport = new StreamableHTTPClientTransport(serverUrl, {
|
|
* authProvider: provider
|
|
* });
|
|
*/
|
|
export class PrivateKeyJwtProvider {
|
|
constructor(options) {
|
|
this._clientInfo = {
|
|
client_id: options.clientId,
|
|
issuer: checkedExpectedIssuer(options.expectedIssuer)
|
|
};
|
|
this._clientMetadata = {
|
|
client_name: options.clientName ?? 'private-key-jwt-client',
|
|
redirect_uris: [],
|
|
grant_types: ['client_credentials'],
|
|
token_endpoint_auth_method: 'private_key_jwt',
|
|
scope: options.scope
|
|
};
|
|
this.addClientAuthentication = createPrivateKeyJwtAuth({
|
|
issuer: options.clientId,
|
|
subject: options.clientId,
|
|
privateKey: options.privateKey,
|
|
alg: options.algorithm,
|
|
lifetimeSeconds: options.jwtLifetimeSeconds
|
|
});
|
|
}
|
|
get redirectUrl() {
|
|
return undefined;
|
|
}
|
|
get clientMetadata() {
|
|
return this._clientMetadata;
|
|
}
|
|
clientInformation() {
|
|
return this._clientInfo;
|
|
}
|
|
saveClientInformation(info) {
|
|
this._clientInfo = info;
|
|
}
|
|
tokens() {
|
|
return this._tokens;
|
|
}
|
|
saveTokens(tokens) {
|
|
this._tokens = tokens;
|
|
}
|
|
redirectToAuthorization() {
|
|
throw new Error('redirectToAuthorization is not used for client_credentials flow');
|
|
}
|
|
saveCodeVerifier() {
|
|
// Not used for client_credentials
|
|
}
|
|
codeVerifier() {
|
|
throw new Error('codeVerifier is not used for client_credentials flow');
|
|
}
|
|
prepareTokenRequest(scope) {
|
|
const params = new URLSearchParams({ grant_type: 'client_credentials' });
|
|
if (scope)
|
|
params.set('scope', scope);
|
|
return params;
|
|
}
|
|
}
|
|
/**
|
|
* OAuth provider for client_credentials grant with a static private_key_jwt assertion.
|
|
*
|
|
* This provider mirrors {@link PrivateKeyJwtProvider} but instead of constructing and
|
|
* signing a JWT on each request, it accepts a pre-built JWT assertion string and
|
|
* uses it directly for authentication.
|
|
*/
|
|
export class StaticPrivateKeyJwtProvider {
|
|
constructor(options) {
|
|
this._clientInfo = {
|
|
client_id: options.clientId,
|
|
issuer: checkedExpectedIssuer(options.expectedIssuer)
|
|
};
|
|
this._clientMetadata = {
|
|
client_name: options.clientName ?? 'static-private-key-jwt-client',
|
|
redirect_uris: [],
|
|
grant_types: ['client_credentials'],
|
|
token_endpoint_auth_method: 'private_key_jwt',
|
|
scope: options.scope
|
|
};
|
|
const assertion = options.jwtBearerAssertion;
|
|
this.addClientAuthentication = async (_headers, params) => {
|
|
params.set('client_assertion', assertion);
|
|
params.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
|
|
};
|
|
}
|
|
get redirectUrl() {
|
|
return undefined;
|
|
}
|
|
get clientMetadata() {
|
|
return this._clientMetadata;
|
|
}
|
|
clientInformation() {
|
|
return this._clientInfo;
|
|
}
|
|
saveClientInformation(info) {
|
|
this._clientInfo = info;
|
|
}
|
|
tokens() {
|
|
return this._tokens;
|
|
}
|
|
saveTokens(tokens) {
|
|
this._tokens = tokens;
|
|
}
|
|
redirectToAuthorization() {
|
|
throw new Error('redirectToAuthorization is not used for client_credentials flow');
|
|
}
|
|
saveCodeVerifier() {
|
|
// Not used for client_credentials
|
|
}
|
|
codeVerifier() {
|
|
throw new Error('codeVerifier is not used for client_credentials flow');
|
|
}
|
|
prepareTokenRequest(scope) {
|
|
const params = new URLSearchParams({ grant_type: 'client_credentials' });
|
|
if (scope)
|
|
params.set('scope', scope);
|
|
return params;
|
|
}
|
|
}
|
|
//# sourceMappingURL=auth-extensions.js.map |