| import pkceChallenge from 'pkce-challenge';
|
| import { LATEST_PROTOCOL_VERSION } from '../types.js';
|
| import { OAuthErrorResponseSchema, OpenIdProviderDiscoveryMetadataSchema } from '../shared/auth.js';
|
| import { OAuthClientInformationFullSchema, OAuthMetadataSchema, OAuthProtectedResourceMetadataSchema, OAuthTokensSchema } from '../shared/auth.js';
|
| import { checkResourceAllowed, resourceUrlFromServerUrl } from '../shared/auth-utils.js';
|
| import { InvalidClientError, InvalidClientMetadataError, InvalidGrantError, OAUTH_ERRORS, OAuthError, ServerError, UnauthorizedClientError } from '../server/auth/errors.js';
|
| export class UnauthorizedError extends Error {
|
| constructor(message) {
|
| super(message ?? 'Unauthorized');
|
| }
|
| }
|
| function isClientAuthMethod(method) {
|
| return ['client_secret_basic', 'client_secret_post', 'none'].includes(method);
|
| }
|
| const AUTHORIZATION_CODE_RESPONSE_TYPE = 'code';
|
| const AUTHORIZATION_CODE_CHALLENGE_METHOD = 'S256';
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export function selectClientAuthMethod(clientInformation, supportedMethods) {
|
| const hasClientSecret = clientInformation.client_secret !== undefined;
|
|
|
|
|
|
|
| if ('token_endpoint_auth_method' in clientInformation &&
|
| clientInformation.token_endpoint_auth_method &&
|
| isClientAuthMethod(clientInformation.token_endpoint_auth_method) &&
|
| (supportedMethods.length === 0 || supportedMethods.includes(clientInformation.token_endpoint_auth_method))) {
|
| return clientInformation.token_endpoint_auth_method;
|
| }
|
|
|
|
|
|
|
| if (supportedMethods.length === 0) {
|
| return hasClientSecret ? 'client_secret_basic' : 'none';
|
| }
|
|
|
| if (hasClientSecret && supportedMethods.includes('client_secret_basic')) {
|
| return 'client_secret_basic';
|
| }
|
| if (hasClientSecret && supportedMethods.includes('client_secret_post')) {
|
| return 'client_secret_post';
|
| }
|
| if (supportedMethods.includes('none')) {
|
| return 'none';
|
| }
|
|
|
| return hasClientSecret ? 'client_secret_post' : 'none';
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| function applyClientAuthentication(method, clientInformation, headers, params) {
|
| const { client_id, client_secret } = clientInformation;
|
| switch (method) {
|
| case 'client_secret_basic':
|
| applyBasicAuth(client_id, client_secret, headers);
|
| return;
|
| case 'client_secret_post':
|
| applyPostAuth(client_id, client_secret, params);
|
| return;
|
| case 'none':
|
| applyPublicAuth(client_id, params);
|
| return;
|
| default:
|
| throw new Error(`Unsupported client authentication method: ${method}`);
|
| }
|
| }
|
| |
| |
|
|
| function applyBasicAuth(clientId, clientSecret, headers) {
|
| if (!clientSecret) {
|
| throw new Error('client_secret_basic authentication requires a client_secret');
|
| }
|
| const credentials = btoa(`${clientId}:${clientSecret}`);
|
| headers.set('Authorization', `Basic ${credentials}`);
|
| }
|
| |
| |
|
|
| function applyPostAuth(clientId, clientSecret, params) {
|
| params.set('client_id', clientId);
|
| if (clientSecret) {
|
| params.set('client_secret', clientSecret);
|
| }
|
| }
|
| |
| |
|
|
| function applyPublicAuth(clientId, params) {
|
| params.set('client_id', clientId);
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export async function parseErrorResponse(input) {
|
| const statusCode = input instanceof Response ? input.status : undefined;
|
| const body = input instanceof Response ? await input.text() : input;
|
| try {
|
| const result = OAuthErrorResponseSchema.parse(JSON.parse(body));
|
| const { error, error_description, error_uri } = result;
|
| const errorClass = OAUTH_ERRORS[error] || ServerError;
|
| return new errorClass(error_description || '', error_uri);
|
| }
|
| catch (error) {
|
|
|
| const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ''}Invalid OAuth error response: ${error}. Raw body: ${body}`;
|
| return new ServerError(errorMessage);
|
| }
|
| }
|
| |
| |
| |
| |
| |
|
|
| export async function auth(provider, options) {
|
| try {
|
| return await authInternal(provider, options);
|
| }
|
| catch (error) {
|
|
|
| if (error instanceof InvalidClientError || error instanceof UnauthorizedClientError) {
|
| await provider.invalidateCredentials?.('all');
|
| return await authInternal(provider, options);
|
| }
|
| else if (error instanceof InvalidGrantError) {
|
| await provider.invalidateCredentials?.('tokens');
|
| return await authInternal(provider, options);
|
| }
|
|
|
| throw error;
|
| }
|
| }
|
| async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
|
|
|
| const cachedState = await provider.discoveryState?.();
|
| let resourceMetadata;
|
| let authorizationServerUrl;
|
| let metadata;
|
|
|
|
|
| let effectiveResourceMetadataUrl = resourceMetadataUrl;
|
| if (!effectiveResourceMetadataUrl && cachedState?.resourceMetadataUrl) {
|
| effectiveResourceMetadataUrl = new URL(cachedState.resourceMetadataUrl);
|
| }
|
| if (cachedState?.authorizationServerUrl) {
|
|
|
| authorizationServerUrl = cachedState.authorizationServerUrl;
|
| resourceMetadata = cachedState.resourceMetadata;
|
| metadata =
|
| cachedState.authorizationServerMetadata ?? (await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn }));
|
|
|
| if (!resourceMetadata) {
|
| try {
|
| resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn);
|
| }
|
| catch {
|
|
|
| }
|
| }
|
|
|
| if (metadata !== cachedState.authorizationServerMetadata || resourceMetadata !== cachedState.resourceMetadata) {
|
| await provider.saveDiscoveryState?.({
|
| authorizationServerUrl: String(authorizationServerUrl),
|
| resourceMetadataUrl: effectiveResourceMetadataUrl?.toString(),
|
| resourceMetadata,
|
| authorizationServerMetadata: metadata
|
| });
|
| }
|
| }
|
| else {
|
|
|
| const serverInfo = await discoverOAuthServerInfo(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl, fetchFn });
|
| authorizationServerUrl = serverInfo.authorizationServerUrl;
|
| metadata = serverInfo.authorizationServerMetadata;
|
| resourceMetadata = serverInfo.resourceMetadata;
|
|
|
|
|
|
|
|
|
| await provider.saveDiscoveryState?.({
|
| authorizationServerUrl: String(authorizationServerUrl),
|
| resourceMetadataUrl: effectiveResourceMetadataUrl?.toString(),
|
| resourceMetadata,
|
| authorizationServerMetadata: metadata
|
| });
|
| }
|
| const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
|
|
|
|
|
|
|
|
|
|
|
| const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
|
|
|
| let clientInformation = await Promise.resolve(provider.clientInformation());
|
| if (!clientInformation) {
|
| if (authorizationCode !== undefined) {
|
| throw new Error('Existing OAuth client information is required when exchanging an authorization code');
|
| }
|
| const supportsUrlBasedClientId = metadata?.client_id_metadata_document_supported === true;
|
| const clientMetadataUrl = provider.clientMetadataUrl;
|
| if (clientMetadataUrl && !isHttpsUrl(clientMetadataUrl)) {
|
| throw new InvalidClientMetadataError(`clientMetadataUrl must be a valid HTTPS URL with a non-root pathname, got: ${clientMetadataUrl}`);
|
| }
|
| const shouldUseUrlBasedClientId = supportsUrlBasedClientId && clientMetadataUrl;
|
| if (shouldUseUrlBasedClientId) {
|
|
|
| clientInformation = {
|
| client_id: clientMetadataUrl
|
| };
|
| await provider.saveClientInformation?.(clientInformation);
|
| }
|
| else {
|
|
|
| if (!provider.saveClientInformation) {
|
| throw new Error('OAuth client information must be saveable for dynamic registration');
|
| }
|
| const fullInformation = await registerClient(authorizationServerUrl, {
|
| metadata,
|
| clientMetadata: provider.clientMetadata,
|
| scope: resolvedScope,
|
| fetchFn
|
| });
|
| await provider.saveClientInformation(fullInformation);
|
| clientInformation = fullInformation;
|
| }
|
| }
|
|
|
| const nonInteractiveFlow = !provider.redirectUrl;
|
|
|
| if (authorizationCode !== undefined || nonInteractiveFlow) {
|
| const tokens = await fetchToken(provider, authorizationServerUrl, {
|
| metadata,
|
| resource,
|
| authorizationCode,
|
| fetchFn
|
| });
|
| await provider.saveTokens(tokens);
|
| return 'AUTHORIZED';
|
| }
|
| const tokens = await provider.tokens();
|
|
|
| if (tokens?.refresh_token) {
|
| try {
|
|
|
| const newTokens = await refreshAuthorization(authorizationServerUrl, {
|
| metadata,
|
| clientInformation,
|
| refreshToken: tokens.refresh_token,
|
| resource,
|
| addClientAuthentication: provider.addClientAuthentication,
|
| fetchFn
|
| });
|
| await provider.saveTokens(newTokens);
|
| return 'AUTHORIZED';
|
| }
|
| catch (error) {
|
|
|
| if (!(error instanceof OAuthError) || error instanceof ServerError) {
|
|
|
| }
|
| else {
|
|
|
| throw error;
|
| }
|
| }
|
| }
|
| const state = provider.state ? await provider.state() : undefined;
|
|
|
| const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, {
|
| metadata,
|
| clientInformation,
|
| state,
|
| redirectUrl: provider.redirectUrl,
|
| scope: resolvedScope,
|
| resource
|
| });
|
| await provider.saveCodeVerifier(codeVerifier);
|
| await provider.redirectToAuthorization(authorizationUrl);
|
| return 'REDIRECT';
|
| }
|
| |
| |
| |
|
|
| export function isHttpsUrl(value) {
|
| if (!value)
|
| return false;
|
| try {
|
| const url = new URL(value);
|
| return url.protocol === 'https:' && url.pathname !== '/';
|
| }
|
| catch {
|
| return false;
|
| }
|
| }
|
| export async function selectResourceURL(serverUrl, provider, resourceMetadata) {
|
| const defaultResource = resourceUrlFromServerUrl(serverUrl);
|
|
|
| if (provider.validateResourceURL) {
|
| return await provider.validateResourceURL(defaultResource, resourceMetadata?.resource);
|
| }
|
|
|
| if (!resourceMetadata) {
|
| return undefined;
|
| }
|
|
|
| if (!checkResourceAllowed({ requestedResource: defaultResource, configuredResource: resourceMetadata.resource })) {
|
| throw new Error(`Protected resource ${resourceMetadata.resource} does not match expected ${defaultResource} (or origin)`);
|
| }
|
|
|
| return new URL(resourceMetadata.resource);
|
| }
|
| |
| |
|
|
| export function extractWWWAuthenticateParams(res) {
|
| const authenticateHeader = res.headers.get('WWW-Authenticate');
|
| if (!authenticateHeader) {
|
| return {};
|
| }
|
| const [type, scheme] = authenticateHeader.split(' ');
|
| if (type.toLowerCase() !== 'bearer' || !scheme) {
|
| return {};
|
| }
|
| const resourceMetadataMatch = extractFieldFromWwwAuth(res, 'resource_metadata') || undefined;
|
| let resourceMetadataUrl;
|
| if (resourceMetadataMatch) {
|
| try {
|
| resourceMetadataUrl = new URL(resourceMetadataMatch);
|
| }
|
| catch {
|
|
|
| }
|
| }
|
| const scope = extractFieldFromWwwAuth(res, 'scope') || undefined;
|
| const error = extractFieldFromWwwAuth(res, 'error') || undefined;
|
| return {
|
| resourceMetadataUrl,
|
| scope,
|
| error
|
| };
|
| }
|
| |
| |
| |
| |
| |
| |
|
|
| function extractFieldFromWwwAuth(response, fieldName) {
|
| const wwwAuthHeader = response.headers.get('WWW-Authenticate');
|
| if (!wwwAuthHeader) {
|
| return null;
|
| }
|
| const pattern = new RegExp(`${fieldName}=(?:"([^"]+)"|([^\\s,]+))`);
|
| const match = wwwAuthHeader.match(pattern);
|
| if (match) {
|
|
|
| return match[1] || match[2];
|
| }
|
| return null;
|
| }
|
| |
| |
| |
|
|
| export function extractResourceMetadataUrl(res) {
|
| const authenticateHeader = res.headers.get('WWW-Authenticate');
|
| if (!authenticateHeader) {
|
| return undefined;
|
| }
|
| const [type, scheme] = authenticateHeader.split(' ');
|
| if (type.toLowerCase() !== 'bearer' || !scheme) {
|
| return undefined;
|
| }
|
| const regex = /resource_metadata="([^"]*)"/;
|
| const match = regex.exec(authenticateHeader);
|
| if (!match) {
|
| return undefined;
|
| }
|
| try {
|
| return new URL(match[1]);
|
| }
|
| catch {
|
| return undefined;
|
| }
|
| }
|
| |
| |
| |
| |
| |
|
|
| export async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn = fetch) {
|
| const response = await discoverMetadataWithFallback(serverUrl, 'oauth-protected-resource', fetchFn, {
|
| protocolVersion: opts?.protocolVersion,
|
| metadataUrl: opts?.resourceMetadataUrl
|
| });
|
| if (!response || response.status === 404) {
|
| await response?.body?.cancel();
|
| throw new Error(`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`);
|
| }
|
| if (!response.ok) {
|
| await response.body?.cancel();
|
| throw new Error(`HTTP ${response.status} trying to load well-known OAuth protected resource metadata.`);
|
| }
|
| return OAuthProtectedResourceMetadataSchema.parse(await response.json());
|
| }
|
| |
| |
|
|
| async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
|
| try {
|
| return await fetchFn(url, { headers });
|
| }
|
| catch (error) {
|
| if (error instanceof TypeError) {
|
| if (headers) {
|
|
|
| return fetchWithCorsRetry(url, undefined, fetchFn);
|
| }
|
| else {
|
|
|
| return undefined;
|
| }
|
| }
|
| throw error;
|
| }
|
| }
|
| |
| |
|
|
| function buildWellKnownPath(wellKnownPrefix, pathname = '', options = {}) {
|
|
|
| if (pathname.endsWith('/')) {
|
| pathname = pathname.slice(0, -1);
|
| }
|
| return options.prependPathname ? `${pathname}/.well-known/${wellKnownPrefix}` : `/.well-known/${wellKnownPrefix}${pathname}`;
|
| }
|
| |
| |
|
|
| async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
|
| const headers = {
|
| 'MCP-Protocol-Version': protocolVersion
|
| };
|
| return await fetchWithCorsRetry(url, headers, fetchFn);
|
| }
|
| |
| |
|
|
| function shouldAttemptFallback(response, pathname) {
|
| return !response || (response.status >= 400 && response.status < 500 && pathname !== '/');
|
| }
|
| |
| |
|
|
| async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, opts) {
|
| const issuer = new URL(serverUrl);
|
| const protocolVersion = opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION;
|
| let url;
|
| if (opts?.metadataUrl) {
|
| url = new URL(opts.metadataUrl);
|
| }
|
| else {
|
|
|
| const wellKnownPath = buildWellKnownPath(wellKnownType, issuer.pathname);
|
| url = new URL(wellKnownPath, opts?.metadataServerUrl ?? issuer);
|
| url.search = issuer.search;
|
| }
|
| let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
|
|
|
| if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) {
|
| const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
|
| response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn);
|
| }
|
| return response;
|
| }
|
| |
| |
| |
| |
| |
| |
| |
|
|
| export async function discoverOAuthMetadata(issuer, { authorizationServerUrl, protocolVersion } = {}, fetchFn = fetch) {
|
| if (typeof issuer === 'string') {
|
| issuer = new URL(issuer);
|
| }
|
| if (!authorizationServerUrl) {
|
| authorizationServerUrl = issuer;
|
| }
|
| if (typeof authorizationServerUrl === 'string') {
|
| authorizationServerUrl = new URL(authorizationServerUrl);
|
| }
|
| protocolVersion ?? (protocolVersion = LATEST_PROTOCOL_VERSION);
|
| const response = await discoverMetadataWithFallback(authorizationServerUrl, 'oauth-authorization-server', fetchFn, {
|
| protocolVersion,
|
| metadataServerUrl: authorizationServerUrl
|
| });
|
| if (!response || response.status === 404) {
|
| await response?.body?.cancel();
|
| return undefined;
|
| }
|
| if (!response.ok) {
|
| await response.body?.cancel();
|
| throw new Error(`HTTP ${response.status} trying to load well-known OAuth metadata`);
|
| }
|
| return OAuthMetadataSchema.parse(await response.json());
|
| }
|
| |
| |
| |
| |
| |
|
|
| export function buildDiscoveryUrls(authorizationServerUrl) {
|
| const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
|
| const hasPath = url.pathname !== '/';
|
| const urlsToTry = [];
|
| if (!hasPath) {
|
|
|
| urlsToTry.push({
|
| url: new URL('/.well-known/oauth-authorization-server', url.origin),
|
| type: 'oauth'
|
| });
|
|
|
| urlsToTry.push({
|
| url: new URL(`/.well-known/openid-configuration`, url.origin),
|
| type: 'oidc'
|
| });
|
| return urlsToTry;
|
| }
|
|
|
| let pathname = url.pathname;
|
| if (pathname.endsWith('/')) {
|
| pathname = pathname.slice(0, -1);
|
| }
|
|
|
|
|
| urlsToTry.push({
|
| url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin),
|
| type: 'oauth'
|
| });
|
|
|
|
|
| urlsToTry.push({
|
| url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
|
| type: 'oidc'
|
| });
|
|
|
| urlsToTry.push({
|
| url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin),
|
| type: 'oidc'
|
| });
|
| return urlsToTry;
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn = fetch, protocolVersion = LATEST_PROTOCOL_VERSION } = {}) {
|
| const headers = {
|
| 'MCP-Protocol-Version': protocolVersion,
|
| Accept: 'application/json'
|
| };
|
|
|
| const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
|
|
|
| for (const { url: endpointUrl, type } of urlsToTry) {
|
| const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
|
| if (!response) {
|
| |
| |
| |
|
|
| continue;
|
| }
|
| if (!response.ok) {
|
| await response.body?.cancel();
|
|
|
| if (response.status >= 400 && response.status < 500) {
|
| continue;
|
| }
|
| throw new Error(`HTTP ${response.status} trying to load ${type === 'oauth' ? 'OAuth' : 'OpenID provider'} metadata from ${endpointUrl}`);
|
| }
|
|
|
| if (type === 'oauth') {
|
| return OAuthMetadataSchema.parse(await response.json());
|
| }
|
| else {
|
| return OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
|
| }
|
| }
|
| return undefined;
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export async function discoverOAuthServerInfo(serverUrl, opts) {
|
| let resourceMetadata;
|
| let authorizationServerUrl;
|
| try {
|
| resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn);
|
| if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
|
| authorizationServerUrl = resourceMetadata.authorization_servers[0];
|
| }
|
| }
|
| catch {
|
|
|
| }
|
|
|
|
|
| if (!authorizationServerUrl) {
|
| authorizationServerUrl = String(new URL('/', serverUrl));
|
| }
|
| const authorizationServerMetadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn: opts?.fetchFn });
|
| return {
|
| authorizationServerUrl,
|
| authorizationServerMetadata,
|
| resourceMetadata
|
| };
|
| }
|
| |
| |
|
|
| export async function startAuthorization(authorizationServerUrl, { metadata, clientInformation, redirectUrl, scope, state, resource }) {
|
| let authorizationUrl;
|
| if (metadata) {
|
| authorizationUrl = new URL(metadata.authorization_endpoint);
|
| if (!metadata.response_types_supported.includes(AUTHORIZATION_CODE_RESPONSE_TYPE)) {
|
| throw new Error(`Incompatible auth server: does not support response type ${AUTHORIZATION_CODE_RESPONSE_TYPE}`);
|
| }
|
| if (metadata.code_challenge_methods_supported &&
|
| !metadata.code_challenge_methods_supported.includes(AUTHORIZATION_CODE_CHALLENGE_METHOD)) {
|
| throw new Error(`Incompatible auth server: does not support code challenge method ${AUTHORIZATION_CODE_CHALLENGE_METHOD}`);
|
| }
|
| }
|
| else {
|
| authorizationUrl = new URL('/authorize', authorizationServerUrl);
|
| }
|
|
|
| const challenge = await pkceChallenge();
|
| const codeVerifier = challenge.code_verifier;
|
| const codeChallenge = challenge.code_challenge;
|
| authorizationUrl.searchParams.set('response_type', AUTHORIZATION_CODE_RESPONSE_TYPE);
|
| authorizationUrl.searchParams.set('client_id', clientInformation.client_id);
|
| authorizationUrl.searchParams.set('code_challenge', codeChallenge);
|
| authorizationUrl.searchParams.set('code_challenge_method', AUTHORIZATION_CODE_CHALLENGE_METHOD);
|
| authorizationUrl.searchParams.set('redirect_uri', String(redirectUrl));
|
| if (state) {
|
| authorizationUrl.searchParams.set('state', state);
|
| }
|
| if (scope) {
|
| authorizationUrl.searchParams.set('scope', scope);
|
| }
|
| if (scope?.includes('offline_access')) {
|
|
|
|
|
|
|
| authorizationUrl.searchParams.append('prompt', 'consent');
|
| }
|
| if (resource) {
|
| authorizationUrl.searchParams.set('resource', resource.href);
|
| }
|
| return { authorizationUrl, codeVerifier };
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export function prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, redirectUri) {
|
| return new URLSearchParams({
|
| grant_type: 'authorization_code',
|
| code: authorizationCode,
|
| code_verifier: codeVerifier,
|
| redirect_uri: String(redirectUri)
|
| });
|
| }
|
| |
| |
| |
|
|
| async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams, clientInformation, addClientAuthentication, resource, fetchFn }) {
|
| const tokenUrl = metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL('/token', authorizationServerUrl);
|
| const headers = new Headers({
|
| 'Content-Type': 'application/x-www-form-urlencoded',
|
| Accept: 'application/json'
|
| });
|
| if (resource) {
|
| tokenRequestParams.set('resource', resource.href);
|
| }
|
| if (addClientAuthentication) {
|
| await addClientAuthentication(headers, tokenRequestParams, tokenUrl, metadata);
|
| }
|
| else if (clientInformation) {
|
| const supportedMethods = metadata?.token_endpoint_auth_methods_supported ?? [];
|
| const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
|
| applyClientAuthentication(authMethod, clientInformation, headers, tokenRequestParams);
|
| }
|
| const response = await (fetchFn ?? fetch)(tokenUrl, {
|
| method: 'POST',
|
| headers,
|
| body: tokenRequestParams
|
| });
|
| if (!response.ok) {
|
| throw await parseErrorResponse(response);
|
| }
|
| return OAuthTokensSchema.parse(await response.json());
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export async function exchangeAuthorization(authorizationServerUrl, { metadata, clientInformation, authorizationCode, codeVerifier, redirectUri, resource, addClientAuthentication, fetchFn }) {
|
| const tokenRequestParams = prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, redirectUri);
|
| return executeTokenRequest(authorizationServerUrl, {
|
| metadata,
|
| tokenRequestParams,
|
| clientInformation,
|
| addClientAuthentication,
|
| resource,
|
| fetchFn
|
| });
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn }) {
|
| const tokenRequestParams = new URLSearchParams({
|
| grant_type: 'refresh_token',
|
| refresh_token: refreshToken
|
| });
|
| const tokens = await executeTokenRequest(authorizationServerUrl, {
|
| metadata,
|
| tokenRequestParams,
|
| clientInformation,
|
| addClientAuthentication,
|
| resource,
|
| fetchFn
|
| });
|
|
|
| return { refresh_token: refreshToken, ...tokens };
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export async function fetchToken(provider, authorizationServerUrl, { metadata, resource, authorizationCode, fetchFn } = {}) {
|
| const scope = provider.clientMetadata.scope;
|
|
|
| let tokenRequestParams;
|
| if (provider.prepareTokenRequest) {
|
| tokenRequestParams = await provider.prepareTokenRequest(scope);
|
| }
|
|
|
| if (!tokenRequestParams) {
|
| if (!authorizationCode) {
|
| throw new Error('Either provider.prepareTokenRequest() or authorizationCode is required');
|
| }
|
| if (!provider.redirectUrl) {
|
| throw new Error('redirectUrl is required for authorization_code flow');
|
| }
|
| const codeVerifier = await provider.codeVerifier();
|
| tokenRequestParams = prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, provider.redirectUrl);
|
| }
|
| const clientInformation = await provider.clientInformation();
|
| return executeTokenRequest(authorizationServerUrl, {
|
| metadata,
|
| tokenRequestParams,
|
| clientInformation: clientInformation ?? undefined,
|
| addClientAuthentication: provider.addClientAuthentication,
|
| resource,
|
| fetchFn
|
| });
|
| }
|
| |
| |
| |
| |
| |
| |
|
|
| export async function registerClient(authorizationServerUrl, { metadata, clientMetadata, scope, fetchFn }) {
|
| let registrationUrl;
|
| if (metadata) {
|
| if (!metadata.registration_endpoint) {
|
| throw new Error('Incompatible auth server: does not support dynamic client registration');
|
| }
|
| registrationUrl = new URL(metadata.registration_endpoint);
|
| }
|
| else {
|
| registrationUrl = new URL('/register', authorizationServerUrl);
|
| }
|
| const response = await (fetchFn ?? fetch)(registrationUrl, {
|
| method: 'POST',
|
| headers: {
|
| 'Content-Type': 'application/json'
|
| },
|
| body: JSON.stringify({
|
| ...clientMetadata,
|
| ...(scope !== undefined ? { scope } : {})
|
| })
|
| });
|
| if (!response.ok) {
|
| throw await parseErrorResponse(response);
|
| }
|
| return OAuthClientInformationFullSchema.parse(await response.json());
|
| }
|
| |