Hi everyone,
I’m currently developing an Atlassian Connect app that needs to authenticate requests to the Jira Cloud REST API using a JWT token. However, I keep getting a 401 Unauthorized error when making requests from my server-side code.
What I’m Trying to Do: I have a Node.js/TypeScript setup where:
- I store the
clientKey, sharedSecret, and baseUrl after installing the Connect app. - I generate a JWT token for each request using the
atlassian-jwt library, including the qsh claim. - I make a GET request to Jira’s
/issue/CL-988 endpoint as a test.
Code snippet:
atlassian-connect.json:
{
"name": "MyCompany Bot",
"description": "MyCompany Bot for Atlassian.",
"key": "com.MyCompany.bot",
"apiVersion": 1,
"baseUrl": "https://{AYN_IP}.ngrok-free.app/atlassian",
"vendor": {
"name": "MyCompany, Inc.",
"url": "https://example.com"
},
"authentication": {
"type": "jwt"
},
"lifecycle": {
"installed": "https://{AYN_IP}.ngrok-free.app/atlassian/installed"
},
"scopes": [
"READ",
"WRITE",
"DELETE",
"PROJECT_ADMIN",
"ADMIN",
"ACT_AS_USER"
],
"modules": {},
"icon": {
"url": "https://{AYN_IP}.ngrok-free.app/atlassian/image.png"
}
}
atlassian.service.ts:
import {AtlassianCLientV2} from "./atlassian.client"
public async getIssueV2(): Promise<any> {
return (await AtlassianClientV2()).get("/rest/api/2/issue/CL-988")
.then(res => res.data)
.catch(e => {
throw new Error(JSON.stringify(e.response.data))
})
}
atlassian.client.ts
import axios, {AxiosInstance, AxiosResponse} from "axios";
import cache from "../utils/cache";
import * as jwt from 'atlassian-jwt';
import moment from 'moment';
import {SymmetricAlgorithm} from "atlassian-jwt/dist/lib/jwt";
export async function AtlassianClientV2(): Promise<AxiosInstance> {
const installedData: ConnectHook = await cache.cache("tokens:atlassian:connect:data") as unknown as ConnectHook;
async function createJwtToken(method: string, resource: string): Promise<string> {
console.log(resource)
const token = jwt.encodeSymmetric(
{
iss: installedData.clientKey,
iat: moment().utc().unix(),
exp: moment().utc().add(3, 'minutes').unix(),
qsh: jwt.createQueryStringHash(
jwt.fromMethodAndUrl(method, resource)
)
},
installedData.sharedSecret,
SymmetricAlgorithm.HS256
);
console.log(jwt.decodeSymmetric(token, installedData.sharedSecret, SymmetricAlgorithm.HS256))
return token;
}
const instance: AxiosInstance = axios.create({
baseURL: installedData.baseUrl,
});
instance.interceptors.request.use(async (config) => {
config.headers.Authorization = "JWT " + await createJwtToken(
config.method.toUpperCase(),
config.url
);
config.headers.Accept = 'application/json';
config.headers["Content-Type"] = 'application/json';
return config;
});
console.log(instance.defaults.headers)
return instance;
}
What I’ve Checked So Far:
- Verified that installedData.clientKey and installedData.sharedSecret are correct.
- Confirmed that baseUrl is correct, pointing to the Jira Cloud instance (e.g., https://mycompany.atlassian.net).
- Ensured that the qsh is calculated using only the relative path (e.g., /rest/api/2/issue/CL-988) instead of the full URL.
- Decoded the JWT on jwt.io to verify claims look correct (iss, iat, exp, qsh).
- Confirmed that my server’s time is accurate.
Despite these checks, I still receive a 401 Unauthorized response. I would greatly appreciate any guidance on what I might be missing. Are there any additional claims required, or is there a known issue with certain endpoints or HTTP methods? If you need more details, please let me know. Thanks in advance for your help!