Atlassian JWT Auth
Atlassian JWT Auth provides a Python implementation of the Atlassian Service to Service Authentication specification, wrapping the PyJWT library. It enables applications to securely sign and verify JSON Web Tokens (JWTs) for communication with Atlassian products using both symmetric and asymmetric key pairs. The current version is 22.0.0, with major releases typically occurring annually or bi-annually.
Common errors
-
jwt.exceptions.InvalidSignatureError: Signature verification failed
cause This typically indicates an incorrect public/private key pair, a mismatched signature algorithm, or an improperly calculated `qsh` (Query String Hash).fixDouble-check your private key (for signing) and public key (for verification). Ensure the `qsh` parameter matches the canonicalized URL and query parameters of the request being made to the Atlassian host. Also, verify that the `alg` (algorithm) header in the JWT matches the expected algorithm. -
jwt.exceptions.ExpiredSignatureError: Signature has expired
cause The `exp` (expiration time) claim in the JWT is in the past, often due to clock skew between the signing server and the verifying server, or too short a `token_lifetime_seconds`.fixSynchronize server clocks using NTP. Increase the `token_lifetime_seconds` if necessary (e.g., to 300 seconds for a 5-minute validity). Ensure your system clock is accurate. -
TypeError: AsymmetricSigningRequestAuthentication.__init__() got an unexpected keyword argument 'signature_algorithm'
cause Attempting to pass `signature_algorithm` to `AsymmetricSigningRequestAuthentication` in a version where it's no longer accepted (since v15.0.0).fixRemove the `signature_algorithm` argument from the constructor. The algorithm is now inferred automatically from the provided private key. -
ImportError: cannot import name 'algorithms' from 'jwt'
cause This error occurs if you are using an older version of PyJWT (e.g., `PyJWT<2.0.0`) where the `algorithms` module might not have been directly exposed or structured differently, or if you're trying to import it from `atlassian_jwt_auth` instead of `jwt`.fixEnsure you are running `PyJWT>=2.0.0`. If you need to access algorithms directly, import them from `jwt.algorithms` or `jwt` depending on the specific PyJWT version, not `atlassian_jwt_auth`.
Warnings
- breaking Python 2 support was dropped in version 18.0.0. Projects still on Python 2 must either remain on an older version of `atlassian-jwt-auth` or migrate to Python 3.
- breaking The constructor for `SigningRequestAuthentication` removed the `signature_algorithm` parameter in version 15.0.0. The algorithm is now inferred from the key material.
- breaking The function `create_asap_jwt` was renamed to `create_oauth_2_bearer_token` in version 16.0.0 for better clarity regarding its specific use case (OAuth 2.0 bearer tokens).
- gotcha Version 22.0.0 (and newer) requires `PyJWT>=2.0.0` and `cryptography>=3.3.1`. Using older versions of these dependencies, especially `PyJWT<2.0.0`, will lead to import errors or runtime issues due to API changes in PyJWT.
- gotcha Incorrect `qsh` (Query String Hash) calculation for requests to Atlassian products will result in 'Invalid JWT signature' or 'Authentication Failed' errors, even if the token itself is well-formed. The `qsh` is critical for Atlassian Connect authentication.
- gotcha Time synchronization issues (clock skew) between your application and the Atlassian instance can cause JWTs to be rejected with 'token expired' errors, even if they appear valid.
Install
-
pip install atlassian-jwt-auth
Imports
- AsymmetricSigningRequestAuthentication
from atlassian_jwt_auth import AsymmetricSigningRequestAuthentication
- SigningRequestAuthentication
from atlassian_jwt_auth import SigningRequestAuthentication
- algorithms
from atlassian_jwt_auth import algorithms
from jwt import algorithms
Quickstart
import os
import time
import requests
from atlassian_jwt_auth import AsymmetricSigningRequestAuthentication
# --- Configuration (replace with your actual values) ---
# Your Atlassian product's client key (usually from a descriptor or Atlassian Connect setup)
ATLASSIAN_CLIENT_KEY = os.environ.get('ATLASSIAN_CLIENT_KEY', 'your-client-key')
# Your Add-on's base URL (e.g., 'https://your-app.atlassian.net')
ADDON_BASE_URL = os.environ.get('ADDON_BASE_URL', 'https://your-addon.example.com')
# Path to your private key file (PEM format)
PRIVATE_KEY_PATH = os.environ.get('PRIVATE_KEY_PATH', 'path/to/your/private_key.pem')
# Your 'kid' (Key ID) for the private key
KEY_ID = os.environ.get('KEY_ID', 'your-key-id')
# Atlassian instance base URL you are communicating with (e.g., 'https://your-instance.atlassian.net')
ATLASSIAN_BASE_URL = os.environ.get('ATLASSIAN_BASE_URL', 'https://your-atlassian-instance.net')
# Ensure dummy values are not used in production
if 'your-' in ATLASSIAN_CLIENT_KEY or 'your-addon' in ADDON_BASE_URL or 'path/to/your/' in PRIVATE_KEY_PATH:
print("WARNING: Using dummy configuration values. Please set actual environment variables or hardcoded values.")
# For a runnable example, let's create a dummy key file if it doesn't exist
if not os.path.exists(PRIVATE_KEY_PATH):
try:
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
with open(PRIVATE_KEY_PATH, 'wb') as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
))
print(f"Dummy private key generated at {PRIVATE_KEY_PATH}")
except ImportError:
print("Cannot generate dummy key: cryptography not fully installed or missing. Please provide a real key.")
exit(1)
try:
with open(PRIVATE_KEY_PATH, 'rb') as key_file:
private_key_bytes = key_file.read()
# 1. Initialize the authentication provider
auth_provider = AsymmetricSigningRequestAuthentication(
client_key=ATLASSIAN_CLIENT_KEY,
key_id=KEY_ID,
private_key_pem=private_key_bytes,
base_url=ADDON_BASE_URL
)
# 2. Define the HTTP method and request URL
method = 'GET'
target_url_path = '/rest/api/latest/myself'
canonical_path = ADDON_BASE_URL + target_url_path
# 3. Create a JWT token for the request
# 'uri' is the canonical path of the request being made to the Atlassian host
# 'qsh' (Query String Hash) is typically generated by Atlassian Connect frameworks.
# For simple cases without query params, you might pass an empty string or rely on framework behavior.
# This example assumes a basic GET request with no query parameters.
# In a real app, 'qsh' is crucial and typically provided by the Atlassian Connect lifecycle.
# For a GET request with no query params, qsh is calculated on canonical path without query.
# For this example, we'll use a placeholder 'qsh'. Real applications should calculate this correctly.
# You might need to use `atlassian_jwt_auth.url_utils.create_canonical_query_string()`
# and `atlassian_jwt_auth.url_utils.create_query_string_hash()` to generate a proper qsh.
# For this quickstart, we'll demonstrate the signing process assuming a qsh can be generated/provided.
# A simple placeholder for qsh for demonstration. In real Atlassian Connect apps, this is vital.
qsh_value = 'some-pre-calculated-qsh' # REPLACE WITH ACTUAL QSH CALCULATION IF QUERY PARAMS EXIST
jwt_token = auth_provider.create_asymmetric_jwt(
method=method,
uri=target_url_path,
qsh=qsh_value,
token_lifetime_seconds=300 # Token valid for 5 minutes
)
# 4. Make an authenticated request using the JWT in the Authorization header
headers = {
'Authorization': f'JWT {jwt_token}',
'Accept': 'application/json'
}
print(f"Generated JWT: {jwt_token}")
print(f"Making request to {ATLASSIAN_BASE_URL}{target_url_path}")
# This part requires a real Atlassian instance to verify
# response = requests.get(f'{ATLASSIAN_BASE_URL}{target_url_path}', headers=headers)
# print(f"Response status: {response.status_code}")
# print(f"Response body: {response.json()}")
print("Request demonstration complete. Uncomment the 'requests.get' line to make a real call.")
print("Remember to replace placeholder configuration and qsh calculation with real values.")
except FileNotFoundError:
print(f"Error: Private key file not found at {PRIVATE_KEY_PATH}. Please ensure it exists and is accessible.")
except Exception as e:
print(f"An unexpected error occurred: {e}")