@atproto/oauth-client
Version:
OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).
88 lines (76 loc) • 2.72 kB
text/typescript
import { Keyset } from '@atproto/jwk'
import {
OAuthClientMetadataInput,
assertOAuthDiscoverableClientId,
assertOAuthLoopbackClientId,
} from '@atproto/oauth-types'
import { ClientMetadata, clientMetadataSchema } from './types.js'
const TOKEN_ENDPOINT_AUTH_METHOD = `token_endpoint_auth_method`
const TOKEN_ENDPOINT_AUTH_SIGNING_ALG = `token_endpoint_auth_signing_alg`
export function validateClientMetadata(
input: OAuthClientMetadataInput,
keyset?: Keyset,
): ClientMetadata {
if (input.jwks) {
if (!keyset) {
throw new TypeError(`Keyset must not be provided when jwks is provided`)
}
for (const key of input.jwks.keys) {
if (!key.kid) {
throw new TypeError(`Key must have a "kid" property`)
} else if (!keyset.has(key.kid)) {
throw new TypeError(`Key with kid "${key.kid}" not found in 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]
switch (method) {
case undefined:
throw new TypeError(`${TOKEN_ENDPOINT_AUTH_METHOD} must be provided`)
case 'none':
if (metadata[TOKEN_ENDPOINT_AUTH_SIGNING_ALG]) {
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 (!keyset?.size) {
throw new TypeError(
`A non-empty keyset must be provided when ${TOKEN_ENDPOINT_AUTH_METHOD} is "${method}"`,
)
}
if (!metadata[TOKEN_ENDPOINT_AUTH_SIGNING_ALG]) {
throw new TypeError(
`${TOKEN_ENDPOINT_AUTH_SIGNING_ALG} must be provided when ${TOKEN_ENDPOINT_AUTH_METHOD} is "${method}"`,
)
}
break
default:
throw new TypeError(
`Invalid "token_endpoint_auth_method" value: ${method}`,
)
}
return metadata
}