···1+import type {
2+ ClientMetadata,
3+ Keyset,
4+ OAuthAuthorizationServerMetadata,
5+} from "@atproto/oauth-client-node";
6+7+import type { ClientAuthMethod } from "@atproto/oauth-client/dist/oauth-client-auth";
8+9+export const FALLBACK_ALG = "ES256";
10+11+function supportedMethods(serverMetadata: OAuthAuthorizationServerMetadata) {
12+ return serverMetadata["token_endpoint_auth_methods_supported"];
13+}
14+15+function 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+30+export 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+}