diff --git a/packages/fxa-auth-server/docs/swagger/oauth-server-api.ts b/packages/fxa-auth-server/docs/swagger/oauth-server-api.ts index 80230797eee..1bf130957dd 100644 --- a/packages/fxa-auth-server/docs/swagger/oauth-server-api.ts +++ b/packages/fxa-auth-server/docs/swagger/oauth-server-api.ts @@ -67,7 +67,6 @@ const OAUTH_SERVER_API_DESCRIPTION = { - [POST /v1/destroy](#tag/OAuth-Server-API-Overview/operation/postDestroy) - [POST /v1/introspect](#tag/OAuth-Server-API-Overview/operation/postIntrospect) - [GET /v1/jwks](#tag/OAuth-Server-API-Overview/operation/getJwks) - - [POST /v1/key-data](#tag/OAuth-Server-API-Overview/operation/postKeydata) - [POST /v1/token](#tag/OAuth-Server-API-Overview/operation/postToken) - [POST /v1/verify](#tag/OAuth-Server-API-Overview/operation/postVerify) `, @@ -413,41 +412,6 @@ const JWKS_GET = { }, }; -const KEY_DATA_POST = { - ...TAGS_OAUTH_SERVER, - description: '/v1/key-data', - notes: ['This endpoint returns the required scoped key metadata.'], - plugins: { - 'hapi-swagger': { - responses: { - 200: { - description: swaggerText` - A valid response will return JSON the scoped key information for every scope that has scoped keys. -
- **Example:** - \`\`\` js - { - "https://identity.mozilla.com/apps/sample-scope-can-scope-key": { - "identifier": "https://identity.mozilla.com/apps/sample-scope-can-scope-key", - "keyRotationSecret": "0000000000000000000000000000000000000000000000000000000000000000", - "keyRotationTimestamp": 1506970363512 - } - } - \`\`\` - `, - }, - }, - 'x-codeSamples': [ - { - lang: 'JavaScript', - source: - 'curl -X POST \\\n "https://oauth.accounts.firefox.com/v1/key-data" \\\n -H \'cache-control: no-cache\' \\\n -H \'content-type: application/json\' \\\n -d \'{\n "client_id": "5901bd09376fadaa",\n "assertion": "eyJhbGciOiJSUzI1NiJ9.eyJwdWJsaWMta2V5Ijp7Imt0eSI6IlJTQSIsIm4iOiJvWmdsNkpwM0Iwcm5BVXppNThrdS1iT0RvR3ZuUGNnWU1UdXQ1WkpyQkJiazBCdWU4VUlRQ0dnYVdrYU5Xb29INkktMUZ6SXU0VFpZYnNqWGJ1c2JRRlQxOGREUkN6VVRubFlXdVZXUzhoSWhKc3lhZHJwSHJOVkI1VndmSlRKZVgwTjFpczBXcU1qdUdOc2VMLXluYnFjOVhueElncFJaai05QnZqY2ZKYXNOUTNZdHR3VHZVaFJOLVFGNWgxQkY1MnA2QmdOTVBvWmQ5MC1EU0xydlpseXp6MEh0Q2tFZnNsc013czVkR0ExTlZ1dEwtcGVDeU50VTFzOEtFaDlzcGxXeF9lQlFybTlYQU1kYXp5ZWR6VUpJU1UyMjZmQzhEUHh5c0ZreXpCbjlDQnFDQUpTNjQzTGFydUVDaS1rMGhKOWFmM2JXTmJnWmpSNVJ2NXF4THciLCJlIjoiQVFBQiJ9LCJwcmluY2lwYWwiOnsiZW1haWwiOiIwNjIxMzM0YzIwNjRjNmYzNmJlOGFkOWE0N2M1NTliY2FwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9LCJpYXQiOjE1MDY5Njk2OTU0MzksImV4cCI6MTUwNjk2OTY5NjQzOSwiZnhhLXZlcmlmaWVkRW1haWwiOiIzMjM2NzJiZUBtb3ppbGxhLmNvbSIsImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.hFZd5zFheXOFrXKkJvw6Vpv2l7ctlxuBTvuh5f_jLPAjZoJ9ri-vaJjL_WYBFUvS2xHzfx3-ldxLddyTKwCDAJeB_NkOFL_WJSrMet9C7_Z1hH9HmydeXIT82xJmhrwzW-WOO4ibQvRbocEFiNujynKsg1gS8v0iiYjIX-0cXCrlkxkbVx_8EXJFKDDOGzK9v7Zq6D7gkhP-CHEaNYaTHMn65tLQtBS6snGdaXlxoGHMWmDL6STbnJzWa7sa4QwHf-AgT1rUkQQAUHNa_XLZ0FEzqiCPctMadlihiUZL2V6vxIDBS4mHUF4qj0FvIMJflivDnJVkRNijDuP-h-Lh_A~eyJhbGciOiJSUzI1NiJ9.eyJhdWQiOiJvYXV0aC5meGEiLCJleHAiOjE1MDY5Njk2OTY0MzksImlzcyI6ImFwaS5hY2NvdW50cy5maXJlZm94LmNvbSJ9.M5xyk3RffucgaavjbUm7Eqnt47hzeGbGa2VR3jnVEIlRHfz5S25Qf3ngejwee7XECvIywbaKWeijXFOwS-EkB-7qP1gl4oNJjPmbnCk7S1lgckLWvdMIU-HLGKjrN6Mw76__LzvAbsusSeGmsvTCIVuOJ49Xs3tC1fLyB_re0QNpCcS6AUnJ1KOxIMEM3Om7ysNO5F_AqcD3PwlEti5lbwSk8iP5TWL12C2Nkb_6Hxze_mA1NZNAHOips9bF2J7oy1hqGoMYj1XYZrsyjpPWEuZQATAPlKSjbh1hq-UtDeT7DlwEmIbIUd3JA8qh1MkHKGgavd4fIMap0IPmr9rs4A",\n "scope": "https://identity.mozilla.com/apps/sample-scope-can-scope-key"\n}\'', - }, - ], - }, - }, -}; - const TOKEN_POST = { ...TAGS_OAUTH_SERVER, description: '/v1/token', @@ -551,7 +515,6 @@ const API_DOCS = { CLIENT_CLIENTID_GET, INTROSPECT_POST, JWKS_GET, - KEY_DATA_POST, TOKEN_POST, VERIFY_POST, }; diff --git a/packages/fxa-auth-server/lib/routes/oauth/key_data.js b/packages/fxa-auth-server/lib/routes/oauth/key_data.js index 1566cc84ac6..5fb0da34dfb 100644 --- a/packages/fxa-auth-server/lib/routes/oauth/key_data.js +++ b/packages/fxa-auth-server/lib/routes/oauth/key_data.js @@ -10,11 +10,7 @@ const validators = require('../../oauth/validators'); const verifyAssertion = require('../../oauth/assertion'); const { validateRequestedGrant } = require('../../oauth/grant'); const { makeAssertionJWT } = require('../../oauth/util'); -const DESCRIPTION = - require('../../../docs/swagger/shared/descriptions').default; const OAUTH_DOCS = require('../../../docs/swagger/oauth-api').default; -const OAUTH_SERVER_DOCS = - require('../../../docs/swagger/oauth-server-api').default; const { getClientServiceTags } = require('../../metrics/client-tags'); /** @@ -97,33 +93,6 @@ module.exports = ({ log, oauthDB, statsd }) => { } return [ - { - method: 'POST', - path: '/key-data', - config: { - ...OAUTH_SERVER_DOCS.KEY_DATA_POST, - cors: { origin: 'ignore' }, - validate: { - payload: Joi.object({ - client_id: validators.clientId.description(DESCRIPTION.clientId), - assertion: validators.assertion - .required() - .description(DESCRIPTION.assertion), - scope: validators.scope.required().description(DESCRIPTION.scope), - }), - }, - response: { - schema: Joi.object().pattern(/^/, [ - Joi.object({ - identifier: Joi.string().required(), - keyRotationSecret: Joi.string().required(), - keyRotationTimestamp: Joi.number().required(), - }), - ]), - }, - handler: keyDataHandler, - }, - }, { method: 'POST', path: '/account/scoped-key-data', diff --git a/packages/fxa-auth-server/test/lib/server.ts b/packages/fxa-auth-server/test/lib/server.ts index acdc485a2b6..38d779581c0 100644 --- a/packages/fxa-auth-server/test/lib/server.ts +++ b/packages/fxa-auth-server/test/lib/server.ts @@ -10,6 +10,8 @@ const version = config.get('apiVersion'); config.set('log.level', 'critical'); config.set('cloudTasks.oidc.aud', 'cloud-tasks'); config.set('cloudTasks.oidc.serviceAccountEmail', 'testo@iam.gcp.g.co'); +// Session-token routes sign assertions that the in-process oauth server must verify. +config.set('oauth.secretKey', config.get('oauthServer.authServerSecrets')[0]); const testConfig = config.getProperties(); const createServer = require('../../bin/key_server'); const { CapabilityService } = require('../../lib/payments/capability'); diff --git a/packages/fxa-auth-server/test/remote/oauth_api.in.spec.ts b/packages/fxa-auth-server/test/remote/oauth_api.in.spec.ts index 74e14167215..39ea9a24cc5 100644 --- a/packages/fxa-auth-server/test/remote/oauth_api.in.spec.ts +++ b/packages/fxa-auth-server/test/remote/oauth_api.in.spec.ts @@ -2644,182 +2644,159 @@ describe('#integration - /v1', function () { }); }); }); + }); - describe('POST /key-data', function () { - let genericRequest; - - beforeEach(function () { - genericRequest = { - url: '/key-data', - payload: { - assertion: AN_ASSERTION, - client_id: SCOPED_CLIENT_ID, - scope: SCOPE_CAN_SCOPE_KEY, - }, - }; - }); - - it('works with a correct response', () => { - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(200); - assertSecurityHeaders(res); - expect(Object.keys(res.result).length).toBe(1); - - const body = res.result[SCOPE_CAN_SCOPE_KEY]; - - expect(body.identifier).toBe( - 'https://identity.mozilla.com/apps/sample-scope-can-scope-key' - ); - expect(body.keyRotationSecret).toBe( - '0000000000000000000000000000000000000000000000000000000000000000' - ); - expect(body.keyRotationTimestamp).toBe(123456); - }); - }); - - it('works with multiple scopes', () => { - const ANOTHER_CAN_SCOPE_KEY = - 'https://identity.mozilla.com/apps/another-can-scope-key'; - genericRequest.payload.scope = `${SCOPE_CAN_SCOPE_KEY} ${ANOTHER_CAN_SCOPE_KEY}`; - - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(200); - assertSecurityHeaders(res); - expect(Object.keys(res.result).length).toBe(2); - - const keyOne = res.result[SCOPE_CAN_SCOPE_KEY]; - const keyTwo = res.result[ANOTHER_CAN_SCOPE_KEY]; + describe('POST /account/scoped-key-data', function () { + const ZERO_KEY_ROTATION_SECRET = '0'.repeat(64); + let genericRequest; - expect(keyOne.identifier).toBe(SCOPE_CAN_SCOPE_KEY); - expect(keyOne.keyRotationSecret).toBe( - '0000000000000000000000000000000000000000000000000000000000000000' - ); - expect(keyOne.keyRotationTimestamp).toBe(123456); + // Injected credentials skip the Hawk scheme so each test controls the assertion claims. + function sessionCredentials(overrides = {}) { + return { + id: unique(32).toString('hex'), + uid: USERID, + email: VEMAIL, + emailVerified: true, + tokenVerified: true, + verifierSetAt: 123456, + lastAuthAt: () => AUTH_AT, + authenticationMethods: AMR, + authenticatorAssuranceLevel: AAL, + ...overrides, + }; + } - expect(keyTwo.identifier).toBe(ANOTHER_CAN_SCOPE_KEY); - expect(keyTwo.keyRotationSecret).toBe( - '0000000000000000000000000000000000000000000000000000000000000000' - ); - expect(keyTwo.keyRotationTimestamp).toBe(123456); - }); - }); + beforeEach(function () { + genericRequest = { + url: '/account/scoped-key-data', + auth: { strategy: 'sessionToken', credentials: sessionCredentials() }, + payload: { + client_id: SCOPED_CLIENT_ID, + scope: SCOPE_CAN_SCOPE_KEY, + }, + }; + }); - it('fails with non-existent client_id', () => { - genericRequest.payload.client_id = BAD_CLIENT_ID; - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(400); - assertSecurityHeaders(res); - const body = res.result; - expect(body.errno).toBe(101); - expect(body.message).toBe('Unknown client'); - }); + it('works with a correct response', async () => { + const res = await Server.api.post(genericRequest); + expect(res.statusCode).toBe(200); + assertSecurityHeaders(res); + expect(res.result).toEqual({ + [SCOPE_CAN_SCOPE_KEY]: { + identifier: SCOPE_CAN_SCOPE_KEY, + keyRotationSecret: ZERO_KEY_ROTATION_SECRET, + keyRotationTimestamp: 123456, + }, }); + }); - it('succeeds with a non-scoped-key scope', () => { - genericRequest.payload.scope = - 'https://identity.mozilla.com/apps/sample-scope'; - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(200); - assertSecurityHeaders(res); - expect(Object.keys(res.result).length).toBe(0); - }); - }); + it('works with multiple scopes', async () => { + const ANOTHER_CAN_SCOPE_KEY = + 'https://identity.mozilla.com/apps/another-can-scope-key'; + genericRequest.payload.scope = `${SCOPE_CAN_SCOPE_KEY} ${ANOTHER_CAN_SCOPE_KEY}`; - it('succeeds with scopes that arent explicitly defined in config', () => { - genericRequest.payload.scope += ' kv'; - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(200); - assertSecurityHeaders(res); - expect(Object.keys(res.result)).toEqual([SCOPE_CAN_SCOPE_KEY]); - }); + const res = await Server.api.post(genericRequest); + expect(res.statusCode).toBe(200); + assertSecurityHeaders(res); + expect(res.result).toEqual({ + [SCOPE_CAN_SCOPE_KEY]: { + identifier: SCOPE_CAN_SCOPE_KEY, + keyRotationSecret: ZERO_KEY_ROTATION_SECRET, + keyRotationTimestamp: 123456, + }, + [ANOTHER_CAN_SCOPE_KEY]: { + identifier: ANOTHER_CAN_SCOPE_KEY, + keyRotationSecret: ZERO_KEY_ROTATION_SECRET, + keyRotationTimestamp: 123456, + }, }); + }); - it('fails with bad assertion', () => { - genericRequest.payload.assertion = AN_ASSERTION + 'invalid'; - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(401); - assertSecurityHeaders(res); - const body = res.result; - expect(body.message).toBe('Invalid assertion'); - }); - }); + it('fails with non-existent client_id', async () => { + genericRequest.payload.client_id = BAD_CLIENT_ID; + const res = await Server.api.post(genericRequest); + expect(res.statusCode).toBe(400); + assertSecurityHeaders(res); + expect(res.result.errno).toBe(162); + expect(res.result.message).toBe('Unknown client_id'); + }); - it('fails for clients that are not allowed the requested scope', () => { - genericRequest.payload.client_id = NO_KEY_SCOPES_CLIENT_ID; + it('succeeds with a non-scoped-key scope', async () => { + genericRequest.payload.scope = + 'https://identity.mozilla.com/apps/sample-scope'; + const res = await Server.api.post(genericRequest); + expect(res.statusCode).toBe(200); + assertSecurityHeaders(res); + expect(res.result).toEqual({}); + }); - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(400); - expect(res.result.message).toBe('Requested scopes are not allowed'); - assertSecurityHeaders(res); - }); - }); + it('succeeds with scopes that arent explicitly defined in config', async () => { + genericRequest.payload.scope += ' kv'; + const res = await Server.api.post(genericRequest); + expect(res.statusCode).toBe(200); + assertSecurityHeaders(res); + expect(Object.keys(res.result)).toEqual([SCOPE_CAN_SCOPE_KEY]); + }); - it('fails for clients that have no allowedScopes', () => { - genericRequest.payload.client_id = NO_ALLOWED_SCOPES_CLIENT_ID; + it('fails with an invalid session token', async () => { + delete genericRequest.auth; + genericRequest.headers = { authorization: `Bearer ${'0'.repeat(64)}` }; + const res = await Server.api.post(genericRequest); + expect(res.statusCode).toBe(401); + assertSecurityHeaders(res); + expect(res.result.errno).toBe(110); + }); - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(400); - expect(res.result.message).toBe('Requested scopes are not allowed'); - assertSecurityHeaders(res); - }); - }); + it.each([ + ['are not allowed the requested scope', NO_KEY_SCOPES_CLIENT_ID], + ['have no allowedScopes', NO_ALLOWED_SCOPES_CLIENT_ID], + ])('fails for clients that %s', async (_, clientId) => { + genericRequest.payload.client_id = clientId; + const res = await Server.api.post(genericRequest); + expect(res.statusCode).toBe(400); + expect(res.result.message).toBe('Requested scopes are not allowed'); + assertSecurityHeaders(res); + }); - it('correctly handles authAt timestamp for newly-created accounts', async () => { - genericRequest.payload.assertion = await genAssertion({ - 'fxa-generation': 1549910733629, - 'fxa-verifiedEmail': VEMAIL, - 'fxa-lastAuthAt': 1549910733, - 'fxa-tokenVerified': true, - 'fxa-amr': AMR, - 'fxa-aal': AAL, - 'fxa-profileChangedAt': Date.now(), - }); - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(200); - assertSecurityHeaders(res); - expect(Object.keys(res.result).length).toBe(1); - }); + it('correctly handles authAt timestamp for newly-created accounts', async () => { + genericRequest.auth.credentials = sessionCredentials({ + verifierSetAt: 1549910733629, + lastAuthAt: () => 1549910733, }); + const res = await Server.api.post(genericRequest); + expect(res.statusCode).toBe(200); + assertSecurityHeaders(res); + expect(Object.keys(res.result)).toEqual([SCOPE_CAN_SCOPE_KEY]); + }); - it('uses fxa-keysChangedAt for the key rotation timestamp', async () => { - genericRequest.payload.assertion = await genAssertion({ - 'fxa-generation': 1549910740000, - 'fxa-verifiedEmail': VEMAIL, - 'fxa-lastAuthAt': 1549910733, - 'fxa-tokenVerified': true, - 'fxa-amr': AMR, - 'fxa-aal': AAL, - 'fxa-profileChangedAt': Date.now(), - 'fxa-keysChangedAt': 1549910340000, - }); - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(200); - assertSecurityHeaders(res); - expect(Object.keys(res.result).length).toBe(1); - const keyOne = res.result[SCOPE_CAN_SCOPE_KEY]; - expect(keyOne.keyRotationTimestamp).toBe(1549910340000); - }); + it('uses fxa-keysChangedAt for the key rotation timestamp', async () => { + genericRequest.auth.credentials = sessionCredentials({ + verifierSetAt: 1549910740000, + lastAuthAt: () => 1549910733, + keysChangedAt: 1549910340000, }); + const res = await Server.api.post(genericRequest); + expect(res.statusCode).toBe(200); + assertSecurityHeaders(res); + expect(Object.keys(res.result)).toEqual([SCOPE_CAN_SCOPE_KEY]); + expect(res.result[SCOPE_CAN_SCOPE_KEY].keyRotationTimestamp).toBe( + 1549910340000 + ); + }); - it('falls back to fxa-generation when fxa-keysChangedAt is falsy', async () => { - genericRequest.payload.assertion = await genAssertion({ - 'fxa-generation': 1549910730000, - 'fxa-verifiedEmail': VEMAIL, - 'fxa-lastAuthAt': 1549910733, - 'fxa-tokenVerified': true, - 'fxa-amr': AMR, - 'fxa-aal': AAL, - 'fxa-profileChangedAt': Date.now(), - 'fxa-keysChangedAt': undefined, - }); - return Server.api.post(genericRequest).then((res) => { - expect(res.statusCode).toBe(200); - assertSecurityHeaders(res); - expect(Object.keys(res.result).length).toBe(1); - const keyOne = res.result[SCOPE_CAN_SCOPE_KEY]; - expect(keyOne.keyRotationTimestamp).toBe(1549910730000); - }); + it('falls back to fxa-generation when fxa-keysChangedAt is falsy', async () => { + genericRequest.auth.credentials = sessionCredentials({ + verifierSetAt: 1549910730000, + lastAuthAt: () => 1549910733, + keysChangedAt: undefined, }); + const res = await Server.api.post(genericRequest); + expect(res.statusCode).toBe(200); + assertSecurityHeaders(res); + expect(Object.keys(res.result)).toEqual([SCOPE_CAN_SCOPE_KEY]); + expect(res.result[SCOPE_CAN_SCOPE_KEY].keyRotationTimestamp).toBe( + 1549910730000 + ); }); });