I have a forge app that can comment on tickets and respond to events and call a remote backend. This all works fine. However while following this documentation
I cannot figure out how to authenticate properly. I can see that the x-forge-oauth-system header is passed to the request but I have tried so many configurations to get this to authenticate but I keep getting a 401 error when calling the Jira APIs from the remote. I had previously used a personal access token with the atlassian python library https://atlassian-python-api.readthedocs.io/index.html
and I tried using both the session method of authentication as well as just the “token” method but nothing works.
Hi @joe1 , I can’t definitively say what’s going wrong here with the information provided, are you able to provide me with your appId and a traceId of a request that leads to a 401 (this is value of the X-B3-Traceid response header).
Here are some things to check as well:
Did you define the required scope for this API in your app’s manifest.yml
OAuth tokens that you receive from Forge are tenant isolated, which means that you can’t use them outside the tenant which the invocation occurred. I would recommend using the baseUrl from the FIT claims (the app.apiBaseUrl claim) instead of constructing it yourself.
@BoZhang thank you so much! that was it, I was trying to reconstruct the url based on my jira instance but I was able to pull the apiBaseUrl from the FIT Token. Here is a simplified python version for anyone that comes after me:
from authlib.jose import jwt, JsonWebKey
from atlassian import Jira
import requests
# need to add authlib, requests, and atlassian to your project
def validate_context_token(invocation_token, app_id) -> dict:
# URL for Atlassian Forge JWKS
jwks_url = 'https://forge.cdn.prod.atlassian-dev.net/.well-known/jwks.json'
# Step 1: Retrieve the JWKS
response = requests.get(jwks_url)
jwks = JsonWebKey.import_key_set(response.json())
# Step 2: Verify the JWT
claims = jwt.decode(invocation_token, key=jwks, claims_options={"aud": {"essential": True, "value": app_id}})
# Step 3: Validate the claims (e.g., expiration)
claims.validate(leeway=0)
return claims
# this is a FastAPI endpoint, but any server framework is fine
@router.post('my-remote-url')
async def my_remote_url(request: Request):
authorization = request.headers.get("authorization")
claims = validate_forge_jwt_token(authorization)
system_token = request.headers.get("x-forge-oauth-system")
jira_client = Jira(url=claims.get('app').get('apiBaseUrl') , token=system_token, cloud=True)
jira_client.issue_add_comment("ABC-123", "Hello World!")