@atproto/oauth-client
Version:
OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).
54 lines • 2.98 kB
JavaScript
import { oauthAuthorizationServerMetadataValidator, oauthIssuerIdentifierSchema, } from '@atproto/oauth-types';
import { FetchResponseError, bindFetch, cancelBody, } from '@atproto-labs/fetch';
import { CachedGetter, swallowStoreErrors, } from '@atproto-labs/simple-store';
import { contentMime } from './util.js';
/**
* @see {@link https://datatracker.ietf.org/doc/html/rfc8414}
*/
export class OAuthAuthorizationServerMetadataResolver extends CachedGetter {
constructor(cache, fetch, config) {
super(async (issuer, options) => this.fetchMetadata(issuer, options), swallowStoreErrors(cache, config?.onCacheError));
this.fetch = bindFetch(fetch);
this.allowHttpIssuer = config?.allowHttpIssuer === true;
}
async get(input, options) {
const issuer = oauthIssuerIdentifierSchema.parse(String(input));
if (!this.allowHttpIssuer && issuer.startsWith('http:')) {
throw new TypeError('Unsecure issuer URL protocol only allowed in development and test environments');
}
return super.get(issuer, options);
}
async fetchMetadata(issuer, options) {
const url = new URL(`/.well-known/oauth-authorization-server`, issuer);
const request = new Request(url, {
headers: { accept: 'application/json' },
cache: options?.noCache ? 'no-cache' : undefined,
signal: options?.signal,
redirect: 'manual', // response must be 200 OK
});
const response = await this.fetch(request);
// https://datatracker.ietf.org/doc/html/rfc8414#section-3.2
if (response.status !== 200) {
await cancelBody(response, 'log');
throw await FetchResponseError.from(response, `Unexpected status code ${response.status} for "${url}"`, undefined, { cause: request });
}
if (contentMime(response.headers) !== 'application/json') {
await cancelBody(response, 'log');
throw await FetchResponseError.from(response, `Unexpected content type for "${url}"`, undefined, { cause: request });
}
const metadata = oauthAuthorizationServerMetadataValidator.parse(await response.json());
// Validate the issuer (MIX-UP attacks)
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-mix-up-attacks
// https://datatracker.ietf.org/doc/html/rfc8414#section-2
if (metadata.issuer !== issuer) {
throw new TypeError(`Invalid issuer ${metadata.issuer}`);
}
// ATPROTO requires client_id_metadata_document
// https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/
if (metadata.client_id_metadata_document_supported !== true) {
throw new TypeError(`Authorization server "${issuer}" does not support client_id_metadata_document`);
}
return metadata;
}
}
//# sourceMappingURL=oauth-authorization-server-metadata-resolver.js.map