Null client in OAuth2 Multi-Factor Authentication - spring

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

Related

How can spring security deny access instead of redirecting to the login page

When accessing a specific URL with a different IP address than allowed, spring security will auto forward the request to the login page instead of denying the request.
Here's my Spring Security Expression
http.authorizeRequests()
.antMatchers("/" + WELCOME_PAGE,
"/" + FOOD_SELECTION_PAGE + "/**",
"/" + CHECKOUT_URL,
"/" + VERIFY_BADGE_URL,
"/" + VERIFY_NAME_URL).hasIpAddress("x.x.x.x")
.antMatchers("/" + ADMIN_PAGE,
"/" + NEW_FOOD_PAGE,
"/" + HIDE_FOOD_URL,
"/" + SHOW_FOOD_URL,
"/" + DELETE_FOOD_URL,
"/" + EDIT_FOOD_URL,
"/" + REFRESH_EMPS_URL).hasAnyAuthority("ROLES_USER")
.antMatchers("/**").permitAll()
.and().formLogin().loginPage("/" + LOGIN_PAGE).usernameParameter("username").passwordParameter("pin").defaultSuccessUrl("/" + ADMIN_PAGE).permitAll().failureUrl("/" + LOGIN_PAGE + "?badlogin");
If I do login, it will take me back to the original request URL and it will throw a 403 denial. I don't want it to forward to the login page, I just want it to deny immediately.
I even tried a denyAll() instead of hasIpAddress() and it did the same thing.
I did a scan through the spring docs at https://docs.spring.io/spring-security/site/docs/current/reference/html5/ various google searches and I couldn't find anything that talks about this specifically.
I would like the users to still be able to go to any page in that second antMatchers and have it auto forward to the login page if they're not authenticated yet.
So I wrote a comment on this which said that this was hard to google, which is true, and that what you want is a custom AuthenticationFailureHandler, which is false.
In actual fact, what you want is a custom AuthenticationEntryPoint. The class that's responsible for the behaviour you mislike is ExceptionTranslatorFilter. From its javadoc:
If an AuthenticationException is detected, the filter will launch the authenticationEntryPoint. [...] If an AccessDeniedException is detected, the filter will determine whether or not the user is an anonymous user. If they are an anonymous user, the authenticationEntryPoint will be launched. If they are not an anonymous user, the filter will delegate to the AccessDeniedHandler.
It turns out that an AccessDeniedException is thrown both when the user does not have a required role and when the request does not have one of the permitted remote addresses. So the ExceptionTranslatorFilter calls its authenticationEntryPoint in both cases. That authenticationEntryPoint is set when you call HttpSecurity.formLogin(); specifically, it's set to a LoginUrlAuthenticationEntryPoint.
This particular bit of Spring Security default configuration is what you want to override -- you want the ExceptionTranslationFilter to use an AuthenticationEntryPoint that does different things based on what request it's handling. That's what the Spring Security-provided DelegatingAuthenticationEntryPoint is for.
The way to customize the authentication entry point is to call exceptionHandling() on your HttpSecurity. Thus, we arrive at the configuration you see in the following Spring Boot test:
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
// many non-static imports
#SpringBootTest
class SecurityTest {
#Autowired WebApplicationContext wac;
MockMvc mvc;
#RestController
static class BigController {
#GetMapping("/needs-ip")
public String needsIp() {
return "Needs IP";
}
#GetMapping("/needs-no-ip")
public String needsNoIp() {
return "Needs no IP";
}
}
#TestConfiguration
#EnableWebSecurity
static class ConfigurationForTest extends WebSecurityConfigurerAdapter {
#Bean BigController controller() { return new BigController(); }
#Override
public void configure(HttpSecurity http) throws Exception {
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = new LinkedHashMap<>();
// Http403ForbiddenEntryPoint basically just says "don't bother with authentication, return 403 instead"
map.put(new AntPathRequestMatcher("/needs-ip"), new Http403ForbiddenEntryPoint());
// normally, Spring Boot adds this authentication entry point on its own, but we've taken
// over the configuration so we do it ourselves
map.put(new AntPathRequestMatcher("/needs-no-ip"), new LoginUrlAuthenticationEntryPoint("/login"));
http.authorizeRequests()
.antMatchers("/needs-ip").hasIpAddress("192.168.12.13")
.antMatchers("/needs-no-ip").hasRole("USER")
.anyRequest().permitAll() // more readable than "antMatchers("/**")"
.and().formLogin() // with the default url, which is "/login"
// the line that follows is the interesting one
.and().exceptionHandling().authenticationEntryPoint(new DelegatingAuthenticationEntryPoint(map));
;
}
}
#BeforeEach
public void configureMockMvc() {
this.mvc = MockMvcBuilders.webAppContextSetup(wac).apply(springSecurity()).build();
}
#Test
void needsIpCorrectIp() throws Exception {
mvc.perform(get("/needs-ip").with(req -> {req.setRemoteAddr("192.168.12.13"); return req;}))
.andExpect(status().isOk())
.andExpect(content().string("Needs IP"));
}
#Test
void needsIpWrongIpAnonymousUser() throws Exception {
mvc.perform(get("/needs-ip"))
.andExpect(status().isForbidden());
}
#Test
#WithMockUser(roles = "USER")
void needsIpWrongIpLoggedInUser() throws Exception {
mvc.perform(get("/needs-ip"))
.andExpect(status().isForbidden());
}
#Test
void needsNoIpAnonymousUser() throws Exception {
mvc.perform(get("/needs-no-ip"))
.andExpect(status().isFound())
// apparently, the default hostname in Spring MockMvc tests is localhost
// you can change it, but why bother?
.andExpect(header().string("Location", "http://localhost/login"));
}
#Test
#WithMockUser(roles = "USER")
void needsNoIpAuthorizedUser() throws Exception {
mvc.perform(get("/needs-no-ip"))
.andExpect(status().isOk())
.andExpect(content().string("Needs no IP"));
}
#Test
#WithMockUser(roles = "NOTUSER")
void needsNoIpUnauthorizedUser() throws Exception {
mvc.perform(get("/needs-no-ip"))
.andExpect(status().isForbidden());
}
}
If you download a project from Spring Initializr, add spring-security-test, spring-starter-web and spring-starter-security as dependencies, and run mvn verify, the tests should pass. I've included a #RestController so it's obvious the happy path also works as expected. Of course, this configuration is less complex than yours, and I don't tend to write maintainable code for Stack Overflow answers. (There should be more constants and less strings in there). But you can probably modify the configuration so it works for you.
Incidentally, I'm not sure that your configuration is complex enough -- personally, I would want even requests from whitelisted IP addresses to fail if no authentication/authorization is present -- but I don't know your security requirements, so I can't really judge.

Spring Security - Authenticate Users With Active Directory against LDAP in Spring Boot

I am gettting LDAP authentication error when we configured for LDAP authentication. My property files as below configuration:
ldap.urls=ldap://***.***.local:8389
ldap.base.dn=dc=test,dc=com
ldap.user.dn.pattern=(&(objectClass=user)(userPrincipalName={0})(memberof=CN=Group Name,OU=***,OU=****,DC=test,DC=com))
While accessing wsdl by passing valid username & password getting below error:
While accessing wsdl it is asking username & Password. If we provided then it saying that “ActiveDirectoryLdapAuthenticationProvider - Active Directory authentication failed: Supplied password was invalid
and while starting the application i am able to see below log on console:
`org.springframework.ldap.core.support.AbstractContextSource - Property 'userDn' not set - anonymous context will be used for read-write operation`
for SOAP Calls as i have provided some more in SOAPWebServiceConfig.java even not working.
//XwsSecurityInterceptor
#Bean
public XwsSecurityInterceptor securityInterceptor(){
XwsSecurityInterceptor securityInterceptor = new XwsSecurityInterceptor();
//Callback Handler -> SimplePasswordValidationCallbackHandler
securityInterceptor.setCallbackHandler(callbackHandler());
//Security Policy -> securityPolicy.xml
securityInterceptor.setPolicyConfiguration(new ClassPathResource("securityPolicy.xml"));
return securityInterceptor;
}
#Bean
public SimplePasswordValidationCallbackHandler callbackHandler() {
SimplePasswordValidationCallbackHandler handler = new SimplePasswordValidationCallbackHandler();
handler.setUsersMap(Collections.singletonMap("user", "password"));
return handler;
}
//Interceptors.add -> XwsSecurityInterceptor
#Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(securityInterceptor());
}
I am not getting what is the issue here. Can anyone please suggest on this.
Active-Directory has LDAP compatible protocol but uses some special conventions compared to other ldap directories.
To get those configured right (e.g. append the domain to usernames) use ActiveDirectoryLdapAuthenticationProvider instead of the LdapAuthenticationProvider which will be uses when using the auto-configuration via Properties. Remove or rename "ldap.urls" and the other properties from your application.yml then.
package com.test;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
ActiveDirectoryLdapAuthenticationProvider adProvider = new ActiveDirectoryLdapAuthenticationProvider("domain.org",
"ldap://activedirectory-url:389");
adProvider.setConvertSubErrorCodesToExceptions(true);
adProvider.setUseAuthenticationRequestCredentials(true);
auth.authenticationProvider(adProvider);
}
}

Exception "At least one JPA metamodel must be present" thrown in Controller-Test

I am trying to test a controller class annotated with #RestController. I am using Spring-Boot 1.5.10.
The application itself starts up properly, but the unit test fails. Please bear in mind, that I am currently just trying to test the controller (and mock away the service - I will be testing the services later).
Here are some of my classes:
Application.java
package com.particles.authservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
#SpringBootApplication
#Import({ ApplicationConfiguration.class })
public class Application {
public static void main(final String[] args) {
SpringApplication.run(Application.class, args);
}
}
ApplicationConfiguration.java
package com.particles.authservice;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
#EntityScan(basePackageClasses = { Application.class, Jsr310JpaConverters.class })
#EnableJpaRepositories
public class ApplicationConfiguration {
}
AccountController.java
package com.particles.authservice.accountservice;
import ...
#RestController
public class AccountController {
#Autowired
private AccountService accountService;
/**
* This method attempts to login a user and issue a token if the login was successful.
* <p>
* If login fails due a login attempt with a non-existent username or an invalid password, an exception is thrown.
*
* #param credentials
* ({#link Credentials}) credentials in a JSON-form, that can be unserialized into an object of this type
* #param response
* ({#link HttpServletResponse}) response, which will be sent to the client;
* if the credentials were valid the response receives a JWT as an additional header
* #return ({#link PersistableAccount}) JSON (automatically serialized from the given TO);
* if the request was successful, an additional header containing the freshly issued JWT is added into the response
*/
#RequestMapping(value = "/login", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public PersistableAccount login(#RequestBody final Credentials credentials, final HttpServletResponse response)
throws IOException, URISyntaxException {
final Optional<PersistableAccount> account = accountService.login(credentials);
if (!account.isPresent()) {
throw new AccountLoginFailedException(credentials);
}
response.setHeader("Token", jwtService.tokenForPersistableAccount(account.get()));
return account.get();
}
}
AccountControllerTest.java
package com.particles.authservice;
import static ...
import ...
#RunWith(SpringRunner.class)
#WebAppConfiguration
#WebMvcTest(AccountController.class)
public class AccountControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private AccountService accountServiceMock;
#Test
public void test() throws Exception {
final Credentials credentials = TestHelper.createCredentials();
final Optional<PersistableAccount> account = Optional.of(TestHelper.createPersistableAccount());
given(accountServiceMock.login(credentials))
.willReturn(account);
mockMvc
.perform(MockMvcRequestBuilders.post("/login").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
}
I have reduced the AccountController to just one endpoint and omitted imports for brevity purposes.
The test compiles just fine, but whenever I run the test, I receive the following (nested) exception (shortened - let me know if you need the full stacktrace):
Caused by: java.lang.IllegalArgumentException: At least one JPA metamodel must be present!
at org.springframework.util.Assert.notEmpty(Assert.java:277)
at org.springframework.data.jpa.mapping.JpaMetamodelMappingContext.<init>(JpaMetamodelMappingContext.java:52)
at org.springframework.data.jpa.repository.config.JpaMetamodelMappingContextFactoryBean.createInstance(JpaMetamodelMappingContextFactoryBean.java:71)
at org.springframework.data.jpa.repository.config.JpaMetamodelMappingContextFactoryBean.createInstance(JpaMetamodelMappingContextFactoryBean.java:26)
at org.springframework.beans.factory.config.AbstractFactoryBean.afterPropertiesSet(AbstractFactoryBean.java:134)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624)
... 40 more
I have checked out a lot of similar questions, but the usual ways to resolve this did not seem to work in my case. In particular I tried the following:
Using spring-boot-starter-data-jpa (did not apply to me since I was using that dependency to begin with), managed version
Separating application from its (JPA-related) configuration due to problems in regards to #EnableJpaRepositories and possibly #EntityScan - to no avail (following the first reply in Getting "At least one JPA metamodel must be present" with #WebMvcTest, but while my application still starts just fine, the test still fails)
I have tried using JacksonTester - in fact I just want to test the controller functionality at the moment - to no avail either (ended up needing the context)
As far as I understand I am mocking away the actual service; so in fact I am not using any JPA Metamodels, am I?
Removing the #EnableJpaRepositories annotation solves the issue, but unfortunately it seems to break my application.
What am I doing wrong?
Add ContextConfiguration. The test did not see ApplicationConfiguration, hence did not see any entity.
#ContextConfiguration(classes = {ApplicationConfiguration.class})
public class AccountControllerTest { ... }
Update:
Another thing the code is missing is #SpringBootTest. Try annotating the test class with this one.

What is the expected behavior when setting `security.oauth2.resource.jwk.key-set-uri` in spring boot

In spring boot, upon configuring a Resource server we have the option to set the security.oauth2.resource.jwk.key-set-uri property if the access tokens will be JWTs and the issuer provides an endpoint for clients to acquire the public RSA key for verification in JWK format.
What is the expected behavior to initiate a keystore from this JWK? The property is being loaded in the ResourceServerProperties.JWK but then what. Should spring boot call this URI and fetch the jwks then create a store for me to use in verification?
I am following this tutorial to setup the configuration of the keystore http://www.baeldung.com/spring-security-oauth-jwt
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}
But instead of loading a .pem public key I think I want to load it from a jwk.
If you want to use JWKS, use JwkTokenStore in place of JwtTokenStore.
spring-security-oauth2/jwk internally implements key loading and management according to the auth0 spec
You can also see docs on auto-configuration of the same, however i feel configuring it in quite straight-forward (see below).
We don't have to do any verification as JwkTokenStore sets up the verification with JwkDefinitionSource JwkVerifyingJwtAccessTokenConverter using JWKS exposed at #Value("{jsecurity.oauth2.resource.jwk.key-set-uri}")
However, the spring-security-oauth2/jwk classes from spring don't have any public constructors, we often need and can perform any custom steps in AccessTokenConversion, like a common need is to extract jwt content to auth context, we can always inject a custom converter to JwkTokenStore
import org.springframework.security.oauth2.provider.token.store.jwk.*;
import org.springframework.security.oauth2.provider.token.store.*
import org.springframework.security.oauth2.provider.token.*;
import java.utl.*;
#Configuration
class JwtConfiguration {
#Bean
public DefaultTokenServices tokenServices(final TokenStore tokenStore) {
final DefaultTokenServices dts = new DefaultTokenServices();
dts.setTokenStore(tokenStore);
dts.setSupportRefreshToken(true);
return dts;
}
#Bean
public TokenStore tokenStore(
#Value("{jsecurity.oauth2.resource.jwk.key-set-uri}") final String jwksUrl,
final JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwkTokenStore(jwksUrl, jwtAccessTokenConverter, null);
}
#Bean
public JwtAccessTokenConverter createJwtAccessTokenConverter() {
final JwtAccessTokenConverter converter;
converter.setAccessTokenConverter(new DefaultAccessTokenConverter() {
#Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
final OAuth2Authentication auth = super.extractAuthentication(map);
auth.setDetails(map); //this will get spring to copy JWT content into
return auth;
}
}
return conveter;
}
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
#Configuration
#EnableResourceServer
class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private String resourceId;
private TokenStore tokenStore;
public ResourceServerConfig(
#Value("\${jwt.reourceId}") private String resourceId,
private TokenStore tokenStore) {
this.resourceId = resourceId;
this.tokenStore = tokenStore;
}
/**
* Ensures request to all endpoints ore a
#Override
public void configure(final HttpSecurity http) {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/**").authenticated();
}
/**
* Configure resources
* Spring OAuth expects "aud" claim in JWT token. That claim's value should match to the resourceId value
* (if not specified it defaults to "oauth2-resource").
*/
#Override
public void configure(final ResourceServerSecurityConfigurer resources) {
resources.resourceId(resourceId).tokenStore(tokenStore);
}
}
The main goal of this implementation would be to verify a JWT locally using the corresponding JWK(JSON WEB TOKEN KEY SET). The JWK used for verification is matched using the kid header parameter of the JWT and the kid attribute of the JWK.
The server can validate this token locally without making any network requests, talking to a database, etc. This can potentially make session management faster because instead of needing to load the user from a database (or cache) on every request, you just need to run a small bit of local code. This is probably the single biggest reason people like using JWTs: they are stateless.

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