How do I logout when using Spring WebFlux and OAuth 2.0 Login? - spring-boot

I have the following LogoutResource that works well with Spring Boot 2.2 using Spring MVC:
#RestController
public class LogoutResource {
private ClientRegistration registration;
public LogoutResource(ClientRegistrationRepository registrations) {
this.registration = registrations.findByRegistrationId("oidc");
}
/**
* {#code POST /api/logout} : logout the current user.
*
* #param request the {#link HttpServletRequest}.
* #param idToken the ID token.
* #return the {#link ResponseEntity} with status {#code 200 (OK)} and a body with a global logout URL and ID token.
*/
#PostMapping("/api/logout")
public ResponseEntity<?> logout(HttpServletRequest request,
#AuthenticationPrincipal(expression = "idToken") OidcIdToken idToken) {
String logoutUrl = this.registration.getProviderDetails()
.getConfigurationMetadata().get("end_session_endpoint").toString();
Map<String, String> logoutDetails = new HashMap<>();
logoutDetails.put("logoutUrl", logoutUrl);
logoutDetails.put("idToken", idToken.getTokenValue());
request.getSession().invalidate();
return ResponseEntity.ok().body(logoutDetails);
}
}
The Angular client takes this response and redirects to Keycloak to log the user out. This all works great.
logout(): void {
this.authServerProvider.logout().subscribe((logout: Logout) => {
let logoutUrl = logout.logoutUrl;
const redirectUri = `${location.origin}${this.location.prepareExternalUrl('/')}`;
// if Keycloak, uri has protocol/openid-connect/token
if (logoutUrl.includes('/protocol')) {
logoutUrl = logoutUrl + '?redirect_uri=' + redirectUri;
} else {
// Okta
logoutUrl = logoutUrl + '?id_token_hint=' + logout.idToken + '&post_logout_redirect_uri=' + redirectUri;
}
window.location.href = logoutUrl;
});
}
Now I'm trying to change it so it works with Spring WebFlux. The following seems to work and returns the expected response (I've verified this by printing the values in my Angular logout() method).
#RestController
public class LogoutResource {
private Mono<ClientRegistration> registration;
public LogoutResource(ReactiveClientRegistrationRepository registrations) {
this.registration = registrations.findByRegistrationId("oidc");
}
/**
* {#code POST /api/logout} : logout the current user.
*
* #param idToken the ID token.
* #return the {#link ResponseEntity} with status {#code 200 (OK)} and a body with a global logout URL and ID token.
*/
#PostMapping("/api/logout")
public Mono<Map<String, String>> logout(#AuthenticationPrincipal(expression = "idToken") OidcIdToken idToken) {
return this.registration.map(oidc ->
oidc.getProviderDetails().getConfigurationMetadata().get("end_session_endpoint").toString())
.map(logoutUrl -> {
Map<String, String> logoutDetails = new HashMap<>();
logoutDetails.put("logoutUrl", logoutUrl);
logoutDetails.put("idToken", idToken.getTokenValue());
SecurityContextHolder.clearContext();
return logoutDetails;
});
}
}
You might notice I tried using SecurityContextHolder.clearContext() instead of request.getSession().invalidate(). This doesn't seem to work because the user is still logged in. Any idea what I need to do to kill the user's security session with WebFlux?

I was able to fix this by injecting a WebSession and calling invalidate() on it.
#PostMapping("/api/logout")
public Mono<Map<String, String>> logout(#AuthenticationPrincipal(expression = "idToken") OidcIdToken idToken, WebSession session) {
return session.invalidate().then(
this.registration.map(oidc -> oidc.getProviderDetails().getConfigurationMetadata().get("end_session_endpoint").toString())
.map(logoutUrl -> {
Map<String, String> logoutDetails = new HashMap<>();
logoutDetails.put("logoutUrl", logoutUrl);
logoutDetails.put("idToken", idToken.getTokenValue());
return logoutDetails;
})
);
}

Related

Should someRestController be made for receiving lang parameter for i18n along with LocaleResolver?

I am developing Spring Boot application, and I need to work with i18n. I watched a lot of tutorials and I implemented new class LocaleConfiguration
#Configuration
public class LocaleConfiguration implements WebMvcConfigurer {
/**
* * #return default Locale set by the user
*/
#Bean(name = "localeResolver")
public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver();
slr.setDefaultLocale(Locale.US);
return slr;
}
/**
* an interceptor bean that will switch to a new locale based on the value of the language parameter appended to a request:
*
* #param registry
* #language should be the name of the request param i.e localhost:8010/api/get-greeting?language=fr
* <p>
* Note: All requests to the backend needing Internationalization should have the "language" request param
*/
#Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
registry.addInterceptor(localeChangeInterceptor);
}
}
And also, I made few messages_code.propertie files with proper languages. I set thymeleaf template just to see if everything is working and that is okay. FrontEnd developer just need to send me lang param and that is it. But my question is, should I make a new controller which will handle that call with lang parameter or all that is somehow automatically done via this LocaleConfiguration class?
Because I get proper translations when I make this call in Postman/Browser:
http://localhost:8080/?lang=fra
So my question is, do I need to make new Controller to handle that or is it automatically done by LocaleResolver class?
I will answer your question first answer is LocaleResolver !
Because you have LocaleResolver Bean, and add localeChangeInterceptor, And its class hierarchy is
LocaleChangeInterceptor is an interceptor. It is known from the source code that it is executed before the request reaches RequestMapping. Its role is simply to obtain the request parameters from the request (the default is locale), and then set the current locale in LocaleResolver.
source code:
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException {
//Note here
**String newLocale = request.getParameter(getParamName());**
if (newLocale != null) {
if (checkHttpMethod(request.getMethod())) {
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException(
"No LocaleResolver found: not in a DispatcherServlet request?");
}
try {
localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
}
catch (IllegalArgumentException ex) {
if (isIgnoreInvalidLocale()) {
logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());
}
else {
throw ex;
}
}
}
}
// Proceed in any case.
return true;
}
See i have to comment.
String newLocale = request.getParameter(getParamName());
transfer
/**
* Return the name of the parameter that contains a locale specification
* in a locale change request.
*/
public String getParamName() {
return this.paramName;
}
among them this.paramName is
/**
* Default name of the locale specification parameter: "locale".
*/
public static final String DEFAULT_PARAM_NAME = "locale";
So do you understand

Validate request param names for optional fields - spring boot

If you have an endpoint described like this :
#GetMapping("/hello")
public String sayHello(#RequestParam(name= "my_id", required=false) myId, #RequestParam(name= "my_name") myName){
// return something
}
Issue:
The consumers of this endpoint could send invalid param names in the request;
/hello?my_name=adam&a=1&b=2 and it will work normally.
Goal:
Be able to validate optional request parameters names, using a proper way to do it.
Actual solution:
I've implemented a solution (For me it's not the right solution), where I've extends a HandlerInterceptorAdapter and register it in the WebMvcConfigurer :
/**
*
* Interceptor for unknown request parameters
*
*/
public class MethodParamNamesHandler extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ArrayList<String> queryParams = Collections.list(request.getParameterNames());
MethodParameter[] methodParameters = ((HandlerMethod) handler).getMethodParameters();
Map<String, String> methodParametersMap = new HashMap<>();
if(Objects.nonNull(methodParameters)){
methodParametersMap = Arrays.stream(methodParameters)
.map(m -> m.getParameter().getName())
.collect(Collectors.toMap(Function.identity(),Function.identity()));
}
Set<String> unknownParameters = collectUnknownParameters(methodParametersMap, queryParams);
if(!CollectionUtils.isEmpty(unknownParameters)){
throw new InvalidParameterNameException("Invalid parameters names", unknownParameters);
}
return super.preHandle(request,response,handler);
}
/**
* Extract unknown properties from a list of parameters
* #param allParams
* #param queryParam
* #return
*/
private Set<String> collectUnknownParameters(Map<String, String> allParams, List<String> queryParam){
return queryParam.stream()
.filter(param -> !allParams.containsKey(param))
.collect(Collectors.toSet());
}
}
Drawbacks:
The drawbacks of this solution is that it's based on method parameters,
It will take the name myId instead of taking my_id, this could have a workaround by transforming the uppercase word to snake-case; but this is not a good solution because you can have something like this : sayHello(#RequestParam(name= "my_id", required=false) anotherName).
Question:
Is there a proper way to achieve this ?

Webflux JWT Authorization not working fine

I am following a tutorial about JWT in a spring reactive context (webflux).
The token generation is working fine, however the authorization is not working when I use the Authorization with bearer
Here is what I have done:
#EnableWebFluxSecurity
#EnableReactiveMethodSecurity
public class WebSecurityConfig{
#Autowired private JWTReactiveAuthenticationManager authenticationManager;
#Autowired private SecurityContextRepository securityContext;
#Bean public SecurityWebFilterChain configure(ServerHttpSecurity http){
return http.exceptionHandling()
.authenticationEntryPoint((swe , e) -> {
return Mono.fromRunnable(()->{
System.out.println( "authenticationEntryPoint user trying to access unauthorized api end points : "+
swe.getRequest().getRemoteAddress()+
" in "+swe.getRequest().getPath());
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(()->{
System.out.println( "accessDeniedHandler user trying to access unauthorized api end points : "+
swe.getPrincipal().block().getName()+
" in "+swe.getRequest().getPath());
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
})
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContext)
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers("/auth/login").permitAll()
.anyExchange().authenticated()
.and()
.build();
}
As you can see, I want to simply deny all not authorized requests other than login or options based ones.
The login is working fine and I'm getting a token.
But trying to logout (a tweak that I implemented my self to make it state-full since I m only learning) is not working.
Here is my logout controller:
#RestController
#RequestMapping(AuthController.AUTH)
public class AuthController {
static final String AUTH = "/auth";
#Autowired
private AuthenticationService authService;
#PostMapping("/login")
public Mono<ResponseEntity<?>> login(#RequestBody AuthRequestParam arp) {
String username = arp.getUsername();
String password = arp.getPassword();
return authService.authenticate(username, password);
}
#PostMapping("/logout")
public Mono<ResponseEntity<?>> logout(#RequestBody LogoutRequestParam lrp) {
String token = lrp.getToken();
return authService.logout(token);
}
}
The logout request is as below:
As stated in images above, I believe that I m doing fine, however I m getting the error log message:
authenticationEntryPoint user trying to access unauthorized api end points : /127.0.0.1:45776 in /auth/logout
Here is my security context content:
/**
* we use this class to handle the bearer token extraction
* and pass it to the JWTReactiveAuthentication manager so in the end
* we produce
*
* simply said we extract the authorization we authenticate and
* depending on our implementation we produce a security context
*/
#Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
#Autowired
private JWTReactiveAuthenticationManager authenticationManager;
#Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
String authorizationHeaderContent = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if( authorizationHeaderContent !=null && !authorizationHeaderContent.isEmpty() && authorizationHeaderContent.startsWith("Bearer ")){
String token = authorizationHeaderContent.substring(7);
Authentication authentication = new UsernamePasswordAuthenticationToken(token, token);
return this.authenticationManager.authenticate(authentication).map((auth) -> {
return new SecurityContextImpl(auth);
});
}
return Mono.empty();
}
#Override
public Mono<Void> save(ServerWebExchange arg0, SecurityContext arg1) {
throw new UnsupportedOperationException("Not supported yet.");
}
}
I'm unable to see or find any issue or error that I have made. Where is the mistake?
There's a difference in writing
//Wrong
Jwts.builder()
.setSubject(username)
.setClaims(claims)
and
//Correct
Jwts.builder()
.setClaims(claims)
.setSubject(username)
Indeed, look at setSubject method in the DefaultJwtBuilder class :
#Override
public JwtBuilder setSubject(String sub) {
if (Strings.hasText(sub)) {
ensureClaims().setSubject(sub);
} else {
if (this.claims != null) {
claims.setSubject(sub);
}
}
return this;
}
When setSubject(username) is called first, ensureClaims() creates a DefaultClaims without yours and if you call setClaims(claims) the precedent subject is lost ! This JWT builder is bogus.
Otherwise, you're importing the wrong Role class in JWTReactiveAuthenticationManager, you have to replace :
import org.springframework.context.support.BeanDefinitionDsl.Role;
by
import com.bridjitlearning.www.jwt.tutorial.domain.Role;
Last and not least, validateToken() will return always false because of the check(token). put call is coming too late, you have to be aware of that. Either you remove this check or you move the put execution before calling the check method.
I'am not sure about what you want to do with resignTokenMemory, so i'll let you fix it by your own:
public Boolean validateToken(String token) {
return !isTokenExpired(token) && resignTokenMemory.check(token);
}
Another thing, your token is valid only 28,8 second, for testing raison i recommend you to expiraiton * 1000.

Spring boot + Swagger UI how to tell endpoint to require bearer token

I'm using Spring Boot to build a REST API. I've added Swagger-ui to handle documentation. I'm having a problem implementation the client authentication flow into swagger, the problem being I can get swagger-ui to authorise a supplied client-id(username) and client-secret(password) via basic auth, but swagger UI doesn't appear to be then applying to resulting access token to endpoint calls.
To confirm, my authorisation process;
- Use basic auth to send base64 encoded username/password & grant_type=client_credentials to /oauth/token. Spring returns an access_token
- On future API calls, use the supplied access_token as the bearer token
I think that the problem may be because I need to place something on each method in my controllers to tell swagger that the endpoint requires authentication and what type, but I can't find any clear documentation on how to do this, and I don't know if I need to apply any further changes to my swagger config.
Here's an example of a controller (with most methods removed to reduce size);
#Api(value="Currencies", description="Retrieve, create, update and delete currencies", tags = "Currencies")
#RestController
#RequestMapping("/currency")
public class CurrencyController {
private CurrencyService currencyService;
public CurrencyController(#Autowired CurrencyService currencyService) {
this.currencyService = currencyService;
}
/**
* Deletes the requested currency
* #param currencyId the Id of the currency to delete
* #return 200 OK if delete successful
*/
#ApiOperation(value = "Deletes a currency item", response = ResponseEntity.class)
#RequestMapping(value="/{currencyId}", method=RequestMethod.DELETE)
public ResponseEntity<?> deleteCurrency(#PathVariable("currencyId") Long currencyId) {
try {
currencyService.deleteCurrencyById(currencyId);
} catch (EntityNotFoundException e) {
return new ErrorResponse("Unable to delete, currency with Id " + currencyId + " not found!").response(HttpStatus.NOT_FOUND);
}
return new ResponseEntity(HttpStatus.OK);
}
/**
* Returns a single currency by it's Id
* #param currencyId the currency Id to return
* #return the found currency item or an error
*/
#ApiOperation(value = "Returns a currency item", response = CurrencyResponse.class)
#RequestMapping(value="/{currencyId}", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<RestResponse> getCurrency(#PathVariable("currencyId") Long currencyId) {
Currency currency = null;
try {
currency = currencyService.findById(currencyId);
} catch (EntityNotFoundException e) {
return new ErrorResponse("Currency with Id " + currencyId + " could not be found!").response(HttpStatus.NOT_FOUND);
}
return new CurrencyResponse(currency).response(HttpStatus.OK);
}
/**
* Returns a list of all currencies available in the system
* #return Rest response of all currencies
*/
#ApiOperation(value = "Returns a list of all currencies ordered by priority", response = CurrencyListResponse.class)
#RequestMapping(value="", method=RequestMethod.GET, produces="application/json")
public ResponseEntity<RestResponse> getCurrencies() {
return new CurrencyListResponse(currencyService.getAllCurrencies()).response(HttpStatus.OK);
}
}
Here is my current swagger config;
#Configuration
#EnableSwagger2
public class SwaggerConfig extends WebMvcConfigurationSupport {
#Bean
public SecurityConfiguration security() {
return SecurityConfigurationBuilder.builder()
.clientId("12345")
.clientSecret("12345")
.scopeSeparator(" ")
.useBasicAuthenticationWithAccessCodeGrant(true)
.build();
}
#Bean
public Docket productApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.xompare.moo.controllers"))
.build()
.securitySchemes(Arrays.asList(securityScheme()))
.securityContexts(Arrays.asList(securityContext()))
.apiInfo(metaData());
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(Arrays.asList(new SecurityReference("spring_oauth", scopes())))
.forPaths(PathSelectors.regex("/.*"))
.build();
}
private AuthorizationScope[] scopes() {
AuthorizationScope[] scopes = {
new AuthorizationScope("read", "for read operations"),
new AuthorizationScope("write", "for write operations") };
return scopes;
}
public SecurityScheme securityScheme() {
GrantType grantType = new ClientCredentialsGrant("http://localhost:8080/oauth/token");
SecurityScheme oauth = new OAuthBuilder().name("spring_oauth")
.grantTypes(Arrays.asList(grantType))
.scopes(Arrays.asList(scopes()))
.build();
return oauth;
}
#Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
Authentication via spring works perfectly at this point, my only problem is getting it working with Swagger UI.
I think that you need to add "Bearer " in front of your key, just like it is shown at this post:
Spring Boot & Swagger UI. Set JWT token
I managed to resolve this by reverting from swagger-ui version 2.8.0 to 2.7.0 after reading the contents of this link which suggested it was a problem with version 2.8.0
https://github.com/springfox/springfox/issues/1961

Retrofit returns content length 0 with spring data rest

I am using spring boot application with spring data rest deployed on heroku. I have a /api/userDatas end point on which an entity can be created by a POST request. I have tested it using Postman and it gets created.
Now I am using retrofit on android to perform the same functionality. But the problem is that entity gets created on the server but onFailure() always gets called. I have debugged and found that content-length is always 0.
CreateUser
private void createUser() {
Gson gson = new GsonBuilder()
.setLenient()
.create();
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(logging).build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(ServeNetworking.ServeURLS.getBaseURL())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
ServeNetworking serveNetworking = retrofit.create(ServeNetworking.class);
Call<UserData> getUserByIdResponseCall = serveNetworking.socialConnectAPI(userData);
getUserByIdResponseCall.enqueue(new Callback<UserData>() {
#Override
public void onResponse(Call<UserData> call, Response<UserData> response) {
Log.d("getUserById", "onResponse");
Toast.makeText(OTPVerificationActivity.this, "userId : onResponse", Toast.LENGTH_LONG).show();
/**
* Save data in local db and navigate to map screen.
*/
ActivityAnimationUtils.presentActivity(OTPVerificationActivity.this, MapActivity.class);
try{
DBManager.getDBManager(OTPVerificationActivity.this).setIsVerified(true);
} catch (Exception e){
e.printStackTrace();
}
}
#Override
public void onFailure(Call<UserData> call, Throwable t) {
Log.d("getUserById", "onFailure");
Utils.showSnackBar(OTPVerificationActivity.this,"Somethign went wrong", root);
}
});
}
and interface:
public interface ServeNetworking {
#POST("/api/userDatas")
#Headers({ "Content-Type: application/json;charset=UTF-8"})
Call<UserData> socialConnectAPI(
#Body UserData user
);
#GET("/api/userDatas/{userId}")
#Headers({ "Content-Type: application/json; charset=utf-8"})
Call<UserData> getUser(
#Path("userId") String userId
);
/**
* this class is used to get the donor base urls...
*/
class ServeURLS{
private static String baseURL="https://fabrimizer-serve.herokuapp.com";
public static String getBaseURL() {
return baseURL;
}
}
}
I am getting following error:
java.io.EOFException: End of input at line 1 column 1 path $
Found the solution.
added Accept header in ServeNetworking:
#POST("/api/userDatas")
#Headers({ "Content-Type: application/json,"Accept: application/json"})
Call<UserData> socialConnectAPI(
#Body UserData user
);

Resources