Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

Problem with JWT token header

Brahim ESSALIH November 3, 2016

Hello everybody,

I developped an Atlassian connect addon using spring-boot. In the html veiw I make a POST request using ajax, when I uppload the connect addon into JIRA cloud instance and making the request,  Java display this error. 

java.lang.UnsupportedOperationException
	at java.util.Collections$UnmodifiableCollection.add(Collections.java:1055) ~[?:1.8.0_91]
	at org.springframework.http.HttpHeaders.add(HttpHeaders.java:1031) ~[spring-web-4.2.6.RELEASE.jar:4.2.6.RELEASE]
	at com.atlassian.connect.spring.internal.request.jwt.JwtSigningClientHttpRequestInterceptor$JwtSignedHttpRequestWrapper.setJwtHeaders(JwtSigningClientHttpRequestInterceptor.java:171) ~[atlassian-connect-spring-boot-core-1.0.0.jar:?]
	at com.atlassian.connect.spring.internal.request.jwt.JwtSigningClientHttpRequestInterceptor$JwtSignedHttpRequestWrapper.<init>(JwtSigningClientHttpRequestInterceptor.java:161) ~[atlassian-connect-spring-boot-core-1.0.0.jar:?]
	at com.atlassian.connect.spring.internal.request.jwt.JwtSigningClientHttpRequestInterceptor.wrapRequest(JwtSigningClientHttpRequestInterceptor.java:115) ~[atlassian-connect-spring-boot-core-1.0.0.jar:?]
	at com.atlassian.connect.spring.internal.request.jwt.JwtSigningClientHttpRequestInterceptor.lambda$intercept$0(JwtSigningClientHttpRequestInterceptor.java:43) ~[atlassian-connect-spring-boot-core-1.0.0.jar:?]
	at java.util.Optional.map(Optional.java:215) ~[?:1.8.0_91]

 

There is a ticket about this https://ecosystem.atlassian.net/browse/ACSPRING-19

In my pom.xml I use 

<dependency>
	<groupId>com.atlassian.connect</groupId>
	<artifactId>atlassian-connect-spring-boot-starter</artifactId>
	<version>1.0.0</version>
</dependency>

When I go to the exception the private class is not like in the fix https://ecosystem.atlassian.net/browse/ACSPRING-19

 

private class JwtSignedHttpRequestWrapper extends HttpRequestWrapper {
        private final String jwt;
        private final URI uri;
        public JwtSignedHttpRequestWrapper(HttpRequest request, String jwt, URI uri) {
            super(request);
            this.jwt = jwt;
            this.uri = uri;
            setJwtHeaders();       // this still here
        }
        @Override
        public URI getURI() {
            return uri;
        }
        private void setJwtHeaders() {
            HttpHeaders headers = super.getHeaders();
            headers.add(HttpHeaders.AUTHORIZATION, String.format("JWT %s", jwt));
            headers.add(HttpHeaders.USER_AGENT, String.format("%s/%s", USER_AGENT_PRODUCT, atlassianConnectClientVersion));
        }
    }

 

Would you have any suggestions how I can fix this issue ?

 

Best regards

 

 

4 answers

1 accepted

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

1 vote
Answer accepted
Einar Pehrson
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
December 28, 2016

With your last comment, it's finally clear to me what is going on.

Problem

For any HTTP requests with a body in org.springframework.web.client.RestTemplate (POST, PUT or exchange()), the inner HttpEntityRequestCallback class copies any headers provided for the HttpEntity to the request. This is a good idea because the HttpHeaders in the HttpEntity are made immutable when the HttpEntity is constructed. However, copying the values of the HttpHeaders only overcomes the fact that the map of headers is immutable. But it doesn't consider that each list of header values is also immutable.

This breaks because you set the Authorization header, and then JwtSigningClientHttpRequestInterceptor tries to add a value for the same header.

Solution

Instead of auto-wiring a RestTemplate object and thereby using the JwtSigningRestTemplate provided by atlassian-connect-spring-boot, you should create a new RestTemplate using the default constructor.

Follow-Up

To avoid this somewhat cryptic error message, I will update JwtSigningRestTemplate to overwrite any existing Authorization header value (since that header is only allowed to have one value).

Other headers (especially custom ones) could potentially accept multiple values though, so I'll raise this as a bug in the Spring project as well (even though it could be considered misuse of the framework API).

Brahim ESSALIH December 28, 2016

Ok thanks @Einar Pehrson

1 vote
Einar Pehrson
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
December 27, 2016

@Brahim ESSALIH, I was able to reproduce this problem using this ClientHttpRequestInterceptor. Is there any chance you have added an interceptor that returns a read-only HttpHeaders?

@Component
public class BadClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        return execution.execute(new HttpRequestWrapper(request) {

            @Override
            public HttpHeaders getHeaders() {
                return HttpHeaders.readOnlyHttpHeaders(super.getHeaders());
            }
        }, body);
    }
}


(Looking at the stack trace from your comment on another question, I first thought your use of RestTemplate#exchange(...) may have triggered this problem due to the call to HttpHeaders.readOnlyHttpHeaders(HttpHeaders) in the constructor of HttpEntity, but I wasn't able to confirm that. It seems like Spring properly copies headers, and never runs into problems with that instance being read-only.)

Brahim ESSALIH December 27, 2016

Thanks @Einar Pehrson, So you think that restemplate.exchange... was the cause of that error. Would you have any alernatif of the method exchange using Httpheader ?  

When I don't use basic authentication headers it works without problem

Einar Pehrson
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
December 27, 2016

No, @Brahim ESSALIH, what I meant was that I had a guess that exchange() was the problem, but it turned out to be wrong. I wrote a passing test using exchange().

Rather, I think the problem is that somewhere you have a piece of code that calls HttpHeaders.readOnlyHttpHeaders(). You could try setting a breakpoint inside that method.

When I don't use basic authentication headers it works without problem

I'm not sure what you mean by this. I wonder if you're trying to use both Basic authentication and JWT authentication with a single RestTemplate.

Brahim ESSALIH December 27, 2016

I mean by this: @Einar Pehrson

When I don't use basic authentication headers it works without problem

 that when I disable basic authentication and then making request to the web service without adding header using restemplate.postforEntity or putForEntity... all works good.

I have this problem only when the Webhook triggre my service that make a request using Http basic authentication header

 

 

Einar Pehrson
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
December 27, 2016

Hm, I don't think I have the full context of your problem then, @Brahim ESSALIH.

So your add-on receives a webhook from JIRA. And as part of handling that webhook, you make a request to another service using Basic authentication? If so, what code do you use to make the request? How do you create and configure that RestTemplate (or whichever client you use)? How do you create the Authorization header for that request?

Brahim ESSALIH December 27, 2016

@Einar Pehrson this is my context: 

In my descriptor.json I define this webhook like this snippet:

"webhooks": [
			{
				"name": "Issue created event",
				 "event": "jira:issue_created",
				"filter": "project = DEVTEST",
				"url": "/webhook/issueEvent",
				"excludeBody": false
			},
...

This is how I create Authirization header:

import org.springframework.http.HttpHeaders;
import org.apache.commons.codec.binary.Base64;
 // Authorization Header
    public HttpHeaders getHeader() {
        HttpHeaders headers = new HttpHeaders();
        String plainCreds = userJira + ":" + userPasswordJira;
        byte[] plainCredsBytes = plainCreds.getBytes();
        byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes);
        String base64Creds = new String(base64CredsBytes);
        headers.add("Authorization", "Basic " + base64Creds);
        return headers;
    }

If the service make a request HTTP using resttemplate.exchange, I get errors like in this post https://answers.atlassian.com/questions/44341020

 

This is my service:

...	
import org.springframework.http.HttpEntity;
 
    @Autowired
    RestTemplate restTemplate;  // Because I use this dependency: "atlassian-connect-spring-boot-starter"
    
 @RequestMapping(value = "/webhook/issueEvent", method = RequestMethod.POST)
    public String issueEvent(@RequestBody WebhookBodyWrapper webhookBody) throws JsonProcessingException {
			...
			HttpEntity<IssueUserWrapper> httpEntity = new HttpEntity<IssueUserWrapper>(assigneeWrapper, getHeader());
			// THIS THROWs ERROR BELOW
            restTemplate.exchange(url, HttpMethod.PUT, httpEntity, Object.class);
  return statusCode;
}

I hope I was clear enough

0 votes
Einar Pehrson
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
December 27, 2016

@Brahim ESSALIH, do you still see this error or were you able work around it? Can you provide any more information or perhaps steps to reproduce?

The stack trace can hardly be related to an AJAX POST request, since the responsibility of JwtSigningClientHttpRequestInterceptor is to add an Authorization header for JWT authentication to outbound requests made from the server-side of your Spring Boot application.

0 votes
James Hazelwood
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
November 7, 2016

Hi, @Brahim ESSALIH , the issue you mention is fixed in 1.0.0 so that specific bug is unlikely to be the problem.

Is there any chance you could share some of your code? It's hard to work out what's going wrong with just the stack trace and the connect-spring-boot code.

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

TAGS
AUG Leaders

Atlassian Community Events