diff --git a/__tests__/auth.js b/__tests__/auth.js index 59aece5..0b3ee98 100644 --- a/__tests__/auth.js +++ b/__tests__/auth.js @@ -58,9 +58,12 @@ describe('Auth', () => { test('Unknown issuer', async () => { const loadCredentials = jest.fn() - const token = jwt.encode({ - iss: jiraPayload.clientKey - }, jiraPayload.sharedSecret) + const token = jwt.encode( + { + iss: jiraPayload.clientKey + }, + jiraPayload.sharedSecret + ) const req = { body: jiraPayload, @@ -68,20 +71,23 @@ describe('Auth', () => { query: {} } - await expect(jiraAddon.auth(req, { - loadCredentials - })).rejects.toMatchError( - new AuthError('Unknown issuer', 'UNKNOWN_ISSUER') - ) + await expect( + jiraAddon.auth(req, { + loadCredentials + }) + ).rejects.toMatchError(new AuthError('Unknown issuer', 'UNKNOWN_ISSUER')) expect(loadCredentials).toHaveBeenCalledWith(req.body.clientKey) }) test('Invalid signature', async () => { const loadCredentials = jest.fn().mockReturnValue(jiraPayload) - const token = jwt.encode({ - iss: jiraPayload.clientKey - }, 'invalid-shared-secret') + const token = jwt.encode( + { + iss: jiraPayload.clientKey + }, + 'invalid-shared-secret' + ) const req = { body: jiraPayload, @@ -105,10 +111,13 @@ describe('Auth', () => { const loadCredentials = jest.fn().mockReturnValue(jiraPayload) const now = Math.floor(Date.now() / 1000) - const token = jwt.encode({ - iss: jiraPayload.clientKey, - exp: now - 1000 - }, jiraPayload.sharedSecret) + const token = jwt.encode( + { + iss: jiraPayload.clientKey, + exp: now - 1000 + }, + jiraPayload.sharedSecret + ) const req = { body: jiraPayload, @@ -124,10 +133,13 @@ describe('Auth', () => { test('Invalid QSH', async () => { const loadCredentials = jest.fn().mockReturnValue(jiraPayload) - const token = jwt.encode({ - iss: jiraPayload.clientKey, - qsh: 'invalid-qsh' - }, jiraPayload.sharedSecret) + const token = jwt.encode( + { + iss: jiraPayload.clientKey, + qsh: 'invalid-qsh' + }, + jiraPayload.sharedSecret + ) const req = { body: jiraPayload, @@ -136,19 +148,22 @@ describe('Auth', () => { method: 'POST' } - await expect(jiraAddon.auth(req, { - loadCredentials - })).rejects.toMatchError( - new AuthError('Invalid QSH', 'INVALID_QSH') - ) + await expect( + jiraAddon.auth(req, { + loadCredentials + }) + ).rejects.toMatchError(new AuthError('Invalid QSH', 'INVALID_QSH')) expect(loadCredentials).toHaveBeenCalledWith(req.body.clientKey) }) test('No "qsh" in JWT token provided', async () => { const loadCredentials = jest.fn().mockReturnValue(jiraPayload) - const token = jwt.encode({ - iss: jiraPayload.clientKey - }, jiraPayload.sharedSecret) + const token = jwt.encode( + { + iss: jiraPayload.clientKey + }, + jiraPayload.sharedSecret + ) const req = { body: jiraPayload, @@ -157,19 +172,27 @@ describe('Auth', () => { method: 'POST' } - await expect(jiraAddon.auth(req, { - loadCredentials - })).rejects.toMatchError( - new AuthError('JWT did not contain the query string hash (qsh) claim', 'MISSED_QSH') + await expect( + jiraAddon.auth(req, { + loadCredentials + }) + ).rejects.toMatchError( + new AuthError( + 'JWT did not contain the query string hash (qsh) claim', + 'MISSED_QSH' + ) ) expect(loadCredentials).toHaveBeenCalledWith(req.body.clientKey) }) test('No "qsh" in JWT token provided for Bitbucket add-on', async () => { const loadCredentials = jest.fn().mockReturnValue(jiraPayload) - const token = jwt.encode({ - iss: bitbucketPayload.clientKey - }, bitbucketPayload.sharedSecret) + const token = jwt.encode( + { + iss: bitbucketPayload.clientKey + }, + bitbucketPayload.sharedSecret + ) const req = { body: bitbucketPayload, @@ -189,9 +212,12 @@ describe('Auth', () => { test('"skipQsh" passed', async () => { const loadCredentials = jest.fn().mockReturnValue(jiraPayload) - const token = jwt.encode({ - iss: jiraPayload.clientKey - }, jiraPayload.sharedSecret) + const token = jwt.encode( + { + iss: jiraPayload.clientKey + }, + jiraPayload.sharedSecret + ) const req = { body: jiraPayload, @@ -308,4 +334,43 @@ describe('Auth', () => { } `) }) + + test('Extract token from a custom place', async () => { + const token = jwt.encode( + { + iss: jiraPayload.clientKey, + sub: 'test:account-id' + }, + jiraPayload.sharedSecret + ) + + const req = { + headers: {}, + body: jiraPayload, + query: { state: `JWT ${token}` }, + pathname: '/account', + originalUrl: '/api/account', + method: 'POST' + } + + const result = await jiraAddon.auth(req, { + loadCredentials: () => jiraPayload, + customExtractToken: () => req.query.state, + skipQsh: true + }) + + expect(result).toMatchInlineSnapshot(` + Object { + "credentials": Object { + "baseUrl": "https://test.atlassian.net", + "clientKey": "jira-client-key", + "sharedSecret": "shh-secret-cat", + }, + "payload": Object { + "iss": "jira-client-key", + "sub": "test:account-id", + }, + } + `) + }) }) diff --git a/lib/Addon.js b/lib/Addon.js index 68753ce..d61af33 100644 --- a/lib/Addon.js +++ b/lib/Addon.js @@ -55,8 +55,23 @@ class Addon { throw new AuthError('Unauthorized update request', 'UNAUTHORIZED_REQUEST') } - async auth (req, { skipQsh, loadCredentials }) { - const token = util.extractToken(req) + /** + * Callback for extracting token. + * + * @callback loadCredentialsCallback + */ + + /** + * + * @param {*} req + * @param {Object} options + * @param {boolean} options.skipQsh + * @param {() => {}} options.loadCredentials + * @param {() => {}} options.customExtractToken - custom function to extract + * token in addition to default `req.headers.authorization` and `req.query.jwt` + */ + async auth (req, { skipQsh, loadCredentials, customExtractToken }) { + const token = util.extractToken(req, customExtractToken) if (!token) { throw new AuthError('Missed token', 'MISSED_TOKEN') diff --git a/lib/util.js b/lib/util.js index 47acbdf..2635156 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,8 +1,10 @@ const jwt = require('atlassian-jwt') const AuthError = require('./AuthError') -function extractToken (req) { - const token = req.headers.authorization || req.query.jwt || '' +const noop = () => {} + +function extractToken (req, customExtractToken = noop) { + const token = req.headers.authorization || req.query.jwt || customExtractToken() || '' return token.replace(/^JWT /, '') }