JWT token to download attachments

Hi team,

I’m having some problems when creating the token for JIRA attachments when the attachment URL has encoded characters.

For example, the issue’s attachment has encoded characters:

The suggested way to create a JWT token in the documentation doesn’t work. The URL returns 401 Forbidden,

@Test
    public void createUriWithJwt()
        throws UnsupportedEncodingException, NoSuchAlgorithmException {
        long issuedAt = System.currentTimeMillis() / 1000L;
        long expiresAt = issuedAt + 180L;
        String key = "<hidden>"; //the key from the add-on descriptor
        String sharedSecret = "<hidden>";

        //during the add-on installation handshake
        String method = "GET";
        String baseUrl = "https://<hidden>.atlassian.net";
        String contextPath = "/";
        String apiPath = "/secure/attachment/12600/Some+Image+%28dev%29+-+JIRA+-+Google+Chrome_121+%288%29.png";
        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;
        System.out.print(apiUrl);
    }

If I change the the path to

String apiPath ="/rest/api/latest/serverInfo";

or

String apiPath = "/secure/attachment/12600/fakeName.png";

The generated URL works and I can get the server info and download the attachment.

What would be the CanonicalHttpUriRequest’s expected encoding for the apiPath parameter?

UPDATE:

I have also tried atlassian connect spring boot library with the same result, encoded filenames are unnauthorized. This is my test:

import com.atlassian.connect.spring.AtlassianHost;
import com.atlassian.connect.spring.AtlassianHostRepository;
import com.atlassian.connect.spring.internal.AtlassianConnectProperties;
import com.atlassian.connect.spring.internal.descriptor.AddonDescriptor;
import com.atlassian.connect.spring.internal.descriptor.AddonDescriptorLoader;
import com.atlassian.connect.spring.internal.request.AtlassianHostUriResolver;
import com.atlassian.connect.spring.internal.request.jwt.JwtGenerator;
import com.atlassian.connect.spring.internal.request.jwt.JwtQueryHashGenerator;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.HttpMethod;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;


public class JWTTest {

    @Test
    public void downloadFileURL() throws URISyntaxException {

        AtlassianHostRepository repository = Mockito.mock(AtlassianHostRepository.class);

        AtlassianHost host = new AtlassianHost();
        host.setBaseUrl("https://<hidden>.atlassian.net");
        String addonKey = "<hidden>";
        host.setSharedSecret("<hidden>");
        Mockito.when(repository.findFirstByBaseUrl(Mockito.anyString())).thenReturn(Optional.of(host));

        AtlassianHostUriResolver atlassianHostUriResolver = new AtlassianHostUriResolver(repository);
        AddonDescriptorLoader loader = Mockito.mock(AddonDescriptorLoader.class);
        AddonDescriptor addonDescriptor = Mockito.mock(AddonDescriptor.class);
        Mockito.when(addonDescriptor.getKey()).thenReturn(addonKey);
        Mockito.when(loader.getDescriptor()).thenReturn(addonDescriptor);
        JwtGenerator jwtGenerator = new JwtGenerator(atlassianHostUriResolver, loader, new AtlassianConnectProperties(), new JwtQueryHashGenerator());

        String apiPath = "/secure/attachment/12600/Some+Image+%28dev%29+-+JIRA+-+Google+Chrome_121+%288%29.png";


        String url = host.getBaseUrl() + apiPath;
        String jwt = jwtGenerator.createJwtToken(HttpMethod.GET, new URI(url), host);

        System.out.println(url + "?jwt=" + jwt);

    }
}

If I change the name to something without encoded characters (like /fakenme.png) I can download the file. I don’t want to patch URLs in my production code.

Any help will be highly appreciated!

Cheers
Fernando

4 Likes

Hi Fernando,

Thanks for raising this. I’m looking in to this, not sure what exactly is going wrong yet. I can reproduce the issue, and it definitely looks like a bug in the sample code and possibly the AtlassianJwt library. atlassian-connect-spring-boot will successfully make the request for you if you use the AtlassianHostRestClients#authenticatedAsAddon API method. Using the createJwt method in that same class to generate a JWT will fail, unless you first percent-encode the percent signs themselves as %25. So for your example url, that would be https://<hostname>.atlassian.net/secure/attachment/12600/Some+Image+%2528dev%2529+-+JIRA+-+Google+Chrome_121+%25288%2529.png. If you make a request to that url with the jwt generated using createJwt with that url, it will succeed. I haven’t yet been able to get the sample code from the documentation working, however.

1 Like

Hi Jake,

Thank you so much for pointing me in the right direction. It looks like AtlassianHostRestClients changes the url by using the spring web’s UriComponentsBuilder object that performs the URL encoding. The object is called before the createJwtToken method that’s why I couldn’t see it when creating my own AC Spring Boot Test.

If I change the last 3 lines of the spring boot test to

        String url = host.getBaseUrl() + apiPath;
        URI requestURI = UriComponentsBuilder.fromUriString(url).build().toUri();
        String jwt = jwtGenerator.createJwtToken(HttpMethod.GET, requestURI, host);
        System.out.println(requestURI + "?jwt=" + jwt);

The created URL works!!! :slight_smile: . I’m updating my addon code to use the UriComponentsBuilder when creating the secured URLs (our addon is pre AC spring boot). I still need to test if the new encoding doesn’t break other rest calls.

Could you update your basic JWT creation code in the AC documentation to include UriComponentsBuilder or a similar code that encodes the URLs in the correct JWT way and it doesn’t bring Spring MVC?

Do you know why the original URL is rejected by JIRA’s JWT authenticator?

Cheers
Fernando

I’m not sure why the approach with the double-encoded parentheses (%2528 + %2529) works, but I think the problem is really with JIRA’s REST API. I say that because we found this same problem in Confluence a while back (CE-880): the path to the file is encoded using application/x-www-form-urlencoded, which results in this query-string hash mismatch.

The same workaround also works here: decode the download path using application/x-www-form-urlencoded and reencode it as a URI path. Depending on your HTTP client, you may find that the second step is optional.

String decodedPath = java.net.URLDecoder.decode(path, Charset.defaultCharset().name());
// secure/attachment/10000/Some Image (dev) - JIRA - Google Chrome_121 (8).png
String encodedPath = org.springframework.web.util.UriUtils.encodePath(decodedPath, Charset.defaultCharset().name());
// secure/attachment/10000/Some%20Image%20(dev)%20-%20JIRA%20-%20Google%20Chrome_121%20(8).png

I’ve raised this as ACJIRA-1278.

1 Like