Skip to content

Commit

Permalink
Throw an error when the JWT doesn't contain QSH claim for Confluence …
Browse files Browse the repository at this point in the history
…and Jira add-ons
  • Loading branch information
vsaienko committed Mar 24, 2021
1 parent f6d0c5c commit 25653ef
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 24 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { Addon, AuthError } = require('atlassian-connect-auth')

const addon = new Addon({
baseUrl: 'https://your-addon-url.com',
product: 'jira', // or 'bitbucket'
product: 'jira', // ('jira', 'confluence', or 'bitbucket')
})

const handleInstall = (req, res) => {
Expand Down
137 changes: 115 additions & 22 deletions __tests__/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const jiraPayload = {

const bitbucketPayload = {
principal: { uuid: 'bitbucket-workspace-id' },
clientKey: 'bitbucket-client-key'
clientKey: 'bitbucket-client-key',
sharedSecret: 'shh-secret-cat'
}

const jiraAddon = new Addon({
Expand Down Expand Up @@ -78,37 +79,114 @@ describe('Installation', () => {

test('Passed different id in body and authorization header', async () => {
const loadCredentials = jest.fn()
const token = jwt.encode({
iss: 'different-id'
}, jiraPayload.sharedSecret)
const token = jwt.encode(
{
iss: 'different-id'
},
jiraPayload.sharedSecret
)

const req = {
body: jiraPayload,
headers: { authorization: `JWT ${token}` },
query: {}
}

await expect(jiraAddon.install(req, {
await expect(
jiraAddon.install(req, {
loadCredentials,
saveCredentials
})
).rejects.toMatchError(new AuthError('Wrong issuer', 'WRONG_ISSUER'))

expect(loadCredentials).toHaveBeenCalledWith(req.body.clientKey)
expect(saveCredentials).not.toHaveBeenCalled()
})

test('Second and subsequent installation of Jira add-on with no qsh', async () => {
const loadCredentials = jest.fn().mockReturnValue(jiraPayload)
const token = jwt.encode(
{
iss: jiraPayload.clientKey
},
jiraPayload.sharedSecret
)

const req = {
body: jiraPayload,
headers: { authorization: `JWT ${token}` },
query: {}
}

await expect(
jiraAddon.install(req, {
loadCredentials,
saveCredentials
})
).rejects.toMatchError(
new AuthError(
'JWT did not contain the query string hash (qsh) claim',
'MISSED_QSH'
)
)
})

test('Second and subsequent installation of Bitbucket add-on with no qsh', async () => {
const loadCredentials = jest.fn().mockReturnValue(bitbucketPayload)
const token = jwt.encode(
{
iss: bitbucketPayload.clientKey
},
bitbucketPayload.sharedSecret
)

const req = {
body: bitbucketPayload,
headers: { authorization: `JWT ${token}` },
query: {}
}

const result = await bitbucketAddon.install(req, {
loadCredentials,
saveCredentials
})).rejects.toMatchError(
new AuthError('Wrong issuer', 'WRONG_ISSUER')
)
})

expect(result.credentials).toEqual(bitbucketPayload)
expect(loadCredentials).toHaveBeenCalledWith(req.body.clientKey)
expect(saveCredentials).not.toHaveBeenCalled()
expect(saveCredentials).toHaveBeenCalledWith(
req.body.clientKey,
req.body,
bitbucketPayload
)
})

test('Second and subsequent Jira add-on install', async () => {
const loadCredentials = jest.fn().mockReturnValue(jiraPayload)
const token = jwt.encode({
iss: jiraPayload.clientKey
}, jiraPayload.sharedSecret)
const expectedHash = jwt.createQueryStringHash(
{
body: jiraPayload,
query: {},
pathname: '/api/install',
method: 'POST'
},
false,
baseUrl
)
const token = jwt.encode(
{
iss: jiraPayload.clientKey,
qsh: expectedHash
},
jiraPayload.sharedSecret
)

const req = {
body: jiraPayload,
headers: { authorization: `JWT ${token}` },
query: {}
query: {},
pathname: '/install',
originalUrl: '/api/install',
method: 'POST'
}

const result = await jiraAddon.install(req, {
Expand All @@ -117,21 +195,36 @@ describe('Installation', () => {
})

expect(loadCredentials).toHaveBeenCalledWith(req.body.clientKey)
expect(saveCredentials).toHaveBeenCalledWith(req.body.clientKey, req.body, jiraPayload)
expect(result.credentials).toEqual(jiraPayload)
expect(result.payload).toEqual({
iss: jiraPayload.clientKey
})
expect(saveCredentials).toHaveBeenCalledWith(
req.body.clientKey,
req.body,
jiraPayload
)
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",
"qsh": "308ba56cff8ed9ae4d1a5fde6c4add0c3de1c7bdf6ddcb220a8763711645e298",
},
}
`)
})

test('Unauthorized request to updated existing instance', async () => {
const loadCredentials = jest.fn().mockReturnValue(jiraPayload)
const req = { body: jiraPayload, headers: {}, query: {} }

await expect(jiraAddon.install(req, {
loadCredentials,
saveCredentials
})).rejects.toMatchError(
await expect(
jiraAddon.install(req, {
loadCredentials,
saveCredentials
})
).rejects.toMatchError(
new AuthError('Unauthorized update request', 'UNAUTHORIZED_REQUEST')
)
expect(loadCredentials).toHaveBeenCalledWith(req.body.clientKey)
Expand Down
20 changes: 19 additions & 1 deletion lib/Addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ const AuthError = require('./AuthError')
const util = require('./util')

class Addon {
constructor ({ product, baseUrl }) {
/**
*
* @param {Object} options
* @param {'jira'|'confluence'|'bitbucket'} options.product
* @param {string} options.baseUrl
*/
constructor ({
product = throwIfMissing('product'),
baseUrl = throwIfMissing('baseUrl')
}) {
this.product = product
this.baseUrl = baseUrl
}
Expand All @@ -28,6 +37,11 @@ class Addon {
// Update allowed only if request was signed
if (credentials && token) {
const payload = util.validateToken(token, credentials.sharedSecret)

if (!payload.qsh && ['jira', 'confluence'].includes(this.product)) {
throw new AuthError('JWT did not contain the query string hash (qsh) claim', 'MISSED_QSH')
}

util.validateQsh(req, payload, this.baseUrl)

const updatedCredentials = await saveCredentials(clientKey, req.body, credentials)
Expand Down Expand Up @@ -65,4 +79,8 @@ class Addon {
}
}

function throwIfMissing (param) {
throw new Error(`'${param}' is missing`)
}

module.exports = Addon

0 comments on commit 25653ef

Please sign in to comment.