Accessing cloud REST api via generated JWT token

Hello,

I am trying to set a user property (/rest/api/3/user/properties/?accountId=xxxxx) via the Jira Cloud REST API outside the connect app scope, but for the moment I get 401 unauthorized response.

What I am trying to accomplish is as follows:

  1. User clicks on connect to our service on an issue glance
  2. A window is opened where he logs into our system with his credentials (from our system)
  3. I send the context JWT (from Cloud app) as a parameter as well to the window (and on installed hook I save the tenant data as well to db)
  4. I use the clientKey (present in context JWT sent earlier) to retrieve tenant data from our db (validate token and generate a JWT token which I would use to set a user property (our API key) for the user by accountId

The documentation says that I am able to:
POST https://.atlassian.net/jira/rest/api/2/issue/AC-1/attachments
“Authorization” header value: "JWT " but I get unauthorized when I try to access the endpoint.

Any help would be appreciated!
Thank you!

How are you generating the JWT? Are you using ACE, or is it a custom implementation?

Hello,

I am using a custom implementation.
My JWT contains the following:

iss => which i set to clientKey
sub => which is the users accountId
iat => current unix timestamp
exp => current unix timestamp + 15 min

I am not generating a qsh at the moment. I don’t know if this is the cause of the Unauthorized response.

After the JWT is made I use the SharedSecret to sign it (I use firebase-php-jwt encode method).

Regards,
Robert

Yes, the lack of QSH is the reason why you’re getting a 401. This is a required claim. If you’re using NodeJS or Java, you can check how Atlassian creates the JWT in ACE / ACSB source code.

Hello,

I will try to add the qsh as well to see that it helps (I hope).

I hope that the issue is not related to calling the REST API from a third party app and needing an API key :(.

And yes I use NodeJs with ACE (for the Connect app part).

Thank you!

Hello,

I am trying to generate the qsh but to no avail so far.

The language that I use is PHP.
I don’t know the last part: how to exactly get utf8 bytes from the cannonical-url and hash it with sha256.

$cannonicalMethod = ‘GET’;
$cannonicalUrl = ‘/rest/api/3/user/properties’;
$cannonicalQueryString = ‘accountId=5cdae9b3254e450fd8d21090’;

$qsh = hash(‘sha256’, utf8_encode($cannonicalMethod.’&’.$cannonicalUrl.’&’.$cannonicalQueryString));

This is how I am trying at the moment, but im pretty sure the get UTF8 Bytes part is not correct.

Any help would be appreciated!
Thank you!

I think you need to URI encode it

Hello,

I added urlencode but still nothing:
$qsh = hash(‘sha256’, utf8_encode(urlencode($cannonicalMethod.’&’.$cannonicalUrl.’&’.$cannonicalQueryString))).

@RobertTutsek,

I wrote a better query string hash specification for Bitbucket long ago (not sure why the doc was never ported to Jira & Confluence docs because it is the same). This should help you test a QSH calculation function in any language:
https://developer.atlassian.com/cloud/bitbucket/query-string-hash/

Specifically, I think you have the urlencode method in the wrong place. The canonical query string should be URL encoded, but if you put it around the whole canonical request, then you incorrectly encode the ampersands (should be & not %26).

Edit: And URL encoding should not apply to the equal in the canonical query string (should be = not %3D).

2 Likes

Hello,

Thank you for your link I will look into in more detail. For now I tried moving the urlencode to the canonical query string but I still have some errors because I receive 401 Unauthorized.

Also here because I have only 1 parameter without spaces and ampersands urlencode does not help I think in my case.

In my final use case I will have only 1 GET parameter because I want to set a user property to a specific account (PUT ./rest/api/3/user/properties/myProperty?accountId=5cdae9b3254e450fd8d21090 and property data in body with Authorization header JWT ). I am trying to use the GET properties endpoint for now to try to get a sense of a valid qsh claim.

The problem is somewhere else I think, maybe on the utf8 bytes or constructing the sha256. Do you know some kind of sandbox area where I could validate if my qsh is in a correct format? or only the documentation that you provided exists?

Regards,
Robert

@RobertTutsek,

I would “bisect” the problem. Construct the canonical request (the string just before hashing) and then hash it. Post both here and I can act as your validator. As a hash, there’s nothing secret in there. I can help make sure you are constructing the canonical request correctly and there aren’t any PHP-specific tricks in hashing.

Generally, I recommend following the examples in my link with unit tests so you can make sure that any QSH would work.

Hello,

First of all thank you for your time and patience!

My full cannonical string is constructed in the following way:

$cannonicalMethod = ‘GET’;
$cannonicalUrl = ‘/rest/api/3/user/properties’;
$cannonicalQueryString = ‘accountId=5cdae9b3254e450fd8d21090’;

$encodedCannonicalQueryString = urlencode($cannonicalQueryString);
$encodedCannonicalQueryString = str_replace("%3D", “=”, $encodedCannonicalQueryString);

$fullCannonicalQueryString = utf8_encode($cannonicalMethod.’&’.$cannonicalUrl.’&’.$encodedCannonicalQueryString);

The result:
GET&/rest/api/3/user/properties&accountId=5cdae9b3254e450fd8d21090

And the hash(256) results in:
4570fca97261dfb49210390ab2cfebb5d193bdc4f0ff9f19439a37124470f313

So yeah I think the utf8_encode() is not what I need (in the doc says that I need to get bytes encoded as utf8) and hash it after.

I am not sure how to achieve this final two steps.

Regards,
Robert

@RobertTutsek,

I confirm the fullCannonicalQueryString looks correct. So I agree with your assessment that we’re getting tangled up in hash. That bit about utf8 encoding as bytes might be too language specific. I think that’s necessary in JavaScript, and maybe Java too. But it looks like modern PHP may already handle that in the hash function. Could you try a known example:

$qsh=hash('sha256', 'POST&/rest/api/2/issue&')
$known_qsh='43dd1779e33c34fae00c308d62e5dd153a32147d1bcb5d40b3936457fda0ece4'
echo $qsh === $known_qsh

Hello,

I tried hashing what you sent and indeed it is the same that the known hash that you sent. This was without using utf8_encode.
I tried my qsh without using utf8_encode and same result (401 Unauthorized) :frowning:

I found a listed PHP library accepted by Atlasian: atlassian-connect-bundle/QSHGenerator.php at master · thecatontheflat/atlassian-connect-bundle · GitHub (path to generator script)
and tried strictly the qsh generate function from there still with no luck meaning the problem might be elsewhere.

So to recap I am making a GET request to my develop mode jira cloud site /rest endpoints where I have my Connect app. The request is made from outside but I have the tenant details stored (crypted) in db and I use that shared secret to sign the JWT token. I provide an Authorization header called JWT with this token.

So can you please clarify if I am doing something incorrect in the steps above?

Regards,
Robert

@RobertTutsek,

Auth is such a complicated chain! Fortunately, we’ve eliminated the QSH, which is often the “prime suspect” in JWT. For background context, you might find this JWT explainer video useful:

But to debug your case (and try to eliminate JWT itself), can you post the JWT token?

1 Like

Hello,

This is my JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI3NGE0NjFlNi0yN2U0LTM1YWYtYTE1OS0yYWM0MGEwY2I2ODYiLCJzdWIiOiI1Y2RhZTliMzI1NGU0NTBmZDhkMjEwOTAiLCJxc2giOiI0NTcwZmNhOTcyNjFkZmI0OTIxMDM5MGFiMmNmZWJiNWQxOTNiZGM0ZjBmZjlmMTk0MzlhMzcxMjQ0NzBmMzEzIiwiaWF0IjoxNjIzMjQ2OTgwLCJleHAiOjE2MjMyNDc4ODB9.Q4Bu8i6si_Yg-w3Sh1RvSb3Mt__ou73Yq3iZupckpKA

Regards,
Robert

@RobertTutsek,

Decoding yields this claims set:

{
  "iss": "74a461e6-27e4-35af-a159-2ac40a0cb686",
  "sub": "5cdae9b3254e450fd8d21090",
  "qsh": "4570fca97261dfb49210390ab2cfebb5d193bdc4f0ff9f19439a37124470f313",
  "iat": 1623246980,
  "exp": 1623247880
}

We’ve just confirmed the qsh claim. And for the others you explained:

iss => which i set to clientKey
sub => which is the users accountId
iat => current unix timestamp
exp => current unix timestamp + 15 min

The only suspect remaining seems to be sub. Can you try removing it? If you need the attachment to be uploaded “as the user”, then I think you’ll need to use:
https://developer.atlassian.com/cloud/jira/platform/user-impersonation-for-connect-apps/

Hello,

Does setting a user property require user impersonation? or can I set a user property using the available tenant details (/rest/api/3/user/properties/myProperty?accountId=123)?

I tried a request without the sub claim and still got 401 :frowning:

Regards,
Robert

Hello,

Thank you very much for your time and effort but for now I think I found an alternative path for handling this:

  1. I redirect the user to our site where he logs in (using his credentials in our site) and I validate that his Cloud JWT token is valid
  2. I generate the API key required in our side in case I receive a valid JWT and store the API key in the DB associating it to that user
  3. When the window closes I make a request to our endpoint providing a secret AccessKey and the user account id and if there is a record set for the ApiKey for him then I retrieve it and set the user property inside the Cloud app in the users context.

I know this is not a great solution, but until I can fully clarify what is the issue from what was discussed above this remains an option for me. I hope it is regarded as a viable solution for this problem.

Regards.
Robert

@RobertTutsek

Does that work for longer than 15 mins? I worry that those JWT are so short lived that they aren’t worth storing.