@atproto/oauth-client
Version:
OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).
126 lines • 6.71 kB
JavaScript
import { CLIENT_ASSERTION_TYPE_JWT_BEARER, } from '@atproto/oauth-types';
import { FALLBACK_ALG } from './constants.js';
import { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js';
export function negotiateClientAuthMethod(serverMetadata, clientMetadata, keyset) {
const method = clientMetadata.token_endpoint_auth_method;
// @NOTE ATproto spec requires that AS support both "none" and
// "private_key_jwt", and that clients use one of the other. The following
// check ensures that the AS is indeed compliant with this client's
// configuration.
const methods = supportedMethods(serverMetadata);
if (!methods.includes(method)) {
throw new Error(`The server does not support "${method}" authentication. Supported methods are: ${methods.join(', ')}.`);
}
if (method === 'private_key_jwt') {
// Invalid client configuration. This should not happen as
// "validateClientMetadata" already check this.
if (!keyset)
throw new Error('A keyset is required for private_key_jwt');
const alg = supportedAlgs(serverMetadata);
// @NOTE we can't use `keyset.findPrivateKey` here because we can't enforce
// that the returned key contains a "kid". The following implementation is
// more robust against keysets containing keys without a "kid" property.
for (const key of keyset.list({ alg, usage: 'sign' })) {
// Return the first key from the key set that matches the server's
// supported algorithms.
if (key.kid)
return { method: 'private_key_jwt', kid: key.kid };
}
throw new Error(alg.includes(FALLBACK_ALG)
? `Client authentication method "${method}" requires at least one "${FALLBACK_ALG}" signing key with a "kid" property`
: // AS is not compliant with the ATproto OAuth spec.
`Authorization server requires "${method}" authentication method, but does not support "${FALLBACK_ALG}" algorithm.`);
}
if (method === 'none') {
return { method: 'none' };
}
throw new Error(`The ATProto OAuth spec requires that client use either "none" or "private_key_jwt" authentication method.` +
(method === 'client_secret_basic'
? ' You might want to explicitly set "token_endpoint_auth_method" to one of those values in the client metadata document.'
: ` You set "${method}" which is not allowed.`));
}
/**
* @throws {AuthMethodUnsatisfiableError} if the authentication method is no
* long usable (either because the AS changed, of because the key is no longer
* available in the keyset).
*/
export function createClientCredentialsFactory(authMethod, serverMetadata, clientMetadata, runtime, keyset) {
// Ensure the AS still supports the auth method.
if (!supportedMethods(serverMetadata).includes(authMethod.method)) {
throw new AuthMethodUnsatisfiableError(`Client authentication method "${authMethod.method}" no longer supported`);
}
if (authMethod.method === 'none') {
return () => ({
payload: {
client_id: clientMetadata.client_id,
},
});
}
if (authMethod.method === 'private_key_jwt') {
try {
// The client used to be a confidential client but no longer has a keyset.
if (!keyset)
throw new Error('A keyset is required for private_key_jwt');
// @NOTE throws if no matching key can be found
const { key, alg } = keyset.findPrivateKey({
usage: 'sign',
kid: authMethod.kid,
alg: supportedAlgs(serverMetadata),
});
// https://www.rfc-editor.org/rfc/rfc7523.html#section-3
return async () => ({
payload: {
client_id: clientMetadata.client_id,
client_assertion_type: CLIENT_ASSERTION_TYPE_JWT_BEARER,
client_assertion: await key.createJwt({ alg }, {
// > The JWT MUST contain an "iss" (issuer) claim that contains a
// > unique identifier for the entity that issued the JWT.
iss: clientMetadata.client_id,
// > For client authentication, the subject MUST be the
// > "client_id" of the OAuth client.
sub: clientMetadata.client_id,
// > The JWT MUST contain an "aud" (audience) claim containing a value
// > that identifies the authorization server as an intended audience.
// > The token endpoint URL of the authorization server MAY be used as a
// > value for an "aud" element to identify the authorization server as an
// > intended audience of the JWT.
aud: serverMetadata.issuer,
// > The JWT MAY contain a "jti" (JWT ID) claim that provides a
// > unique identifier for the token.
jti: await runtime.generateNonce(),
// > The JWT MAY contain an "iat" (issued at) claim that
// > identifies the time at which the JWT was issued.
iat: Math.floor(Date.now() / 1000),
// > The JWT MUST contain an "exp" (expiration time) claim that
// > limits the time window during which the JWT can be used.
exp: Math.floor(Date.now() / 1000) + 60, // 1 minute
}),
},
});
}
catch (cause) {
throw new AuthMethodUnsatisfiableError('Failed to load private key', {
cause,
});
}
}
throw new AuthMethodUnsatisfiableError(
// @ts-expect-error
`Unsupported auth method ${authMethod.method}`);
}
function supportedMethods(serverMetadata) {
return serverMetadata['token_endpoint_auth_methods_supported'];
}
function supportedAlgs(serverMetadata) {
return (serverMetadata['token_endpoint_auth_signing_alg_values_supported'] ?? [
// @NOTE If not specified, assume that the server supports the ES256
// algorithm, as prescribed by the spec:
//
// > Clients and Authorization Servers currently must support the ES256
// > cryptographic system [for client authentication].
//
// https://atproto.com/specs/oauth#confidential-client-authentication
FALLBACK_ALG,
]);
}
//# sourceMappingURL=oauth-client-auth.js.map