@ContextJwt when where and huh? for Atlassian Connect Spring Boot

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.

5 Likes