A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at main 116 lines 3.6 kB view raw
1import { 2 type AuthorizeOptions, 3 NodeOAuthClient, 4 type NodeOAuthClientOptions, 5 type OAuthAuthorizationRequestParameters, 6} from "@atproto/oauth-client-node"; 7import { FALLBACK_ALG, negotiateClientAuthMethod } from "./oauth-client-auth"; 8 9export class CustomOAuthClient extends NodeOAuthClient { 10 constructor(options: NodeOAuthClientOptions) { 11 super(options); 12 } 13 14 async authorize( 15 input: string, 16 { signal, ...options }: AuthorizeOptions = {}, 17 ): Promise<URL> { 18 const redirectUri = 19 options?.redirect_uri ?? this.clientMetadata.redirect_uris[0]; 20 if (!this.clientMetadata.redirect_uris.includes(redirectUri)) { 21 // The server will enforce this, but let's catch it early 22 throw new TypeError("Invalid redirect_uri"); 23 } 24 25 const { identityInfo, metadata } = await this.oauthResolver.resolve(input, { 26 signal, 27 }); 28 29 const pkce = await this.runtime.generatePKCE(); 30 const dpopKey = await this.runtime.generateKey( 31 metadata.dpop_signing_alg_values_supported || [FALLBACK_ALG], 32 ); 33 34 const authMethod = negotiateClientAuthMethod( 35 metadata, 36 this.clientMetadata, 37 this.keyset, 38 ); 39 const state = await this.runtime.generateNonce(); 40 41 await this.stateStore.set(state, { 42 iss: metadata.issuer, 43 authMethod, 44 dpopKey, 45 verifier: pkce.verifier, 46 appState: options?.state, 47 }); 48 49 const parameters: OAuthAuthorizationRequestParameters = { 50 ...options, 51 52 client_id: this.clientMetadata.client_id, 53 redirect_uri: redirectUri, 54 code_challenge: pkce.challenge, 55 code_challenge_method: pkce.method, 56 state, 57 login_hint: identityInfo && !options.prompt ? input : undefined, 58 response_mode: this.responseMode, 59 response_type: "code" as const, 60 scope: options?.scope ?? this.clientMetadata.scope, 61 }; 62 63 const authorizationUrl = new URL(metadata.authorization_endpoint); 64 65 // Since the user will be redirected to the authorization_endpoint url using 66 // a browser, we need to make sure that the url is valid. 67 if ( 68 authorizationUrl.protocol !== "https:" && 69 authorizationUrl.protocol !== "http:" 70 ) { 71 throw new TypeError( 72 `Invalid authorization endpoint protocol: ${authorizationUrl.protocol}`, 73 ); 74 } 75 76 if (metadata.pushed_authorization_request_endpoint) { 77 const server = await this.serverFactory.fromMetadata( 78 metadata, 79 authMethod, 80 dpopKey, 81 ); 82 const parResponse = await server.request( 83 "pushed_authorization_request", 84 parameters, 85 ); 86 87 authorizationUrl.searchParams.set( 88 "client_id", 89 this.clientMetadata.client_id, 90 ); 91 authorizationUrl.searchParams.set("request_uri", parResponse.request_uri); 92 return authorizationUrl; 93 } else if (metadata.require_pushed_authorization_requests) { 94 throw new Error( 95 "Server requires pushed authorization requests (PAR) but no PAR endpoint is available", 96 ); 97 } else { 98 for (const [key, value] of Object.entries(parameters)) { 99 if (value) authorizationUrl.searchParams.set(key, String(value)); 100 } 101 102 // Length of the URL that will be sent to the server 103 const urlLength = 104 authorizationUrl.pathname.length + authorizationUrl.search.length; 105 if (urlLength < 2048) { 106 return authorizationUrl; 107 } else if (!metadata.pushed_authorization_request_endpoint) { 108 throw new Error("Login URL too long"); 109 } 110 } 111 112 throw new Error( 113 "Server does not support pushed authorization requests (PAR)", 114 ); 115 } 116}