@atproto/lex-server 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/service-auth.js +1 -1
- package/dist/service-auth.js.map +1 -1
- package/package.json +5 -5
- package/src/service-auth.test.ts +87 -0
- package/src/service-auth.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @atproto/lex-server
|
|
2
2
|
|
|
3
|
+
## 0.0.18
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#4892](https://github.com/bluesky-social/atproto/pull/4892) [`6ccfa49`](https://github.com/bluesky-social/atproto/commit/6ccfa49cdf3f995dd6f30f594d284aa0fe395e61) Thanks [@essential-randomness](https://github.com/essential-randomness)! - Fix `lxm` service token validation
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`2fd8d62`](https://github.com/bluesky-social/atproto/commit/2fd8d62708dc23de6ed21cbcccfebab68b19f588), [`907edfa`](https://github.com/bluesky-social/atproto/commit/907edfa1d16b1074bab4dc617d0bd1a810f3da02)]:
|
|
10
|
+
- @atproto/lex-schema@0.0.20
|
|
11
|
+
- @atproto/lex-client@0.0.21
|
|
12
|
+
|
|
3
13
|
## 0.0.17
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/dist/service-auth.js
CHANGED
|
@@ -182,7 +182,7 @@ async function parseJwt(token, options) {
|
|
|
182
182
|
timeDiff(now, payload.iat) > options.maxAge) {
|
|
183
183
|
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'JWT token too old', { Bearer: { error: 'JwtTooOld' } });
|
|
184
184
|
}
|
|
185
|
-
if (payload.lxm != null &&
|
|
185
|
+
if (payload.lxm != null && payload.lxm !== options.lxm) {
|
|
186
186
|
throw new errors_js_1.LexServerAuthError('AuthenticationRequired', 'Invalid JWT lexicon method ("lxm")', { Bearer: { error: 'BadJwtLexiconMethod' } });
|
|
187
187
|
}
|
|
188
188
|
if (payload.nonce != null && !(await (0, options.unique)(payload.nonce))) {
|
package/dist/service-auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-auth.js","sourceRoot":"","sources":["../src/service-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import * as crypto from '@atproto/crypto'\nimport {\n AtprotoDid,\n AtprotoDidDocument,\n Did,\n matchesIdentifier,\n} from '@atproto/did'\nimport { fromBase64, isPlainObject, utf8FromBase64 } from '@atproto/lex-data'\nimport { DidString, isDidString } from '@atproto/lex-schema'\nimport {\n CreateDidResolverOptions,\n createDidResolver,\n} from '@atproto-labs/did-resolver'\nimport { LexServerAuthError } from './errors.js'\nimport type { LexRouterAuth } from './lex-router.js'\n\nconst BEARER_PREFIX = 'Bearer '\n\n/**\n * Callback function to check and record nonce uniqueness.\n *\n * Used to prevent replay attacks by ensuring each nonce is only used once.\n * The implementation must track nonces for at least the `maxAge` duration\n * (default 5 minutes before and after the current time).\n *\n * @param nonce - The nonce string from the JWT token\n * @returns Promise resolving to `true` if the nonce is unique (first time seen),\n * `false` if it has been seen before\n *\n * @example\n * ```typescript\n * // Using Redis for nonce tracking\n * const checkNonce: UniqueNonceChecker = async (nonce) => {\n * const key = `nonce:${nonce}`\n * const result = await redis.setnx(key, '1')\n * if (result === 1) {\n * await redis.expire(key, 600) // 10 minutes TTL\n * return true\n * }\n * return false\n * }\n * ```\n */\nexport type UniqueNonceChecker = (nonce: string) => Promise<boolean>\n\n/**\n * Configuration options for AT Protocol service authentication.\n *\n * Service auth is used for server-to-server communication in the AT Protocol,\n * where one service authenticates to another using signed JWT tokens tied to\n * the caller's DID.\n *\n * @example\n * ```typescript\n * const options: ServiceAuthOptions = {\n * audience: 'did:web:api.example.com',\n * unique: async (nonce) => nonceStore.checkAndAdd(nonce),\n * maxAge: 300, // 5 minutes\n * // Optional DID resolver options\n * plcDirectoryUrl: 'https://plc.directory'\n * }\n * ```\n */\nexport type ServiceAuthOptions = CreateDidResolverOptions & {\n /**\n * Expected audience (\"aud\") claim in the JWT token.\n *\n * This should be the DID of your service. The token must include this\n * value in its `aud` claim to be accepted. Set to `null` to skip\n * audience verification (not recommended for production).\n */\n audience: null | DidString\n /**\n * Function to check and record nonce uniqueness.\n *\n * This is critical for preventing replay attacks. The value checked here\n * must be unique within `maxAge` seconds before and after the current time.\n *\n * @param nonce - The nonce to check\n * @returns Promise resolving to `true` if unique, `false` if seen before\n */\n unique: UniqueNonceChecker\n /**\n * Maximum age of the JWT token in seconds.\n *\n * Tokens with `iat` (issued at) or `exp` (expiry) timestamps outside\n * this window from the current time will be rejected.\n *\n * @default 300 (5 minutes)\n */\n maxAge?: number\n}\n\n/**\n * Credentials returned after successful service authentication.\n *\n * Contains the verified DID, resolved DID document, and parsed JWT token.\n * These are available in handler context as `ctx.credentials`.\n *\n * @example\n * ```typescript\n * router.add(protectedMethod, {\n * handler: async (ctx) => {\n * const { did, didDocument, jwt } = ctx.credentials\n * console.log('Request from:', did)\n * console.log('Token expires:', new Date(jwt.payload.exp * 1000))\n * return { body: { callerDid: did } }\n * },\n * auth: serviceAuth({ audience: myDid, unique: checkNonce })\n * })\n * ```\n */\nexport type ServiceAuthCredentials = {\n /** The verified AT Protocol DID of the caller. */\n did: AtprotoDid\n /** The resolved DID document of the caller. */\n didDocument: AtprotoDidDocument\n /** The parsed and validated JWT token. */\n jwt: ParsedJwt\n}\n\n/**\n * Creates an authentication handler for verifying AT Protocol service auth JWTs.\n *\n * Service auth is the standard authentication mechanism for server-to-server\n * communication in the AT Protocol. It uses JWT bearer tokens signed by the\n * caller's DID signing key, with the signature verified against the public\n * key in the caller's DID document.\n *\n * The handler performs the following validations:\n * - Extracts and parses the Bearer token from the Authorization header\n * - Validates JWT structure and claims (aud, exp, iat, lxm, nonce)\n * - Resolves the issuer's DID document\n * - Verifies the JWT signature against the `#atproto` verification method\n * - Checks nonce uniqueness to prevent replay attacks\n *\n * @param options - Configuration options for service auth\n * @returns An auth handler function for use with {@link LexRouter.add}\n *\n * @example Basic usage\n * ```typescript\n * import { LexRouter, serviceAuth } from '@atproto/lex-server'\n *\n * const router = new LexRouter()\n *\n * const auth = serviceAuth({\n * audience: 'did:web:api.example.com',\n * unique: async (nonce) => {\n * // Check if nonce has been seen, return true if unique\n * const isNew = await redis.setnx(`nonce:${nonce}`, '1')\n * if (isNew) await redis.expire(`nonce:${nonce}`, 600)\n * return isNew\n * }\n * })\n *\n * router.add(myMethod, {\n * handler: async (ctx) => {\n * console.log('Authenticated as:', ctx.credentials.did)\n * return { body: { success: true } }\n * },\n * auth\n * })\n * ```\n */\nexport function serviceAuth({\n audience,\n maxAge = 5 * 60,\n unique,\n ...options\n}: ServiceAuthOptions): LexRouterAuth<ServiceAuthCredentials> {\n const didResolver = createDidResolver(options)\n\n return async ({ request, method }) => {\n const { signal } = request\n const jwt = await parseJwtBearer(request, {\n lxm: method.nsid,\n maxAge,\n audience,\n unique,\n })\n\n let didDocument: AtprotoDidDocument = await didResolver\n .resolve(jwt.payload.iss, { signal })\n .catch((cause) => {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not resolve DID document',\n { Bearer: { error: 'DidResolutionFailed' } },\n { cause },\n )\n })\n\n const key = getAtprotoSigningKey(didDocument)\n\n if (!key || !(await verifyJwt(jwt, key))) {\n signal.throwIfAborted()\n\n // Try refreshing the DID document in case it was updated\n didDocument = await didResolver\n .resolve(jwt.payload.iss, { signal, noCache: true })\n .catch((cause) => {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not resolve DID document',\n { Bearer: { error: 'DidResolutionFailed' } },\n { cause },\n )\n })\n\n // Verify again with the fresh key (if it changed)\n const keyFresh = getAtprotoSigningKey(didDocument)\n if (!keyFresh || key === keyFresh || !(await verifyJwt(jwt, keyFresh))) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT signature',\n { Bearer: { error: 'BadJwtSignature' } },\n )\n }\n }\n\n return {\n did: didDocument.id,\n didDocument,\n jwt,\n }\n }\n}\n\nasync function verifyJwt(jwt: ParsedJwt, key: Did<'key'>) {\n try {\n return await crypto.verifySignature(key, jwt.message, jwt.signature, {\n jwtAlg: jwt.header.alg,\n allowMalleableSig: true,\n })\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not verify JWT signature',\n { Bearer: { error: 'BadJwtSignature' } },\n { cause },\n )\n }\n}\n\nfunction getAtprotoSigningKey(\n didDocument: AtprotoDidDocument,\n): null | Did<'key'> {\n try {\n const key = didDocument.verificationMethod?.find(\n isAtprotoVerificationMethod,\n didDocument,\n )\n\n if (key?.publicKeyMultibase) {\n if (key.type === 'EcdsaSecp256r1VerificationKey2019') {\n const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n return crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)\n } else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {\n const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n return crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)\n } else if (key.type === 'Multikey') {\n const parsed = crypto.parseMultikey(key.publicKeyMultibase)\n return crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)\n }\n }\n } catch {\n // Invalid key, ignore\n }\n\n return null\n}\n\nfunction isAtprotoVerificationMethod<\n V extends string | { id: string; type: string; publicKeyMultibase?: string },\n>(\n this: AtprotoDidDocument,\n vm: V,\n): vm is Exclude<V, string> & {\n id: `${string}#atproto`\n} {\n return typeof vm === 'object' && matchesIdentifier(this.id, 'atproto', vm.id)\n}\n\nasync function parseJwtBearer(\n request: Request,\n options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n const authorization = request.headers.get('authorization')\n if (!authorization?.startsWith(BEARER_PREFIX)) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Bearer token required',\n { Bearer: { error: 'MissingBearer' } },\n )\n }\n\n const token = authorization.slice(BEARER_PREFIX.length).trim()\n\n return parseJwt(token, options)\n}\n\n/**\n * Options for parsing and validating a JWT token.\n */\nexport type ParseJwtOptions = {\n /** Maximum age in seconds for token validity window. */\n maxAge: number\n /** Expected audience claim, or null to skip audience verification. */\n audience: null | DidString\n /** Function to check nonce uniqueness. */\n unique: UniqueNonceChecker\n /** Expected lexicon method NSID for the `lxm` claim. */\n lxm: string\n}\n\n/**\n * A parsed and partially validated JWT token.\n *\n * Contains the decoded header and payload, along with the raw bytes\n * needed for signature verification.\n *\n * @example\n * ```typescript\n * const jwt: ParsedJwt = {\n * header: { alg: 'ES256K', typ: 'JWT' },\n * payload: {\n * iss: 'did:plc:abc123',\n * aud: 'did:web:api.example.com',\n * exp: 1704067200,\n * iat: 1704066900,\n * lxm: 'com.atproto.sync.getBlob'\n * },\n * message: new Uint8Array([...]),\n * signature: new Uint8Array([...])\n * }\n * ```\n */\nexport type ParsedJwt = {\n /** The decoded JWT header containing algorithm and type. */\n header: HeaderObject\n /** The decoded JWT payload containing claims. */\n payload: PayloadObject\n /** The raw header.payload bytes for signature verification. */\n message: Uint8Array\n /** The decoded signature bytes. */\n signature: Uint8Array\n}\n\nasync function parseJwt(\n token: string,\n options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n const {\n length,\n 0: headerB64,\n 1: payloadB64,\n 2: signatureB64,\n } = token.split('.')\n if (length !== 3) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n )\n }\n\n let header: HeaderObject\n try {\n header = jsonFromBase64(headerB64, isHeaderObject)\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n { cause },\n )\n }\n\n if (\n header.alg === 'none' ||\n // service tokens are not OAuth 2.0 access tokens\n // https://datatracker.ietf.org/doc/html/rfc9068\n header.typ === 'at+jwt' ||\n // \"refresh+jwt\" is a non-standard type used by the @atproto packages\n header.typ === 'refresh+jwt' ||\n // \"DPoP\" proofs are not meant to be used as service tokens\n // https://datatracker.ietf.org/doc/html/rfc9449\n header.typ === 'dpop+jwt'\n ) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n )\n }\n\n let payload: PayloadObject\n try {\n payload = jsonFromBase64(payloadB64, isPayloadObject)\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n { cause },\n )\n }\n\n if (options.audience !== null && options.audience !== payload.aud) {\n throw new LexServerAuthError('AuthenticationRequired', 'Invalid audience', {\n Bearer: { error: 'InvalidAudience' },\n })\n }\n\n const now = Math.floor(Date.now() / 1000)\n\n if (payload.nbf != null && now < payload.nbf) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token not yet valid',\n { Bearer: { error: 'JwtNotYetValid' } },\n )\n }\n\n if (now > payload.exp) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token expired',\n { Bearer: { error: 'JwtExpired' } },\n )\n }\n\n // Prevent issuer from generating very long-lived tokens\n if (\n timeDiff(now, payload.exp) > options.maxAge ||\n timeDiff(now, payload.iat) > options.maxAge\n ) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token too old',\n { Bearer: { error: 'JwtTooOld' } },\n )\n }\n\n if (payload.lxm != null && typeof payload.lxm !== options.lxm) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT lexicon method (\"lxm\")',\n { Bearer: { error: 'BadJwtLexiconMethod' } },\n )\n }\n\n if (payload.nonce != null && !(await (0, options.unique)(payload.nonce))) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Replay attack detected: nonce is not unique',\n { Bearer: { error: 'NonceNotUnique' } },\n )\n }\n\n return {\n header,\n payload,\n message: textEncoder.encode(`${headerB64}.${payloadB64}`),\n signature: fromBase64(signatureB64, 'base64url'),\n }\n}\n\nconst textEncoder = /*#__PURE__*/ new TextEncoder()\n\ntype HeaderObject = { alg: string; typ?: string }\nfunction isHeaderObject(obj: unknown): obj is HeaderObject {\n return (\n isPlainObject(obj) &&\n typeof obj.alg === 'string' &&\n (obj.typ === undefined || typeof obj.typ === 'string')\n )\n}\n\ntype PayloadObject = {\n iss: DidString\n aud: DidString\n exp: number\n iat?: number\n nbf?: number\n lxm?: string\n nonce?: string\n}\nexport function isPayloadObject(obj: unknown): obj is PayloadObject {\n return (\n isPlainObject(obj) &&\n typeof obj.iss === 'string' &&\n typeof obj.aud === 'string' &&\n (obj.lxm === undefined || typeof obj.lxm === 'string') &&\n (obj.nonce === undefined || typeof obj.nonce === 'string') &&\n (obj.iat === undefined || isPositiveInt(obj.iat)) &&\n (obj.nbf === undefined || isPositiveInt(obj.nbf)) &&\n isPositiveInt(obj.exp) &&\n isDidString(obj.iss) &&\n isDidString(obj.aud)\n )\n}\n\nfunction timeDiff(t1: number, t2?: number): number {\n if (t2 === undefined) return 0\n return Math.abs(t1 - t2)\n}\n\nfunction isPositiveInt(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value > 0\n}\n\nfunction jsonFromBase64<T>(b64: string, isType: (obj: unknown) => obj is T): T {\n const obj = JSON.parse(utf8FromBase64(b64, 'base64url'))\n if (isType(obj)) return obj\n throw new Error('Invalid type')\n}\n"]}
|
|
1
|
+
{"version":3,"file":"service-auth.js","sourceRoot":"","sources":["../src/service-auth.ts"],"names":[],"mappings":"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","sourcesContent":["import * as crypto from '@atproto/crypto'\nimport {\n AtprotoDid,\n AtprotoDidDocument,\n Did,\n matchesIdentifier,\n} from '@atproto/did'\nimport { fromBase64, isPlainObject, utf8FromBase64 } from '@atproto/lex-data'\nimport { DidString, isDidString } from '@atproto/lex-schema'\nimport {\n CreateDidResolverOptions,\n createDidResolver,\n} from '@atproto-labs/did-resolver'\nimport { LexServerAuthError } from './errors.js'\nimport type { LexRouterAuth } from './lex-router.js'\n\nconst BEARER_PREFIX = 'Bearer '\n\n/**\n * Callback function to check and record nonce uniqueness.\n *\n * Used to prevent replay attacks by ensuring each nonce is only used once.\n * The implementation must track nonces for at least the `maxAge` duration\n * (default 5 minutes before and after the current time).\n *\n * @param nonce - The nonce string from the JWT token\n * @returns Promise resolving to `true` if the nonce is unique (first time seen),\n * `false` if it has been seen before\n *\n * @example\n * ```typescript\n * // Using Redis for nonce tracking\n * const checkNonce: UniqueNonceChecker = async (nonce) => {\n * const key = `nonce:${nonce}`\n * const result = await redis.setnx(key, '1')\n * if (result === 1) {\n * await redis.expire(key, 600) // 10 minutes TTL\n * return true\n * }\n * return false\n * }\n * ```\n */\nexport type UniqueNonceChecker = (nonce: string) => Promise<boolean>\n\n/**\n * Configuration options for AT Protocol service authentication.\n *\n * Service auth is used for server-to-server communication in the AT Protocol,\n * where one service authenticates to another using signed JWT tokens tied to\n * the caller's DID.\n *\n * @example\n * ```typescript\n * const options: ServiceAuthOptions = {\n * audience: 'did:web:api.example.com',\n * unique: async (nonce) => nonceStore.checkAndAdd(nonce),\n * maxAge: 300, // 5 minutes\n * // Optional DID resolver options\n * plcDirectoryUrl: 'https://plc.directory'\n * }\n * ```\n */\nexport type ServiceAuthOptions = CreateDidResolverOptions & {\n /**\n * Expected audience (\"aud\") claim in the JWT token.\n *\n * This should be the DID of your service. The token must include this\n * value in its `aud` claim to be accepted. Set to `null` to skip\n * audience verification (not recommended for production).\n */\n audience: null | DidString\n /**\n * Function to check and record nonce uniqueness.\n *\n * This is critical for preventing replay attacks. The value checked here\n * must be unique within `maxAge` seconds before and after the current time.\n *\n * @param nonce - The nonce to check\n * @returns Promise resolving to `true` if unique, `false` if seen before\n */\n unique: UniqueNonceChecker\n /**\n * Maximum age of the JWT token in seconds.\n *\n * Tokens with `iat` (issued at) or `exp` (expiry) timestamps outside\n * this window from the current time will be rejected.\n *\n * @default 300 (5 minutes)\n */\n maxAge?: number\n}\n\n/**\n * Credentials returned after successful service authentication.\n *\n * Contains the verified DID, resolved DID document, and parsed JWT token.\n * These are available in handler context as `ctx.credentials`.\n *\n * @example\n * ```typescript\n * router.add(protectedMethod, {\n * handler: async (ctx) => {\n * const { did, didDocument, jwt } = ctx.credentials\n * console.log('Request from:', did)\n * console.log('Token expires:', new Date(jwt.payload.exp * 1000))\n * return { body: { callerDid: did } }\n * },\n * auth: serviceAuth({ audience: myDid, unique: checkNonce })\n * })\n * ```\n */\nexport type ServiceAuthCredentials = {\n /** The verified AT Protocol DID of the caller. */\n did: AtprotoDid\n /** The resolved DID document of the caller. */\n didDocument: AtprotoDidDocument\n /** The parsed and validated JWT token. */\n jwt: ParsedJwt\n}\n\n/**\n * Creates an authentication handler for verifying AT Protocol service auth JWTs.\n *\n * Service auth is the standard authentication mechanism for server-to-server\n * communication in the AT Protocol. It uses JWT bearer tokens signed by the\n * caller's DID signing key, with the signature verified against the public\n * key in the caller's DID document.\n *\n * The handler performs the following validations:\n * - Extracts and parses the Bearer token from the Authorization header\n * - Validates JWT structure and claims (aud, exp, iat, lxm, nonce)\n * - Resolves the issuer's DID document\n * - Verifies the JWT signature against the `#atproto` verification method\n * - Checks nonce uniqueness to prevent replay attacks\n *\n * @param options - Configuration options for service auth\n * @returns An auth handler function for use with {@link LexRouter.add}\n *\n * @example Basic usage\n * ```typescript\n * import { LexRouter, serviceAuth } from '@atproto/lex-server'\n *\n * const router = new LexRouter()\n *\n * const auth = serviceAuth({\n * audience: 'did:web:api.example.com',\n * unique: async (nonce) => {\n * // Check if nonce has been seen, return true if unique\n * const isNew = await redis.setnx(`nonce:${nonce}`, '1')\n * if (isNew) await redis.expire(`nonce:${nonce}`, 600)\n * return isNew\n * }\n * })\n *\n * router.add(myMethod, {\n * handler: async (ctx) => {\n * console.log('Authenticated as:', ctx.credentials.did)\n * return { body: { success: true } }\n * },\n * auth\n * })\n * ```\n */\nexport function serviceAuth({\n audience,\n maxAge = 5 * 60,\n unique,\n ...options\n}: ServiceAuthOptions): LexRouterAuth<ServiceAuthCredentials> {\n const didResolver = createDidResolver(options)\n\n return async ({ request, method }) => {\n const { signal } = request\n const jwt = await parseJwtBearer(request, {\n lxm: method.nsid,\n maxAge,\n audience,\n unique,\n })\n\n let didDocument: AtprotoDidDocument = await didResolver\n .resolve(jwt.payload.iss, { signal })\n .catch((cause) => {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not resolve DID document',\n { Bearer: { error: 'DidResolutionFailed' } },\n { cause },\n )\n })\n\n const key = getAtprotoSigningKey(didDocument)\n\n if (!key || !(await verifyJwt(jwt, key))) {\n signal.throwIfAborted()\n\n // Try refreshing the DID document in case it was updated\n didDocument = await didResolver\n .resolve(jwt.payload.iss, { signal, noCache: true })\n .catch((cause) => {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not resolve DID document',\n { Bearer: { error: 'DidResolutionFailed' } },\n { cause },\n )\n })\n\n // Verify again with the fresh key (if it changed)\n const keyFresh = getAtprotoSigningKey(didDocument)\n if (!keyFresh || key === keyFresh || !(await verifyJwt(jwt, keyFresh))) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT signature',\n { Bearer: { error: 'BadJwtSignature' } },\n )\n }\n }\n\n return {\n did: didDocument.id,\n didDocument,\n jwt,\n }\n }\n}\n\nasync function verifyJwt(jwt: ParsedJwt, key: Did<'key'>) {\n try {\n return await crypto.verifySignature(key, jwt.message, jwt.signature, {\n jwtAlg: jwt.header.alg,\n allowMalleableSig: true,\n })\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Could not verify JWT signature',\n { Bearer: { error: 'BadJwtSignature' } },\n { cause },\n )\n }\n}\n\nfunction getAtprotoSigningKey(\n didDocument: AtprotoDidDocument,\n): null | Did<'key'> {\n try {\n const key = didDocument.verificationMethod?.find(\n isAtprotoVerificationMethod,\n didDocument,\n )\n\n if (key?.publicKeyMultibase) {\n if (key.type === 'EcdsaSecp256r1VerificationKey2019') {\n const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n return crypto.formatDidKey(crypto.P256_JWT_ALG, keyBytes)\n } else if (key.type === 'EcdsaSecp256k1VerificationKey2019') {\n const keyBytes = crypto.multibaseToBytes(key.publicKeyMultibase)\n return crypto.formatDidKey(crypto.SECP256K1_JWT_ALG, keyBytes)\n } else if (key.type === 'Multikey') {\n const parsed = crypto.parseMultikey(key.publicKeyMultibase)\n return crypto.formatDidKey(parsed.jwtAlg, parsed.keyBytes)\n }\n }\n } catch {\n // Invalid key, ignore\n }\n\n return null\n}\n\nfunction isAtprotoVerificationMethod<\n V extends string | { id: string; type: string; publicKeyMultibase?: string },\n>(\n this: AtprotoDidDocument,\n vm: V,\n): vm is Exclude<V, string> & {\n id: `${string}#atproto`\n} {\n return typeof vm === 'object' && matchesIdentifier(this.id, 'atproto', vm.id)\n}\n\nasync function parseJwtBearer(\n request: Request,\n options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n const authorization = request.headers.get('authorization')\n if (!authorization?.startsWith(BEARER_PREFIX)) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Bearer token required',\n { Bearer: { error: 'MissingBearer' } },\n )\n }\n\n const token = authorization.slice(BEARER_PREFIX.length).trim()\n\n return parseJwt(token, options)\n}\n\n/**\n * Options for parsing and validating a JWT token.\n */\nexport type ParseJwtOptions = {\n /** Maximum age in seconds for token validity window. */\n maxAge: number\n /** Expected audience claim, or null to skip audience verification. */\n audience: null | DidString\n /** Function to check nonce uniqueness. */\n unique: UniqueNonceChecker\n /** Expected lexicon method NSID for the `lxm` claim. */\n lxm: string\n}\n\n/**\n * A parsed and partially validated JWT token.\n *\n * Contains the decoded header and payload, along with the raw bytes\n * needed for signature verification.\n *\n * @example\n * ```typescript\n * const jwt: ParsedJwt = {\n * header: { alg: 'ES256K', typ: 'JWT' },\n * payload: {\n * iss: 'did:plc:abc123',\n * aud: 'did:web:api.example.com',\n * exp: 1704067200,\n * iat: 1704066900,\n * lxm: 'com.atproto.sync.getBlob'\n * },\n * message: new Uint8Array([...]),\n * signature: new Uint8Array([...])\n * }\n * ```\n */\nexport type ParsedJwt = {\n /** The decoded JWT header containing algorithm and type. */\n header: HeaderObject\n /** The decoded JWT payload containing claims. */\n payload: PayloadObject\n /** The raw header.payload bytes for signature verification. */\n message: Uint8Array\n /** The decoded signature bytes. */\n signature: Uint8Array\n}\n\nasync function parseJwt(\n token: string,\n options: ParseJwtOptions,\n): Promise<ParsedJwt> {\n const {\n length,\n 0: headerB64,\n 1: payloadB64,\n 2: signatureB64,\n } = token.split('.')\n if (length !== 3) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n )\n }\n\n let header: HeaderObject\n try {\n header = jsonFromBase64(headerB64, isHeaderObject)\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n { cause },\n )\n }\n\n if (\n header.alg === 'none' ||\n // service tokens are not OAuth 2.0 access tokens\n // https://datatracker.ietf.org/doc/html/rfc9068\n header.typ === 'at+jwt' ||\n // \"refresh+jwt\" is a non-standard type used by the @atproto packages\n header.typ === 'refresh+jwt' ||\n // \"DPoP\" proofs are not meant to be used as service tokens\n // https://datatracker.ietf.org/doc/html/rfc9449\n header.typ === 'dpop+jwt'\n ) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n )\n }\n\n let payload: PayloadObject\n try {\n payload = jsonFromBase64(payloadB64, isPayloadObject)\n } catch (cause) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT token',\n { Bearer: { error: 'BadJwt' } },\n { cause },\n )\n }\n\n if (options.audience !== null && options.audience !== payload.aud) {\n throw new LexServerAuthError('AuthenticationRequired', 'Invalid audience', {\n Bearer: { error: 'InvalidAudience' },\n })\n }\n\n const now = Math.floor(Date.now() / 1000)\n\n if (payload.nbf != null && now < payload.nbf) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token not yet valid',\n { Bearer: { error: 'JwtNotYetValid' } },\n )\n }\n\n if (now > payload.exp) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token expired',\n { Bearer: { error: 'JwtExpired' } },\n )\n }\n\n // Prevent issuer from generating very long-lived tokens\n if (\n timeDiff(now, payload.exp) > options.maxAge ||\n timeDiff(now, payload.iat) > options.maxAge\n ) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'JWT token too old',\n { Bearer: { error: 'JwtTooOld' } },\n )\n }\n\n if (payload.lxm != null && payload.lxm !== options.lxm) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Invalid JWT lexicon method (\"lxm\")',\n { Bearer: { error: 'BadJwtLexiconMethod' } },\n )\n }\n\n if (payload.nonce != null && !(await (0, options.unique)(payload.nonce))) {\n throw new LexServerAuthError(\n 'AuthenticationRequired',\n 'Replay attack detected: nonce is not unique',\n { Bearer: { error: 'NonceNotUnique' } },\n )\n }\n\n return {\n header,\n payload,\n message: textEncoder.encode(`${headerB64}.${payloadB64}`),\n signature: fromBase64(signatureB64, 'base64url'),\n }\n}\n\nconst textEncoder = /*#__PURE__*/ new TextEncoder()\n\ntype HeaderObject = { alg: string; typ?: string }\nfunction isHeaderObject(obj: unknown): obj is HeaderObject {\n return (\n isPlainObject(obj) &&\n typeof obj.alg === 'string' &&\n (obj.typ === undefined || typeof obj.typ === 'string')\n )\n}\n\ntype PayloadObject = {\n iss: DidString\n aud: DidString\n exp: number\n iat?: number\n nbf?: number\n lxm?: string\n nonce?: string\n}\nexport function isPayloadObject(obj: unknown): obj is PayloadObject {\n return (\n isPlainObject(obj) &&\n typeof obj.iss === 'string' &&\n typeof obj.aud === 'string' &&\n (obj.lxm === undefined || typeof obj.lxm === 'string') &&\n (obj.nonce === undefined || typeof obj.nonce === 'string') &&\n (obj.iat === undefined || isPositiveInt(obj.iat)) &&\n (obj.nbf === undefined || isPositiveInt(obj.nbf)) &&\n isPositiveInt(obj.exp) &&\n isDidString(obj.iss) &&\n isDidString(obj.aud)\n )\n}\n\nfunction timeDiff(t1: number, t2?: number): number {\n if (t2 === undefined) return 0\n return Math.abs(t1 - t2)\n}\n\nfunction isPositiveInt(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value > 0\n}\n\nfunction jsonFromBase64<T>(b64: string, isType: (obj: unknown) => obj is T): T {\n const obj = JSON.parse(utf8FromBase64(b64, 'base64url'))\n if (isType(obj)) return obj\n throw new Error('Invalid type')\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/lex-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.18",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Request router for Atproto Lexicon protocols and schemas",
|
|
6
6
|
"keywords": [
|
|
@@ -49,17 +49,17 @@
|
|
|
49
49
|
"@atproto/crypto": "^0.4.5",
|
|
50
50
|
"@atproto/did": "^0.3.0",
|
|
51
51
|
"@atproto/lex-cbor": "^0.0.16",
|
|
52
|
-
"@atproto/lex-client": "^0.0.
|
|
52
|
+
"@atproto/lex-client": "^0.0.21",
|
|
53
53
|
"@atproto/lex-data": "^0.0.15",
|
|
54
54
|
"@atproto/lex-json": "^0.0.16",
|
|
55
|
-
"@atproto/lex-schema": "^0.0.
|
|
55
|
+
"@atproto/lex-schema": "^0.0.20"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/ws": "^8.18.1",
|
|
59
59
|
"@vitest/coverage-v8": "4.0.16",
|
|
60
60
|
"vitest": "^4.0.16",
|
|
61
|
-
"@atproto/lex": "^0.0.
|
|
62
|
-
"@atproto/lex-client": "^0.0.
|
|
61
|
+
"@atproto/lex": "^0.0.26",
|
|
62
|
+
"@atproto/lex-client": "^0.0.21"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"build": "tsc --build tsconfig.build.json",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { serviceAuth } from './service-auth.js'
|
|
3
|
+
|
|
4
|
+
describe('serviceAuth - lxm validation', () => {
|
|
5
|
+
const audience = 'did:web:api.example.com'
|
|
6
|
+
const issuer = 'did:web:caller.example.com'
|
|
7
|
+
const nsid = 'io.example.test'
|
|
8
|
+
|
|
9
|
+
function makeJwt(payload: Record<string, unknown>): string {
|
|
10
|
+
const header = Buffer.from(
|
|
11
|
+
JSON.stringify({ alg: 'ES256K', typ: 'JWT' }),
|
|
12
|
+
).toString('base64url')
|
|
13
|
+
const body = Buffer.from(JSON.stringify(payload)).toString('base64url')
|
|
14
|
+
const sig = Buffer.from([0]).toString('base64url')
|
|
15
|
+
return `${header}.${body}.${sig}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function basePayload(overrides: Record<string, unknown> = {}) {
|
|
19
|
+
const now = Math.floor(Date.now() / 1000)
|
|
20
|
+
return { iss: issuer, aud: audience, iat: now, exp: now + 60, ...overrides }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function setup() {
|
|
24
|
+
const resolve = vi.fn(async () => {
|
|
25
|
+
throw new Error('stop after lxm check')
|
|
26
|
+
})
|
|
27
|
+
const auth = serviceAuth({
|
|
28
|
+
audience,
|
|
29
|
+
unique: async () => true,
|
|
30
|
+
didResolver: { resolve },
|
|
31
|
+
})
|
|
32
|
+
return { auth, resolve }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
it('rejects with BadJwtLexiconMethod when lxm does not match method.nsid', async () => {
|
|
36
|
+
const { auth, resolve } = setup()
|
|
37
|
+
const jwt = makeJwt(basePayload({ lxm: 'io.example.different' }))
|
|
38
|
+
const request = new Request(`https://api.example.com/xrpc/${nsid}`, {
|
|
39
|
+
headers: { authorization: `Bearer ${jwt}` },
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
await expect(
|
|
43
|
+
auth({ request, method: { nsid } as any, params: {} }),
|
|
44
|
+
).rejects.toThrow('Invalid JWT lexicon method ("lxm")')
|
|
45
|
+
expect(resolve).not.toHaveBeenCalled()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('passes lxm check when payload.lxm matches method.nsid', async () => {
|
|
49
|
+
const { auth, resolve } = setup()
|
|
50
|
+
const jwt = makeJwt(basePayload({ lxm: nsid }))
|
|
51
|
+
const request = new Request(`https://api.example.com/xrpc/${nsid}`, {
|
|
52
|
+
headers: { authorization: `Bearer ${jwt}` },
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
await expect(
|
|
56
|
+
auth({ request, method: { nsid } as any, params: {} }),
|
|
57
|
+
).rejects.toThrow()
|
|
58
|
+
// The DID resolver isn't called unless "lxm" validation succeeded
|
|
59
|
+
expect(resolve).toHaveBeenCalled()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('skips lxm check when payload has no lxm claim', async () => {
|
|
63
|
+
const { auth, resolve } = setup()
|
|
64
|
+
const jwt = makeJwt(basePayload())
|
|
65
|
+
const request = new Request(`https://api.example.com/xrpc/${nsid}`, {
|
|
66
|
+
headers: { authorization: `Bearer ${jwt}` },
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
await expect(
|
|
70
|
+
auth({ request, method: { nsid } as any, params: {} }),
|
|
71
|
+
).rejects.toThrow()
|
|
72
|
+
expect(resolve).toHaveBeenCalled()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('rejects an empty-string lxm claim against a real NSID', async () => {
|
|
76
|
+
const { auth, resolve } = setup()
|
|
77
|
+
const jwt = makeJwt(basePayload({ lxm: '' }))
|
|
78
|
+
const request = new Request(`https://api.example.com/xrpc/${nsid}`, {
|
|
79
|
+
headers: { authorization: `Bearer ${jwt}` },
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
await expect(
|
|
83
|
+
auth({ request, method: { nsid } as any, params: {} }),
|
|
84
|
+
).rejects.toThrow('Invalid JWT lexicon method ("lxm")')
|
|
85
|
+
expect(resolve).not.toHaveBeenCalled()
|
|
86
|
+
})
|
|
87
|
+
})
|
package/src/service-auth.ts
CHANGED
|
@@ -442,7 +442,7 @@ async function parseJwt(
|
|
|
442
442
|
)
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
if (payload.lxm != null &&
|
|
445
|
+
if (payload.lxm != null && payload.lxm !== options.lxm) {
|
|
446
446
|
throw new LexServerAuthError(
|
|
447
447
|
'AuthenticationRequired',
|
|
448
448
|
'Invalid JWT lexicon method ("lxm")',
|