// Copyright Inrupt Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal in // the Software without restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the // Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // /** * @hidden * @packageDocumentation */ /** * Responsible for fetching an IDP configuration */ import type { IIssuerConfig, IIssuerConfigFetcher, IStorageUtility, } from "@inrupt/solid-client-authn-core"; import { ConfigurationError } from "@inrupt/solid-client-authn-core"; // Camelcase identifiers are required in the OIDC specification. /* eslint-disable camelcase*/ export const WELL_KNOWN_OPENID_CONFIG = ".well-known/openid-configuration"; const issuerConfigKeyMap: Record< string, { toKey: string; convertToUrl?: boolean } > = { issuer: { toKey: "issuer", convertToUrl: true, }, authorization_endpoint: { toKey: "authorizationEndpoint", convertToUrl: true, }, token_endpoint: { toKey: "tokenEndpoint", convertToUrl: true, }, userinfo_endpoint: { toKey: "userinfoEndpoint", convertToUrl: true, }, jwks_uri: { toKey: "jwksUri", convertToUrl: true, }, registration_endpoint: { toKey: "registrationEndpoint", convertToUrl: true, }, end_session_endpoint: { toKey: "endSessionEndpoint", convertToUrl: true, }, scopes_supported: { toKey: "scopesSupported" }, response_types_supported: { toKey: "responseTypesSupported" }, response_modes_supported: { toKey: "responseModesSupported" }, grant_types_supported: { toKey: "grantTypesSupported" }, acr_values_supported: { toKey: "acrValuesSupported" }, subject_types_supported: { toKey: "subjectTypesSupported" }, id_token_signing_alg_values_supported: { toKey: "idTokenSigningAlgValuesSupported", }, id_token_encryption_alg_values_supported: { toKey: "idTokenEncryptionAlgValuesSupported", }, id_token_encryption_enc_values_supported: { toKey: "idTokenEncryptionEncValuesSupported", }, userinfo_signing_alg_values_supported: { toKey: "userinfoSigningAlgValuesSupported", }, userinfo_encryption_alg_values_supported: { toKey: "userinfoEncryptionAlgValuesSupported", }, userinfo_encryption_enc_values_supported: { toKey: "userinfoEncryptionEncValuesSupported", }, request_object_signing_alg_values_supported: { toKey: "requestObjectSigningAlgValuesSupported", }, request_object_encryption_alg_values_supported: { toKey: "requestObjectEncryptionAlgValuesSupported", }, request_object_encryption_enc_values_supported: { toKey: "requestObjectEncryptionEncValuesSupported", }, token_endpoint_auth_methods_supported: { toKey: "tokenEndpointAuthMethodsSupported", }, token_endpoint_auth_signing_alg_values_supported: { toKey: "tokenEndpointAuthSigningAlgValuesSupported", }, display_values_supported: { toKey: "displayValuesSupported" }, claim_types_supported: { toKey: "claimTypesSupported" }, claims_supported: { toKey: "claimsSupported" }, service_documentation: { toKey: "serviceDocumentation" }, claims_locales_supported: { toKey: "claimsLocalesSupported" }, ui_locales_supported: { toKey: "uiLocalesSupported" }, claims_parameter_supported: { toKey: "claimsParameterSupported" }, request_parameter_supported: { toKey: "requestParameterSupported" }, request_uri_parameter_supported: { toKey: "requestUriParameterSupported" }, require_request_uri_registration: { toKey: "requireRequestUriRegistration" }, op_policy_uri: { toKey: "opPolicyUri", convertToUrl: true, }, op_tos_uri: { toKey: "opTosUri", convertToUrl: true, }, }; function processConfig( config: Record, ): IIssuerConfig { const parsedConfig: Record = {}; Object.keys(config).forEach((key) => { if (issuerConfigKeyMap[key]) { // TODO: PMcB55: Validate URL if "issuerConfigKeyMap[key].convertToUrl" // if (issuerConfigKeyMap[key].convertToUrl) { // validateUrl(config[key]); // } parsedConfig[issuerConfigKeyMap[key].toKey] = config[key]; } }); if (!Array.isArray(parsedConfig.scopesSupported)) { parsedConfig.scopesSupported = ["openid"]; } return parsedConfig as unknown as IIssuerConfig; } /** * @hidden */ export default class IssuerConfigFetcher implements IIssuerConfigFetcher { constructor(private storageUtility: IStorageUtility) { this.storageUtility = storageUtility; } // This method needs no state (so can be static), and can be exposed to allow // callers to know where this implementation puts state it needs. public static getLocalStorageKey(issuer: string): string { return `issuerConfig:${issuer}`; } async fetchConfig(issuer: string): Promise { let issuerConfig: IIssuerConfig; const openIdConfigUrl = new URL( WELL_KNOWN_OPENID_CONFIG, // Make sure to append a slash at issuer URL, so that the .well-known URL // includes the full issuer path. See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig. issuer.endsWith("/") ? issuer : `${issuer}/`, ).href; const issuerConfigRequestBody = await fetch(openIdConfigUrl); // Check the validity of the fetched config try { issuerConfig = processConfig(await issuerConfigRequestBody.json()); } catch (err) { throw new ConfigurationError( `[${issuer.toString()}] has an invalid configuration: ${ (err as { message: string }).message }`, ); } // Update store with fetched config await this.storageUtility.set( IssuerConfigFetcher.getLocalStorageKey(issuer), JSON.stringify(issuerConfig), ); return issuerConfig; } }