Keycloak to save newly registered user into application database - spring

In my app I have some entity relations such as User and Organization. I thought that I would use Keycloak so that I don't have to implement custom registration, login, password reset, authorization and authentication. Problem is, that when user registers to Keycloak I don't have new User entity in my internal database for the application. Is there any way to automatically create new User entity in my internal database upon his registration throughout Keycloak?
I thought of custom Security filter on in my app that would look something like that:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.keycloak.KeycloakSecurityContext;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
public class UserRegistrationFilter extends GenericFilterBean {
private static final Logger console = LogManager.getLogger(UserRegistrationFilter.class);
private final UserRepository userRepo;
public UserRegistrationFilter(UserRepository userRepository) {
this.userRepo = userRepository;
}
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
KeycloakSecurityContext ctx = (KeycloakSecurityContext) servletRequest.getAttribute(KeycloakSecurityContext.class.getName());
console.warn("executed");
if (ctx != null) {
console.warn(ctx.getToken().getSubject());
Optional<User> existing = userRepo.findById(UUID.fromString(ctx.getToken().getSubject()));
if (existing.isEmpty()) {
User user = new User();
user.setUuid(UUID.fromString(ctx.getToken().getSubject()));
userRepo.save(user);
}
}
}
}
but it seems like a bad idea.
Thank you for any advice.

I don't see a problem with your approach, I would probably do something similar.
Well, probably what I would do throughout the entire app is to retrieve the user not directly from UserRepository but from a UserService that uses UserRepository.
In UserService I would have a findById method that would internally ask the repo to find the user, and if it doesn't exists, then create it and return it.
This way, I wouldn't need the filter, which anyways I don't think it's that bad.
An alternative I can think of, is to listen to keycloak events via webhooks:
https://github.com/zonaut/keycloak-extensions/blob/master/spi-event-listener/README.md
https://github.com/jessylenne/keycloak-event-listener-http
So keycloak informs you about any changes, such as user creation.
This way you can create users directly in keycloak in a way that even if they never log in your app, you would still be able to register them in your app.
PS: I never tried keycloak events, so can't help with the details

Related

Spring Boot OAuth2 with Github

I am implementing OAuth2 architecture with GitHub as authorization server to log into my dummy web application. Everything works perfectly fine. I got problem when I log in using my GitHub credentials I want the redirected page to show some message for user for example Welcome XYZ, but I could not get username who is logged in currently but a number is shown instead. I used principal object as well as Authentication object. Please would any body tell how do I achieve it?
Here is code for OAuth2 GitHub Configuration.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
#Configuration
public class WebSecurityConfigurerAdapterImp extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login((c)->
{
c.clientRegistrationRepository(this.clientRegistrationRepository());
}
);
http.authorizeRequests().anyRequest().authenticated();
}
private ClientRegistrationRepository clientRegistrationRepository()
{
ClientRegistration c=this.clientRegistration();
return new InMemoryClientRegistrationRepository(c);
}
private ClientRegistration clientRegistration()
{
return CommonOAuth2Provider.GITHUB.getBuilder("github").clientId("72bc31d8b0304575442c").clientSecret("XYZSECRET").build();
}
}
Code for main_controller to which user will be redirected after logging in.
package com.controllers;
import java.security.Principal;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
#Controller
public class Main_Controller {
#GetMapping("/")
#ResponseBody
public String HomePage(Authentication p)
{
System.out.println("Hellow "+p.getName());
return "Hello People.";
}
}
By default if you use "CommonOAuth2Provider.GITHUB" then principal name is map to "id" of your github user. So, you need to map principal name attribute to "login"

Unable to Login in Spring Boot

I am new to spring boot, i am trying to login in my application. I am facing some issues.
I am not able to login. It cant authenticate to login with my credential and return with message login invalid.
I want the user to be authenticate when they try to access client site (eg localhost:8080/). I also want to implement logout when user dont valid on a link file.
Here is my main application
package oidc.controller;
import eu.olympus.client.interfaces.UserClient;
import eu.olympus.model.Attribute;
import eu.olympus.model.AttributeIdentityProof;
import eu.olympus.model.Operation;
import eu.olympus.model.Policy;
import eu.olympus.model.Predicate;
import eu.olympus.model.exceptions.AuthenticationFailedException;
import eu.olympus.model.exceptions.ExistingUserException;
import eu.olympus.model.exceptions.OperationFailedException;
import eu.olympus.model.exceptions.TokenGenerationException;
import eu.olympus.model.exceptions.UserCreationFailedException;
import eu.olympus.model.server.rest.IdentityProof;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import oidc.model.AttributeContainer;
import oidc.model.ChangeAttributesRequest;
import oidc.model.ChangePasswordRequest;
import oidc.model.CreateUserRequest;
import oidc.model.DeleteAccountRequest;
import oidc.model.LoginRequest;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.view.RedirectView;
#Controller
public class OidcController {
private static final Logger logger = LoggerFactory.getLogger(OidcController.class);
#Autowired
UserClient userClient;
#Autowired
Policy policy;
// Login
#RequestMapping("/login")
public String login(Model model, #RequestParam String redirect_uri, #RequestParam String state, #RequestParam String nonce, HttpServletRequest request) {
request.getSession().setAttribute("redirectUrl", redirect_uri);
request.getSession().setAttribute("state", state);
request.getSession().setAttribute("nonce", nonce);
LoginRequest loginRequest = new LoginRequest();
model.addAttribute("loginRequest", loginRequest);
policy.setPolicyId(nonce);
return "/login";
}
#RequestMapping("/loginFailed")
public String login(Model model) {
LoginRequest loginRequest = new LoginRequest();
model.addAttribute("loginRequest", loginRequest);
model.addAttribute("loginError", true);
return "/login";
}
#RequestMapping("/loginPage")
public String loginPage(Model model) {
LoginRequest loginRequest = new LoginRequest();
model.addAttribute("loginRequest", loginRequest);
model.addAttribute("hasCreated", false);
return "/login";
}
#PostMapping("/authenticate")
public RedirectView authenticate(LoginRequest loginRequest, Model model, HttpServletRequest request) throws AuthenticationFailedException, TokenGenerationException {
try {
// TODO We need to get the audience somehow?
policy.getPredicates().add(new Predicate("audience", Operation.REVEAL, new Attribute("olympus-service-provider")));
String token = userClient.authenticate(loginRequest.getUsername(), loginRequest.getPassword(), policy, null, "NONE");
model.addAttribute("username", loginRequest.getUsername());
model.addAttribute("token", token);
String redirectUrl = (String) request.getSession().getAttribute("redirectUrl");
String state = (String) request.getSession().getAttribute("state");
return new RedirectView(redirectUrl + "#state=" + state + "&id_token=" + token + "&token_type=bearer");
} catch (Exception e) {
e.printStackTrace();
if (ExceptionUtils.indexOfThrowable(e, AuthenticationFailedException.class) != -1) {
return new RedirectView("/loginFailed", true);
} else {
throw e;
}
} finally {
userClient.clearSession();
}
}
here is login Request
package oidc.model;
import lombok.Getter;
import lombok.Setter;
/**
* A container for a login request
*/
#Getter
#Setter
public class LoginRequest {
private String username;
private String password;
}
I suggest you to use Spring Security. It is a dependency and you can add it via your build tool such as Maven, Gradle etc. After studying your code what I can see is that you are trying to build security mechanism from scratch.
I wouldn't advice you to do that unless you have a high motivation factor to do so. If you can use Spring Security, it is very powerful and equipped with all the features you are looking for. You can easily overcome user authentication, authorization and even it can provide a default login page.
When it comes to authentication, you can have few types of user stores such as in-memory user store, JDBC user store, LDAP user store or even your own custom user store. Apart from username and password authentication via a GUI, you are able to do sys-to-sys authentication. You can easily achieve JWT token authentication with few steps just like adding a filter and minor configuration.
It is very difficult to cover and give the whole source code as an answer here but I will provide you a sample code such that you can get a glimpse of it. Please be advice that the below mentioned code is purely for demonstration purpose and can modify it to your standards.
Spring Security Configuration Class
package com.example.sankalpaspringbootcicd.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* Security configuration class.
* Created date - 2021/08/02
*/
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* Authentication
* Responsible for configuring user-store.
* Overridden from WebSecurityConfigurerAdapter level.
* #param theAuthenticationManager AuthenticationManagerBuilder
* #throws Exception - Exception
*/
#Override
public void configure(AuthenticationManagerBuilder theAuthenticationManager) throws Exception {
theAuthenticationManager.inMemoryAuthentication()
//Admin user
.withUser("admin")
.password("super")
.roles("ADMIN")
.and()
//Normal user
.withUser("user")
.password("password")
.roles("USER");
}
/**
* Authorization
* Responsible for security configuration.
* Overridden from WebSecurityConfigurerAdapter level.
* #param theHttpSecurity HttpSecurity
* #throws Exception - Exception
*/
#Override
public void configure(HttpSecurity theHttpSecurity) throws Exception {
theHttpSecurity.csrf().disable();
theHttpSecurity
.authorizeRequests()
.antMatchers("/welcome/**").access("permitAll")
.antMatchers("/user/**").hasRole("ADMIN")
.anyRequest().fullyAuthenticated() //All requests should be authenticated
.and().headers().frameOptions().sameOrigin() //To allow H2-Console
.and().httpBasic();
}
/**
* Method constructing a password encoder bean.
* Constructs 'NoOpPasswordEncoder'.
* #return PasswordEncoder
*/
#Bean
public PasswordEncoder getPasswordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
This is a very simple example and I will take you through each method. The first method is configure(AuthenticationManagerBuilder theAuthenticationManager) and what it does is basically creating you a in-memory user store.
The second method is configure(HttpSecurity theHttpSecurity) and it does allow you to do customizations to your security behaviour. It does allow some routes for everyone, restrict some routes for only with users with particular roles, allows route for H2 embedded database console, disable CSRF (Cross-Site Request Forgery) etc. This will prompt you a default login page as well. You can further add logics related to your login and logout mechanisms here as well.
The third method is PasswordEncoder getPasswordEncoder() and it does create a password encoder bean and put it in Spring Application Context to be used anytime.

Null client in OAuth2 Multi-Factor Authentication

Complete code for a Spring OAuth2 implementation of multi-factor authentication has been uploaded to a file sharing site that you can download by clicking on this link. Instructions below explain how to use the link to recreate the current problem on any computer.
THE CURRENT ERROR:
An error is being triggered when a user tries to authenticate using two factor authentication in the Spring Boot OAuth2 app from the link in the preceding paragraph. The error is thrown at the point in the process when the app should serve up a second page asking the user for a pin code to confirm the user's identity.
Given that a null client is triggering this error, the problem seems to be how to connect a ClientDetailsService to a Custom OAuth2RequestFactory in Spring Boot OAuth2.
The entire debug log can be read at a file sharing site by clicking on this link. The complete stack trace in the logs contains only one reference to code that is actually in the app, and that line of code is:
AuthorizationRequest authorizationRequest =
oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
The error thrown in the debug logs is:
org.springframework.security.oauth2.provider.NoSuchClientException:
No client with requested id: null
CONTROL FLOW WHEN ERROR IS THROWN:
I created the following flowchart to illustrate the intended flow of multi-factor authentication requests in #James' suggested implementation:
In the preceding flowchart, the current error is being thrown at some point between the Username & Password View and the GET /secure/two_factor_authenticated steps.
The solution to this OP is limited in scope to the FIRST PASS that 1.) travels through the /oauth/authorize endpoint and then 2.) returns back to the /oauth/authorize endpoint via TwoFactorAuthenticationController.
So we simply want to resolve the NoSuchClientException while also demonstrating that the client has been successfully granted ROLE_TWO_FACTOR_AUTHENTICATED in the POST /secure/two_factor_authenticated. Given that the subsequent steps are boiler-plate, it is acceptable for the flow to demonstrably break in the SECOND PASS entry into CustomOAuth2RequestFactory, as long as the user enters the SECOND PASS with all the artifacts of successfully having completed the FIRST PASS. The SECOND PASS can be a separate question as long as we successfully resolve the FIRST PASS here.
RELEVANT CODE EXCERPTS:
Here is the code for the AuthorizationServerConfigurerAdapter, where I attempt to set up the connection:
#Configuration
#EnableAuthorizationServer
protected static class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired//ADDED AS A TEST TO TRY TO HOOK UP THE CUSTOM REQUEST FACTORY
private ClientDetailsService clientDetailsService;
#Autowired//Added per: https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2
private CustomOAuth2RequestFactory customOAuth2RequestFactory;
//THIS NEXT BEAN IS A TEST
#Bean CustomOAuth2RequestFactory customOAuth2RequestFactory(){
return new CustomOAuth2RequestFactory(clientDetailsService);
}
#Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(
new ClassPathResource("keystore.jks"), "foobar".toCharArray()
)
.getKeyPair("test");
converter.setKeyPair(keyPair);
return converter;
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("acme")//API: http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/config/annotation/builders/ClientDetailsServiceBuilder.ClientBuilder.html
.secret("acmesecret")
.authorizedGrantTypes("authorization_code", "refresh_token", "password")
.scopes("openid");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints//API: http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerEndpointsConfigurer.html
.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.requestFactory(customOAuth2RequestFactory);//Added per: https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer//API: http://docs.spring.io/spring-security/oauth/apidocs/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.html
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
}
Here is the code for the TwoFactorAuthenticationFilter, which contains the code above that is triggering the error:
package demo;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
//This class is added per: https://stackoverflow.com/questions/30319666/two-factor-authentication-with-spring-security-oauth2
/**
* Stores the oauth authorizationRequest in the session so that it can
* later be picked by the {#link com.example.CustomOAuth2RequestFactory}
* to continue with the authoriztion flow.
*/
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private OAuth2RequestFactory oAuth2RequestFactory;
//These next two are added as a test to avoid the compilation errors that happened when they were not defined.
public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
public static final String ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED = "ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED";
#Autowired
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream().anyMatch(
authority -> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority())
);
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Check if the user hasn't done the two factor authentication.
if (AuthenticationUtil.isAuthenticated() && !AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authenticatoin. */
if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
// Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
// to return this saved request to the AuthenticationEndpoint after the user successfully
// did the two factor authentication.
request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);
// redirect the the page where the user needs to enter the two factor authentiation code
redirectStrategy.sendRedirect(request, response,
ServletUriComponentsBuilder.fromCurrentContextPath()
.path(TwoFactorAuthenticationController.PATH)
.toUriString());
return;
}
}
filterChain.doFilter(request, response);
}
private Map<String, String> paramsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
}
RE-CREATING THE PROBLEM ON YOUR COMPUTER:
You can recreate the problem on any computer in only a few minutes by following these simple steps:
1.) Download the zipped version of the app from a file sharing site by clicking on this link.
2.) Unzip the app by typing: tar -zxvf oauth2.tar(1).gz
3.) launch the authserver app by navigating to oauth2/authserver and then typing mvn spring-boot:run.
4.) launch the resource app by navigating to oauth2/resource and then typing mvn spring-boot:run
5.) launch the ui app by navigating to oauth2/ui and then typing mvn spring-boot:run
6.) Open a web browser and navigate to http : // localhost : 8080
7.) Click Login and then enter Frodo as the user and MyRing as the password, and click to submit. This will trigger the error shown above.
You can view the complete source code by:
a.) importing the maven projects into your IDE, or by
b.) navigating within the unzipped directories and opening with a text editor.
Note: The code in the file sharing link above is a combination of the Spring Boot OAuth2 GitHub sample at this link, and the suggestions for 2 Factor Authentication offered by #James at this link. The only changes to the Spring Boot GitHub sample have been in the authserver app, specifically in authserver/src/main/java and in authserver/src/main/resources/templates.
NARROWING THE PROBLEM:
Per #AbrahamGrief's suggestion, I added a FilterConfigurationBean, which resolved the NoSuchClientException. But the OP asks how to complete the FIRST PASS through the control flow in the diagram.
I then narrowed the problem by setting ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED in Users.loadUserByUername() as follows:
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password;
List<GrantedAuthority> auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER");
if (username.equals("Samwise")) {//ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED will need to come from the resource, NOT the user
auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_HOBBIT, ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED");
password = "TheShire";
}
else if (username.equals("Frodo")){//ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED will need to come from the resource, NOT the user
auth = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_HOBBIT, ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED");
password = "MyRing";
}
else{throw new UsernameNotFoundException("Username was not found. ");}
return new org.springframework.security.core.userdetails.User(username, password, auth);
}
This eliminates the need to configure clients and resources, so that the current problem remains narrow. However, the next roadblock is that Spring Security is rejecting the user's request for /security/two_factor_authentication. What further changes need to be made to complete the FIRST PASS through the control flow, so that the POST /secure/two_factor_authentication can SYSO ROLE_TWO_FACTOR_AUTHENTICATED?
There are a lot of modifications needed for that project to implement the described flow, more than should be in scope for a single question. This answer will focus solely on how to resolve:
org.springframework.security.oauth2.provider.NoSuchClientException: No
client with requested id: null
when trying to use a SecurityWebApplicationInitializer and a Filter bean while running in a Spring Boot authorization server.
The reason this exception is happening is because WebApplicationInitializer instances are not run by Spring Boot. That includes any AbstractSecurityWebApplicationInitializer subclasses that would work in a WAR deployed to a standalone Servlet container. So what is happening is Spring Boot creates your filter because of the #Bean annotation, ignores your AbstractSecurityWebApplicationInitializer, and applies your filter to all URLs. Meanwhile, you only want your filter applied to those URLs that you're trying to pass to addMappingForUrlPatterns.
Instead, to apply a servlet Filter to particular URLs in Spring Boot, you should define a FilterConfigurationBean. For the flow described in the question, which is trying to apply a custom TwoFactorAuthenticationFilter to /oauth/authorize, that would look as follows:
#Bean
public FilterRegistrationBean twoFactorAuthenticationFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(twoFactorAuthenticationFilter());
registration.addUrlPatterns("/oauth/authorize");
registration.setName("twoFactorAuthenticationFilter");
return registration;
}
#Bean
public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter() {
return new TwoFactorAuthenticationFilter();
}

Binding snake_case request parameters to a Spring form

I'm implementing a simple RESTful service using Spring Boot, with the interface defined by a .NET (I think) client. Their parameter names are snake_case, rather than camelCase, which obviously means I need to customise how they are mapped.
In the case of JSON input/output, that's fine, I've just customised the ObjectMapper, like so:
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
return objectMapper;
}
That works fine. Now my problem is form data. I have a Spring form like:
public class MyForm {
private String myValue;
public String getMyValue() {return myValue;}
public void setMyValue(String myValue) {this.myValue = myValue;}
}
But the requests I need to accept will look like:
POST /foo/bar HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
my_value=5
I feel like there must be some simple hook into Spring's binding, like the equivalent setting in Jackon's ObjectMapper, but I'm struggling to find one. The only somewhat-relevant post I can find on here is this one, about completely changing the parameter names, which has some suggestions that seem like overkill for my use case.
The simple solution is simply to use snake case for the fields in MyForm, which works fine, but is a bit ugly.
A final suggestion I've seen elsewhere is to use an interceptor to modify the request parameters on the way in, which seems like it would be straightforward but it feels like there are bound to be exceptions that make it non-trivial, and I'm concerned that having code hidden away in an interceptor makes it really hard to find when you hit the one obscure case where it doesn't work.
Is there some 'proper' Spring-y way of handling this that I'm missing, or do I just need to pick one of the above not-quite-perfect solutions?
probably you already have solved this issue, I was fighting with this today and answered a question on StackOverflow PT.
So here is the deal:
Create a filter to be executed before the request reach the controller, and format the parameters accordingly (from snake case to camel case on my scenario).
Talk is cheap, show me the code!
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;
import com.google.common.base.CaseFormat;
#Configuration
public class AppConfig {
#Bean
public Filter snakeConverter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final Map<String, String[]> formattedParams = new ConcurrentHashMap<>();
for (String param : request.getParameterMap().keySet()) {
String formattedParam = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, param);
formattedParams.put(formattedParam, request.getParameterValues(param));
}
filterChain.doFilter(new HttpServletRequestWrapper(request) {
#Override
public String getParameter(String name) {
return formattedParams.containsKey(name) ? formattedParams.get(name)[0] : null;
}
#Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(formattedParams.keySet());
}
#Override
public String[] getParameterValues(String name) {
return formattedParams.get(name);
}
#Override
public Map<String, String[]> getParameterMap() {
return formattedParams;
}
}, response);
}
};
}
}
The snakeConverter do the magic.
In there, the doFilterInternal is executed always before the request reach the controller, the parameters are stored in a new Map in their formatted form, and are forwarded to the controller through the filterChain.doFilter.
The HttpServletRequestWrapper do the job of provide our new parameters to the controller.
This code is completely based on the azhawkes filter.
Testing it using a simple controller in the the following URL: http://localhost:8080/snakecase?foo_bar=123

Spring Test & Security: How to mock authentication?

I was trying to figure out how to unit test if my the URLs of my controllers are properly secured. Just in case someone changes things around and accidentally removes security settings.
My controller method looks like this:
#RequestMapping("/api/v1/resource/test")
#Secured("ROLE_USER")
public #ResonseBody String test() {
return "test";
}
I set up a WebTestEnvironment like so:
import javax.annotation.Resource;
import javax.naming.NamingException;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
#RunWith(SpringJUnit4ClassRunner.class)
#WebAppConfiguration
#ContextConfiguration({
"file:src/main/webapp/WEB-INF/spring/security.xml",
"file:src/main/webapp/WEB-INF/spring/applicationContext.xml",
"file:src/main/webapp/WEB-INF/spring/servlet-context.xml" })
public class WebappTestEnvironment2 {
#Resource
private FilterChainProxy springSecurityFilterChain;
#Autowired
#Qualifier("databaseUserService")
protected UserDetailsService userDetailsService;
#Autowired
private WebApplicationContext wac;
#Autowired
protected DataSource dataSource;
protected MockMvc mockMvc;
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected UsernamePasswordAuthenticationToken getPrincipal(String username) {
UserDetails user = this.userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
user,
user.getPassword(),
user.getAuthorities());
return authentication;
}
#Before
public void setupMockMvc() throws NamingException {
// setup mock MVC
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.wac)
.addFilters(this.springSecurityFilterChain)
.build();
}
}
In my actual test I tried to do something like this:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import eu.ubicon.webapp.test.WebappTestEnvironment;
public class CopyOfClaimTest extends WebappTestEnvironment {
#Test
public void signedIn() throws Exception {
UsernamePasswordAuthenticationToken principal =
this.getPrincipal("test1");
SecurityContextHolder.getContext().setAuthentication(principal);
super.mockMvc
.perform(
get("/api/v1/resource/test")
// .principal(principal)
.session(session))
.andExpect(status().isOk());
}
}
I picked this up here:
http://java.dzone.com/articles/spring-test-mvc-junit-testing here:
http://techdive.in/solutions/how-mock-securitycontextholder-perfrom-junit-tests-spring-controller or here:
How to JUnit tests a #PreAuthorize annotation and its spring EL specified by a spring MVC Controller?
Yet if one looks closely this only helps when not sending actual requests to URLs, but only when testing services on a function level. In my case an "access denied" exception was thrown:
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE]
...
The following two log messages are noteworthy basically saying that no user was authenticated indicating that setting the Principal did not work, or that it was overwritten.
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER]
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
Seaching for answer I couldn't find any to be easy and flexible at the same time, then I found the Spring Security Reference and I realized there are near to perfect solutions. AOP solutions often are the greatest ones for testing, and Spring provides it with #WithMockUser, #WithUserDetails and #WithSecurityContext, in this artifact:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
In most cases, #WithUserDetails gathers the flexibility and power I need.
How #WithUserDetails works?
Basically you just need to create a custom UserDetailsService with all the possible users profiles you want to test. E.g
#TestConfiguration
public class SpringSecurityWebAuxTestConfig {
#Bean
#Primary
public UserDetailsService userDetailsService() {
User basicUser = new UserImpl("Basic User", "user#company.com", "password");
UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("PERM_FOO_READ")
));
User managerUser = new UserImpl("Manager User", "manager#company.com", "password");
UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList(
new SimpleGrantedAuthority("ROLE_MANAGER"),
new SimpleGrantedAuthority("PERM_FOO_READ"),
new SimpleGrantedAuthority("PERM_FOO_WRITE"),
new SimpleGrantedAuthority("PERM_FOO_MANAGE")
));
return new InMemoryUserDetailsManager(Arrays.asList(
basicActiveUser, managerActiveUser
));
}
}
Now we have our users ready, so imagine we want to test the access control to this controller function:
#RestController
#RequestMapping("/foo")
public class FooController {
#Secured("ROLE_MANAGER")
#GetMapping("/salute")
public String saluteYourManager(#AuthenticationPrincipal User activeUser)
{
return String.format("Hi %s. Foo salutes you!", activeUser.getUsername());
}
}
Here we have a get mapped function to the route /foo/salute and we are testing a role based security with the #Secured annotation, although you can test #PreAuthorize and #PostAuthorize as well.
Let's create two tests, one to check if a valid user can see this salute response and the other to check if it's actually forbidden.
#RunWith(SpringRunner.class)
#SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = SpringSecurityWebAuxTestConfig.class
)
#AutoConfigureMockMvc
public class WebApplicationSecurityTest {
#Autowired
private MockMvc mockMvc;
#Test
#WithUserDetails("manager#company.com")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isOk())
.andExpect(content().string(containsString("manager#company.com")));
}
#Test
#WithUserDetails("user#company.com")
public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isForbidden());
}
}
As you see we imported SpringSecurityWebAuxTestConfig to provide our users for testing. Each one used on its corresponding test case just by using a straightforward annotation, reducing code and complexity.
Better use #WithMockUser for simpler Role Based Security
As you see #WithUserDetails has all the flexibility you need for most of your applications. It allows you to use custom users with any GrantedAuthority, like roles or permissions. But if you are just working with roles, testing can be even easier and you could avoid constructing a custom UserDetailsService. In such cases, specify a simple combination of user, password and roles with #WithMockUser.
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#Documented
#WithSecurityContext(
factory = WithMockUserSecurityContextFactory.class
)
public #interface WithMockUser {
String value() default "user";
String username() default "";
String[] roles() default {"USER"};
String password() default "password";
}
The annotation defines default values for a very basic user. As in our case the route we are testing just requires that the authenticated user be a manager, we can quit using SpringSecurityWebAuxTestConfig and do this.
#Test
#WithMockUser(roles = "MANAGER")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isOk())
.andExpect(content().string(containsString("user")));
}
Notice that now instead of the user manager#company.com we are getting the default provided by #WithMockUser: user; yet it won't matter because what we really care about is his role: ROLE_MANAGER.
Conclusions
As you see with annotations like #WithUserDetails and #WithMockUser we can switch between different authenticated users scenarios without building classes alienated from our architecture just for making simple tests. Its also recommended you to see how #WithSecurityContext works for even more flexibility.
Since Spring 4.0+, the best solution is to annotate the test method with #WithMockUser
#Test
#WithMockUser(username = "user1", password = "pwd", roles = "USER")
public void mytest1() throws Exception {
mockMvc.perform(get("/someApi"))
.andExpect(status().isOk());
}
Remember to add the following dependency to your project
'org.springframework.security:spring-security-test:4.2.3.RELEASE'
It turned out that the SecurityContextPersistenceFilter, which is part of the Spring Security filter chain, always resets my SecurityContext, which I set calling SecurityContextHolder.getContext().setAuthentication(principal) (or by using the .principal(principal) method). This filter sets the SecurityContext in the SecurityContextHolder with a SecurityContext from a SecurityContextRepository OVERWRITING the one I set earlier. The repository is a HttpSessionSecurityContextRepository by default. The HttpSessionSecurityContextRepository inspects the given HttpRequest and tries to access the corresponding HttpSession. If it exists, it will try to read the SecurityContext from the HttpSession. If this fails, the repository generates an empty SecurityContext.
Thus, my solution is to pass a HttpSession along with the request, which holds the SecurityContext:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import eu.ubicon.webapp.test.WebappTestEnvironment;
public class Test extends WebappTestEnvironment {
public static class MockSecurityContext implements SecurityContext {
private static final long serialVersionUID = -1386535243513362694L;
private Authentication authentication;
public MockSecurityContext(Authentication authentication) {
this.authentication = authentication;
}
#Override
public Authentication getAuthentication() {
return this.authentication;
}
#Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
}
#Test
public void signedIn() throws Exception {
UsernamePasswordAuthenticationToken principal =
this.getPrincipal("test1");
MockHttpSession session = new MockHttpSession();
session.setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
new MockSecurityContext(principal));
super.mockMvc
.perform(
get("/api/v1/resource/test")
.session(session))
.andExpect(status().isOk());
}
}
Add in pom.xml:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.0.RC2</version>
</dependency>
and use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors for authorization request.
See the sample usage at https://github.com/rwinch/spring-security-test-blog
(https://jira.spring.io/browse/SEC-2592).
Update:
4.0.0.RC2 works for spring-security 3.x.
For spring-security 4 spring-security-test become part of spring-security (http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test, version is the same).
Setting Up is changed: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
Sample for basic-authentication: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication.
Here is an example for those who want to Test Spring MockMvc Security Config using Base64 basic authentication.
String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
Maven Dependency
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.3</version>
</dependency>
Short answer:
#Autowired
private WebApplicationContext webApplicationContext;
#Autowired
private Filter springSecurityFilterChain;
#Before
public void setUp() throws Exception {
final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
.defaultRequest(defaultRequestBuilder)
.alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
.apply(springSecurity(springSecurityFilterChain))
.build();
}
private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
final MockHttpServletRequest request) {
requestBuilder.session((MockHttpSession) request.getSession());
return request;
}
After perform formLogin from spring security test each of your requests will be automatically called as logged in user.
Long answer:
Check this solution (the answer is for spring 4): How to login a user with spring 3.2 new mvc testing
Options to avoid using SecurityContextHolder in tests:
Option 1: use mocks - I mean mock SecurityContextHolder using some mock library - EasyMock for example
Option 2: wrap call SecurityContextHolder.get... in your code in some service - for example in SecurityServiceImpl with method getCurrentPrincipal that implements SecurityService interface and then in your tests you can simply create mock implementation of this interface that returns the desired principal without access to SecurityContextHolder.
Pretty Late answer though. But This has worked for me , and could be useful.
While Using Spring Security ans mockMvc, all you need to is use #WithMockUser annotation like others are mentioned.
Spring security also provides another annotation called #WithAnonymousUser for testing unauthenticated requests. However you should be careful here. You would be expecting 401, but I got 403 Forbidden Error by default. In actual scenarios, when you are running actual service, It is redirected and you end up getting the correct 401 response code.Use this annotation for anonymous requests.
You may also think of ommitting the annotaions and simply keep it unauthorized. But this usually raises the correct exceptions(like AuthenticationException), but you will get correct status code if it is handled correctly(If you are using custom handler). I used to get 500 for this. So look for the exceptions raised in the debugger, and check if it is handled rightly and returns the correct status code.
Create a class TestUserDetailsImpl on your test package:
#Service
#Primary
#Profile("test")
public class TestUserDetailsImpl implements UserDetailsService {
public static final String API_USER = "apiuser#example.com";
private User getAdminUser() {
User user = new User();
user.setUsername(API_USER);
SimpleGrantedAuthority role = new SimpleGrantedAuthority("ROLE_API_USER");
user.setAuthorities(Collections.singletonList(role));
return user;
}
#Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
if (Objects.equals(username, ADMIN_USERNAME))
return getAdminUser();
throw new UsernameNotFoundException(username);
}
}
Rest endpoint:
#GetMapping("/invoice")
#Secured("ROLE_API_USER")
public Page<InvoiceDTO> getInvoices(){
...
}
Test endpoint:
#Test
#WithUserDetails("apiuser#example.com")
public void testApi() throws Exception {
...
}
When using MockMvcBuilders.webAppContextSetup(wac).addFilters(...) than springSecurityFilterChain (more specifically SecurityContextPersistenceFilter) will take over and will remove the SecurityContext prepared by #WithMockUser (pretty silly); this happens because SecurityContextPersistenceFilter tries to "restore" the SecurityContext from the HttpSession where finds none. Well, use this simple AutoStoreSecurityContextHttpFilter defined below which will take care of putting #WithMockUser's preppared SecurityContext into the HttpSession such that later SecurityContextPersistenceFilter will be able to find it.
#ContextConfiguration(...) // the issue doesn't occur when using #SpringBootTest
public class SomeTest {
#Autowired
private Filter springSecurityFilterChain;
private MockMvc mockMvc;
#BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac)
.addFilters(new AutoStoreSecurityContextHttpFilter(), springSecurityFilterChain).build();
}
#WithMockUser
#Test
void allowAccessToAuthenticated() {
...
}
}
// don't use this Filter in production because it's only intended for tests, to solve the
// #WithMockUser & springSecurityFilterChain (more specifically SecurityContextPersistenceFilter) "misunderstandings"
public class AutoStoreSecurityContextHttpFilter extends HttpFilter {
protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
req.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
super.doFilter(req, res, chain);
}
}

Resources