Skip to content

Support Jira Connect asymmetric JWTs

Andy Schoenen requested to merge 338177-jira-connect-support-asymmetric-jwt into master

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:

  1. Decode JWT headers to get the public key ID
  2. Fetch the public key from https://connect-install-keys.atlassian.com/KEY_ID
  3. Decode JWT body using the public key
  4. Verify that the claims contained by the JWT body are correct
    1. qsh matches the request url and HTTP method
    2. iss matches the clientKey in params
    3. aud matches jira_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.

  1. Start a Gitpod from this branch. (Read more about Gitpod and GDK here)
  2. Run bundle exec rails console on the Gitpod
  3. Run Feature.enable(:jira_connect_asymmetric_jwt)
  4. Follow the install the app in Jira guide using your Gitpod instance.
  5. 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.

Edited by Andy Schoenen

Merge request reports