Rate this page:
Atlassian Connect uses a technology called JWT (JSON Web Token) to authenticate apps. JSON Web Tokens (JWT) are a standard way of representing security claims between the app and the Atlassian host product. A JWT token is a signed JSON object that contains information which enables the receiver to authenticate the sender of the request.
The format of a JWT token is: <base64url-encoded header>.<base64url-encoded claims>.<signature>
.
For more information about the structure of a JWT token, see Manually creating a JWT.
The high-level steps in creating a JWT token are:
encodedHeader
.encodedClaims
..
) and the encoded claims set. That gives you:
signingInput = encodedHeader+ "." + encodedClaims
.encodedSignature
.jwtToken = signingInput + "." + encodedSignature
Once you have the JWT token, you can use it to make calls like the ones in these examples.
Query string example:
1
GET https://<my-dev-environment>.atlassian.net/jira/rest/api/2/issue/AC-1.json?jwt=<jwt-token>
Headers example:
1 2
POST https://<my-dev-environment>.atlassian.net/jira/rest/api/2/issue/AC-1/attachments
"Authorization" header value: "JWT <jwt-token>"
Most modern languages have JWT libraries available. We recommend you use one of these libraries (or other JWT-compatible libraries) before trying to hand-craft the JWT token.
Language | Library |
---|---|
Java | atlassian-jwt and jsontoken |
Python | pyjwt |
Node.js | atlassian-jwt-js |
Ruby | atlassian-jwt-ruby |
PHP | firebase php-jwt and luciferous jwt |
.NET | jwt |
Haskell | haskell-jwt |
The JWT decoder is a handy web based decoder for Atlassian Connect JWT tokens.
Here is an example of creating a JWT token, in Java using atlassian-jwt and nimbus-jwt (last tested with atlassian-jwt version 1.5.3 and nimbus-jwt version 2.16):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import com.atlassian.jwt.*;
import com.atlassian.jwt.core.writer.*;
import com.atlassian.jwt.httpclient.CanonicalHttpUriRequest;
import com.atlassian.jwt.writer.JwtJsonBuilder;
import com.atlassian.jwt.writer.JwtWriterFactory;
public class JWTSample {
public String createUriWithJwt()
throws UnsupportedEncodingException, NoSuchAlgorithmException {
long issuedAt = System.currentTimeMillis() / 1000L;
long expiresAt = issuedAt + 180L;
String key = "atlassian-connect-addon"; //the key from the app descriptor
String sharedSecret = "..."; //the sharedsecret key received
//during the app installation handshake
String method = "GET";
String baseUrl = "https://<my-dev-environment>.atlassian.net/";
String contextPath = "/";
String apiPath = "/rest/api/latest/serverInfo";
JwtJsonBuilder jwtBuilder = new JsonSmartJwtJsonBuilder()
.issuedAt(issuedAt)
.expirationTime(expiresAt)
.issuer(key);
CanonicalHttpUriRequest canonical = new CanonicalHttpUriRequest(method,
apiPath, contextPath, new HashMap());
JwtClaimsBuilder.appendHttpRequestClaims(jwtBuilder, canonical);
JwtWriterFactory jwtWriterFactory = new NimbusJwtWriterFactory();
String jwtbuilt = jwtBuilder.build();
String jwtToken = jwtWriterFactory.macSigningWriter(SigningAlgorithm.HS256,
sharedSecret).jsonToJwt(jwtbuilt);
String apiUrl = baseUrl + apiPath + "?jwt=" + jwtToken;
return apiUrl;
}
}
The high-level steps of decoding and verifying a JWT token are:
jwt
query parameter or the authorization header.iss
) claim from the decoded, unverified claims object. This is the clientKey
for the tenant -
an identifier for the Atlassian product making the call, which should have been stored by the app as part of the
installation handshake.sharedSecret
for the clientKey
, as stored by the app during the installation handshakesharedSecret
and the algorithm specified in the header's alg
field. This
should be the HS256 algorithm.qsh
claim on the verified token.These steps must be executed before processing the request, and the request must be rejected if any of these steps fail.
Here is a minimal example of decoding and verifying a JWT token, in Java, using atlassian-jwt and nimbus-jwt (last tested with atlassian-jwt version 1.5.3 and nimbus-jwt version 2.16).
NOTE: This example does not include any error handling.
See AbstractJwtAuthenticator
from atlassian-jwt for recommendations of how to handle the different error cases.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
import com.atlassian.jwt.*;
import com.atlassian.jwt.core.http.JavaxJwtRequestExtractor;
import com.atlassian.jwt.core.reader.*;
import com.atlassian.jwt.exception.*;
import com.atlassian.jwt.reader.*;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
public class JWTVerificationSample {
public Jwt verifyRequest(HttpServletRequest request,
JwtIssuerValidator issuerValidator,
JwtIssuerSharedSecretService issuerSharedSecretService)
throws UnsupportedEncodingException, NoSuchAlgorithmException,
JwtVerificationException, JwtIssuerLacksSharedSecretException,
JwtUnknownIssuerException, JwtParseException {
JwtReaderFactory jwtReaderFactory = new NimbusJwtReaderFactory(
issuerValidator, issuerSharedSecretService);
JavaxJwtRequestExtractor jwtRequestExtractor = new JavaxJwtRequestExtractor();
CanonicalHttpRequest canonicalHttpRequest
= jwtRequestExtractor.getCanonicalHttpRequest(request);
Map<String, ? extends JwtClaimVerifier> requiredClaims = JwtClaimVerifiersBuilder.build(canonicalHttpRequest);
String jwt = jwtRequestExtractor.extractJwt(request);
return jwtReaderFactory.getReader(jwt).readAndVerify(jwt, requiredClaims);
}
}
Decoding the JWT token reverses the steps followed during the creation of the token, to extract the header, claims and signature. Here is an example in Java:
1 2 3 4 5 6 7
String jwtToken = ...;//e.g. extracted from the request
String[] base64UrlEncodedSegments = jwtToken.split('.');
String base64UrlEncodedHeader = base64UrlEncodedSegments[0];
String base64UrlEncodedClaims = base64UrlEncodedSegments[1];
String signature = base64UrlEncodedSegments[2];
String header = base64Urldecode(base64UrlEncodedHeader);
String claims = base64Urldecode(base64UrlEncodedClaims);
This gives us the following:
Header:
1 2 3 4
{
"alg": "HS256",
"typ": "JWT"
}
Claims:
1 2 3 4 5 6
{
"iss": "jira:15489595",
"iat": 1386898951,
"qsh": "8063ff4ca1e41df7bc90c8ab6d0f6207d491cf6dad7c66ea797b4614b71922e9",
"exp": 1386899131
}
Signature:
1
uKqU9dTB6gKwG6jQCuXYAiMNdfNRw98Hw_IWuA5MaMo
JWT libraries typically provide methods to be able to verify a received JWT token. Here is an example using nimbus-jose-jwt and json-smart:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import net.minidev.json.JSONObject;
public JWTClaimsSet read(String jwt, JWSVerifier verifier) throws ParseException, JOSEException
{
JWSObject jwsObject = JWSObject.parse(jwt);
if (!jwsObject.verify(verifier))
{
throw new IllegalArgumentException("Fraudulent JWT token: " + jwt);
}
JSONObject jsonPayload = jwsObject.getPayload().toJSONObject();
return JWTClaimsSet.parse(jsonPayload);
}
A query string hash is a signed canonical request for the URI of the API you want to call.
1 2
qsh = `sign(canonical-request)`
canonical-request = `canonical-method + '&' + canonical-URI + '&' + canonical-query-string`
A canonical request is a normalised representation of the URI. Here is an example. For the following URL, assuming you want to do a "GET" operation:
1
"https://<my-dev-environment>.atlassian.net/path/to/service?zee_last=param&repeated=parameter 1&first=param&repeated=parameter 2"
The canonical request is
1
"GET&/path/to/service&first=param&repeated=parameter%201,parameter%202&zee_last=param"
To create a query string hash, follow the detailed instructions below:
"GET"
or "PUT"
)'&'
baseUrl
in the app descriptor."jira.example.com/getsomething"
to "example.com/jira/getsomething"
without breaking authentication. The requester cannot know that the reverse proxy
will prepend the context path "/jira"
to the originally requested path "/getsomething"
"/"
instead.'&'
characters in the path.'/'
character unless it is the only character. e.g."https://example.atlassian.net/wiki/some/path/?param=value"
is "/some/path"
"https://example.atlassian.net"
is "/"
'&'
sort(["a", "A", "b", "B"]) => ["A", "B", "a", "b"]
'='
character and then its percent-encoded value. If the parameter has no value, the '='
character must still be included: &a=&b=foo&c=
','
character (i.e., "%2C"
) and subsequent percent-encoded values.jwt
parameter, if present."%20"
,
- ","
as "%2C"
,
- "+"
as "%2B"
,
- "*"
as "%2A"
and
- "~"
as "~"
.UTF-8
SHA-256
algorithmSHA-256
hash of "foo"
is "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
A JWT token looks like this:
1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEzODY4OTkxMzEsImlzcyI6ImppcmE6MTU0ODk1OTUiLCJxc2giOiI4MDYzZmY0Y2ExZTQxZGY3YmM5MGM4YWI2ZDBmNjIwN2Q0OTFjZjZkYWQ3YzY2ZWE3OTdiNDYxNGI3MTkyMmU5IiwiaWF0IjoxMzg2ODk4OTUxfQ.uKqU9dTB6gKwG6jQCuXYAiMNdfNRw98Hw_IWuA5MaMo
The basic format is:
1
<base64url-encoded header>.<base64url-encoded claims>.<base64url-encoded signature>
In other words:
You shouldn't actually have to do this manually, as there are libraries available in most languages, as we describe in the JWT libraries section.
However it is important you understand the fields in the JSON header and claims objects described in the next sections:
The header object declares the type of the encoded object and the algorithm used for the cryptographic signature. Atlassian Connect always uses the same values for these. The typ property will be "JWT" and the alg property will be "HS256".
1 2 3 4
{
"typ":"JWT",
"alg":"HS256"
}
Attribute | Type | Description |
---|---|---|
typ | String | Type for the token, defaulted to "JWT". Specifies that this is a JWT token |
alg (mandatory) | String | Algorithm. specifies the algorithm used to sign the token. In atlassian-connect version 1.0 we support the HMAC SHA-256 algorithm, which the JWT specification identifies using the string HS256. |
The claims object contains security information about the message you're transmitting. The attributes of this object provide information to ensure the authenticity of the claim. The information includes the issuer, when the token was issued, when the token will expire, and other contextual information, described below.
1 2 3 4 5 6 7
{
"iss": "jira:1234567",
"iat": 1300819370,
"exp": 1300819380,
"qsh": "8063ff4ca1e41df7bc90c8ab6d0f6207d491cf6dad7c66ea797b4614b71922e9",
"sub": "123456:1234abcd-1234-abcd-1234-1234abcd1234"
}
Attribute | Type | Description |
---|---|---|
iss (mandatory) | String | the issuer of the claim. Connect uses it to identify the application making the call. for example:
|
iat (mandatory) | Long | Issued-at time. Contains the UTC Unix time at which this token was issued. There are no hard requirements around this claim but it does not make sense for it to be significantly in the future. Also, significantly old issued-at times may indicate the replay of suspiciously old tokens. |
exp (mandatory) | Long | Expiration time. It contains the UTC Unix time after which you should no longer accept this token. It should be after the issued-at time. |
qsh (mandatory) | String | query string hash. A custom Atlassian claim that prevents URL tampering. |
sub (optional) | String | The subject of this token. This is the user associated with the action, defined by their Atlassian Account ID. If there is no user logged in, this attribute may not be present. |
aud (optional) | String or String[] | The audience(s) of this token. For REST API calls from an app to a product, the audience claim can be used to disambiguate the intended recipients. This attribute is not used for Jira and Confluence at the moment, but will become mandatory when making REST calls from an app to e.g. the bitbucket.org domain. |
context (optional) | Object | The context claim is an extension added by Atlassian Connect that may contain useful context for outbound requests (from the product to your app). |
You should use a little leeway when processing time-based claims, as clocks may drift apart. The JWT specification suggests no more than a few minutes. Judicious use of the time-based claims allows for replays within a limited window. This can be useful when all or part of a page is refreshed or when it is valid for a user to repeatedly perform identical actions (e.g. clicking the same button).
The signature of the token is a combination of a hashing algorithm combined with the header and claims sections of the token. This provides a way to verify that the claims and headers haven't been been compromised during transmission. The signature will also detect if a different secret is used for signing. In the JWT spec, there are multiple algorithms you can use to create the signature, but Atlassian Connect uses the HMAC SHA-256 algorithm. If the JWT token has no specified algorithm, you should discard that token as they're not able to be signature verified.
Here is an example in Java using gson, commons-codec, and the Java security and crypto libraries:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
public class JwtClaims {
protected String iss;
protected long iat;
protected long exp;
protected String qsh;
protected String sub;
// + getters/setters/constructors
}
[...]
public class JwtHeader {
protected String alg;
protected String typ;
// + getters/setters/constructors
}
[...]
import static org.apache.commons.codec.binary.Base64.encodeBase64URLSafeString;
import static org.apache.commons.codec.binary.Hex.encodeHexString;
import java.io.UnsupportedEncodingException;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import com.google.gson.Gson;
public class JwtBuilder {
public static String generateJWTToken(String requestUrl, String canonicalUrl,
String key, String sharedSecret)
throws NoSuchAlgorithmException, UnsupportedEncodingException,
InvalidKeyException {
JwtClaims claims = new JwtClaims();
claims.setIss(key);
claims.setIat(System.currentTimeMillis() / 1000L);
claims.setExp(claims.getIat() + 180L);
claims.setQsh(getQueryStringHash(canonicalUrl));
String jwtToken = sign(claims, sharedSecret);
return jwtToken;
}
private static String sign(JwtClaims claims, String sharedSecret)
throws InvalidKeyException, NoSuchAlgorithmException {
String signingInput = getSigningInput(claims, sharedSecret);
String signed256 = signHmac256(signingInput, sharedSecret);
return signingInput + "." + signed256;
}
private static String getSigningInput(JwtClaims claims, String sharedSecret)
throws InvalidKeyException, NoSuchAlgorithmException {
JwtHeader header = new JwtHeader();
header.alg = "HS256";
header.typ = "JWT";
Gson gson = new Gson();
String headerJsonString = gson.toJson(header);
String claimsJsonString = gson.toJson(claims);
String signingInput = encodeBase64URLSafeString(headerJsonString
.getBytes())
+ "."
+ encodeBase64URLSafeString(claimsJsonString.getBytes());
return signingInput;
}
private static String signHmac256(String signingInput, String sharedSecret)
throws NoSuchAlgorithmException, InvalidKeyException {
SecretKey key = new SecretKeySpec(sharedSecret.getBytes(), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
return encodeBase64URLSafeString(mac.doFinal(signingInput.getBytes()));
}
private static String getQueryStringHash(String canonicalUrl)
throws NoSuchAlgorithmException,UnsupportedEncodingException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(canonicalUrl.getBytes("UTF-8"));
byte[] digest = md.digest();
return encodeHexString(digest);
}
}
[...]
public class Sample {
public String getUrlSample() throws Exception {
String requestUrl =
"https://<my-dev-environment>.atlassian.net/rest/atlassian-connect/latest/license";
String canonicalUrl = "GET&/rest/atlassian-connect/latest/license&";
String key = "..."; //from the app descriptor
//and received during installation handshake
String sharedSecret = "..."; //received during installation Handshake
String jwtToken = JwtBuilder.generateJWTToken(
requestUrl, canonicalUrl, key, sharedSecret);
String restAPIUrl = requestUrl + "?jwt=" + jwtToken;
return restAPIUrl;
}
}
Rate this page: