Why did secure Spring WS integration return Unauthorized [401]? - spring

I'm trying to consume a secure third-party SOAP web service in Spring boot, and I'm following Spring WS plus an online resource describing secure integration Spring WS - HTTPS Client-Server Example. After locating the certificate .cer and .jks files under resources folder, and used this code to wrap my configuration:
#Configuration
public class WSGeneralConfig {
#Autowired
#Qualifier("WifiTouristConfig")
CredentailsConfig wifiTouristConfig;
#Autowired
#Qualifier("TradeinWSConfig")
CredentailsConfig tradeinWSConfig;
#Autowired
ClientInterceptor[] interceptors;
#Bean
public WifiToursitWrapper wifiToursitWrapper(#Value("${tibco.wifi.tourist.url}") String uri) throws Exception {
WifiToursitWrapper wifiToursitWrapper = new WifiToursitWrapper();
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath("com.usp.tibco.wifi");
wifiToursitWrapper.setMarshaller(marshaller);
wifiToursitWrapper.setUnmarshaller(marshaller);
wifiToursitWrapper.setDefaultUri(uri);
wifiToursitWrapper.setInterceptors(interceptors);
wifiToursitWrapper.setMessageSender(
httpComponentsMessageSender(wifiTouristConfig.getUsername(), wifiTouristConfig.getPassword()));
return wifiToursitWrapper;
}
public HttpComponentsMessageSender httpComponentsMessageSender(String userName, String password) throws Exception {
HttpComponentsMessageSender httpComponentsMessageSender = new HttpComponentsMessageSender();
httpComponentsMessageSender.setCredentials(new UsernamePasswordCredentials(userName, password));
httpComponentsMessageSender.setHttpClient(httpClient());
httpComponentsMessageSender.setAuthScope(AuthScope.ANY);
httpComponentsMessageSender.afterPropertiesSet();
return httpComponentsMessageSender;
}
public HttpClient httpClient() throws Exception {
return HttpClientBuilder
.create()
.setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext(), NoopHostnameVerifier.INSTANCE))
.addInterceptorFirst(new RemoveSoapHeadersInterceptor()).build();
}
public SSLContext sslContext() throws Exception {
File file = new ClassPathResource("cert/cacerts.jks").getFile();
return SSLContextBuilder.create().loadTrustMaterial(file, "jksPassword".toCharArray()).build();
}
}
The client class also like this
public class WifiToursitWrapper extends WebServiceGatewaySupport {
#SoapOperationName(systemName = SystemName.TIBCO, operationName = "RegisterByIntlNo")
public RegisterByIntlNoResponse registerByIntlNo(RegisterByIntlNoRequest request) {
return (RegisterByIntlNoResponse) getWebServiceTemplate().marshalSendAndReceive(request,
message -> ((SoapMessage) message)
.setSoapAction("/RegisterByIntlNo"));
}
}
Unfortunately I got Unauthorized exception, and I don't know what the root cause of that? here is a snapshot from Exception stack trace
org.springframework.ws.client.WebServiceTransportException: Unauthorized [401]
at org.springframework.ws.client.core.WebServiceTemplate.handleError(WebServiceTemplate.java:699)
at org.springframework.ws.client.core.WebServiceTemplate.doSendAndReceive(WebServiceTemplate.java:609)
at org.springframework.ws.client.core.WebServiceTemplate.sendAndReceive(WebServiceTemplate.java:555)
at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:390)
at org.springframework.ws.client.core.WebServiceTemplate.marshalSendAndReceive(WebServiceTemplate.java:383)
at com.etisalat.account.wrapper.WifiToursitWrapper.registerByIntlNo(WifiToursitWrapper.java:25)

I solved the issue by adding Base Authentication to my request, with a good help from this question's answer Consuming SOAP WS returns Error 401 unauthorized , I changed some little code for my case.
private HttpComponentsMessageSender httpComponentsMessageSender(String userName, String password) throws Exception {
HttpComponentsMessageSender httpComponentsMessageSender = new HttpComponentsMessageSender();
httpComponentsMessageSender.setHttpClient(httpClient(userName, password));
return httpComponentsMessageSender;
}
private HttpClient httpClient(String username, String password) throws Exception {
List<Header> headers = new ArrayList<>();
BasicHeader authHeader = new BasicHeader("Authorization", "Basic " + base64authUserPassword(username, password));
headers.add(authHeader);
RequestDefaultHeaders reqHeader = new RequestDefaultHeaders(headers);
return HttpClientBuilder.create()
.setSSLSocketFactory(new SSLConnectionSocketFactory(sslContext(), NoopHostnameVerifier.INSTANCE))
.addInterceptorFirst(new RemoveSoapHeadersInterceptor()).addInterceptorLast(reqHeader).build();
}
private String base64authUserPassword(String username, String password) {
String userpassword = username + ":" + password;
String encodedAuthorization = new String(Base64.getEncoder().encode(userpassword.getBytes()));
return encodedAuthorization;
}

Related

Spring Webflux OAuth 2 resource server

I have a Spring OAuth 2 server based on Spring Boot 1.5 (Spring Security v4) which generates customized tokens and a few resource servers who communicate with this authorization server, making use of /oauth/check_token endpoint by configuration of RemoteTokenServices.
All the logic related to storing/retrieving tokens on Authorization server side is done with JdbcTokenStore.
I am building a new Spring Boot 2 application which is build with Spring webflux module and trying to implement client_credentials flow with existing Authorization Server using Spring Security 5.1.1.
I found that support for resource servers was added in 5.1.0.RC1 (https://spring.io/blog/2018/08/21/spring-security-5-1-0-rc1-released#oauth2-resource-servers) and updated in 5.1.0.RC2 (https://spring.io/blog/2018/09/10/spring-security-5-1-0-rc2-released#oauth2-resource-server) but looks like it's only possible to configure it with JWT support.
I might be messing up with concepts here but looking for more info and a way to configure all these components together.
I'm in same situation as you.I solve that problem in next way, maybe it can help you:
spring-boot-starter-parent.version: 2.1.1
spring-cloud-dependencies.version: Greenwich.R1
Security configuration:
#EnableWebFluxSecurity
public class SecurityConfig {
#Autowired
private ReactiveAuthenticationManager manager; //custom implementation
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/role").hasRole("ADMIN")
.pathMatchers("/test").access(new HasScope("server")) //custom implementation
.anyExchange().authenticated()
.and()
.httpBasic().disable()
.oauth2ResourceServer()
.jwt()
.authenticationManager(manager)
.and().and()
.build();
}
}
ReactiveAuthorizationManager (HasScope) implementation:
Helper which allow search for scopes in authentication object
public class HasScope implements ReactiveAuthorizationManager<AuthorizationContext> {
public HasScope(String...scopes) {
this.scopes = Arrays.asList(scopes);
}
private final Collection<String> scopes;
#Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) {
return authentication
.flatMap(it -> {
OAuth2Authentication auth = (OAuth2Authentication) it;
Set<String> requestScopes = auth.getOAuth2Request().getScope();
boolean allow = requestScopes.containsAll(scopes);
return Mono.just(new AuthorizationDecision(allow));
});
}
}
ReactiveAuthenticationManager implementation:
That is the main component in configuration which create OAuth2Authentication. There is a problem with response for wrong access_token, it returns only status code without body response.
#Component
public class ReactiveAuthenticationManagerImpl implements ReactiveAuthenticationManager {
private final ResourceServerProperties sso;
private final WebClient.Builder webClient;
private final ObjectMapper objectMapper;
private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();
public ReactiveAuthenticationManagerImpl(ResourceServerProperties sso,
#Qualifier("loadBalancedWebClient") WebClient.Builder webClient, ObjectMapper objectMapper) {
this.sso = sso;
this.webClient = webClient;
this.objectMapper = objectMapper;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.cast(BearerTokenAuthenticationToken.class)
.flatMap(it -> getMap(it.getToken()))
.flatMap(result -> Mono.just(extractAuthentication(result)));
}
private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
Object principal = getPrincipal(map);
OAuth2Request request = getRequest(map);
List<GrantedAuthority> authorities = authoritiesExtractor.extractAuthorities(map);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
token.setDetails(map);
return new OAuth2Authentication(request, token);
}
private Object getPrincipal(Map<String, Object> map) {
if (map.containsKey("principal")) {
try {
//that is the case for user authentication
return objectMapper.convertValue(map.get("principal"), UserPrincipal.class);
} catch (IllegalArgumentException ex) {
//that is the case for client authentication
return objectMapper.convertValue(map.get("principal"), String.class);
}
}
return null;
}
#SuppressWarnings({"unchecked"})
private OAuth2Request getRequest(Map<String, Object> map) {
Map<String, Object> request = (Map<String, Object>) map.get("oauth2Request");
String clientId = (String) request.get("clientId");
Set<String> scope = new LinkedHashSet<>(request.containsKey("scope") ?
(Collection<String>) request.get("scope") : Collections.emptySet());
return new OAuth2Request(null, clientId, null, true, new HashSet<>(scope),
null, null, null, null);
}
private Mono<Map<String, Object>> getMap(String accessToken) {
String uri = sso.getUserInfoUri();
return webClient.build().get()
.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + accessToken)
.exchange()
.flatMap(it -> it.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {}))
.onErrorMap(InvalidTokenException.class, mapper -> new InvalidTokenException("Invalid token: " + accessToken));
}

Spring WS is generating empty SOAP Envelop

I want do call a SOAP service with Spring WS WebServiceTemplate. I have used this very often and it always worked so far. But now I just get an soap envelope with empty body:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body/></SOAP-ENV:Envelope>
I have created the request and response classes with the JAXB Maven Plugin. And the generated source code looks exactly like the services which are working.
Example:
#XmlAccessorType(XmlAccessType.FIELD)
#XmlType(
name = "startRequest_RequestParameters",
propOrder = {"url"}
)
#XmlRootElement(
name = "startRequest"
)
public class StartRequest {
#XmlElement(
required = true
)
#XmlSchemaType(
name = "anyURI"
)
protected String url;
public StartRequest() {
}
public String getUrl() {
return this.url;
}
public void setUrl(String value) {
this.url= value;
}
}
I call the webservice template with marshallSendAndReceive
StartRequest request = new StartRequest();
request.setUrl(url);
StartResponse response = (StartResponse) webServiceTemplate.marshalSendAndReceive(endpointUrl, request);
I configure the WebServiceTemplate with java configuration:
public WebServiceTemplate startRequestWebServiceTemplate() throws Exception {
return createWebServiceTemplate(createMarshaller(), createSecurityInterceptor(username, password), createMessageSender(proxyHost, proxyPort));
}
private WebServiceTemplate createWebServiceTemplate(Jaxb2Marshaller marshaller, ClientInterceptor securityInterceptor, WebServiceMessageSender messageSender) {
WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
webServiceTemplate.setMarshaller(marshaller);
webServiceTemplate.setUnmarshaller(marshaller);
webServiceTemplate.setMessageSender(messageSender);
if (securityInterceptor != null) {
webServiceTemplate.setInterceptors((ClientInterceptor[]) Arrays.asList(securityInterceptor, createLoggingInterceptor()).toArray());
} else {
webServiceTemplate.setInterceptors((ClientInterceptor[]) Arrays.asList(createLoggingInterceptor()).toArray());
}
webServiceTemplate.setCheckConnectionForFault(true);
webServiceTemplate.afterPropertiesSet();
return webServiceTemplate;
}
private Jaxb2Marshaller createMarshaller() throws Exception {
Jaxb2Marshaller jaxb2Marshaller = new Jaxb2Marshaller();
jaxb2Marshaller.setClassesToBeBound(StartRequest.class, StartResponse.class);
jaxb2Marshaller.afterPropertiesSet();
return jaxb2Marshaller;
}
private ClientInterceptor createLoggingInterceptor() {
return new SoapLoggingInterceptor(systemName);
}
private Wss4jSecurityInterceptor createSecurityInterceptor(String username, String password) {
Wss4jSecurityInterceptor wss4jSecurityInterceptor = new Wss4jSecurityInterceptor();
wss4jSecurityInterceptor.setSecurementPasswordType("PasswordText");
wss4jSecurityInterceptor.setSecurementActions("UsernameToken");
wss4jSecurityInterceptor.setSecurementUsername(username);
wss4jSecurityInterceptor.setSecurementPassword(password);
wss4jSecurityInterceptor.setSkipValidationIfNoHeaderPresent(true);
wss4jSecurityInterceptor.setValidateRequest(false);
wss4jSecurityInterceptor.setValidateResponse(false);
return wss4jSecurityInterceptor;
}
private HttpComponentsMessageSender createMessageSender(String proxyHost, String proxyPort) {
HttpComponentsMessageSender httpComponentsMessageSender = new HttpComponentsMessageSender(createHttpClient(proxyHost, proxyPort));
httpComponentsMessageSender.setAcceptGzipEncoding(true);
return httpComponentsMessageSender;
}
private HttpClient createHttpClient(String proxyHost, String proxyPort) {
RequestConfig.Builder configBuilder = RequestConfig.custom()
.setConnectTimeout(DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS)
.setSocketTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS)
.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT);
addProxySettings(configBuilder, proxyHost, proxyPort);
HttpClientBuilder clientBuilder = HttpClients.custom().setDefaultRequestConfig(configBuilder.build());
addInterceptor(clientBuilder);
addConnectionManager(clientBuilder);
return clientBuilder.build();
}
private void addProxySettings(RequestConfig.Builder configBuilder, String proxyHost, String proxyPort) {
if (StringUtils.isNotBlank(proxyHost)) {
configBuilder.setProxy(new HttpHost(proxyHost, Integer.valueOf(proxyPort)));
}
}
private void addInterceptor(HttpClientBuilder clientBuilder) {
clientBuilder.addInterceptorFirst(new HttpComponentsMessageSender.RemoveSoapHeadersInterceptor());
}
private void addConnectionManager(HttpClientBuilder clientBuilder) {
if (maxConnections > DEFAULT_MAX_CONNECTIONS_PER_ROUTE) {
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(maxConnections);
cm.setDefaultMaxPerRoute(maxConnections);
clientBuilder.setConnectionManager(cm);
}
}
This configuration worked fine for other soap implementations. But here I just get the soap envelope with the empty body.
Has anyone an idea what's wrong here?
I did something wrong when refactoring the LoggingInterceptor. When handling the request it took the response part from the MessageContext instead of the request part, which caused to overwrite the request with the response. So if you have such a problem check your interceptors if they handle response and request correctly

Spring Security OAuth2- POST request to oauth/token redirects to login and role displays ROLE_ANONYMOUS

I am following the link https://spring.io/blog/2015/02/03/sso-with-oauth2-angular-js-and-spring-security-part-v & the github project https://github.com/spring-guides/tut-spring-security-and-angular-js/tree/master/oauth2. I am able to login in to the OAuth provider and get the authorization code back in the client.
Now I make the following call from the client to get the token from the provider (provider is on port 9999)
HttpHeaders headers = new HttpHeaders();
headers.add("Accept",MediaType.APPLICATION_JSON_VALUE);
List<String> cookies = httpEntity.getHeaders().get("Cookie");
headers.put("Cookie", cookies);
String redirectURL= "http://localhost:9999/oauthprovider/oauth/token" + "?" + "response_type=token" + "&" + "grant_type=authorization_code" + "&" + "client_id=acme"+ "&" + "client_secret=acmesecret"+ "&" + "redirect_uri=http://localhost:8081/callback"+"&" + "code=" + authCode + "&" + "state=" + stateValue;
HttpEntity<String> redirectResponse = template.exchange(
redirectURL,
HttpMethod.POST,
responseentity,
String.class);
result=redirectResponse.toString()
The result variable value has the following.(I have disabled csrf and sending client_secret as a query parameter (for the time being), although they are not recommended)
<302 Found,{X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY], Location=[http://localhost:9999/oauthprovider/oauthlogin], Content-Length=[0], Date=[Thu, 09 Nov 2017 12:07:37 GMT]}>
In the console I have these
Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#9055c2bc: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 2B669DF59BCE8047849BFBCA148BEE67; Granted Authorities: ROLE_ANONYMOUS
Does I am redirecting back to login(I am getting it in the logs as mentioned before), since the role is ROLE_ANONYMOUS? How can I fix the issue?
Adding more details on the code (Did only minor changes from the sample code provided in the link). Providers's context path is /oauthprovider and with curl call I am getting the token.
#Configuration
#EnableAuthorizationServer
protected static class OAuth2AuthorizationConfig extends
AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
//................................
//................................
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("acme")
.secret("acmesecret")
.authorizedGrantTypes("authorization_code", "refresh_token",
"password").scopes("openid").autoApprove(true).redirectUris("http://localhost:8081/callback");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager).accessTokenConverter(
jwtAccessTokenConverter());
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
"isAuthenticated()");
}
}
#Configuration
#Order(-20)
protected static class LoginConfig extends WebSecurityConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.formLogin().loginPage("/oauthlogin").loginProcessingUrl("/login").failureUrl("/login?error=true").permitAll()
.and()
.requestMatchers().antMatchers("/login", "/oauthlogin", "/oauth/authorize", "/oauth/token" ,"/oauth/confirm_access")
.and()
.authorizeRequests().anyRequest().authenticated();
// #formatter:on
http.csrf().disable();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.parentAuthenticationManager(authenticationManager);
}
}
For calling token endpoint you need to send Authorization header with value base64(client_id:client_secret) and in body you should send username , password ,grant_type as FORM_URLENCODED:
String oauthHost = InetAddress.getByName(OAUTH_HOST).getHostAddress();
HttpHeaders headers = new HttpHeaders();
RestTemplate restTemplate = new RestTemplate();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
// Basic Auth
String plainCreds = clientId + ":" + clientSecret;
byte[] plainCredsBytes = plainCreds.getBytes();
byte[] base64CredsBytes = org.apache.commons.net.util.Base64.encodeBase64(plainCredsBytes);
String base64Creds = new String(base64CredsBytes);
headers.add("Authorization", "Basic " + base64Creds);
// params
map.add("username", username);
map.add("password", password);
map.add("grant_type", GRANT_TYPE);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map,
headers);
// CALLING TOKEN URL
OauthTokenRespone res = null;
try {
res = restTemplate.postForObject(OAUTH_TOKEN_URL.replace(OAUTH_HOST, oauthHost), request,
OauthTokenRespone.class);
} catch (Exception ex) {
ex.printStackTrace();
}
}
#JsonIgnoreProperties(ignoreUnknown = true)
public class OauthTokenRespone {
private String access_token;
private String token_type;
private String refresh_token;
private String expires_in;
private String scope;
private String organization;
//getter and setter
}
Something went wrong in my last day's testing. I can get access token if my handler is either in client or in provider. If my redirect handler is in provider the user is not anonymous as per the debug logs (may be due to session..?)But looks like I have to use one redirect_url consistently.(Otherwise I get redirect_uri mismatch error..)
Following is the working code that gets json response..
#ResponseBody
#RequestMapping(value = "/clientcallback", method = RequestMethod.GET)
public ResponseEntity<OauthTokenResponse> redirectCallback(#RequestParam (value= "code", defaultValue="") String authCode,#RequestParam (value= "state", defaultValue="") String stateValue,HttpEntity<String> httpEntity)
{
HttpHeaders headers = new HttpHeaders();
RestTemplate restTemplate = new RestTemplate();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
String plainCreds = "acme:acmesecret";
byte[] plainCredsBytes = plainCreds.getBytes();
byte[] base64CredsBytes = org.apache.commons.net.util.Base64.encodeBase64(plainCredsBytes);
String base64Creds = new String(base64CredsBytes);
headers.add("Authorization", "Basic " + base64Creds);
List<String> cookies = httpEntity.getHeaders().get("Cookie");
if(cookies != null)
{
headers.put("Cookie", cookies);
}
else
{
cookies = httpEntity.getHeaders().get("Set-Cookie");
headers.put("Set-Cookie", cookies);
}
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map,
headers);
OauthTokenResponse res = null;
try {
res = restTemplate.postForObject("http://localhost:9999/uaa/oauth/token?grant_type=authorization_code&client_id=acme&redirect_uri=http://localhost:8081/clientcallback&code=" + authCode, request,
OauthTokenResponse.class);
} catch (Exception ex) {
ex.printStackTrace();
}
return new ResponseEntity<OauthTokenResponse>(res, HttpStatus.OK);
}
Thanks again for the tips..
I know this is old but I've recently faced a very similar issue so I'm posting my solution.
I used Simle SSO Example as a base for my modifications. I'm using spring security filter which is mapped to / (web root) and spring oauth with endpoints mapped to /auth/*. When I try to access /auth/oauth/token I get redirect to login page. After some debugging I found out the cause:
By using #EnableAuthorizationServer you are importing AuthorizationServerSecurityConfiguration which secures endpoints /oauth/token, /oauth/token_key and /oauth/check_token. Everything is going to work with this default configuration as long as your authorization server is mapped to the web root. In my case, requests to /auth/oauth/token were simply redirected to login page because spring security could not find a rule for this path.
My solution was to manualy secure those endpoints with /auth prefix in my spring security configuration.
Hope this helps.

Convert Spring Boot and OAuth2 client from using Client Credentials flow to Authorization Code Grant flow

I have been struggling while trying to create a sample client that uses the OAuth 2.0 Authorization Code Grant flow.
I am able to successfully use the Client Credentials flow but when I try to use the Authorization Code flow I do not get redirected to the correct uri.
When calling the OAuth2RestTmplate.exchange method, I get a redirect exception in the RestTemplate.doExecute(...) method. It is thrown from the finally clause.
The response is null, but the if is not stopping it.
finally {
if (response != null) {
response.close();
}
I still get prompted for login and authorization, but am not directed to the response containing the data. I am just redirected back to the client home page. The same call from Postman using the authorization code flow with the same client credentials is successful so I know the client registration is correct.
I could use another pair of eyes to see what I'm missing. Thanks in Advance! Below are my code excerpts.
Working client using oauth2 client credentials flow:
Client App:
#SpringBootApplication(exclude = {SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class})
public class ClientExampleClientCredentials extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(ClientExampleClientCredentials.class, args);
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(ClientExampleClientCredentials.class);
}
}
Controller:
#RestController
public class HomeController {
#Value("${security.oauth2.client.clientId}")
private String clientId;
#Value("${security.oauth2.client.clientSecret}")
private String clientSecret;
#Value("${security.oauth2.client.apiUrl}")
private String apiUrl;
#Value("${security.oauth2.client.scope}")
private List<String> scopes;
#Value("${security.oauth2.client.accessTokenUri}")
private String accessTokenUri;
/**
* Example of using the OAuth2RestTemplate to access external resources
*
* The OAuth2RestTemplate takes care of exchanging credentials with the auth server, as well as adding the
* bearer token to each request to the FHIR services.
*
*/
#RequestMapping("/ex-1")
public String retrievePatientByIdUsingRestTemplate(#RequestParam String id) {
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(getClientCredentialsResourceDetails(), new DefaultOAuth2ClientContext());
ResponseEntity<String> response = oAuth2RestTemplate.exchange(apiUrl + "/Patient/" + id, HttpMethod.GET, null, String.class);
String responseBody = response.getBody();
return responseBody;
}
private ClientCredentialsResourceDetails getClientCredentialsResourceDetails() {
ClientCredentialsResourceDetails clientCredentialsResourceDetails = new ClientCredentialsResourceDetails();
clientCredentialsResourceDetails.setAccessTokenUri(accessTokenUri);
clientCredentialsResourceDetails.setAuthenticationScheme(AuthenticationScheme.header);
clientCredentialsResourceDetails.setClientId(clientId);
clientCredentialsResourceDetails.setClientSecret(clientSecret);
clientCredentialsResourceDetails.setScope(scopes);
return clientCredentialsResourceDetails;
}
}
application.yml
security:
oauth2:
client:
clientId: client_id
clientSecret: secret
apiUrl: http://localhost:8080/testData/data
accessTokenUri: http://localhost:8080/test-auth/token
scope: system/*.read
This works great authenticating me and then redirecting to my service url. However, The Authorization Code flow is not working.
Broken client using oauth2 authorization code flow:
Client App:
#SpringBootApplication (exclude = {SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class})
public class ClientExampleAccessToken extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(ClientExampleAccessToken.class, args);
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(ClientExampleAccessToken.class);
}
}
Controller:
package org.ihc.clinical.controller;
#Configuration
#EnableOAuth2Client
#RestController
public class HomeController {
#Value("${security.oauth2.client.clientId}")
private String clientId;
#Value("${security.oauth2.client.clientSecret}")
private String clientSecret;
#Value("${security.oauth2.client.accessTokenUri}")
private String accessTokenUri;
#Value(("${security.oauth2.client.userAuthorizationUri}"))
private String userAuthorizationUri;
#Value("${security.oauth2.client.apiUrl}")
private String apiUrl;
#Value("${security.oauth2.client.redirectUri}")
private String redirectUri;
#RequestMapping("/ex-1")
public String retrievePatientByIdUsingRestTemplate(#RequestParam String empi) {
OAuth2ProtectedResourceDetails resource = resource();
String path = apiUrl + "/Patient/" + empi;
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext());
***/*error occurs here in RestTemplate.doExcute. error:org.springframework.security.oauth2.client.resource.UserRedirectRequiredException:
A redirect is required to get the users approval */***
ResponseEntity<String> response = oAuth2RestTemplate.exchange(path, HttpMethod.GET, null, String.class);
//AuthorizationCodeAccessTokenProvider provider = new //AuthorizationCodeAccessTokenProvider();
//Token Request
//AccessTokenRequest request = new DefaultAccessTokenRequest();
//String code = provider.obtainAuthorizationCode(resource, request);
//request.setAuthorizationCode(code);
//OAuth2AccessToken oAuth2AccessToken = //provider.obtainAccessToken(resource, request);
//Token Response
//String tokenValue = oAuth2AccessToken.getValue();
//return tokenValue;
}
//Call when ready to send token Request
private OAuth2ProtectedResourceDetails resource() {
AuthorizationCodeResourceDetails authorizationCodeResourceDetails = new AuthorizationCodeResourceDetails();
authorizationCodeResourceDetails.setClientId(clientId);
authorizationCodeResourceDetails.setClientSecret(clientSecret);
authorizationCodeResourceDetails.setAccessTokenUri(accessTokenUri);
authorizationCodeResourceDetails.setUserAuthorizationUri(userAuthorizationUri);
//authorizationCodeResourceDetails.setScope(scopes);
authorizationCodeResourceDetails.setPreEstablishedRedirectUri(redirectUri);
return authorizationCodeResourceDetails;
}
}
application.yml
security:
oauth2:
client:
clientId: clientid
clientSecret: secret
accessTokenUri: http://localhost:8080/test-auth/token
userAuthorizationUri: http://localhost:8080/test-auth/authorize
apiUrl: http://localhost:8080/test-fhir-cdr/data
redirectUri: http://localhost:8080/test-examples-access-token
I finally found the solution here: https://projects.spring.io/spring-security-oauth/docs/oauth2.html
I needed to add the below code to the controller:
#Autowired
private OAuth2ClientContext oauth2Context;
#Bean
public OAuth2RestTemplate getOauth2RestTemplate() {
return new OAuth2RestTemplate(resource(), oauth2Context);
}
Then call getOauth2RestTemplate() like below:
#RequestMapping("/ex-1")
public String retrievePatientByIdUsingRestTemplate(#RequestParam String empi) {
String path = apiUrl + "/Patient/" + empi;
OAuth2RestTemplate oAuth2RestTemplate = getOauth2RestTemplate();
ResponseEntity<String> response = oAuth2RestTemplate.exchange(path, HttpMethod.GET, null, String.class);
return response.getBody();
}

How to use OAuth2RestTemplate?

I'm trying to understand how to use a OAuth2RestTemplate object to consume my OAuth2 secured REST service (which is running under a different project and let's assume also on a different server etc...)
An example of my REST service is:
http://localhost:8082/app/helloworld
-> Accessing this URL generates an error as I am not authenticated
To request a token I would go to:
http://localhost:8082/app/oauth/token?grant_type=password&client_id=restapp&client_secret=restapp&username=**USERNAME**&password=**PASSWORD**
After I receive the token I can then connect to the REST API by using the following URL (example token inserted)
http://localhost:8082/app/helloworld/?access_token=**4855f557-c6ee-43b7-8617-c24591965206**
Now my question is how do I implement a second application which can consume this OAuth2 secured REST API? I really haven't found any working examples where you provide the user name and password (e.g. coming from a login form) and then a token is generated which can be re-used to get data from the REST API.
I currently tried something with the following objects:
BaseOAuth2ProtectedResourceDetails baseOAuth2ProtectedResourceDetails = new BaseOAuth2ProtectedResourceDetails();
baseOAuth2ProtectedResourceDetails.setClientId("restapp");
baseOAuth2ProtectedResourceDetails.setClientSecret("restapp");
baseOAuth2ProtectedResourceDetails.setGrantType("password");
// how to set user name and password ???
DefaultAccessTokenRequest accessTokenRequest = new DefaultAccessTokenRequest();
OAuth2ClientContext oAuth2ClientContext = new DefaultOAuth2ClientContext(accessTokenRequest());
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(baseOAuth2ProtectedResourceDetails,oAuth2ClientContext);
But this just isn't working :(
Any ideas, links to working examples or tutorials are greatly appreciated.
You can find examples for writing OAuth clients here:
https://github.com/spring-projects/spring-security-oauth
In your case you can't just use default or base classes for everything, you have a multiple classes Implementing OAuth2ProtectedResourceDetails. The configuration depends of how you configured your OAuth service but assuming from your curl connections I would recommend:
#EnableOAuth2Client
#Configuration
class MyConfig{
#Value("${oauth.resource:http://localhost:8082}")
private String baseUrl;
#Value("${oauth.authorize:http://localhost:8082/oauth/authorize}")
private String authorizeUrl;
#Value("${oauth.token:http://localhost:8082/oauth/token}")
private String tokenUrl;
#Bean
protected OAuth2ProtectedResourceDetails resource() {
ResourceOwnerPasswordResourceDetails resource;
resource = new ResourceOwnerPasswordResourceDetails();
List scopes = new ArrayList<String>(2);
scopes.add("write");
scopes.add("read");
resource.setAccessTokenUri(tokenUrl);
resource.setClientId("restapp");
resource.setClientSecret("restapp");
resource.setGrantType("password");
resource.setScope(scopes);
resource.setUsername("**USERNAME**");
resource.setPassword("**PASSWORD**");
return resource;
}
#Bean
public OAuth2RestOperations restTemplate() {
AccessTokenRequest atr = new DefaultAccessTokenRequest();
return new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(atr));
}
}
#Service
#SuppressWarnings("unchecked")
class MyService {
#Autowired
private OAuth2RestOperations restTemplate;
public MyService() {
restTemplate.getAccessToken();
}
}
Do not forget about #EnableOAuth2Client on your config class, also I would suggest to try that the urls you are using are working with curl first, also try to trace it with the debugger because lot of exceptions are just consumed and never printed out due security reasons, so it gets little hard to find where the issue is. You should use logger with debug enabled set.
Good luck
I uploaded sample springboot app on github https://github.com/mariubog/oauth-client-sample
to depict your situation because I could not find any samples for your scenario .
In the answer from #mariubog (https://stackoverflow.com/a/27882337/1279002) I was using password grant types too as in the example but needed to set the client authentication scheme to form. Scopes were not supported by the endpoint for password and there was no need to set the grant type as the ResourceOwnerPasswordResourceDetails object sets this itself in the constructor.
...
public ResourceOwnerPasswordResourceDetails() {
setGrantType("password");
}
...
The key thing for me was the client_id and client_secret were not being added to the form object to post in the body if resource.setClientAuthenticationScheme(AuthenticationScheme.form); was not set.
See the switch in:
org.springframework.security.oauth2.client.token.auth.DefaultClientAuthenticationHandler.authenticateTokenRequest()
Finally, when connecting to Salesforce endpoint the password token needed to be appended to the password.
#EnableOAuth2Client
#Configuration
class MyConfig {
#Value("${security.oauth2.client.access-token-uri}")
private String tokenUrl;
#Value("${security.oauth2.client.client-id}")
private String clientId;
#Value("${security.oauth2.client.client-secret}")
private String clientSecret;
#Value("${security.oauth2.client.password-token}")
private String passwordToken;
#Value("${security.user.name}")
private String username;
#Value("${security.user.password}")
private String password;
#Bean
protected OAuth2ProtectedResourceDetails resource() {
ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
resource.setAccessTokenUri(tokenUrl);
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setClientAuthenticationScheme(AuthenticationScheme.form);
resource.setUsername(username);
resource.setPassword(password + passwordToken);
return resource;
}
#Bean
public OAuth2RestOperations restTemplate() {
return new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest()));
}
}
#Service
#SuppressWarnings("unchecked")
class MyService {
#Autowired
private OAuth2RestOperations restTemplate;
public MyService() {
restTemplate.getAccessToken();
}
}
I have different approach if you want access token and make call to other resource system with access token in header
Spring Security comes with automatic security: oauth2 properties access from application.yml file for every request and every request has SESSIONID which it reads and pull user info via Principal, so you need to make sure inject Principal in OAuthUser and get accessToken and make call to resource server
This is your application.yml, change according to your auth server:
security:
oauth2:
client:
clientId: 233668646673605
clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
accessTokenUri: https://graph.facebook.com/oauth/access_token
userAuthorizationUri: https://www.facebook.com/dialog/oauth
tokenName: oauth_token
authenticationScheme: query
clientAuthenticationScheme: form
resource:
userInfoUri: https://graph.facebook.com/me
#Component
public class OAuthUser implements Serializable {
private static final long serialVersionUID = 1L;
private String authority;
#JsonIgnore
private String clientId;
#JsonIgnore
private String grantType;
private boolean isAuthenticated;
private Map<String, Object> userDetail = new LinkedHashMap<String, Object>();
#JsonIgnore
private String sessionId;
#JsonIgnore
private String tokenType;
#JsonIgnore
private String accessToken;
#JsonIgnore
private Principal principal;
public void setOAuthUser(Principal principal) {
this.principal = principal;
init();
}
public Principal getPrincipal() {
return principal;
}
private void init() {
if (principal != null) {
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal;
if (oAuth2Authentication != null) {
for (GrantedAuthority ga : oAuth2Authentication.getAuthorities()) {
setAuthority(ga.getAuthority());
}
setClientId(oAuth2Authentication.getOAuth2Request().getClientId());
setGrantType(oAuth2Authentication.getOAuth2Request().getGrantType());
setAuthenticated(oAuth2Authentication.getUserAuthentication().isAuthenticated());
OAuth2AuthenticationDetails oAuth2AuthenticationDetails = (OAuth2AuthenticationDetails) oAuth2Authentication
.getDetails();
if (oAuth2AuthenticationDetails != null) {
setSessionId(oAuth2AuthenticationDetails.getSessionId());
setTokenType(oAuth2AuthenticationDetails.getTokenType());
// This is what you will be looking for
setAccessToken(oAuth2AuthenticationDetails.getTokenValue());
}
// This detail is more related to Logged-in User
UsernamePasswordAuthenticationToken userAuthenticationToken = (UsernamePasswordAuthenticationToken) oAuth2Authentication.getUserAuthentication();
if (userAuthenticationToken != null) {
LinkedHashMap<String, Object> detailMap = (LinkedHashMap<String, Object>) userAuthenticationToken.getDetails();
if (detailMap != null) {
for (Map.Entry<String, Object> mapEntry : detailMap.entrySet()) {
//System.out.println("#### detail Key = " + mapEntry.getKey());
//System.out.println("#### detail Value = " + mapEntry.getValue());
getUserDetail().put(mapEntry.getKey(), mapEntry.getValue());
}
}
}
}
}
}
public String getAuthority() {
return authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getGrantType() {
return grantType;
}
public void setGrantType(String grantType) {
this.grantType = grantType;
}
public boolean isAuthenticated() {
return isAuthenticated;
}
public void setAuthenticated(boolean isAuthenticated) {
this.isAuthenticated = isAuthenticated;
}
public Map<String, Object> getUserDetail() {
return userDetail;
}
public void setUserDetail(Map<String, Object> userDetail) {
this.userDetail = userDetail;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
#Override
public String toString() {
return "OAuthUser [clientId=" + clientId + ", grantType=" + grantType + ", isAuthenticated=" + isAuthenticated
+ ", userDetail=" + userDetail + ", sessionId=" + sessionId + ", tokenType="
+ tokenType + ", accessToken= " + accessToken + " ]";
}
#RestController
public class YourController {
#Autowired
OAuthUser oAuthUser;
// In case if you want to see Profile of user then you this
#RequestMapping(value = "/profile", produces = MediaType.APPLICATION_JSON_VALUE)
public OAuthUser user(Principal principal) {
oAuthUser.setOAuthUser(principal);
// System.out.println("#### Inside user() - oAuthUser.toString() = " + oAuthUser.toString());
return oAuthUser;
}
#RequestMapping(value = "/createOrder",
method = RequestMethod.POST,
headers = {"Content-type=application/json"},
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public FinalOrderDetail createOrder(#RequestBody CreateOrder createOrder) {
return postCreateOrder_restTemplate(createOrder, oAuthUser).getBody();
}
private ResponseEntity<String> postCreateOrder_restTemplate(CreateOrder createOrder, OAuthUser oAuthUser) {
String url_POST = "your post url goes here";
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Authorization", String.format("%s %s", oAuthUser.getTokenType(), oAuthUser.getAccessToken()));
headers.add("Content-Type", "application/json");
RestTemplate restTemplate = new RestTemplate();
//restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
HttpEntity<String> request = new HttpEntity<String>(createOrder, headers);
ResponseEntity<String> result = restTemplate.exchange(url_POST, HttpMethod.POST, request, String.class);
System.out.println("#### post response = " + result);
return result;
}
}
My simple solution. IMHO it's the cleanest.
First create a application.yml
spring.main.allow-bean-definition-overriding: true
security:
oauth2:
client:
clientId: XXX
clientSecret: XXX
accessTokenUri: XXX
tokenName: access_token
grant-type: client_credentials
Create the main class: Main
#SpringBootApplication
#EnableOAuth2Client
public class Main extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll();
}
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
#Bean
public OAuth2RestTemplate oauth2RestTemplate(ClientCredentialsResourceDetails details) {
return new OAuth2RestTemplate(details);
}
}
Then Create the controller class: Controller
#RestController
class OfferController {
#Autowired
private OAuth2RestOperations restOperations;
#RequestMapping(value = "/<your url>"
, method = RequestMethod.GET
, produces = "application/json")
public String foo() {
ResponseEntity<String> responseEntity = restOperations.getForEntity(<the url you want to call on the server>, String.class);
return responseEntity.getBody();
}
}
Maven dependencies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
</dependencies>

Resources