}
+ /**
+ * Retrieve and parse JSON.
+ * N.B. this absorbs any errors!
+ * @param {URL} urlObj
+ * @returns {Object}
+ */
+ async fetchJSON(urlObj) {
+ const _scope = _fileScope('fetchJSON');
+ const logInfoData = {
+ url: urlObj.href,
+ response: undefined,
+ };
+ let response;
+ try {
+ const fetchJSONConfig = Communication._axiosConfig('GET', urlObj, undefined, undefined, {
+ [Enum.Header.Accept]: [Enum.ContentType.ApplicationJson, Enum.ContentType.Any + ';q=0.1'].join(', '),
+ });
+ response = await this.axios(fetchJSONConfig);
+ } catch (e) {
+ this.logger.error(_scope, 'json request failed', { error: e, ...logInfoData });
+ return;
+ }
+ logInfoData.response = common.axiosResponseLogData(response);
+
+ let data;
+ try {
+ data = JSON.parse(response.data);
+ } catch (e) {
+ this.logger.error(_scope, 'json parsing failed', { error: e, ...logInfoData });
+ }
+
+ return data;
+ }
+
+
/**
* @typedef {Object} ClientIdentifierData
* @property {Object} rels - keyed by relation to array of uris
* @property {String} photo
* @property {String} url
* @property {String} email
- * @property {String} authorizationEndpoint
- * @property {String} tokenEndpoint
+ * @property {String} authorizationEndpoint - deprecated, backwards compatibility for 20201126 spec
+ * @property {String} tokenEndpoint - deprecated, backwards compatibility for 20201126 spec
+ * @property {String} indieauthMetadata authorization server metadata endpoint
+ * @property {Object} metadata - authorization server metadata for profile
+ * @property {String} metadata.issuer
+ * @property {String} metadata.authorizationEndpoint
+ * @property {String} metadata.tokenEndpoint
+ * @property {String} metadata.introspectionEndpoint
+ * @property {String} metadata.introspectionEndpointAuthMethodsSupported
+ * @property {String} metadata.revocationEndpoint
+ * @property {String} metadata.revocationEndpointAuthMethodsSupported
+ * @property {String} metadata.scopesSupported
+ * @property {String} metadata.responseTypesSupported
+ * @property {String} metadata.grantTypesSupported
+ * @property {String} metadata.serviceDocumentation
+ * @property {String} metadata.codeChallengeMethodsSupported
+ * @property {String} metadata.authorizationResponseIssParameterSupported
+ * @property {String} metadata.userinfoEndpoint
*/
/**
- * Fetch all the microformat data from url, but only return the relevant h-card profile information.
+ * Fetch the relevant microformat data from profile url h-card information,
+ * and authorization server metadata.
* @param {URL} urlObj
* @returns {ProfileData} mf2 data filtered for select fields from h-card
*/
async fetchProfile(urlObj) {
+ const _scope = _fileScope('fetchProfile');
+
const mfData = await this.fetchMicroformat(urlObj);
const profile = {
name: undefined,
photo: undefined,
url: undefined,
email: undefined,
+ metadata: {},
};
+ // Locate h-card mf2 items with url field matching profile url,
+ // and populate profile fields with first-encountered card values.
if (mfData && 'items' in mfData) {
const hCards = mfData.items.filter((item) =>
item.type && item.type.includes('h-card') &&
});
});
}
+
+ // Populate legacy mf2 fields from relation links.
+ // These will be overwritten if they also exist in server metadata.
Object.entries({
- authorizationEndpoint: 'authorization_endpoint',
- tokenEndpoint: 'token_endpoint',
+ authorizationEndpoint: 'authorization_endpoint', // backwards compatibility
+ tokenEndpoint: 'token_endpoint', // backwards compatibility
}).forEach(([p, r]) => {
if (mfData && r in mfData.rels) {
- profile[p] = mfData.rels[r][0]; // eslint-disable-line security/detect-object-injection
+ profile.metadata[p] = profile[p] = mfData.rels[r][0]; // eslint-disable-line security/detect-object-injection
}
});
+ // Set metadata field.
+ if (mfData && 'indieauth-metadata' in mfData.rels) {
+ profile.indieauthMetadata = mfData.rels['indieauth-metadata'][0];
+ }
+
+ // Attempt to populate metadata from authorization server.
+ if (profile.indieauthMetadata) {
+ let mdURL;
+ try {
+ mdURL = new URL(profile.indieauthMetadata);
+ } catch (e) /* istanbul ignore next */ {
+ this.logger.error(_scope, 'invalid authorization server metadata url', { profile });
+ }
+ /* istanbul ignore else */
+ if (mdURL) {
+ const metadataResponse = await this.fetchJSON(mdURL);
+ if (metadataResponse) {
+ // Map snake_case fields to camelCase.
+ Object.entries({
+ issuer: 'issuer',
+ authorizationEndpoint: 'authorization_endpoint',
+ tokenEndpoint: 'token_endpoint',
+ introspectionEndpoint: 'introspection_endpoint',
+ introspectionEndpointAuthMethodsSupported: 'introspection_endpoint_auth_methods_supported',
+ revocationEndpoint: 'revocation_endpoint',
+ revocationEndpointAuthMethodsSupported: 'revocation_endpoint_auth_methods_supported',
+ scopesSupported: 'scopes_supported',
+ responseTypesSupported: 'response_types_supported',
+ grantTypesSupported: 'grant_types_supported',
+ serviceDocumentation: 'service_documentation',
+ codeChallengeMethodsSupported: 'code_challenge_methods_supported',
+ authorizationResponseIssParameterSupported: 'authorization_response_iss_parameter_supported',
+ userinfoEndpoint: 'userinfo_endpoint',
+ }).forEach(([c, s]) => {
+ if (s in metadataResponse) {
+ profile.metadata[c] = metadataResponse[s]; // eslint-disable-line security/detect-object-injection
+ }
+ });
+
+ // Populate legacy profile fields.
+ ['authorizationEndpoint', 'tokenEndpoint'].forEach((f) => {
+ if (f in profile.metadata) {
+ profile[f] = profile.metadata[f]; // eslint-disable-line security/detect-object-injection
+ }
+ });
+ }
+ }
+ }
+
return profile;
}