Support Jira Connect asymmetric JWTs
What does this MR do and why?
Related issue: #338177 (closed)
Atlassian is planning to enforce asymmetric JWTs to all apps' install/uninstall lifecycle events by Oct 29, 2021. This MR implements this change by following the pseudo-code example for custom implementations (Atlassian announcement#running-a-custom-implementation)
What does asymmetric JWTs mean?
In addition to a shared secret, a public key is required to decode the JWT token that is sent by Atlassian with every lifecycle event. The GitLab endpoint that receives the lifecycle event needs to do the following to decode and verify the JWT:
- Decode JWT headers to get the public key ID
- Fetch the public key from
https://connect-install-keys.atlassian.com/KEY_ID
- Decode JWT body using the public key
- Verify that the claims contained by the JWT body are correct
-
qsh
matches the request url and HTTP method -
iss
matches theclientKey
in params -
aud
matchesjira_connect_base_url
-
Pseudo-code example
// Authenticate install/uninstall lifecycle hook
Function AuthenticateAsymmetricJWT(request)
// Get Authorization header from request: `Authorization: JWT ${jwt_token}`
jwt_token <- request.Header['Authorization'] or request.QueryString['jwt']
// Decode
jwt_header <- DecodeProtectedHeader(jwt_token)
// Get key id from JWT header
key_id <- jwt_header.kid
if (key_id is empty)
return Error(Unauthorized)
// Fetch RSA Public key string(PEM format)
rsa_public_key <- fetch(`https://connect-install-keys.atlassian.com/${key_id}`);
if (rsa_public_key is empty)
return Error(Unauthorized)
// Verify signature and decode jwt_body and jwt_header
// Verifying the `aud` claim (app baseUrl in your descriptor file) is important.
// Also make sure that the external lib validates other required claims such as `exp`
expectedAudience = "https://your.app.baseUrl";
expectedIssuer = "host_client_key";
{ jwt_body } <- external.jwtlib.JWTVerify(jwt_token, rsa_public_key, expectedAudience, expectedIssuer);
if (jwt_body is empty || caught exception during verify)
return Error(Unauthorized)
else
return jwt_body;
// Decoding jwt header from the token: Use external lib if possible
Function DecodeProtectedHeader(token)
[encoded_header, encoded_body, encoded_signature] <- token.split(".")
header <- base64.decode(encoded_header)
return header
Screenshots or screen recordings
This change should not be visible to the user
How to set up and validate locally
Numbered steps to set up and validate the change are strongly suggested.
- Start a Gitpod from this branch. (Read more about Gitpod and GDK here)
- Run
bundle exec rails console
on the Gitpod - Run
Feature.enable(:jira_connect_asymmetric_jwt)
- Follow the install the app in Jira guide using your Gitpod instance.
- The installation should succeed.
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
-
I have evaluated the MR acceptance checklist for this MR.