- [Update: 12 JUL 2021] Enforcement date is extended from 20th August to 29th October 2021.
- [Update: 15 JUL 2021] Please upgrade Atlassian Connect Express(ACE) versions to
v7.4.0
or later. Previous versions (v7.1.5
~v7.3.x
) automatically opts in to this new feature that results in creating a new app version without the vendor’s action.
This new ACE upgrade(v7.4.0
) will not modify your app descriptor which may result in removing"signed-install"
field that could have been added unintentionally.- When upgrading your app from (
v7.1.5
~v7.3.x
) tov7.4.0
, and- have previously released a new app version by updating
config.json
file with"signed-install"
field.
→ After upgrading tov7.4.0
the app will work the same as before without additional change, but we highly recommend to migratesigned-install
configurations fromconfig.json
file to the app descriptor file to prevent any unexpected app version being created automatically in the future. - had no intention of opting in to this feature yet.
→ upgrading tov7.4.0
may create another new version as ACE will revert and remove the new signed-install field that was added before.
If you are happy to stay opt-in and want to prevent MPAC from creating another version automatically, make sure your app descriptor did not change by setting"signed-install"
field under"apiMigrations"
section.
- have previously released a new app version by updating
- When upgrading from an older version(<
v7.1.5
) of ACE tov7.4.0
→ There won’t be any impact, app developers can choose to opt-in to signed-install feature by updating their descriptor file(Follow the guideline below).
- When upgrading your app from (
What is changing?
We are making a breaking change to the Connect app lifecycle events to improve the security of the Connect framework. In the install
and uninstall
callbacks sent by Atlassian to your app, the Authorization
JWT token now includes a new signature that you can use to verify that the request is genuine. The signature is generated using an asymmetric keypair using the RS256 algorithm. All Jira and Confluence Connect apps will need to opt-in to this change to receive the updated, signed lifecycle hooks.
Why is it changing?
At installation time, the Connect app server and the host product exchange a shared secret used to create and validate JWT tokens for use in API calls. However, the very first install lifecycle hook is not signed because the secret has not been shared yet to the app server. This makes it difficult to verify that the lifecycle hook was genuinely requested from Atlassian, which may lead Connect apps to be vulnerable to Denial of Service (DoS) and/or Man in the Middle(MitM) attacks and opens the possibility for further exploits when an app secret can be specified by a MitM.
Before
- Initial install hook is not signed
- App upgrade install hooks are symmetrically signed with previously shared secret
- Uninstall hook is also symmetrically signed with the shared secret
- Initial and upgrade install hooks provide shared secret which is included in its request body
- Shared secret will be used to create and validate JWT tokens for use in API calls between Atlassian and the app including any subsequent install hooks
After
- All install(including upgrades)/uninstall hooks will be asymmetrically signed with RS256 (RSA Signature with SHA-256) algorithm
- Public key will be provided via a CDN to verify the JWT token.
https://connect-install-keys.atlassian.com/${public_key_id}
- Initial and upgrade install hooks provide shared secret which is included in its request body
- Shared secret will be used to create and validate JWT tokens for use in API calls between Atlassian and the app, excluding install and uninstall hooks
Is my app impacted?
If your app uses an authentication descriptor setting other than none , then your app is impacted and will need to be updated.
This change affects apps regardless of how they are installed; apps listed on the Marketplace and apps installed via the UPM “dev mode” are affected .
Even if your app skips validating install hooks with any reasons, we still recommend to opt-in to the breaking changes and make the install lifecycle hook secure as soon as possible.
Apps running on Bitbucket Connect are not affected .
What do I need to do?
Running an Atlassian-supported Connect framework
- Update Atlassian Connect Spring Boot, or Atlassian Connect Express to the latest version †
- Atlassian Connect Express: v7.4.0 or later
- Atlassian Connect Spring Boot: v2.2.0 or later
- Check if your app has enabled the updated asymmetric install hook by inspecting
signed-install
field in your app descriptor. Also make sure that your app baseUrl is properly configured. ‡ - If you need more time to comply with the change, Opt-out by setting
signed-install
element tofalse
. (See how this can be configured for ACE/ACSB from below). - Test that your app is correctly authenticating install hooks from a test instance and deploy your app
† ACE framework will opt-in to the new behaviour(Asymmetric install hook authentication) by default, but for apps that uses ACSB framework will have to update atlassian-connect.json
to enrol to the new feature.
‡ Your descriptor should define new element like below:
# 1. Atlassian Connect Express
# After updating the ACE version to 'v7.4.0' or later, "signed-install" attribute
# must be added in the descriptor file.
{
...
"baseUrl": "{{localBaseUrl}}",
"apiMigrations": {
"gdpr": true,
"signed-install": true
}
...
}
# In your `config.json`
# Check that 'localBaseUrl' in your config.json matches the baseUrl from the descriptor
# The app baseUrl will be used to verify the audience claim.
{
...
"signed-install": "force",
"product": "jira",
"production": {
"localBaseUrl": "https://app.base.url", # This is used to verify 'audience' claim
...
}
}
# Additionally, you can choose to disable the legacy install hook by setting
# "signed-install" to "force" in "config.json" as global variable.
# NOTE: Setting this to "enable", "disable" will still be used
# to be compatible with previous unstable ACE versions (v7.1.5 ~ v7.3.x)
# but for v7.4.0 and later, please do not use this field unless you want to set it to "force".
# 2. Atlassian Connect Spring Boot
# After updating the ACSB version to 'v2.2.0' or higer, "signed-install" attribute
# must be added in the descriptor file.
# (newly created apps will have this added by default)
{
...
"baseUrl": "http://app.base.url", # This is used to verify 'audience' claim
"apiMigrations": {
"signed-install": true
}
...
}
# Additionally, you can choose to disable the legacy install hook by setting
atlassian.connect.fallback-install-hook-enabled=false`
Running a custom implementation
If you’re using a framework other than Atlassian Connect Spring Boot, or Atlassian Connect Express, then please follow these steps to ensure your app correctly verifies install lifecycle hooks.
- Opt-in to the updated asymmetric install hook by adding a new
signed-install
element to the apiMigrations section of the app descriptor.† - Extract JWT token from install hook request’s
Authorization
header. - Decode only the header of the JWT token and extract
kid
(How to decode a JWT token header) - Use the extracted
kid
to fetch the corresponding public key from CDN.‡ - Verify the JWT token using the public key and retrieve the decoded JWT body to perform post-installation steps after verifying the claims including the audience(
appBaseUrl
).
† Your descriptor should look like this:
...
"baseUrl": "http://app.base.url", # This is used to verify 'audience' claim
"apiMigrations": {
"signed-install": true
}
...
‡ CDN url should have key id as the path to fetch the public key
https://connect-install-keys.atlassian.com/${kid}
Keep in mind that this public key gets rotated regularly (More than once a day).
For reference, a pseudo-code example of JWT verification that uses RSA public key from Atlassian CDN:
// 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
By when do I need to do it?
Enforcement of this breaking change is planned for all apps by 29 Oct 2021. If you do not patch your app to mitigate this vulnerability and opt-in to the signed-install
API migration before this date, your app may not be able to install / update.
Additional minor patch will be required for the apps using ACE / ACSB framework to cleanup support for legacy install callbacks using shared secret.