A decentralized music tracking and discovery platform built on AT Protocol 馃幍
at main 84 lines 3.2 kB view raw
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}