forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
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}