Getting '401 Unauthorized' for Jira Cloud API requests with Atlassian Cloud JWT authentication

I’ve fully implemented the install flow for Connect apps and I am now trying to make some API requests to my test Jira instance. Unfortunately, no matter what I do, I always run into 401 - Unauthorized as the response with no further context whatsoever:

Screenshot 2021-12-02 at 16.55.25
Postman screenshot of the HTML formatted error

Initially I have used the atlassian-jwt library utilities to help generate the appropriate JWT. Since that did not work, I’ve thus far also tried to do every step manually but I keep getting the same result.

Here is my (TS) code. In this example I’m using the /rest/api/3/project/recent endpoint but I’ve tried many others as well. I just wanted to pick a simple GET endpoint with no arguments to make the implementation as simple as possible.

 const connection = await{ clientKey })

  const req = fromMethodAndUrl('GET', '/rest/api/3/project/recent')

  const tokenData = {
    // tried this in both the `jira:{clientKey}` format as well as just `{clientKey}`
    iss: connection.clientKey, // 1000% sure this is correct
    iat: Math.floor( / 1000),
    exp: Math.floor( / 1000) + 3600,
    qsh: createQueryStringHash(req),
    sub: '60f00ad8f749c400680daf47', // tried with and without this

  const token = encodeSymmetric(tokenData, connection.sharedSecret) // also 1000% sure the shared secret is correct
  console.log('Generated JWT token!')

  const res = await Axios.get('', {
    headers: {
      Authorization: `JWT ${token}`,
      Accept: 'application/json',
  }).catch(err => {

I’ve also placed a console.log in the createCononicalRequest function of the atlassian-jwt library to validate what the library is hashing and this results in: GET&/rest/api/3/project/recent&. If I sha-256 this it comes out to:


Which corresponds to what I see encoded in the generated JWT (output from jwt-cli):

➜  ~ jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJiM2UwZjY1MC1hODZjLTNmN2UtYjFkYi00MzY0NTJmODVkZjciLCJpYXQiOjE2Mzg0NjAwODgsImV4cCI6MTYzODQ2MzY4OCwicXNoIjoiNDk1MzM3NmE0YTYyMGEzMzdmYWI1OTU0MDlkZWY0ZjY5NTI3MDc3N2E5NTM2ZmRlZDk4YTQ3OTBiMWFkMzgzMiIsInN1YiI6IjYwZjAwYWQ4Zjc0OWM0MDA2ODBkYWY0NyJ9.kIehfv_fCdgu-ztmtf0t0tjW2jaS1xqu71a7wxb_pBQ

To verify on

✻ Header
  "typ": "JWT",
  "alg": "HS256"

✻ Payload
  "iss": "b3e0f650-a86c-3f7e-b1db-436452f85df7",
  "iat": 1638460088,
  "exp": 1638463688,
  "qsh": "4953376a4a620a337fab595409def4f695270777a9536fded98a4790b1ad3832",
  "sub": "60f00ad8f749c400680daf47"
   Issued At: 1638460088 12/2/2021, 4:48:08 PM
   Expiration Time: 1638463688 12/2/2021, 5:48:08 PM

✻ Signature kIehfv_fCdgu-ztmtf0t0tjW2jaS1xqu71a7wxb_pBQ

It seems to me that I’m doing everything right… Am I overseeing something?

PS, my full atlassian-connect.json:

  "name": "Redacted name",
  "description": "Redacted name Jira Integration",
  "key": "com.redacted-name.jira-integration",
  "baseUrl": "",
  "vendor": {
      "name": "Redacted name,
      "url": ""
  "authentication": {
      "type": "jwt"
  "lifecycle": {
      "installed": "/integrations/jira/installed"
  "scopes": [
  "apiVersion": 1,
  "apiMigrations": {
    "context-qsh": true,
    "signed-install": true
  "modules": {
      "generalPages": [
              "url": "/helloworld.html",
              "key": "hello-world",
              "location": "",
              "name": {
                  "value": "Greeting"

Are you aware of GitHub - DanielHreben/atlassian-connect-auth: Helper for handling webhooks from Atlassian products , and alternative js/ts implementation of the auth flow? This might help.

Hey Marc,

Thank you for your reply. I have already implemented the install flow and have the necessary information to perform API requests from my server to request data from Jira. Handling webhooks already works :grin:. Do you perhaps have a suggestion why I’m getting ‘Unauthorized’ errors when requesting data?



I’ve been stumped by these kinds of cases before and I’m not close enough to implementation & operations to help look at anything specific. What I know is that sometimes the shared secret stored for a given clientKey gets “out of sync” and the JWT is being signed with a secret that Jira does not accept as the current one. You said you are in test, so you could drop the database and get back to an empty state, then install the app again.

As far as I can tell, the problem is localized to your app and an instance (as opposed to affecting all developers or all instances) so it is a good case for developer support. Please open a ticket if you can’t get the shared secrets to “sync up”.

Hey Ian!

Thanks a lot for taking the time to have a look at this. Regarding your comment about the sharedSecret, that sounds really plausible to me. I’m starting to feel that there could possibly be a bug in the Jira API server side where the sharedSecret may not be stored properly on reinstalls. For this dev instance, I have probably reinstalled this addon 30+ times while testing the install flow. I no longer have the very first sharedSecret that I received on record. Every time I reinstall, the clientKey stays the same but the secret changes. I’m going to experiment with this a little bit to see if I can possibly get it to work in a brand new instance and use the very first sharedSecret I receive. If I still can’t figure it out, I will contact developer support and try to work it out with them. I’ll update this topic with any developments.

Have a good weekend!

1 Like

Did as you suggested but no luck unfortunately. Ended up making a bug deport in the developer support system.

Hi @RoemerBakker ,

I think you may have a few issues such as setting iss to clientKey and using encodeSymmetric instead of `encode’. Here’s some code to help you:

const now = moment().utc();
const jwtTokenValidityInMinutes = 3;
const jwtPayload = {
    "iss": appKey,
    "aud": clientKey,
    "iat": now.unix(),
    "exp": now.add(jwtTokenValidityInMinutes, 'minutes').unix(),
    "qsh": jwt.createQueryStringHash(jwt.fromExpressRequest(req))
const jwtToken = jwt.encode(jwtPayload, clientContext.clientSettings.sharedSecret, 'HS256');