Reactive Webclient - spring

I would like to programmatically login to a web application using reactive web client . I could see I am authenticated correctly but could not able to reach redirected url.
I access my application http://localhost:8080/myapplogin
Here my web client response will have a Location header http://localhost:8082/portal/auth/home , and I would like the browser to redirect to this url
I could see the application in 8082 authenticating and redirecting(/auth/home) but do not know how to make my web client return the response and do the redirection through browser. Can someone help
#GetMapping("/myapplogin")
Publisher<ClientResponse> mylogin() {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("username", "testuser");
formData.add("password", "test password");
return WebClient.create()
.post()
.uri("http://localhost:8082/portal/auth/login")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(formData))
.exchange();
}

You need to return status codes that start with 3, and a Location header holding the URL to redirect to.
#GetMapping("/myapplogin")
public Mono<ResponseEntity> mylogin() {
return login()
.map(res -> ResponseEntity
.status(HttpStatus.PERMANENT_REDIRECT)
.location(URI.create("/auth/home"))
.build()
);
}
where login is
Mono<ClientResponse> login() {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("username", "testuser");
formData.add("password", "test password");
return WebClient.create()
.post()
.uri("http://localhost:8082/portal/auth/login")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(formData))
.exchange();
}

Related

Keycloak API using admin user

I'm working with keycloak API to access offline user's sessions; I noticed a strange behavior and thus my question:
a. When I use postman, I get the access token with this url: http://localhost:8080/realms/master/protocol/openid-connect/token
b. From the above, I use said token in postman to retrieve the offline sessions:
http://localhost:8080/admin/realms/master/clients/5729288b-c789-45ac-8915-da32b7b9fe49/offline-sessions
where '5729288b-c789-45ac-8915-da32b7b9fe49' is the admin-cli ID; username and password are all the defaults of the admin user and the client is 'admin-cli'
Everything works fine in postman, and I'm able to retrieve the offline sessions. However, when I do the same with the Keycloak API using the springboot webclient I get 403 Forbidden
a. Get the token from the below:
private String getToken(){
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("username", username);
map.add("password", password);
map.add("client_id", clientId);
map.add("grant_type", grantType);
map.add("scope", "openid");
ResponseEntity<LoginResponse> loginResponse = webclient.post()
.uri(uriBuilder -> UriBuilder.fromUri(tokenEndpoint).build())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromFormData(map))
.retrieve()
.toEntity(LoginResponse.class)
.block();
return loginResponse.getBody().getAccess_token();
}
b. Try to retrieve offline sessions with the above access-token
public UserSessionRepresentation[] getMasterOfflineSessions(){
UserSessionRepresentation[] response = webclient.get()
.uri(uriBuilder -> UriBuilder.fromUri(offlineSessionsUrl)
.build(cliId))
.headers(h -> h.setBearerAuth(getToken()))
.retrieve()
.bodyToMono(UserSessionRepresentation[].class)
.block();
return response;
}
offlineSessionsUrl is: http://localhost:8080/admin/realms/master/clients/5729288b-c789-45ac-8915-da32b7b9fe49/offline-sessions
5729288b-c789-45ac-8915-da32b7b9fe49:is the id for the admin-cli client
What I don't understand is that I can retrieve the sessions in postman, but I can't do so using the API and the springboot webclient with all configurations being equal.
Please help
Answering my own question; the issue here was was the: webclient spring property
In springboot, it was using the definition within the configuration that pointed to another client. To make it work for the admin-cli client, I had to use a clean object of webclient as illustrated in the below code:
public UserSessionRepresentation[] getMasterOfflineSessions(){
UserSessionRepresentation[] response = WebClient.create().get()
.uri(uriBuilder -> UriBuilder.fromUri(offlineSessionsUrl)
.build(cliId))
.headers(h -> h.setBearerAuth(getToken()))
.retrieve()
.bodyToMono(UserSessionRepresentation[].class)
.block();
return response;
}
The WebClient.create() is the piece of code I changed to resolve the issue

How to call a microservice to fetch data in spring webflux

I want to call a microservice from another service using webclient in spring flux. But, I am not able to write the code properly. Can you please suggest how to call another service. Please find my code as below.
I need to call the below service
public Mono<ServerResponse> load(ServerRequest res){
String c1name = res.pathVariable("cust");
String c2name = res.queryParam("cl").orElse("");
String oname = res.queryParam("ol").orElse("");
return res.body()
}
public Mono<ResponseEntity<Void>> ftpFileSend(MultipartFile fileData, String cust, MultiValueMap<String,String) qpar {
MultiValueMap<String,String> qpar=new LinkedMultiValueMap<String,String>();
qpar.add("name","spring");
MultiValueMap<String,Object> body=new LinkedMultiValueMap<String,Object>();
String url="http://localhost:8088/"+ cust+"/load";
try {
body.add("file", fileData.getBytes());
} catch (IOException e) {
return Mono.error(e); // <-- note how to create an error signal
}
return webClient
.post()
.uri(uriBuilder -> uriBuilder.path(url).queryParams(qpar).build() )
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(body))
.retrieve()
.toBodilessEntity();
}
Hmm it would be great if you have provided some error logs or so. Anyway if you want to create a multipart body there is a builder, MultipartBodyBuilder (in org.springframework.http.client.MultipartBodyBuilder).
Example usage is as follows,
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("file", new MultipartFileResource(fileData));
MultiValueMap<String, HttpEntity<?>> multipartBody = builder.build();
Then use this multipartBody in webClient call.
return webClient
...
.body(BodyInserters.fromMultipartData(multipartBody))
.retrieve()
.toBodilessEntity();

Unauthorized 401 error for GET call on stockitem

I am accessing Acumatica API using Java Spring Resttemplate. The POST call to the Login endpoint works fine. But the next call to get StockItems gets a 401 Unauthorized error.
org.springframework.web.client.HttpClientErrorException: 401 Unauthorized at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:91) at org.springframework.web.client.RestTemplate.handleResponseError(RestTemplate.java:615) at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:573) at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:544) at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:465) at
Now this works fine when I use Postman and the Chrome Restlet client. I noticed that in Restlet client the GET call to the API passes a session cookie that was set by the API in the login call. I tried passing the response headers from the login call in the GET request. But I still get a 401. I am using the standard resttemplate configuration.
HttpEntity<Credentials> entity = new HttpEntity<Credentials>(credentials, headers);
ResponseEntity<String> response = restTemplate.exchange("https://xxxx.acumatica.com/entity/auth/login",
HttpMethod.POST, entity, String.class);
HttpHeaders rHeaders = response.getHeaders();
String set_cookie = rHeaders.getFirst(rHeaders.SET_COOKIE);
if (LOG.isInfoEnabled()) { LOG.info("Response: " + response.toString()); }
if (LOG.isInfoEnabled()) { LOG.info("Set-Cookie: " + set_cookie); }
HttpEntity<String> entity2 = new HttpEntity<String>(response.getHeaders());
ResponseEntity<String> response2 = restTemplate.exchange("https://usell.acumatica.com/entity/Default/6.00.001/StockItem?$expand=Attributes,WarehouseDetails", HttpMethod.GET, entity2, String.class);
How did Acumatica API client using Java get around this problem?
I was not setting all the cookies.. this is all I had to do
List<String> cookies = response.getHeaders().get(HttpHeaders.SET_COOKIE);
HttpHeaders requestHeaders = new HttpHeaders();
for (String cookie : cookies) {
requestHeaders.add("Cookie", cookie);
}

RestTemplate call returns 401 Unauthorized

Background
I am trying to consume a REST endpoint hosted on IBM Cloud API from my SpringBoot application using RestTemplate. I am using the following snippet to make the call:
RestTemplate send = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setCacheControl(CacheControl.noCache());
headers.set("x-ibm-client-id", clientId);
headers.set("x-ibm-client-secret", clientSecret);
HttpEntity<BodyEntity> httpEntity = new HttpEntity<>(bodyEntity, headers);
send.exchange(ENDPOINT_URL, HttpMethod.POST, httpEntity, Object.class);
I used the following snippet to configure RestTemplate
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
Problem
Using this snippet, when the call is made I receive 401 Unauthorized. When I made the same call using Postman, I received correct response from server without any problem.
Since I received 401 response code I set to further investigate the request by logging headers and body and other parts of request.
I implemented ClientHttpRequestInterceptor to log outgoing requests to further debug the issue and added this interceptor to my RestTemplate config as follows:
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
// new code
builder.interceptors(new LoggingClientHttpRequestInterceptor());
return builder.build();
}
After making the request again, I could see in the log that the outgoing call contained all details as it should e.g. Headers and Body were correct.
After this, I changed the whole thing to use Apache HTTP Client as follows:
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(URL);
String reqString = "BODY";
httpPost.setEntity(new StringEntity(reqString, ContentType.APPLICATION_JSON));
httpPost.setHeader("accept", "application/json");
httpPost.setHeader("content-type", "application/json");
httpPost.setHeader("cache-control", "no-cache");
httpPost.setHeader("x-ibm-client-id", clientId);
httpPost.setHeader("x-ibm-client-secret", clientSecret);
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
System.out.println("Response status: " + response.getStatusLine());
HttpEntity entity1 = response.getEntity();
System.out.println("Response :" + entity1.toString());
} finally {
response.close();
}
Using the snippet above, I executed the request and received correct response.
Question
Why RestTemplate call returns and error whereas HttpClient returns correct response?
Do I need to further configure RestTemplate?
What have I missed?

Share SPRING_SECURITY_CONTEXT between two applications

I have two different Spring Boot Applications that run on localhost on different ports (8080, 8081) and different configs (application.yml). These apps use SSO with OAuth 2.0 to get authorization token from Authorization Server. I log in to my first application, get authorization and everything works great here. Now I need to share these authentication details with second Spring Boot App (on port 8081) to authorize second app in Authorization Server. Googled and found 2 aproaches: I can try to share HttpSession between two apps (but I think it's redundant) OR HttpSessionSecurityContextRepository as SecurityContextRepository which seems more convenient. The problem here is that I can't manage to do so and I'm still not sure that it's a good idea to share Security Context between 2 apps.
What I tried for now:
Share authorization token from first app via headers in GET request (custom-built in accordance with specification for requests for Authorization Server), but it didn't work - second app doesn't take in mind this token.
Share authorized cookie from first app to second, but it didn't work, too.
I can't do authorization through Authorization Server on second app because it may be not a Spring Boot App with #Controller but any other app without HTML forms, so I need to authorize on first app (with UI), get all the data which is needed to perform authorized requests and pass it to second app (third, fourth...) so they will be able to do authorized requests too.
Thanks in advance!
I presume that your authorization/resource server is external application.And you can login successfully with your first application so flow is working.You have two client application with own client_id, client_secret and etc. parameters.If these parameters are different then authorization/resource server will return different bareer token and sessionid cookie for first and second client application.Otherwise you need to authorize both of them in authorization/resource server.
I would offer when user do login to first app then in background you do login also for second application.
For automatically authorizing second application you can try to do oauth2 login flow manually for second application with own parameters when after successful first application login and send cookies to frontend which you got from oauth2 login.
For manual oauth2 login you can try below code:
private Cookie oauth2Login(String username, String password, String clientId, String clientSecret) {
try {
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);
// form param
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_HOST, request,
OauthTokenRespone.class);
} catch (Exception ex) {
ex.printStackTrace();
}
Optional<OauthTokenRespone> optRes = Optional.ofNullable(res);
String accessToken = optRes.orElseGet(() -> new OauthTokenRespone("", "", "", "", "", ""))
.getAccess_token();
// CALLING RESOURCE
headers.clear();
map.clear();
headers.setContentType(MediaType.APPLICATION_JSON);
map.add("access_token", accessToken);
request = new HttpEntity<MultiValueMap<String, String>>(map, headers);
Cookie oauthCookie = null;
if (accessToken.length() > 0) {
HttpEntity<String> response = restTemplate.exchange(
OAUTH_RESOURCE_URL.replace(OAUTH_HOST, oauthHost) + "?access_token=" + accessToken,
HttpMethod.POST, request, String.class);
String cookie = Optional.ofNullable(response.getHeaders().get("Set-Cookie"))
.orElseGet(() -> Arrays.asList(new String(""))).get(0);
if (cookie.length() > 0) {
String[] c = cookie.split(";")[0].split("=");
oauthCookie = new Cookie(c[0], c[1]);
oauthCookie.setHttpOnly(true);
}
}
return Optional.ofNullable(oauthCookie).orElseGet(() -> new Cookie("Ops", ""));
} catch (Throwable t) {
return new Cookie("Ops", "");
}
}
#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
}
And call this method after first app login as follows :
Cookie oauthCookie = oauth2Login(authenticationRequest.getUsername(), authenticationRequest.getPassword(),
CLIENT_ID, CLIENT_SECRET);
After getting cookie you need change its name (for example JSESSIONID-SECOND) because same cookies will override each other and also need to change its domain path to second app domain.
response.addCookie(oauthCookie);
Last you need add cookie to response (it is HttpServletResponse reference).
Hope it helps!

Resources