forked from
rocksky.app/rocksky
A decentralized music tracking and discovery platform built on AT Protocol 馃幍
1import type {
2 ClientMetadata,
3 Keyset,
4 OAuthAuthorizationServerMetadata,
5} from "@atproto/oauth-client-node";
6
7import type { ClientAuthMethod } from "@atproto/oauth-client/dist/oauth-client-auth";
8
9export const FALLBACK_ALG = "ES256";
10
11function supportedMethods(serverMetadata: OAuthAuthorizationServerMetadata) {
12 return serverMetadata["token_endpoint_auth_methods_supported"];
13}
14
15function supportedAlgs(serverMetadata: OAuthAuthorizationServerMetadata) {
16 return (
17 serverMetadata["token_endpoint_auth_signing_alg_values_supported"] ?? [
18 // @NOTE If not specified, assume that the server supports the ES256
19 // algorithm, as prescribed by the spec:
20 //
21 // > Clients and Authorization Servers currently must support the ES256
22 // > cryptographic system [for client authentication].
23 //
24 // https://atproto.com/specs/oauth#confidential-client-authentication
25 FALLBACK_ALG,
26 ]
27 );
28}
29
30export function negotiateClientAuthMethod(
31 serverMetadata: OAuthAuthorizationServerMetadata,
32 clientMetadata: ClientMetadata,
33 keyset?: Keyset,
34): ClientAuthMethod {
35 const method = clientMetadata.token_endpoint_auth_method;
36
37 // @NOTE ATproto spec requires that AS support both "none" and
38 // "private_key_jwt", and that clients use one of the other. The following
39 // check ensures that the AS is indeed compliant with this client's
40 // configuration.
41 const methods = supportedMethods(serverMetadata);
42 if (!methods.includes(method)) {
43 throw new Error(
44 `The server does not support "${method}" authentication. Supported methods are: ${methods.join(
45 ", ",
46 )}.`,
47 );
48 }
49
50 if (method === "private_key_jwt") {
51 // Invalid client configuration. This should not happen as
52 // "validateClientMetadata" already check this.
53 if (!keyset) throw new Error("A keyset is required for private_key_jwt");
54
55 const alg = supportedAlgs(serverMetadata);
56
57 // @NOTE we can't use `keyset.findPrivateKey` here because we can't enforce
58 // that the returned key contains a "kid". The following implementation is
59 // more robust against keysets containing keys without a "kid" property.
60 for (const key of keyset.list({ alg, usage: "sign" })) {
61 // Return the first key from the key set that matches the server's
62 // supported algorithms.
63 if (key.kid) return { method: "private_key_jwt", kid: key.kid };
64 }
65
66 throw new Error(
67 alg.includes(FALLBACK_ALG)
68 ? `Client authentication method "${method}" requires at least one "${FALLBACK_ALG}" signing key with a "kid" property`
69 : // AS is not compliant with the ATproto OAuth spec.
70 `Authorization server requires "${method}" authentication method, but does not support "${FALLBACK_ALG}" algorithm.`,
71 );
72 }
73
74 if (method === "none") {
75 return { method: "none" };
76 }
77
78 throw new Error(
79 `The ATProto OAuth spec requires that client use either "none" or "private_key_jwt" authentication method.` +
80 (method === "client_secret_basic"
81 ? ' You might want to explicitly set "token_endpoint_auth_method" to one of those values in the client metadata document.'
82 : ` You set "${method}" which is not allowed.`),
83 );
84}