// com.atproto.server.* handler tests: the refusals each endpoint owes a // caller, and the two paths by which an account arrives on an empty server. import { afterEach, describe, expect, it, vi } from 'vitest'; import { createAccessJwt, createRefreshJwt, } from '../packages/core/src/auth.js'; import { generateKeyPair, importPrivateKey, } from '../packages/core/src/crypto.js'; import { createXrpcServerHandlers } from '../packages/core/src/handlers/xrpc-server.js'; import { kemDidKey } from '../packages/core/src/kem.js'; import { publicKeyToDidKey } from '../packages/core/src/plc.js'; import { createSessionRegistry } from '../packages/core/src/session.js'; const DID = 'did:plc:xrpcserveraccount2345672'; const HOSTNAME = 'pds.example.com'; const SECRET = 'test-secret'; const PASSWORD = 'operator-password'; /** * @param {{ * did?: string|null, handle?: string|null, readOnly?: boolean, * noPassword?: boolean, appPasswords?: boolean, * verifier?: {verify: (didKey: string, data: Uint8Array, sig: Uint8Array) => Promise}|null, * didResolver?: ((did: string) => Promise)|null, * spacesEnabled?: boolean, * }} [options] */ function createServer({ did = null, handle = null, readOnly = false, noPassword = false, appPasswords = true, verifier = null, didResolver = null, spacesEnabled = false, } = {}) { const stored = { did, handle, /** @type {Uint8Array|null} */ privateKey: null, /** @type {Uint8Array|null} */ kemKey: null, /** @type {{address: string, verified: boolean}|null} */ email: null, /** @type {string|null} */ status: null, }; /** @type {Map} */ const sessionRows = new Map(); /** @type {Map} */ const appPasswordRows = new Map(); /** @type {Array<{type: string, body: Object}>} */ const events = []; /** @type {string[]} */ const crawled = []; const sharedStorage = /** @type {any} */ ({ putSession: async (/** @type {any} */ s) => void sessionRows.set(s.jti, { ...s }), getSession: async (/** @type {string} */ jti) => sessionRows.get(jti) || null, listSessions: async () => [...sessionRows.values()], deleteSession: async (/** @type {string} */ jti) => sessionRows.delete(jti), putActor: async () => {}, listAppPasswords: async () => [...appPasswordRows.values()], deleteAppPassword: async ( /** @type {string} */ _d, /** @type {string} */ n, ) => appPasswordRows.delete(n), putAppPassword: async ( /** @type {string} */ _d, /** @type {string} */ name, /** @type {string} */ hash, /** @type {boolean} */ privileged, /** @type {string} */ createdAt, ) => { if (appPasswordRows.has(name)) return false; appPasswordRows.set(name, { name, hash, privileged, createdAt }); return true; }, }); // A deployment whose storage predates app passwords answers no lookup. if (appPasswords) { sharedStorage.getAppPasswordByHash = async ( /** @type {string} */ _d, /** @type {string} */ hash, ) => [...appPasswordRows.values()].find((r) => r.hash === hash) || null; } const handlers = createXrpcServerHandlers({ actorStorage: /** @type {any} */ ({ getDid: async () => stored.did, setDid: async (/** @type {string} */ d) => { stored.did = d; }, getHandle: async () => stored.handle, setHandle: async (/** @type {string} */ h) => { stored.handle = h; }, setPrivateKey: async (/** @type {Uint8Array} */ k) => { stored.privateKey = k; }, getKemKey: async () => stored.kemKey ?? null, setKemKey: async (/** @type {Uint8Array} */ k) => { stored.kemKey = k; }, setEmail: async (/** @type {any} */ e) => { stored.email = e; }, setAccountStatus: async (/** @type {string} */ s) => { stored.status = s; }, }), sharedStorage, sessions: createSessionRegistry(sharedStorage), jwtSecret: SECRET, hostname: HOSTNAME, plcUrl: 'https://plc.example.com', blobUploadLimit: 50 * 1024 * 1024, password: noPassword ? undefined : PASSWORD, verifyAccountPassword: async (candidate) => !noPassword && candidate === PASSWORD, passwordConfigured: async () => !noPassword, readOnly, spacesEnabled, verifier: /** @type {any} */ (verifier), didResolver, getDid: async () => stored.did, getSigningKey: async () => null, checkRpcScope: () => null, generateSigningKey: async () => { const keyPair = await generateKeyPair(); return { stored: keyPair.privateKey, didKey: publicKeyToDidKey(keyPair.publicKey), signingKey: await importPrivateKey(keyPair.privateKey), }; }, emailSessionFields: async () => ({}), emitEvent: async (type, body) => { events.push({ type, body }); }, notifyRelay: async (d) => { crawled.push(d); }, readOnlyError: () => Response.json( { error: 'AuthenticationRequired', message: 'This PDS is read-only' }, { status: 401 }, ), setCachedIdentity: () => {}, }); return { handlers, stored, sessionRows, appPasswordRows, events, crawled }; } /** * @param {ReturnType} handlers * @param {string} path * @param {{body?: unknown, raw?: string, headers?: Record, auth?: {did: string, scope: string}|null}} [options] */ async function call(handlers, path, { body, raw, headers, auth = null } = {}) { const route = handlers.routes[path]; const url = new URL(`https://${HOSTNAME}${path}`); /** @type {RequestInit} */ const init = { method: route.method || 'GET', headers }; if (raw !== undefined) init.body = raw; else if (body !== undefined) init.body = JSON.stringify(body); const handler = /** @type {(request: Request, url: URL, auth: {did: string, scope: string}|null) => Promise} */ ( route.handler ); return handler(new Request(url, init), url, auth); } const P = { describe: '/xrpc/com.atproto.server.describeServer', createSession: '/xrpc/com.atproto.server.createSession', createAccount: '/xrpc/com.atproto.server.createAccount', getSession: '/xrpc/com.atproto.server.getSession', createAppPassword: '/xrpc/com.atproto.server.createAppPassword', listAppPasswords: '/xrpc/com.atproto.server.listAppPasswords', revokeAppPassword: '/xrpc/com.atproto.server.revokeAppPassword', deleteSession: '/xrpc/com.atproto.server.deleteSession', refreshSession: '/xrpc/com.atproto.server.refreshSession', onboarding: '/account/onboarding/create', }; const FULL = { did: DID, scope: 'com.atproto.access' }; const APP_PASS = { did: DID, scope: 'com.atproto.appPass' }; afterEach(() => { vi.unstubAllGlobals(); }); describe('describeServer', () => { it('names the did:web identity before an account is hosted', async () => { const { handlers } = createServer(); const response = await call(handlers, P.describe); const body = await response.json(); expect(body.did).toBe(`did:web:${HOSTNAME}`); expect(body.availableUserDomains).toEqual([`.${HOSTNAME}`]); }); it('reports the largest blob it accepts', async () => { const { handlers } = createServer(); const response = await call(handlers, P.describe); const body = await response.json(); expect(body.blobUploadLimit).toBe(50 * 1024 * 1024); }); it('says it serves the spaces alpha only when spaces are on', async () => { const off = await call(createServer().handlers, P.describe); expect((await off.json()).spacesAlpha).toBeUndefined(); const on = await call( createServer({ spacesEnabled: true }).handlers, P.describe, ); expect((await on.json()).spacesAlpha).toBe(true); }); }); describe('createSession', () => { it('refuses a login on a read-only server', async () => { const { handlers } = createServer({ readOnly: true }); const response = await call(handlers, P.createSession, { body: { identifier: DID, password: PASSWORD }, }); expect(response.status).toBe(401); }); it('reports an uninitialized server', async () => { const { handlers } = createServer(); const response = await call(handlers, P.createSession, { body: { identifier: DID, password: PASSWORD }, }); expect(response.status).toBe(400); expect((await response.json()).message).toBe('PDS not initialized'); }); it('refuses an identifier that is neither the DID nor the handle', async () => { const { handlers } = createServer({ did: DID, handle: 'alice.test' }); const response = await call(handlers, P.createSession, { body: { identifier: 'bob.test', password: PASSWORD }, }); expect(response.status).toBe(401); expect((await response.json()).message).toBe('Invalid identifier'); }); it('refuses every password when none is configured', async () => { const { handlers } = createServer({ did: DID, noPassword: true }); const response = await call(handlers, P.createSession, { body: { identifier: DID, password: '' }, }); expect(response.status).toBe(401); expect((await response.json()).message).toContain( 'Password login is not configured', ); }); it('opens a session for the account password and records it', async () => { const { handlers, sessionRows } = createServer({ did: DID, handle: 'alice.test', }); const response = await call(handlers, P.createSession, { body: { identifier: 'alice.test', password: PASSWORD }, }); const body = await response.json(); expect(body.did).toBe(DID); expect(body.handle).toBe('alice.test'); expect(body.accessJwt).toBeTruthy(); expect([...sessionRows.values()][0].label).toBe('Account password'); }); it('opens a restricted session for an app password', async () => { const { handlers, sessionRows } = createServer({ did: DID }); const created = await call(handlers, P.createAppPassword, { body: { name: 'bot' }, auth: FULL, }); const { password } = await created.json(); const response = await call(handlers, P.createSession, { body: { identifier: DID, password }, auth: null, }); const body = await response.json(); expect(body.did).toBe(DID); expect([...sessionRows.values()][0].label).toBe('App password: bot'); expect([...sessionRows.values()][0].scope).toBe('com.atproto.appPass'); }); it('refuses a wrong password', async () => { const { handlers } = createServer({ did: DID }); const response = await call(handlers, P.createSession, { body: { identifier: DID, password: 'wrong' }, }); expect(response.status).toBe(401); expect((await response.json()).message).toBe('Invalid password'); }); it('refuses an app password when storage keeps none', async () => { const { handlers } = createServer({ did: DID, appPasswords: false }); const response = await call(handlers, P.createSession, { body: { identifier: DID, password: 'whatever' }, }); expect(response.status).toBe(401); }); }); describe('createAccount', () => { /** @param {Record} claims */ function serviceToken(claims) { const encode = (/** @type {Object} */ part) => btoa(JSON.stringify(part)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); return `${encode({ alg: 'ES256' })}.${encode(claims)}.c2ln`; } /** @param {number} [offset] - Seconds from now the token expires */ const migrationClaims = (offset = 60) => ({ iss: DID, aud: `did:web:${HOSTNAME}`, lxm: 'com.atproto.server.createAccount', exp: Math.floor(Date.now() / 1000) + offset, }); it('refuses a migration on a read-only server', async () => { const { handlers } = createServer({ readOnly: true }); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'alice.test' }, }); expect(response.status).toBe(401); }); it('refuses a body that is not JSON', async () => { const { handlers } = createServer(); const response = await call(handlers, P.createAccount, { raw: 'not json' }); expect(response.status).toBe(400); expect((await response.json()).message).toBe('Invalid request body'); }); it('refuses a second account', async () => { const { handlers } = createServer({ did: DID }); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'alice.test' }, }); expect(response.status).toBe(400); expect((await response.json()).message).toBe( 'This server already hosts an account', ); }); it('requires an existing DID', async () => { const { handlers } = createServer(); const response = await call(handlers, P.createAccount, { body: { handle: 'alice.test' }, }); expect((await response.json()).error).toBe('UnsupportedDomain'); }); it('requires a valid handle', async () => { const { handlers } = createServer(); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'not a handle' }, }); expect((await response.json()).error).toBe('InvalidHandle'); }); it('adopts the DID and stores the email the migration carries', async () => { const { handlers, stored } = createServer(); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'alice.test', email: 'alice@example.com', password: PASSWORD, }, }); const body = await response.json(); expect(body.did).toBe(DID); expect(stored.status).toBe('deactivated'); // The PLC operation the migration submits publishes it. expect(stored.kemKey).toHaveLength(32); expect(stored.email).toEqual({ address: 'alice@example.com', verified: false, }); }); it('refuses a caller with neither a token nor the password', async () => { const { handlers } = createServer(); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'alice.test' }, }); expect(response.status).toBe(401); expect((await response.json()).message).toContain('service token'); }); it('refuses a service token whose claims do not match', async () => { const { handlers } = createServer(); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'alice.test' }, headers: { authorization: `Bearer ${serviceToken({ ...migrationClaims(), aud: 'did:web:elsewhere.test' })}`, }, }); expect(response.status).toBe(401); expect((await response.json()).message).toContain('Invalid service token'); }); it('refuses an expired service token', async () => { const { handlers } = createServer(); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'alice.test' }, headers: { authorization: `Bearer ${serviceToken(migrationClaims(-60))}`, }, }); expect(response.status).toBe(401); }); /** * The multibase of a real P-256 did:key. The handler reads the curve to * check it against the token header's `alg`, which the stub verifier below * never sees. */ async function atprotoMultibase() { const { publicKey } = await generateKeyPair(); return publicKeyToDidKey(publicKey).replace('did:key:', ''); } it('accepts a service token whose signature verifies', async () => { const multibase = await atprotoMultibase(); const { handlers } = createServer({ verifier: { verify: async () => true }, didResolver: async () => ({ verificationMethod: [ { id: `${DID}#atproto`, publicKeyMultibase: multibase }, ], }), }); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'alice.test' }, headers: { authorization: `Bearer ${serviceToken(migrationClaims())}` }, }); expect((await response.json()).did).toBe(DID); }); it('refuses a service token whose signature does not verify', async () => { const multibase = await atprotoMultibase(); const { handlers } = createServer({ verifier: { verify: async () => false }, didResolver: async () => ({ verificationMethod: [ { id: `${DID}#atproto`, publicKeyMultibase: multibase }, ], }), }); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'alice.test' }, headers: { authorization: `Bearer ${serviceToken(migrationClaims())}` }, }); expect(response.status).toBe(401); }); it('refuses a token whose issuer publishes no atproto key', async () => { const { handlers } = createServer({ verifier: { verify: async () => true }, didResolver: async () => ({ verificationMethod: [] }), }); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'alice.test' }, headers: { authorization: `Bearer ${serviceToken(migrationClaims())}` }, }); expect(response.status).toBe(401); }); it('refuses a token whose issuer cannot be resolved', async () => { const { handlers } = createServer({ verifier: { verify: async () => true }, didResolver: async () => { throw new Error('unreachable directory'); }, }); const response = await call(handlers, P.createAccount, { body: { did: DID, handle: 'alice.test' }, headers: { authorization: `Bearer ${serviceToken(migrationClaims())}` }, }); expect(response.status).toBe(401); }); }); describe('onboarding', () => { const signup = { password: PASSWORD, handle: 'alice.example.com' }; it('refuses onboarding on a read-only server', async () => { const { handlers } = createServer({ readOnly: true }); const response = await call(handlers, P.onboarding, { body: signup }); expect(response.status).toBe(401); }); it('refuses a cross-origin post', async () => { const { handlers } = createServer(); const response = await call(handlers, P.onboarding, { body: signup, headers: { origin: 'https://attacker.test' }, }); expect(response.status).toBe(403); }); it('refuses a body that is not JSON', async () => { const { handlers } = createServer(); const response = await call(handlers, P.onboarding, { raw: '{' }); expect(response.status).toBe(400); }); it('refuses to claim a server that already hosts an account', async () => { const { handlers } = createServer({ did: DID }); const response = await call(handlers, P.onboarding, { body: signup }); expect(response.status).toBe(400); }); it('refuses when no operator password is configured', async () => { const { handlers } = createServer({ noPassword: true }); const response = await call(handlers, P.onboarding, { body: signup }); expect(response.status).toBe(401); expect((await response.json()).message).toContain('PDS_PASSWORD'); }); it('refuses a wrong operator password', async () => { const { handlers } = createServer(); const response = await call(handlers, P.onboarding, { body: { ...signup, password: 'wrong' }, }); expect(response.status).toBe(401); expect((await response.json()).message).toBe('Invalid server password'); }); it('refuses a handle that is not a domain', async () => { const { handlers } = createServer(); const response = await call(handlers, P.onboarding, { body: { ...signup, handle: 'alice' }, }); expect((await response.json()).error).toBe('InvalidHandle'); }); it('reports a PLC directory it cannot reach', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => { throw new Error('connect ECONNREFUSED'); }), ); const { handlers, stored } = createServer(); const response = await call(handlers, P.onboarding, { body: signup }); expect(response.status).toBe(502); expect((await response.json()).message).toContain('connect ECONNREFUSED'); // Nothing local was written, so the operator can retry. expect(stored.did).toBeNull(); }); it('reports a PLC directory that rejects the identity', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => new Response('handle taken', { status: 400 })), ); const { handlers, stored } = createServer(); const response = await call(handlers, P.onboarding, { body: signup }); expect(response.status).toBe(502); expect((await response.json()).message).toContain('handle taken'); expect(stored.did).toBeNull(); }); it('registers the identity and opens an account cookie session', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => new Response('', { status: 200 })), ); const { handlers, stored, events, crawled } = createServer(); const response = await call(handlers, P.onboarding, { body: { ...signup, email: 'alice@example.com' }, }); const body = await response.json(); expect(body.ok).toBe(true); expect(body.did).toMatch(/^did:plc:[a-z2-7]{24}$/); expect(stored.handle).toBe('alice.example.com'); expect(stored.email?.address).toBe('alice@example.com'); expect(response.headers.get('Set-Cookie')).toContain('pdsjs_account='); expect(events.map((e) => e.type)).toEqual(['identity', 'account']); expect(crawled).toEqual([body.did]); }); it('defaults the handle to the server hostname', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => new Response('', { status: 200 })), ); const { handlers, stored } = createServer(); await call(handlers, P.onboarding, { body: { password: PASSWORD } }); expect(stored.handle).toBe(HOSTNAME); }); it('publishes the KEM key it stores in the genesis operation', async () => { const fetchMock = vi.fn(async () => new Response('', { status: 200 })); vi.stubGlobal('fetch', fetchMock); const { handlers, stored } = createServer(); await call(handlers, P.onboarding, { body: signup }); const [, init] = /** @type {[string, RequestInit]} */ ( /** @type {unknown} */ (fetchMock.mock.calls[0]) ); const operation = JSON.parse(/** @type {string} */ (init.body)); expect(stored.kemKey).toHaveLength(32); expect(operation.verificationMethods.atproto_kem).toBe( await kemDidKey(/** @type {Uint8Array} */ (stored.kemKey)), ); }); }); describe('getSession', () => { it('requires authentication', async () => { const { handlers } = createServer({ did: DID }); const response = await call(handlers, P.getSession); expect(response.status).toBe(401); }); it('answers with the DID and handle', async () => { const { handlers } = createServer({ did: DID, handle: 'alice.test' }); const response = await call(handlers, P.getSession, { auth: FULL }); expect(await response.json()).toEqual({ did: DID, handle: 'alice.test' }); }); }); describe('app passwords', () => { it('refuses creation on a read-only server', async () => { const { handlers } = createServer({ readOnly: true }); const response = await call(handlers, P.createAppPassword, { body: { name: 'bot' }, auth: FULL, }); expect(response.status).toBe(401); }); it('requires authentication to create', async () => { const { handlers } = createServer(); const response = await call(handlers, P.createAppPassword, { body: { name: 'bot' }, }); expect(response.status).toBe(401); }); it('refuses an app-password session managing app passwords', async () => { const { handlers } = createServer(); const response = await call(handlers, P.createAppPassword, { body: { name: 'bot' }, auth: APP_PASS, }); expect(response.status).toBe(403); }); it('requires a name', async () => { const { handlers } = createServer(); const response = await call(handlers, P.createAppPassword, { body: {}, auth: FULL, }); expect(response.status).toBe(400); }); it('refuses a name already taken', async () => { const { handlers } = createServer(); await call(handlers, P.createAppPassword, { body: { name: 'bot' }, auth: FULL, }); const response = await call(handlers, P.createAppPassword, { body: { name: 'bot' }, auth: FULL, }); expect(response.status).toBe(400); expect((await response.json()).message).toContain('already exists'); }); it('lists them for a full session only', async () => { const { handlers } = createServer(); await call(handlers, P.createAppPassword, { body: { name: 'bot', privileged: true }, auth: FULL, }); expect((await call(handlers, P.listAppPasswords)).status).toBe(401); expect( (await call(handlers, P.listAppPasswords, { auth: APP_PASS })).status, ).toBe(403); const response = await call(handlers, P.listAppPasswords, { auth: FULL }); const { passwords } = await response.json(); expect(passwords[0].name).toBe('bot'); expect(passwords[0].privileged).toBe(true); }); it('refuses revocation on a read-only server', async () => { const { handlers } = createServer({ readOnly: true }); const response = await call(handlers, P.revokeAppPassword, { body: { name: 'bot' }, auth: FULL, }); expect(response.status).toBe(401); }); it('requires authentication and a full session to revoke', async () => { const { handlers } = createServer(); expect( (await call(handlers, P.revokeAppPassword, { body: { name: 'bot' } })) .status, ).toBe(401); expect( ( await call(handlers, P.revokeAppPassword, { body: { name: 'bot' }, auth: APP_PASS, }) ).status, ).toBe(403); }); it('requires a name to revoke', async () => { const { handlers } = createServer(); const response = await call(handlers, P.revokeAppPassword, { body: {}, auth: FULL, }); expect(response.status).toBe(400); }); it('reports a name it does not know', async () => { const { handlers } = createServer(); const response = await call(handlers, P.revokeAppPassword, { body: { name: 'ghost' }, auth: FULL, }); expect(response.status).toBe(400); expect((await response.json()).message).toContain('No app password named'); }); it('ends the logins the credential opened', async () => { const { handlers, sessionRows } = createServer({ did: DID }); const created = await call(handlers, P.createAppPassword, { body: { name: 'bot' }, auth: FULL, }); const { password } = await created.json(); await call(handlers, P.createSession, { body: { identifier: DID, password }, }); expect(sessionRows.size).toBe(1); const response = await call(handlers, P.revokeAppPassword, { body: { name: 'bot' }, auth: FULL, }); expect(response.status).toBe(200); expect(sessionRows.size).toBe(0); }); }); describe('deleteSession', () => { it('requires a refresh token', async () => { const { handlers } = createServer(); const response = await call(handlers, P.deleteSession); expect(response.status).toBe(401); expect((await response.json()).message).toBe('Refresh token required'); }); it('refuses a token it cannot verify', async () => { const { handlers } = createServer(); const response = await call(handlers, P.deleteSession, { headers: { authorization: 'Bearer not.a.token' }, }); expect(response.status).toBe(401); expect((await response.json()).message).toBe('Invalid refresh token'); }); it('ends the session its token names', async () => { const { handlers, sessionRows } = createServer({ did: DID }); const opened = await call(handlers, P.createSession, { body: { identifier: DID, password: PASSWORD }, }); const { refreshJwt } = await opened.json(); expect(sessionRows.size).toBe(1); const response = await call(handlers, P.deleteSession, { headers: { authorization: `Bearer ${refreshJwt}` }, }); expect(response.status).toBe(200); expect(sessionRows.size).toBe(0); }); }); describe('refreshSession', () => { it('refuses on a read-only server', async () => { const { handlers } = createServer({ readOnly: true }); const response = await call(handlers, P.refreshSession); expect(response.status).toBe(401); }); it('requires an authorization header', async () => { const { handlers } = createServer(); const response = await call(handlers, P.refreshSession); expect(response.status).toBe(401); expect((await response.json()).message).toBe('Refresh token required'); }); it('requires a Bearer scheme', async () => { const { handlers } = createServer(); const response = await call(handlers, P.refreshSession, { headers: { authorization: 'Basic abc' }, }); expect(response.status).toBe(401); expect((await response.json()).message).toBe( 'Invalid authorization header', ); }); it('names an access token used in place of a refresh token', async () => { const { handlers } = createServer(); const accessJwt = await createAccessJwt(DID, SECRET, 3600); const response = await call(handlers, P.refreshSession, { headers: { authorization: `Bearer ${accessJwt}` }, }); expect(response.status).toBe(400); expect((await response.json()).message).toContain('Refresh token required'); }); it('refuses a token signed with another secret', async () => { const { handlers } = createServer(); const foreign = await createRefreshJwt(DID, 'other-secret', 3600); const response = await call(handlers, P.refreshSession, { headers: { authorization: `Bearer ${foreign}` }, }); expect(response.status).toBe(401); }); it('refuses a token whose session the registry does not hold', async () => { const { handlers } = createServer({ did: DID }); const orphan = await createRefreshJwt(DID, SECRET, 3600); const response = await call(handlers, P.refreshSession, { headers: { authorization: `Bearer ${orphan}` }, }); expect(response.status).toBe(401); expect((await response.json()).message).toBe('Session has been revoked'); }); });