In April 2021 we made a breaking API change to Atlassian Connect in Jira and Confluence Cloud to mitigate a vulnerability in the verification of the qsh claim in Connect JWT authentication.
This was documented in
But I was confused about when to use @ContextJwt
in my Atlassian Connect Spring Boot code and why it was needed.
So, here are the use cases for @ContextJwt
, @IgnoreJwt
and the default use case when using Atlassian Connect Spring Boot.
What’s a Query String Hash
When we make calls to endpoints listed in atlassian-connect.json
- whether webhooks or for content (e.g. macros), Atlassian Cloud generates the call and packages up a hash of the query string. This hash is used as part of the authentication of the call to ensure the query string isn’t modified in transit. In the documentation for Atlassian connect Spring boot, it states
the JWT token issued by the Atlassian host cannot be used for other requests to the add-on since it contains the
qsh
(query-string hash) claim.
So, we can’t be clever and extract the JWT and re-use it. Also, when the browser makes the call from your JavaScript, we can’t attach (or re-use) the query string hash, and so it’s empty when we make the call.
When to use @ContextJwt
If you’re calling an endpoint from your client side code in a browser using AP.context.getToken()
, it won’t have the query string hash constructed.
Example
In atlassian-connect.json
let’s have a DynamicContentMacro
like this
"dynamicContentMacros": [
{
"key": "dynamic-macro",
"name": {
"value": "dynamic-macro"
},
"description": {
"value": "Dynamic macro example."
},
"outputType": "block",
"bodyType": "none",
"url": "/dynamic-macro.html",
"parameters": [
{
"identifier": "text",
"name": {
"value": "text"
},
"type": "string",
"required": true
}
]
}
]
And then in dynamic-macro.html
we have
<!DOCTYPE html>
<html lang="en">
<head>
<title>Dynamic Macro</title>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=EDGE">
<script type="text/javascript" src="//connect-cdn.atl-paas.net/all.js"></script>
<script type="text/javascript" src="//unpkg.com/@atlassian/aui@latest/dist/aui/aui-prototyping.js"></script>
<link rel="stylesheet" type="text/css" href="//unpkg.com/@atlassian/aui@latest/dist/aui/aui-prototyping.css"/>
<script type="text/javascript">
let macroDataPromise = new Promise(macro => window.AP.confluence.getMacroData(macro))
let tokenPromise = new Promise(token => window.AP.context.getToken(token))
Promise.all([macroDataPromise, tokenPromise])
.then(data => {
const text = data[0].text || 'No text provided ...'
const token = data[1]
return fetch('dynamic-macro-service' + '?' + new URLSearchParams({'text': text}),
{
method: 'GET',
headers: {
'Authorization': 'JWT ' + token
}
}
)
})
.then(response => response.text())
.then(text => {
document.getElementById('text').innerHTML = text
});
</script>
</head>
<body>
<div id="page">
<div id="content">
<div class="aui-page-panel">
<div class="aui-page-panel-inner">
<div class="aui-page-panel-content">
<div class="ac-content" id="text"></div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
And in our controller we have
@ContextJwt
@GetMapping(value = "/dynamic-macro-service", produces = MediaType.TEXT_PLAIN_VALUE)
public String getDynamicMacro(@RequestParam(name = "text") String text) {
return String.format("<h2>dynamic macro</h2><p>%s</p>", text.toUpperCase());
}
When we use our new macro, we see two calls made in the logs. The first is to /dynamic-macro.html
and the parsed JWT
has a hashed value for qsh
and context
is empty.
Parsed JWT: {"sub":"<hidden>","qsh":"8c50d095fada868630bb611c8f406058ddfca736a4c0199131a7af6887281bc9","iss":"<hidden>","context":{},"exp":1643778276,"iat":1643778096}
Then when the page dynamic-macro.html
makes the call to our service endpoint /dynamic-macro-service
using fetch()
we see
Parsed JWT: {"sub":"<hidden>","qsh":"context-qsh","iss":"<hidden>","context":{"confluence":{"editor":{"version":"\"v2\""},"macro":{"outputType":"display","id":"2ef8461e5b768c5b98fdbdf379bdfd6a","hash":"2ef8461e5b768c5b98fdbdf379bdfd6a"},"content":{"id":"1464107009","type":"page","version":"2"},"space":{"id":"1458667525","key":"D6893"}}},"exp":1643778996,"iat":1643778096}
Here we can see qsh
has the value of "context-qsh"
and the context
is not empty.
If we remove the @ContextJwt
annotation and refresh the page with the macro we get an authorisation error when trying to call the /dynamic-macro
endpoint.
{"status":401,"error":"Unauthorized","timeStamp":"Wed Feb 02 16:11:08 AEDT 2022"}
When not to use @ContextJwt
If Confluence Cloud is calling your endpoint directly, it will have a query string hash included.
Example
In atlassian-connect.json
let’s have a StaticContentMacro
like this
"staticContentMacros": [
{
"key": "static-macro",
"name": {
"value": "static-macro"
},
"description": {
"value": "Static macro example."
},
"url": "/static-macro-service?text={text}",
"parameters": [
{
"identifier": "text",
"name": {
"value": "text"
},
"type": "string",
"required": true
}
]
}
]
And in our controller we have
@GetMapping(value = "/static-macro-service", produces = MediaType.TEXT_PLAIN_VALUE)
public String getStaticMacro(@RequestParam(name = "text") String text) {
return String.format("<h2>static macro</h2><p>%s</p>", text.toUpperCase());
}
When we use our new macro, we see in the logs just one call made to /static-macro-service
and the parsed JWT
has a hashed value for qsh
and context
is empty.
Parsed JWT: {"sub":"<hidden>","qsh":"8c50d095fada868630bb611c8f406058ddfca736a4c0199131a7af6887281bc9","iss":"<hidden>","context":{},"exp":1643778942,"iat":1643778762}
If we add the @ContextJwt
annotation and refresh the page with the macro we see the same authorisation error as before in the macro in the page.
{"status":401,"error":"Unauthorized","timeStamp":"Wed Feb 02 16:17:10 AEDT 2022"}
When to use @IgnoreJwt
When using Atlassian Connect Spring Boot, all calls to endpoints expect a JWT
of some sort. If you have endpoints that can be called directly, that is not via Confluence Cloud the you should add @IgnoreJwt
to those endpoints. If you want JWTs ignored on other endpoints, you can configure them in application.yml
with the parameter atlassian.connect.require-auth-exclude-paths
.
For example to exclude swagger 2.0 documentation endpoints, try
atlassian.connect.require-auth-exclude-paths: /swagger-ui.html,/v3/api-docs/**
And that’s that …
James.