Keycloak API using admin user - spring-boot

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

Related

Extract Mono nonblocking response and store it in a variable and use it globally

In my project I have a requirement where I need to call a third party api authentic url to get the the access token. I need to set that access token in every subsequent request header .The access token has some lifetime and when the lifetime expired I need to regenerate the access token.
application.yml
I have hardcoded the client_id,client_secret,auth_url and grant_type .
AuthController.java
here I have created an endpoint to generate the access token.
**`AuthService.java`**
#Services
#Slf4j
public class AuthService{
#Autowired
private WebClient webClient;
static String accessToken="";
public Mono<SeekResponse> getAccessToken(AuthRequest authRequest) throws InvalidTokenException{
Mono<AuthResponse> authResponse=webClient.post()
.bodyValue(authRequest)
.accept(MediaType.APPLICATION_JSON)
.retrive()
.bodyToMono(AuthResponse.class);
authResponse.doOnNext(response->{
String value=response.getAccess_token();
accessToken=accessToken+value;
})
}
}
Although I have updated the "accessToken" value but it will return me null. I understand as I have made async call this value coming as null. I can't use blocking mechanism here.
Is there any other way to generate the access token and pass it as a header for the subsequent request for authentication. Or how can I use the accessToken value globally so that I can set those token value to my subsequent api request call.
I have tried with oAuth2 by following the below article:
https://medium.com/#asce4s/oauth2-with-spring-webclient-761d16f89cdd
But when I execute I am getting the below error :
"An expected CSRF token cannot found".
I'm also learning Webflux. Here's my thought. Please correct me if I'm wrong.
We are not going to rely on doOnNext() nor doOnSuccess() nor other similar method to try to work on an pre-defined variable accessToken (That's not a way to let Mono flow). What we should focus on is converting a mono to another mono, for example converting mono response to mono access token.
The way to do that is .flatmap()/.map()/.zipwith()/...
For example,
Mono<string> tokenMono = responseMono.flatmap(
// in the map or flatmap, we get the chance to operate on variables/objects.
resp -> {
string token = response.getAccess_token();
return Mono.just(token); // with Mono.just(), we are able to convert object to Mono again.
}
) // this example is not practical, as map() is better to do the same thing. flatmap with Mono.just() is meaningless here.
Mono<string> tokenMono2 = responseMono.map(
resp -> {
string token = response.getAccess_token();
return token;
}
)
Everything starting from Mono should be always Mono until subscribed or blocked. And they provide us ways to operate on those variables inside Mono<variables>. Those are map() flatmap(), zipwith(), etc.
https://stackoverflow.com/a/60105107/18412317
Referring to a point this author said, doOnNext() is for side effect such as logging.
It's hard to understand provided sample and implementation is not really reactive. The method returns Mono but at the same time throws InvalidTokenException or usage of onNext that is a so-called side-effect operation that should be used for logging, metrics, or other similar use cases.
The way you implement oauth flow for WebClient is to create filter, Client Filters.
Spring Security provides some boilerplates for common oauth flows. Check Spring Security OAuth2 for more details.
Here is an example of simple implementation of the client credential provider
private ServerOAuth2AuthorizedClientExchangeFilterFunction oauth(String clientRegistrationId, ClientConfig config) {
var clientRegistration = ClientRegistration
.withRegistrationId(clientRegistrationId)
.tokenUri(config.getAuthUrl() + "/token")
.clientId(config.getClientId())
.clientSecret(config.getClientSecret())
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
var authRepository = new InMemoryReactiveClientRegistrationRepository(clientRegistration);
var authClientService = new InMemoryReactiveOAuth2AuthorizedClientService(authRepository);
var authClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(
authRepository, authClientService);
var oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authClientManager);
oauth.setDefaultClientRegistrationId(clientRegistrationId);
return oauth;
}
then you could use it in the WebClient
WebClient.builder()
.filter(oauth)
.build()
UPDATE
Here is an example of the alternative method without filters
AuthService
#Service
public class AuthService {
private final WebClient webClient;
public AuthService() {
this.webClient = WebClient.create("<url>/token");
}
public Mono<String> getAccessToken() {
return webClient.post()
.bodyValue()
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(AuthResponse.class)
.map(res -> res.getAccessToken());
}
}
ApiService
#Service
public class ApiService {
private final WebClient webClient;
private final Mono<String> requestToken;
public ApiService(AuthService authService) {
this.webClient = WebClient.create("<url>/api");
// cache for token expiration
this.requestToken = authService.getAccessToken().cache(Duration.ofMinutes(10));
}
public Mono<String> request() {
return requestToken
.flatMap(token ->
webClient.get()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
);
}
}

Reactive Webclient

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();
}

How to authorize against an OpenId Connect secured REST-API programmatically?

I have implement a REST-API based on Spring Boot secured by Spring Security 5.2 OpenID Connect resource server. The authorization server is an IdentityServer4. So far so good, the authentication using Bearer Token (the token is determined via a dummy web page) works well.
The challenge now is to call the REST API from a client that does not require user interaction (web page).
I would like to provide the API users with an unsecured endpoint (/authorization) which can be used to receive the Bearer Token for any further secured service. Username and password should be passed as request parameters.
I have search the web and studied the docs from Spring but I did not have found something which addresses my use case.
I implemented a relatively simple solution
#GetMapping
public ResponseEntity<GetTokenResponse> getToken(#RequestBody GetTokenRequest getTokenRequest) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("client_id", clientId);
formData.add("client_secret", clientSecret);
formData.add("grant_type", "password");
formData.add("scope", scopes);
formData.add("username", getTokenRequest.getUsername());
formData.add("password", getTokenRequest.getPassword());
RestTemplate restTemplate = new RestTemplate();
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(formData, headers);
ResponseEntity<GetTokenResponse> response = restTemplate.postForEntity( tokenEndPoint, request , GetTokenResponse.class );
String accessToken = response.getBody().getAccessToken();
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
Jwt jwt = decoder.decode(accessToken);
logger.debug("Headers:\n{}", jwt.getHeaders());
logger.debug("Claims:\n{}", jwt.getClaims());
logger.info("User {}, {} '{}' authorised.", jwt.getClaimAsString("given_name"), jwt.getClaimAsString("family_name"), jwt.getClaimAsString("sub"));
return response;
}
The response contains the bearer token and can therefore be used for the API calls.

How to set base url and query parameters for WebClient?

In my service, I have to get response from some different urls with parameters.
get from http://a.com:8080/path1?param1=v1
get from http://b.com:8080/path2?param2=v2
get from http://c.com:8080/path3?param3=v3
I am using WebClient to do the job as following.
public class WebClientTest {
private WebClient webClient = WebClient.builder().build();
#Test
public void webClientTest() {
Mono<String> a = webClient.get()
.uri(uriBuilder -> uriBuilder.scheme("http").host("a.com").port(8080).path("/path1")
.queryParam("param1", "v1")
.build())
.retrieve()
.bodyToMono(String.class);
Mono<String> b = webClient.get()
.uri(uriBuilder -> uriBuilder.scheme("http").host("b.com").port(8080).path("/path2")
.queryParam("param2", "v2")
.build())
.retrieve()
.bodyToMono(String.class);
Mono<String> c = webClient.get()
.uri(uriBuilder -> uriBuilder.scheme("http").host("c.com").port(8080).path("/path3")
.queryParam("param3", "v3")
.build())
.retrieve()
.bodyToMono(String.class);
//zip the result
}
}
As you can see, I have to set scheme, host, port separately again and again.
So my questions are:
1. Am I using WebClient in a right way?
2. Is it possible to set scheme, host, port in a method together? I know that webClient.get().uri("http://a.com:8080/path1?param1=v1").retrieve() works, but what I am expecting is something like:
webClient.get()
.uri(uriBuilder -> uriBuilder/*.url("http://a.com:8080/path1")*/
.queryParam("param1", "v1")
.build())
.retrieve()
.bodyToMono(String.class);
As of Spring Framework 5.2, there is an additional method that can help with your specific situation:
Mono<String> response = this.webClient
.get()
.uri("http://a.com:8080/path1", uri -> uri.queryParam("param1", "v1").build())
.retrieve()
.bodyToMono(String.class);
I wouldn't advise creating one WebClient per host as a general rule. It really depends on your use case. Here it seems your client might send requests to many hosts, and creating many HTTP clients can be a bit wasteful here.
The way I solved this was to have a WebClient for each different url.
So you would have
private WebClient aClient = WebClient.create("a.com")
private WebClient bClient = WebClient.create("b.com")
private WebClient cClient = WebClient.create("c.com")
Then interact with each WebClient depending on what you're calling.
https://docs.spring.io/spring/docs/5.0.7.RELEASE/spring-framework-reference/web-reactive.html#webflux-client-retrieve

How to access Spring REST API in JHipster with Spring RestTemplate

I have set up JHipster like described on its homepage with some entities. Frontend with AngularJS works great and also the API page, lets me test my services as expected.
Now I am trying to write a REST-Client using Spring's RestTemplate like this:
public List<SomeEntity> getAllEntities(){
URI uri = URI.create("http://localhost:8080/api/entities");
HttpHeaders httpHeaders = this.createHeaders("admin", "admin")
ResponseEntity<SomeEntity[]> responseEntity = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<SomeEntity>(httpHeaders), SomeEntity[].class);
return Arrays.asList(responseEntity.getBody());
}
private HttpHeaders createHeaders(final String username, final String password ){
HttpHeaders headers = new HttpHeaders(){
{
String auth = username + ":" + password;
byte[] encodedAuth = Base64.encode(
auth.getBytes(Charset.forName("US-ASCII")) );
String authHeader = "Basic " + new String( encodedAuth );
set( "Authorization", authHeader );
}
};
headers.add("Content-Type", "application/json");
headers.add("Accept", "application/json");
return headers;
}
But this results in the following error:
[WARN] org.springframework.web.client.RestTemplate - GET request for "http://localhost:8080/api/entities" resulted in 401 (Unauthorized); invoking error handler
Now I am not sure, if and how I need to adapt my HttpHeaders or if my simple basic-auth handling approach at all is wrong.
The way you authenticate is wrong, it seems you chose session authentication when generating your app, so this requires form-based auth not http basic auth and it requires being able to store session cookie and CSRF cookie so most likely using commons http client.
Maybe choosing xauth token authentication when generating your app would be simpler.
Once you get this working you will have CORS issues as soon as your client won't run on same host as your JHipster app.

Resources