Atlassian JWT Auth

22.0.0 · active · verified Thu Apr 16

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

Warnings

Install

Imports

Quickstart

This quickstart demonstrates how to use `AsymmetricSigningRequestAuthentication` to create a JWT for authenticating requests to an Atlassian instance. It initializes the authentication provider with your client key, key ID, private key, and add-on base URL, then generates a JWT to be included in the `Authorization` header of an HTTP request. This example includes a placeholder `qsh` (Query String Hash) which is critical in real Atlassian Connect applications and must be correctly calculated based on the target URL and query parameters. For demonstration purposes, it also includes a mechanism to generate a dummy private key if one doesn't exist, though a real key is required for actual authentication.

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}")

view raw JSON →