UNPKG

@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
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