@atproto/oauth-client
Version:
OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).
99 lines • 4.52 kB
JavaScript
import { extractPdsUrl } from '@atproto/did';
import { oauthIssuerIdentifierSchema, } from '@atproto/oauth-types';
import { OAuthResolverError } from './oauth-resolver-error.js';
export class OAuthResolver {
constructor(identityResolver, protectedResourceMetadataResolver, authorizationServerMetadataResolver) {
this.identityResolver = identityResolver;
this.protectedResourceMetadataResolver = protectedResourceMetadataResolver;
this.authorizationServerMetadataResolver = authorizationServerMetadataResolver;
}
/**
* @param input - A handle, DID, PDS URL or Entryway URL
*/
async resolve(input, options) {
// Allow using an entryway, or PDS url, directly as login input (e.g.
// when the user forgot their handle, or when the handle does not
// resolve to a DID)
return /^https?:\/\//.test(input)
? this.resolveFromService(input, options)
: this.resolveFromIdentity(input, options);
}
/**
* @note this method can be used to verify if a particular uri supports OAuth
* based sign-in (for compatibility with legacy implementation).
*/
async resolveFromService(input, options) {
try {
// Assume first that input is a PDS URL (as required by ATPROTO)
const metadata = await this.getResourceServerMetadata(input, options);
return { metadata };
}
catch (err) {
if (!options?.signal?.aborted && err instanceof OAuthResolverError) {
try {
// Fallback to trying to fetch as an issuer (Entryway)
const result = oauthIssuerIdentifierSchema.safeParse(input);
if (result.success) {
const metadata = await this.getAuthorizationServerMetadata(result.data, options);
return { metadata };
}
}
catch {
// Fallback failed, throw original error
}
}
throw err;
}
}
async resolveFromIdentity(input, options) {
const identityInfo = await this.resolveIdentity(input, options);
options?.signal?.throwIfAborted();
const pds = extractPdsUrl(identityInfo.didDoc);
const metadata = await this.getResourceServerMetadata(pds, options);
return { identityInfo, metadata, pds };
}
async resolveIdentity(input, options) {
try {
return await this.identityResolver.resolve(input, options);
}
catch (cause) {
throw OAuthResolverError.from(cause, `Failed to resolve identity: ${input}`);
}
}
async getAuthorizationServerMetadata(issuer, options) {
try {
return await this.authorizationServerMetadataResolver.get(issuer, options);
}
catch (cause) {
throw OAuthResolverError.from(cause, `Failed to resolve OAuth server metadata for issuer: ${issuer}`);
}
}
async getResourceServerMetadata(pdsUrl, options) {
try {
const rsMetadata = await this.protectedResourceMetadataResolver.get(pdsUrl, options);
if (!rsMetadata) {
return this.getAuthorizationServerMetadata(pdsUrl, options);
}
// ATPROTO requires one, and only one, authorization server entry
if (rsMetadata.authorization_servers?.length !== 1) {
throw new OAuthResolverError(rsMetadata.authorization_servers?.length
? `Unable to determine authorization server for PDS: ${pdsUrl}`
: `No authorization servers found for PDS: ${pdsUrl}`);
}
const issuer = rsMetadata.authorization_servers[0];
options?.signal?.throwIfAborted();
const asMetadata = await this.getAuthorizationServerMetadata(issuer, options);
// https://www.rfc-editor.org/rfc/rfc9728.html#section-4
if (asMetadata.protected_resources) {
if (!asMetadata.protected_resources.includes(rsMetadata.resource)) {
throw new OAuthResolverError(`PDS "${pdsUrl}" not protected by issuer "${issuer}"`);
}
}
return asMetadata;
}
catch (cause) {
throw OAuthResolverError.from(cause, `Failed to resolve OAuth server metadata for resource: ${pdsUrl}`);
}
}
}
//# sourceMappingURL=oauth-resolver.js.map