1
votes

I am a complete noob with Spring Integration Framework.

I am trying to consume a REST API that uses OAuth2. I am using Spring Integration xml-based configuration.

My issue is that cannot seem to get the Gateway and the Rest Template wired properly to send the body (multi-part) for the token request

This is in my spring integration config file:

spring-integration-context.xml

<!-- Rest Template -->
<bean id="oAuth2RestTemplate"
      class="org.springframework.security.oauth2.client.OAuth2RestTemplate">
    <constructor-arg ref="clientCredentialsResource"/>
</bean>

<!-- Used by Rest Template -->
<bean id= "clientCredentialsResource"
      class="org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails">
    <property name="clientId" value="${oauth2.clientId}" />
    <property name="clientSecret" value="${oauth2.clientSecret}" />
    <property name="accessTokenUri" value="${oauth2.accessTokenUri}" />
</bean>

<!-- Channels for requesting token -->
<int:channel id="tokenRequestChannel"/>
<int:channel id="tokenResponseChannel"/>

<!-- Gateway for requesting token -->
<int-http:outbound-gateway id="authRequestGateway"
                           request-channel="tokenRequestChannel"
                           url="${oauth2.endPointUri}"
                           reply-timeout="30000"
                           http-method="GET"
                           rest-template="oAuth2RestTemplate"
                           reply-channel="tokenResponseChannel"
                           charset="UTF-8"
                           expected-response-type="java.lang.String">
</int-http:outbound-gateway>

To obtain the initial token, I know I need to POST my credentials in the header similar to this:

Method: POST
Authorization: Basic <base64-encoded clientId:clientSecret>
Content-Type: application/x-www-form-urlencoded

And a adding multi-part (form) to the body

grant_type=client_credentials&scope=read

The API requires the string grant_type=client_credentials&scope=read to be in the request’s body (not the URL) as this is a POST (not a GET).

I have tried a few things (too many to remember/recount all) but I am not sure where/how to put the payload into my request.

I am missing something (obvious?) and I don't know what it is at this point.

This is what I get in the logs (request/response):

2016-11-10 16:46:22.429 DEBUG 6384 --- [ask-scheduler-3] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@2d1f9cc810 pairs: {POST [redacted]/oauth/token HTTP/1.1: null}{Authorization: Basic [redacted]}{Accept: application/json, application/x-www-form-urlencoded}{Content-Type: application/x-www-form-urlencoded}{Cache-Control: no-cache}{Pragma: no-cache}{User-Agent: Java/1.8.0_72}{Host: [redacted]}{Connection: keep-alive}{Content-Length: 29} 2016-11-10 16:46:22.584 DEBUG 6384 --- [ask-scheduler-3] s.n.www.protocol.http.HttpURLConnection : sun.net.www.MessageHeader@29200db310 pairs: {null: HTTP/1.1 400 Bad Request}{Cache-Control: no-cache}{Pragma: no-cache}{Content-Type: application/json; charset=utf-8}{Expires: -1}{Server: [redacted]}{[redacted]}{[redacted]}{Date: Fri, 11 Nov 2016 00:46:48 GMT}{Content-Length: 46} 2016-11-10 16:46:22.586 ERROR 6384 --- [ask-scheduler-3] o.s.integration.handler.LoggingHandler : org.springframework.messaging.MessageHandlingException: HTTP request execution failed for URI [redacted]; nested exception is error="access_denied", error_description="Access token denied." at org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler.handleRequestMessage(HttpRequestExecutingMessageHandler.java:409) at org.springframework.integration.handler.AbstractReplyProducingMessageHandler.handleMessageInternal(AbstractReplyProducingMessageHandler.java:109) at org.springframework.integration.handler.AbstractMessageHandler.handleMessage(AbstractMessageHandler.java:127) at org.springframework.integration.dispatcher.AbstractDispatcher.tryOptimizedDispatch(AbstractDispatcher.java:116) at org.springframework.integration.dispatcher.UnicastingDispatcher.doDispatch(UnicastingDispatcher.java:148) at org.springframework.integration.dispatcher.UnicastingDispatcher.dispatch(UnicastingDispatcher.java:121) at org.springframework.integration.channel.AbstractSubscribableChannel.doSend(AbstractSubscribableChannel.java:77) at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:423) at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:373) at org.springframework.messaging.core.GenericMessagingTemplate.doSend(GenericMessagingTemplate.java:115) at org.springframework.messaging.core.GenericMessagingTemplate.doSend(GenericMessagingTemplate.java:45) at org.springframework.messaging.core.AbstractMessageSendingTemplate.send(AbstractMessageSendingTemplate.java:105) at org.springframework.integration.endpoint.SourcePollingChannelAdapter.handleMessage(SourcePollingChannelAdapter.java:195) at org.springframework.integration.endpoint.AbstractPollingEndpoint.doPoll(AbstractPollingEndpoint.java:272) at org.springframework.integration.endpoint.AbstractPollingEndpoint.access$000(AbstractPollingEndpoint.java:58) at org.springframework.integration.endpoint.AbstractPollingEndpoint$1.call(AbstractPollingEndpoint.java:190) at org.springframework.integration.endpoint.AbstractPollingEndpoint$1.call(AbstractPollingEndpoint.java:186) at org.springframework.integration.endpoint.AbstractPollingEndpoint$Poller$1.run(AbstractPollingEndpoint.java:353) at org.springframework.integration.util.ErrorHandlingTaskExecutor$1.run(ErrorHandlingTaskExecutor.java:55) at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50) at org.springframework.integration.util.ErrorHandlingTaskExecutor.execute(ErrorHandlingTaskExecutor.java:51) at org.springframework.integration.endpoint.AbstractPollingEndpoint$Poller.run(AbstractPollingEndpoint.java:344) at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:81) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Caused by: error="access_denied", error_description="Access token denied." at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:142) at org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider.obtainAccessToken(ClientCredentialsAccessTokenProvider.java:44) at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:142) at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:118) at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221) at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173) at org.springframework.security.oauth2.client.OAuth2RestTemplate.createRequest(OAuth2RestTemplate.java:105) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:615) at org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute(OAuth2RestTemplate.java:128) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:595) at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:516) at org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler.handleRequestMessage(HttpRequestExecutingMessageHandler.java:382) ... 30 more Caused by: error="invalid_request", error_description="OAuth Error", message="{ "error": "invalid_scope" }" at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:120) at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:33) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3789) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2913) at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:225) at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readInternal(AbstractJackson2HttpMessageConverter.java:205) at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:193) at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport$AccessTokenErrorHandler.handleError(OAuth2AccessTokenSupport.java:235) at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:667) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:620) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:588) at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:137) ... 41 more

So, from what I can tell, I am getting an "Access Denied" message because I am unable to set the scope in the body of the request.

Note1

Also, I already looked at the code in the sample projects and did not see an answer to my issue.

Note2

I have verified (via POSTMAN) that the credentials, endpoints(URIs), scope, etc. are correct.

UPDATE 1

Based on comments from @Artem-Bilan, I changed the method to POST and added the code below to include the body in the request.

<!-- Add Payload -->
<int:inbound-channel-adapter id="oauth2ChannelAdapter" channel="tokenRequestChannel"
                             ref="grantAndScope" method="getGrantTypeAndScope">
    <!-- Triggering requests every 5 seconds -->
    <int:poller fixed-delay="5000" />
</int:inbound-channel-adapter>

<!-- Bean with Grant & Scope Payload -->
<bean id="grantAndScope" class="com.AuthTypeAndScopeInfo"/>

AuthTypeAndScopeInfo.java

public class AuthTypeAndScopeInfo {

    /* adds grant type and scope as body to message */
    public MultiValueMap<String, String> getGrantTypeAndScope() {
        // Create the request body as a MultiValueMap
        MultiValueMap<String, String> body = new LinkedMultiValueMap<String, String>();

        body.add("grant_type", "client_credentials");
        body.add("scope", "read");

        return body;
    }
}

The results, however, are the same :/

2

2 Answers

1
votes

So, you have really to provide POST instead of your current http-method="GET" and send the message to that gateway with the payload as a Map of those required pairs.

1
votes

In the end, I ended up not using the Oauth2RestTemplate.

Instead, I used the code below. The code basically starts by creating a Map payload (as suggested by Artem in his answer) and injects the headers for the first request.

Then it uses the response from the first request (which contains an access token) to inject the obtained token in the header of all subsequent requests.

There may be some room for optimization here but for now, this will suffice my needs.

AuthTypeAndScopeInfo.java

public class AuthTypeAndScopeInfo {

    /* adds grant type and scope as body to message */
    public MultiValueMap<String, String> getGrantTypeAndScope() {
        // Create the request body as a MultiValueMap
        MultiValueMap<String, String> body = new LinkedMultiValueMap<String, String>();

        body.add("grant_type", "client_credentials");
        body.add("scope", "read");

        return body;
    }
}

spring-integration-context.xml

<!-- Add Payload -->
<int:inbound-channel-adapter id="oauth2ChannelAdapter" channel="preTokenRequestChannel"
                             ref="grantAndScope" method="getGrantTypeAndScope">
    <!-- Triggering requests every 5 seconds -->
    <int:poller fixed-delay="5000" />
</int:inbound-channel-adapter>

<!-- POJO with Grant & Scope Payload -->
<bean id="grantAndScope" class="com.AuthTypeAndScopeInfo"/>

<!-- Adding headers for Token Request -->
<int:header-enricher input-channel="preTokenRequestChannel" 
                     output-channel="tokenRequestChannel">
    <int:header name="Authorization" value="Basic <clientId:clientSecret>"/>
    <int:header name="Content-Type" value="application/x-www-form-urlencoded"/>
</int:header-enricher>

<!-- Channels for requesting token -->
<int:channel id="tokenRequestChannel"/>
<int:channel id="tokenResponseChannel"/>

<!-- Channels for Authenticated requests (with valid token) -->
<int:channel id="authenticatedRequestChannel"/>
<int:channel id="authenticatedResponseChannel"/>

<!-- Gateway for requesting token -->
<!-- REST request to authorization server for a token -->
<!-- replies time out after 30 seconds -->
<int-http:outbound-gateway id="tokenRequestGateway"
                           request-channel="tokenRequestChannel"
                           url="${security.oauth2.client.accessTokenUri}"
                           reply-timeout="30000"
                           http-method="POST"
                           reply-channel="tokenResponseChannel"
                           charset="UTF-8"
                           expected-response-type="java.lang.String">
</int-http:outbound-gateway>

<!-- Adding headers for Authenticated Request (contains newly obtained token) -->
<int:chain input-channel="tokenResponseChannel"
           output-channel="authenticatedRequestChannel">
    <int:header-enricher>
        <!-- Adds token_type and the actual (authenticated) 
             access_token to the header of the next request 
             (overwriting the Basic <...> previous entry) -->
        <int:header name="Authorization" overwrite="true"
                    expression="#jsonPath(payload,'$.token_type') + ' ' + #jsonPath(payload,'$.access_token')" />
    </int:header-enricher>
</int:chain>

<!-- REST request with pre-authorized token -->
<!-- replies time out after 30 seconds -->
<int-http:outbound-gateway id="authenticatedRequestGateway"
                           request-channel="authenticatedRequestChannel"
                           url="${security.oauth2.client.endPointUri}"
                           reply-timeout="30000"
                           http-method="GET"
                           reply-channel="authenticatedResponseChannel"
                           charset="UTF-8"
                           expected-response-type="java.lang.String">
</int-http:outbound-gateway>