@atproto/oauth-client
Version:
OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).
76 lines • 3.86 kB
JavaScript
import { assertOAuthDiscoverableClientId, assertOAuthLoopbackClientId, } from '@atproto/oauth-types';
import { FALLBACK_ALG } from './constants.js';
import { clientMetadataSchema } from './types.js';
export function validateClientMetadata(input, keyset) {
// Allow to pass a keyset and omit the jwks/jwks_uri properties
if (!input.jwks && !input.jwks_uri && keyset?.size) {
input = { ...input, jwks: keyset.toJSON() };
}
const metadata = clientMetadataSchema.parse(input);
// Validate client ID
if (metadata.client_id.startsWith('http:')) {
assertOAuthLoopbackClientId(metadata.client_id);
}
else {
assertOAuthDiscoverableClientId(metadata.client_id);
}
const scopes = metadata.scope?.split(' ');
if (!scopes?.includes('atproto')) {
throw new TypeError(`Client metadata must include the "atproto" scope`);
}
if (!metadata.response_types.includes('code')) {
throw new TypeError(`"response_types" must include "code"`);
}
if (!metadata.grant_types.includes('authorization_code')) {
throw new TypeError(`"grant_types" must include "authorization_code"`);
}
const method = metadata.token_endpoint_auth_method;
const methodAlg = metadata.token_endpoint_auth_signing_alg;
switch (method) {
case 'none':
if (methodAlg) {
throw new TypeError(`"token_endpoint_auth_signing_alg" must not be provided when "token_endpoint_auth_method" is "${method}"`);
}
break;
case 'private_key_jwt': {
if (!methodAlg) {
throw new TypeError(`"token_endpoint_auth_signing_alg" must be provided when "token_endpoint_auth_method" is "${method}"`);
}
if (!keyset) {
throw new TypeError(`Client authentication method "${method}" requires a keyset`);
}
// @NOTE This reproduces the logic from `negotiateClientAuthMethod` at
// initialization time to ensure that every key that might end-up being
// used is indeed valid & advertised in the metadata.
const signingKeys = Array.from(keyset.list({ usage: 'sign' })).filter((key) => key.kid);
if (!signingKeys.length) {
throw new TypeError(`Client authentication method "${method}" requires at least one active signing key with a "kid" property`);
}
if (!signingKeys.some((key) => key.algorithms.includes(FALLBACK_ALG))) {
throw new TypeError(`Client authentication method "${method}" requires at least one active "${FALLBACK_ALG}" signing key`);
}
if (metadata.jwks) {
// Ensure that all the signing keys that could end-up being used are
// advertised in the JWKS.
for (const key of signingKeys) {
if (!metadata.jwks.keys.some((k) => k.kid === key.kid && !k.revoked)) {
throw new TypeError(`Missing or inactive key "${key.kid}" in jwks. Make sure that every signing key of the Keyset is declared as an active key in the Metadata's JWKS.`);
}
}
}
else if (metadata.jwks_uri) {
// @NOTE we only ensure that all the signing keys are referenced in JWKS
// when it is available (see previous "if") as we don't want to download
// that file here (for efficiency reasons).
}
else {
throw new TypeError(`Client authentication method "${method}" requires a JWKS`);
}
break;
}
default:
throw new TypeError(`Unsupported "token_endpoint_auth_method" value: ${method}`);
}
return metadata;
}
//# sourceMappingURL=validate-client-metadata.js.map