feat(planning): grille hebdomadaire complète avec API et filtres

- Connexion API via proxy Angular (résolution CORS, base path /api)
- Import CSS ng-zorro global pour les modales et composants
- Filtres Camion/Show câblés sur l'affichage de la grille
- Camions affichés via TrucksService (linkés au show du même créneau)
- Panneau de détails : spectacles + camions du jour sélectionné
- Modale de création de spectacle stylisée avec fond et centrage
- Positionnement précis des events à la minute dans leur créneau
- Auto-scroll vers l'heure courante au chargement
- Ligne "maintenant" sur la colonne du jour actuel
- Régénération des services OpenAPI (nouveaux noms de types)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 20:36:03 +02:00
parent 150b97cd2e
commit 654b297e2e
3131 changed files with 149304 additions and 104334 deletions
+202 -327
View File
@@ -1,19 +1,14 @@
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';
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, InvalidGrantError, OAUTH_ERRORS, OAuthError, ServerError, UnauthorizedClientError } from "../server/auth/errors.js";
export class UnauthorizedError extends Error {
constructor(message) {
super(message ?? 'Unauthorized');
super(message !== null && message !== void 0 ? 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';
/**
* Determines the best client authentication method to use based on server support and client configuration.
*
@@ -26,31 +21,24 @@ const AUTHORIZATION_CODE_CHALLENGE_METHOD = 'S256';
* @param supportedMethods - Authentication methods supported by the authorization server
* @returns The selected authentication method
*/
export function selectClientAuthMethod(clientInformation, supportedMethods) {
function selectClientAuthMethod(clientInformation, supportedMethods) {
const hasClientSecret = clientInformation.client_secret !== undefined;
// If server doesn't specify supported methods, use RFC 6749 defaults
if (supportedMethods.length === 0) {
return hasClientSecret ? 'client_secret_post' : 'none';
}
// Prefer the method returned by the server during client registration if valid and supported
if ('token_endpoint_auth_method' in clientInformation &&
clientInformation.token_endpoint_auth_method &&
isClientAuthMethod(clientInformation.token_endpoint_auth_method) &&
supportedMethods.includes(clientInformation.token_endpoint_auth_method)) {
return clientInformation.token_endpoint_auth_method;
return hasClientSecret ? "client_secret_post" : "none";
}
// Try methods in priority order (most secure first)
if (hasClientSecret && supportedMethods.includes('client_secret_basic')) {
return 'client_secret_basic';
if (hasClientSecret && supportedMethods.includes("client_secret_basic")) {
return "client_secret_basic";
}
if (hasClientSecret && supportedMethods.includes('client_secret_post')) {
return 'client_secret_post';
if (hasClientSecret && supportedMethods.includes("client_secret_post")) {
return "client_secret_post";
}
if (supportedMethods.includes('none')) {
return 'none';
if (supportedMethods.includes("none")) {
return "none";
}
// Fallback: use what we have
return hasClientSecret ? 'client_secret_post' : 'none';
return hasClientSecret ? "client_secret_post" : "none";
}
/**
* Applies client authentication to the request based on the specified method.
@@ -69,13 +57,13 @@ export function selectClientAuthMethod(clientInformation, supportedMethods) {
function applyClientAuthentication(method, clientInformation, headers, params) {
const { client_id, client_secret } = clientInformation;
switch (method) {
case 'client_secret_basic':
case "client_secret_basic":
applyBasicAuth(client_id, client_secret, headers);
return;
case 'client_secret_post':
case "client_secret_post":
applyPostAuth(client_id, client_secret, params);
return;
case 'none':
case "none":
applyPublicAuth(client_id, params);
return;
default:
@@ -87,25 +75,25 @@ function applyClientAuthentication(method, clientInformation, headers, params) {
*/
function applyBasicAuth(clientId, clientSecret, headers) {
if (!clientSecret) {
throw new Error('client_secret_basic authentication requires a client_secret');
throw new Error("client_secret_basic authentication requires a client_secret");
}
const credentials = btoa(`${clientId}:${clientSecret}`);
headers.set('Authorization', `Basic ${credentials}`);
headers.set("Authorization", `Basic ${credentials}`);
}
/**
* Applies POST body authentication (RFC 6749 Section 2.3.1)
*/
function applyPostAuth(clientId, clientSecret, params) {
params.set('client_id', clientId);
params.set("client_id", clientId);
if (clientSecret) {
params.set('client_secret', clientSecret);
params.set("client_secret", clientSecret);
}
}
/**
* Applies public client authentication (RFC 6749 Section 2.1)
*/
function applyPublicAuth(clientId, params) {
params.set('client_id', clientId);
params.set("client_id", clientId);
}
/**
* Parses an OAuth error response from a string or Response object.
@@ -140,24 +128,25 @@ export async function parseErrorResponse(input) {
* instead of linking together the other lower-level functions in this module.
*/
export async function auth(provider, options) {
var _a, _b;
try {
return await authInternal(provider, options);
}
catch (error) {
// Handle recoverable error types by invalidating credentials and retrying
if (error instanceof InvalidClientError || error instanceof UnauthorizedClientError) {
await provider.invalidateCredentials?.('all');
await ((_a = provider.invalidateCredentials) === null || _a === void 0 ? void 0 : _a.call(provider, 'all'));
return await authInternal(provider, options);
}
else if (error instanceof InvalidGrantError) {
await provider.invalidateCredentials?.('tokens');
await ((_b = provider.invalidateCredentials) === null || _b === void 0 ? void 0 : _b.call(provider, 'tokens'));
return await authInternal(provider, options);
}
// Throw otherwise
throw error;
}
}
async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn, }) {
let resourceMetadata;
let authorizationServerUrl;
try {
@@ -166,69 +155,56 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
authorizationServerUrl = resourceMetadata.authorization_servers[0];
}
}
catch {
catch (_a) {
// Ignore errors and fall back to /.well-known/oauth-authorization-server
}
/**
* If we don't get a valid authorization server metadata from protected resource metadata,
* fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server base URL acts as the Authorization server.
* fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server.
*/
if (!authorizationServerUrl) {
authorizationServerUrl = new URL('/', serverUrl);
authorizationServerUrl = serverUrl;
}
const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
const metadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, {
fetchFn
fetchFn,
});
// Handle client registration if needed
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');
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}`);
if (!provider.saveClientInformation) {
throw new Error("OAuth client information must be saveable for dynamic registration");
}
const shouldUseUrlBasedClientId = supportsUrlBasedClientId && clientMetadataUrl;
if (shouldUseUrlBasedClientId) {
// SEP-991: URL-based Client IDs
clientInformation = {
client_id: clientMetadataUrl
};
await provider.saveClientInformation?.(clientInformation);
}
else {
// Fallback to dynamic registration
if (!provider.saveClientInformation) {
throw new Error('OAuth client information must be saveable for dynamic registration');
}
const fullInformation = await registerClient(authorizationServerUrl, {
metadata,
clientMetadata: provider.clientMetadata,
fetchFn
});
await provider.saveClientInformation(fullInformation);
clientInformation = fullInformation;
}
}
// Non-interactive flows (e.g., client_credentials, jwt-bearer) don't need a redirect URL
const nonInteractiveFlow = !provider.redirectUrl;
// Exchange authorization code for tokens, or fetch tokens directly for non-interactive flows
if (authorizationCode !== undefined || nonInteractiveFlow) {
const tokens = await fetchToken(provider, authorizationServerUrl, {
const fullInformation = await registerClient(authorizationServerUrl, {
metadata,
resource,
clientMetadata: provider.clientMetadata,
fetchFn,
});
await provider.saveClientInformation(fullInformation);
clientInformation = fullInformation;
}
// Exchange authorization code for tokens
if (authorizationCode !== undefined) {
const codeVerifier = await provider.codeVerifier();
const tokens = await exchangeAuthorization(authorizationServerUrl, {
metadata,
clientInformation,
authorizationCode,
fetchFn
codeVerifier,
redirectUri: provider.redirectUrl,
resource,
addClientAuthentication: provider.addClientAuthentication,
fetchFn: fetchFn,
});
await provider.saveTokens(tokens);
return 'AUTHORIZED';
return "AUTHORIZED";
}
const tokens = await provider.tokens();
// Handle token refresh or new authorization
if (tokens?.refresh_token) {
if (tokens === null || tokens === void 0 ? void 0 : tokens.refresh_token) {
try {
// Attempt to refresh the token
const newTokens = await refreshAuthorization(authorizationServerUrl, {
@@ -237,10 +213,10 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
refreshToken: tokens.refresh_token,
resource,
addClientAuthentication: provider.addClientAuthentication,
fetchFn
fetchFn,
});
await provider.saveTokens(newTokens);
return 'AUTHORIZED';
return "AUTHORIZED";
}
catch (error) {
// If this is a ServerError, or an unknown type, log it out and try to continue. Otherwise, escalate so we can fix things and retry.
@@ -260,33 +236,18 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
clientInformation,
state,
redirectUrl: provider.redirectUrl,
scope: scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope,
resource
scope: scope || provider.clientMetadata.scope,
resource,
});
await provider.saveCodeVerifier(codeVerifier);
await provider.redirectToAuthorization(authorizationUrl);
return 'REDIRECT';
}
/**
* SEP-991: URL-based Client IDs
* Validate that the client_id is a valid URL with https scheme
*/
export function isHttpsUrl(value) {
if (!value)
return false;
try {
const url = new URL(value);
return url.protocol === 'https:' && url.pathname !== '/';
}
catch {
return false;
}
return "REDIRECT";
}
export async function selectResourceURL(serverUrl, provider, resourceMetadata) {
const defaultResource = resourceUrlFromServerUrl(serverUrl);
// If provider has custom validation, delegate to it
if (provider.validateResourceURL) {
return await provider.validateResourceURL(defaultResource, resourceMetadata?.resource);
return await provider.validateResourceURL(defaultResource, resourceMetadata === null || resourceMetadata === void 0 ? void 0 : resourceMetadata.resource);
}
// Only include resource parameter when Protected Resource Metadata is present
if (!resourceMetadata) {
@@ -299,62 +260,11 @@ export async function selectResourceURL(serverUrl, provider, resourceMetadata) {
// Prefer the resource from metadata since it's what the server is telling us to request
return new URL(resourceMetadata.resource);
}
/**
* Extract resource_metadata, scope, and error from WWW-Authenticate header.
*/
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 {
// Ignore invalid URL
}
}
const scope = extractFieldFromWwwAuth(res, 'scope') || undefined;
const error = extractFieldFromWwwAuth(res, 'error') || undefined;
return {
resourceMetadataUrl,
scope,
error
};
}
/**
* Extracts a specific field's value from the WWW-Authenticate header string.
*
* @param response The HTTP response object containing the headers.
* @param fieldName The name of the field to extract (e.g., "realm", "nonce").
* @returns The field value
*/
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) {
// Pattern matches: field_name="value" or field_name=value (unquoted)
return match[1] || match[2];
}
return null;
}
/**
* Extract resource_metadata from response header.
* @deprecated Use `extractWWWAuthenticateParams` instead.
*/
export function extractResourceMetadataUrl(res) {
const authenticateHeader = res.headers.get('WWW-Authenticate');
const authenticateHeader = res.headers.get("WWW-Authenticate");
if (!authenticateHeader) {
return undefined;
}
@@ -370,7 +280,7 @@ export function extractResourceMetadataUrl(res) {
try {
return new URL(match[1]);
}
catch {
catch (_a) {
return undefined;
}
}
@@ -382,15 +292,13 @@ export function extractResourceMetadataUrl(res) {
*/
export async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn = fetch) {
const response = await discoverMetadataWithFallback(serverUrl, 'oauth-protected-resource', fetchFn, {
protocolVersion: opts?.protocolVersion,
metadataUrl: opts?.resourceMetadataUrl
protocolVersion: opts === null || opts === void 0 ? void 0 : opts.protocolVersion,
metadataUrl: opts === null || opts === void 0 ? void 0 : 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());
@@ -424,14 +332,16 @@ function buildWellKnownPath(wellKnownPrefix, pathname = '', options = {}) {
if (pathname.endsWith('/')) {
pathname = pathname.slice(0, -1);
}
return options.prependPathname ? `${pathname}/.well-known/${wellKnownPrefix}` : `/.well-known/${wellKnownPrefix}${pathname}`;
return options.prependPathname
? `${pathname}/.well-known/${wellKnownPrefix}`
: `/.well-known/${wellKnownPrefix}${pathname}`;
}
/**
* Tries to discover OAuth metadata at a specific URL
*/
async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
const headers = {
'MCP-Protocol-Version': protocolVersion
"MCP-Protocol-Version": protocolVersion
};
return await fetchWithCorsRetry(url, headers, fetchFn);
}
@@ -439,27 +349,28 @@ async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
* Determines if fallback to root discovery should be attempted
*/
function shouldAttemptFallback(response, pathname) {
return !response || (response.status >= 400 && response.status < 500 && pathname !== '/');
return !response || response.status === 404 && pathname !== '/';
}
/**
* Generic function for discovering OAuth metadata with fallback support
*/
async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, opts) {
var _a, _b;
const issuer = new URL(serverUrl);
const protocolVersion = opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION;
const protocolVersion = (_a = opts === null || opts === void 0 ? void 0 : opts.protocolVersion) !== null && _a !== void 0 ? _a : LATEST_PROTOCOL_VERSION;
let url;
if (opts?.metadataUrl) {
if (opts === null || opts === void 0 ? void 0 : opts.metadataUrl) {
url = new URL(opts.metadataUrl);
}
else {
// Try path-aware discovery first
const wellKnownPath = buildWellKnownPath(wellKnownType, issuer.pathname);
url = new URL(wellKnownPath, opts?.metadataServerUrl ?? issuer);
url = new URL(wellKnownPath, (_b = opts === null || opts === void 0 ? void 0 : opts.metadataServerUrl) !== null && _b !== void 0 ? _b : issuer);
url.search = issuer.search;
}
let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
// If path-aware discovery fails with 404 and we're not already at root, try fallback to root discovery
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) {
if (!(opts === null || opts === void 0 ? void 0 : opts.metadataUrl) && shouldAttemptFallback(response, issuer.pathname)) {
const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn);
}
@@ -473,7 +384,7 @@ async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, o
*
* @deprecated This function is deprecated in favor of `discoverAuthorizationServerMetadata`.
*/
export async function discoverOAuthMetadata(issuer, { authorizationServerUrl, protocolVersion } = {}, fetchFn = fetch) {
export async function discoverOAuthMetadata(issuer, { authorizationServerUrl, protocolVersion, } = {}, fetchFn = fetch) {
if (typeof issuer === 'string') {
issuer = new URL(issuer);
}
@@ -483,17 +394,15 @@ export async function discoverOAuthMetadata(issuer, { authorizationServerUrl, pr
if (typeof authorizationServerUrl === 'string') {
authorizationServerUrl = new URL(authorizationServerUrl);
}
protocolVersion ?? (protocolVersion = LATEST_PROTOCOL_VERSION);
protocolVersion !== null && protocolVersion !== void 0 ? protocolVersion : (protocolVersion = LATEST_PROTOCOL_VERSION);
const response = await discoverMetadataWithFallback(authorizationServerUrl, 'oauth-authorization-server', fetchFn, {
protocolVersion,
metadataServerUrl: authorizationServerUrl
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());
@@ -502,7 +411,8 @@ export async function discoverOAuthMetadata(issuer, { authorizationServerUrl, pr
* Builds a list of discovery URLs to try for authorization server metadata.
* URLs are returned in priority order:
* 1. OAuth metadata at the given URL
* 2. OIDC metadata endpoints at the given URL
* 2. OAuth metadata at root (if URL has path)
* 3. OIDC metadata endpoints
*/
export function buildDiscoveryUrls(authorizationServerUrl) {
const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
@@ -532,7 +442,12 @@ export function buildDiscoveryUrls(authorizationServerUrl) {
url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin),
type: 'oauth'
});
// 2. OIDC metadata endpoints
// Root path: https://example.com/.well-known/oauth-authorization-server
urlsToTry.push({
url: new URL('/.well-known/oauth-authorization-server', url.origin),
type: 'oauth'
});
// 3. OIDC metadata endpoints
// RFC 8414 style: Insert /.well-known/openid-configuration before the path
urlsToTry.push({
url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
@@ -561,11 +476,9 @@ export function buildDiscoveryUrls(authorizationServerUrl) {
* @param options.protocolVersion - MCP protocol version to use, defaults to LATEST_PROTOCOL_VERSION
* @returns Promise resolving to authorization server metadata, or undefined if discovery fails
*/
export async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn = fetch, protocolVersion = LATEST_PROTOCOL_VERSION } = {}) {
const headers = {
'MCP-Protocol-Version': protocolVersion,
Accept: 'application/json'
};
export async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn = fetch, protocolVersion = LATEST_PROTOCOL_VERSION, } = {}) {
var _a;
const headers = { 'MCP-Protocol-Version': protocolVersion };
// Get the list of URLs to try
const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
// Try each URL in order
@@ -579,7 +492,6 @@ export async function discoverAuthorizationServerMetadata(authorizationServerUrl
continue;
}
if (!response.ok) {
await response.body?.cancel();
// Continue looking for any 4xx response code.
if (response.status >= 400 && response.status < 500) {
continue; // Try next URL
@@ -591,7 +503,12 @@ export async function discoverAuthorizationServerMetadata(authorizationServerUrl
return OAuthMetadataSchema.parse(await response.json());
}
else {
return OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
// MCP spec requires OIDC providers to support S256 PKCE
if (!((_a = metadata.code_challenge_methods_supported) === null || _a === void 0 ? void 0 : _a.includes('S256'))) {
throw new Error(`Incompatible OIDC provider at ${endpointUrl}: does not support S256 code challenge method required by MCP specification`);
}
return metadata;
}
}
return undefined;
@@ -599,97 +516,49 @@ export async function discoverAuthorizationServerMetadata(authorizationServerUrl
/**
* Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
*/
export async function startAuthorization(authorizationServerUrl, { metadata, clientInformation, redirectUrl, scope, state, resource }) {
export async function startAuthorization(authorizationServerUrl, { metadata, clientInformation, redirectUrl, scope, state, resource, }) {
const responseType = "code";
const codeChallengeMethod = "S256";
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.response_types_supported.includes(responseType)) {
throw new Error(`Incompatible auth server: does not support response type ${responseType}`);
}
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}`);
if (!metadata.code_challenge_methods_supported ||
!metadata.code_challenge_methods_supported.includes(codeChallengeMethod)) {
throw new Error(`Incompatible auth server: does not support code challenge method ${codeChallengeMethod}`);
}
}
else {
authorizationUrl = new URL('/authorize', authorizationServerUrl);
authorizationUrl = new URL("/authorize", authorizationServerUrl);
}
// Generate PKCE challenge
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));
authorizationUrl.searchParams.set("response_type", responseType);
authorizationUrl.searchParams.set("client_id", clientInformation.client_id);
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
authorizationUrl.searchParams.set("code_challenge_method", codeChallengeMethod);
authorizationUrl.searchParams.set("redirect_uri", String(redirectUrl));
if (state) {
authorizationUrl.searchParams.set('state', state);
authorizationUrl.searchParams.set("state", state);
}
if (scope) {
authorizationUrl.searchParams.set('scope', scope);
authorizationUrl.searchParams.set("scope", scope);
}
if (scope?.includes('offline_access')) {
if (scope === null || scope === void 0 ? void 0 : scope.includes("offline_access")) {
// if the request includes the OIDC-only "offline_access" scope,
// we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
authorizationUrl.searchParams.append('prompt', 'consent');
authorizationUrl.searchParams.append("prompt", "consent");
}
if (resource) {
authorizationUrl.searchParams.set('resource', resource.href);
authorizationUrl.searchParams.set("resource", resource.href);
}
return { authorizationUrl, codeVerifier };
}
/**
* Prepares token request parameters for an authorization code exchange.
*
* This is the default implementation used by fetchToken when the provider
* doesn't implement prepareTokenRequest.
*
* @param authorizationCode - The authorization code received from the authorization endpoint
* @param codeVerifier - The PKCE code verifier
* @param redirectUri - The redirect URI used in the authorization request
* @returns URLSearchParams for the authorization_code grant
*/
export function prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, redirectUri) {
return new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
code_verifier: codeVerifier,
redirect_uri: String(redirectUri)
});
}
/**
* Internal helper to execute a token request with the given parameters.
* Used by exchangeAuthorization, refreshAuthorization, and fetchToken.
*/
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());
}
/**
* Exchanges an authorization code for an access token with the given server.
*
@@ -702,16 +571,48 @@ async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequ
* @returns Promise resolving to OAuth tokens
* @throws {Error} When token exchange fails or authentication is invalid
*/
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 exchangeAuthorization(authorizationServerUrl, { metadata, clientInformation, authorizationCode, codeVerifier, redirectUri, resource, addClientAuthentication, fetchFn, }) {
var _a;
const grantType = "authorization_code";
const tokenUrl = (metadata === null || metadata === void 0 ? void 0 : metadata.token_endpoint)
? new URL(metadata.token_endpoint)
: new URL("/token", authorizationServerUrl);
if ((metadata === null || metadata === void 0 ? void 0 : metadata.grant_types_supported) &&
!metadata.grant_types_supported.includes(grantType)) {
throw new Error(`Incompatible auth server: does not support grant type ${grantType}`);
}
// Exchange code for tokens
const headers = new Headers({
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
});
const params = new URLSearchParams({
grant_type: grantType,
code: authorizationCode,
code_verifier: codeVerifier,
redirect_uri: String(redirectUri),
});
if (addClientAuthentication) {
addClientAuthentication(headers, params, authorizationServerUrl, metadata);
}
else {
// Determine and apply client authentication method
const supportedMethods = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.token_endpoint_auth_methods_supported) !== null && _a !== void 0 ? _a : [];
const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
applyClientAuthentication(authMethod, clientInformation, headers, params);
}
if (resource) {
params.set("resource", resource.href);
}
const response = await (fetchFn !== null && fetchFn !== void 0 ? fetchFn : fetch)(tokenUrl, {
method: "POST",
headers,
body: params,
});
if (!response.ok) {
throw await parseErrorResponse(response);
}
return OAuthTokensSchema.parse(await response.json());
}
/**
* Exchange a refresh token for an updated access token.
@@ -725,96 +626,70 @@ export async function exchangeAuthorization(authorizationServerUrl, { metadata,
* @returns Promise resolving to OAuth tokens (preserves original refresh_token if not replaced)
* @throws {Error} When token refresh fails or authentication is invalid
*/
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
});
// Preserve original refresh token if server didn't return a new one
return { refresh_token: refreshToken, ...tokens };
}
/**
* Unified token fetching that works with any grant type via provider.prepareTokenRequest().
*
* This function provides a single entry point for obtaining tokens regardless of the
* OAuth grant type. The provider's prepareTokenRequest() method determines which grant
* to use and supplies the grant-specific parameters.
*
* @param provider - OAuth client provider that implements prepareTokenRequest()
* @param authorizationServerUrl - The authorization server's base URL
* @param options - Configuration for the token request
* @returns Promise resolving to OAuth tokens
* @throws {Error} When provider doesn't implement prepareTokenRequest or token fetch fails
*
* @example
* // Provider for client_credentials:
* class MyProvider implements OAuthClientProvider {
* prepareTokenRequest(scope) {
* const params = new URLSearchParams({ grant_type: 'client_credentials' });
* if (scope) params.set('scope', scope);
* return params;
* }
* // ... other methods
* }
*
* const tokens = await fetchToken(provider, authServerUrl, { metadata });
*/
export async function fetchToken(provider, authorizationServerUrl, { metadata, resource, authorizationCode, fetchFn } = {}) {
const scope = provider.clientMetadata.scope;
// Use provider's prepareTokenRequest if available, otherwise fall back to authorization_code
let tokenRequestParams;
if (provider.prepareTokenRequest) {
tokenRequestParams = await provider.prepareTokenRequest(scope);
}
// Default to authorization_code grant if no custom prepareTokenRequest
if (!tokenRequestParams) {
if (!authorizationCode) {
throw new Error('Either provider.prepareTokenRequest() or authorizationCode is required');
export async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn, }) {
var _a;
const grantType = "refresh_token";
let tokenUrl;
if (metadata) {
tokenUrl = new URL(metadata.token_endpoint);
if (metadata.grant_types_supported &&
!metadata.grant_types_supported.includes(grantType)) {
throw new Error(`Incompatible auth server: does not support grant type ${grantType}`);
}
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
else {
tokenUrl = new URL("/token", authorizationServerUrl);
}
// Exchange refresh token
const headers = new Headers({
"Content-Type": "application/x-www-form-urlencoded",
});
const params = new URLSearchParams({
grant_type: grantType,
refresh_token: refreshToken,
});
if (addClientAuthentication) {
addClientAuthentication(headers, params, authorizationServerUrl, metadata);
}
else {
// Determine and apply client authentication method
const supportedMethods = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.token_endpoint_auth_methods_supported) !== null && _a !== void 0 ? _a : [];
const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
applyClientAuthentication(authMethod, clientInformation, headers, params);
}
if (resource) {
params.set("resource", resource.href);
}
const response = await (fetchFn !== null && fetchFn !== void 0 ? fetchFn : fetch)(tokenUrl, {
method: "POST",
headers,
body: params,
});
if (!response.ok) {
throw await parseErrorResponse(response);
}
return OAuthTokensSchema.parse({ refresh_token: refreshToken, ...(await response.json()) });
}
/**
* Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591.
*/
export async function registerClient(authorizationServerUrl, { metadata, clientMetadata, fetchFn }) {
export async function registerClient(authorizationServerUrl, { metadata, clientMetadata, fetchFn, }) {
let registrationUrl;
if (metadata) {
if (!metadata.registration_endpoint) {
throw new Error('Incompatible auth server: does not support dynamic client registration');
throw new Error("Incompatible auth server: does not support dynamic client registration");
}
registrationUrl = new URL(metadata.registration_endpoint);
}
else {
registrationUrl = new URL('/register', authorizationServerUrl);
registrationUrl = new URL("/register", authorizationServerUrl);
}
const response = await (fetchFn ?? fetch)(registrationUrl, {
method: 'POST',
const response = await (fetchFn !== null && fetchFn !== void 0 ? fetchFn : fetch)(registrationUrl, {
method: "POST",
headers: {
'Content-Type': 'application/json'
"Content-Type": "application/json",
},
body: JSON.stringify(clientMetadata)
body: JSON.stringify(clientMetadata),
});
if (!response.ok) {
throw await parseErrorResponse(response);