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);
}
}
Related
I'm setting up a Resource Server with Spring Boot and to secure the endpoints I'm using OAuth2 provided by Spring Security. So I'm using the Spring Boot 2.1.8.RELEASE which for instance uses Spring Security 5.1.6.RELEASE.
As Authorization Server I'm using Keycloak. All processes between authentication, issuing access tokens and validation of the tokens in the Resource Server are working correctly. Here is an example of an issued and decoded token (with some parts are cut):
{
"jti": "5df54cac-8b06-4d36-b642-186bbd647fbf",
"exp": 1570048999,
"aud": [
"myservice",
"account"
],
"azp": "myservice",
"realm_access": {
"roles": [
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"myservice": {
"roles": [
"ROLE_user",
"ROLE_admin"
]
},
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "openid email offline_access microprofile-jwt profile address phone",
}
How can I configure Spring Security to use the information in the access token to provide conditional authorization for different endpoints?
Ultimately I want to write a controller like this:
#RestController
public class Controller {
#Secured("ROLE_user")
#GetMapping("userinfo")
public String userinfo() {
return "not too sensitive action";
}
#Secured("ROLE_admin")
#GetMapping("administration")
public String administration() {
return "TOOOO sensitive action";
}
}
After messing around a bit more, I was able to find a solution implementing a custom jwtAuthenticationConverter, which is able to append resource-specific roles to the authorities collection.
http.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new JwtAuthenticationConverter()
{
#Override
protected Collection<GrantedAuthority> extractAuthorities(final Jwt jwt)
{
Collection<GrantedAuthority> authorities = super.extractAuthorities(jwt);
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource = null;
Collection<String> resourceRoles = null;
if (resourceAccess != null &&
(resource = (Map<String, Object>) resourceAccess.get("my-resource-id")) !=
null && (resourceRoles = (Collection<String>) resource.get("roles")) != null)
authorities.addAll(resourceRoles.stream()
.map(x -> new SimpleGrantedAuthority("ROLE_" + x))
.collect(Collectors.toSet()));
return authorities;
}
});
Where my-resource-id is both the resource identifier as it appears in the resource_access claim and the value associated to the API in the ResourceServerSecurityConfigurer.
Notice that extractAuthorities is actually deprecated, so a more future-proof solution should be implementing a full-fledged converter
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>
{
private static Collection<? extends GrantedAuthority> extractResourceRoles(final Jwt jwt, final String resourceId)
{
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
Map<String, Object> resource;
Collection<String> resourceRoles;
if (resourceAccess != null && (resource = (Map<String, Object>) resourceAccess.get(resourceId)) != null &&
(resourceRoles = (Collection<String>) resource.get("roles")) != null)
return resourceRoles.stream()
.map(x -> new SimpleGrantedAuthority("ROLE_" + x))
.collect(Collectors.toSet());
return Collections.emptySet();
}
private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private final String resourceId;
public CustomJwtAuthenticationConverter(String resourceId)
{
this.resourceId = resourceId;
}
#Override
public AbstractAuthenticationToken convert(final Jwt source)
{
Collection<GrantedAuthority> authorities = Stream.concat(defaultGrantedAuthoritiesConverter.convert(source)
.stream(),
extractResourceRoles(source, resourceId).stream())
.collect(Collectors.toSet());
return new JwtAuthenticationToken(source, authorities);
}
}
I have tested both solutions using Spring Boot 2.1.9.RELEASE, Spring Security 5.2.0.RELEASE and an official Keycloak 7.0.0 Docker image.
Generally speaking, I suppose that whatever the actual Authorization Server (i.e. IdentityServer4, Keycloak...) this seems to be the proper place to convert claims into Spring Security grants.
Here is another solution
private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
}
The difficulty you are experiencing is partly due to your roles being positioned in the JWT under resource_server->client_id. This then requires a custom token converter to extract them.
You can configure keycloak to use a client mapper that will present the roles under a top-level claim name such as "roles". This makes the Spring Security configuration simpler as you only need JwtGrantedAuthoritiesConverter with the authoritiesClaimName set as shown in the approach taken by #hillel_guy.
The keycloak client mapper would be configured like this:
As already mentioned by #hillel_guy's answer, using an AbstractHttpConfigurer should be the way to go. This worked seamlessly for me with spring-boot 2.3.4 and spring-security 5.3.4.
See the spring-security API documentation for reference: OAuth2ResourceServerConfigurer
UPDATE
Full example, as asked in the comments:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final String JWT_ROLE_NAME = "roles";
private static final String ROLE_PREFIX = "ROLES_";
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and().csrf().disable()
.cors()
.and().oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
// create a custom JWT converter to map the roles from the token as granted authorities
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(JWT_ROLE_NAME); // default is: scope, scp
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(ROLE_PREFIX ); // default is: SCOPE_
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
}
In my case, I wanted to map roles from the JWT instead of scope.
Hope this helps.
2022 update
I maintain a set of tutorials and samples to configure resource-servers security for:
both servlet and reactive applications
decoding JWTs and introspecting access-tokens
default or custom Authentication implementations
any OIDC authorization-server(s), including Keycloak of course (most samples support multiple realms / identity-providers)
The repo also contains a set of libs published on maven-central to:
mock OAuth2 identities during unit and integration tests (with authorities and any OpenID claim, including private ones)
configure resource-servers from properties file (including source claims for roles, roles prefix and case processing, CORS configuration, session-management, public routes and more)
Sample for a servlet with JWT decoder
#EnableMethodSecurity(prePostEnabled = true)
#Configuration
public class SecurityConfig {}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
com.c4-soft.springaddons.security.cors[0].path=/sample/**
<dependency>
<groupId>com.c4-soft.springaddons</groupId>
<artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
<version>6.0.3</version>
</dependency>
No, nothing more requried.
Unit-tests with mocked authentication
Secured #Component without http request (#Service, #Repository, etc.)
#Import({ SecurityConfig.class, SecretRepo.class })
#AutoConfigureAddonsSecurity
class SecretRepoTest {
// auto-wire tested component
#Autowired
SecretRepo secretRepo;
#Test
void whenNotAuthenticatedThenThrows() {
// call tested components methods directly (do not use MockMvc nor WebTestClient)
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
#Test
#WithMockJwtAuth(claims = #OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenAuthenticatedAsSomeoneElseThenThrows() {
assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
}
#Test
#WithMockJwtAuth(claims = #OpenIdClaims(preferredUsername = "ch4mpy"))
void whenAuthenticatedWithSameUsernameThenReturns() {
assertEquals("Don't ever tell it", secretRepo.findSecretByUsername("ch4mpy"));
}
}
Secured #Controller (sample for #WebMvcTest but works for #WebfluxTest too)
#WebMvcTest(GreetingController.class) // Use WebFluxTest or WebMvcTest
#AutoConfigureAddonsWebSecurity // If your web-security depends on it, setup spring-addons security
#Import({ SecurityConfig.class }) // Import your web-security configuration
class GreetingControllerAnnotatedTest {
// Mock controller injected dependencies
#MockBean
private MessageService messageService;
#Autowired
MockMvcSupport api;
#BeforeEach
public void setUp() {
when(messageService.greet(any())).thenAnswer(invocation -> {
final JwtAuthenticationToken auth = invocation.getArgument(0, JwtAuthenticationToken.class);
return String.format("Hello %s! You are granted with %s.", auth.getName(), auth.getAuthorities());
});
when(messageService.getSecret()).thenReturn("Secret message");
}
#Test
void greetWitoutAuthentication() throws Exception {
api.get("/greet").andExpect(status().isUnauthorized());
}
#Test
#WithMockAuthentication(authType = JwtAuthenticationToken.class, principalType = Jwt.class, authorities = "ROLE_AUTHORIZED_PERSONNEL")
void greetWithDefaultMockAuthentication() throws Exception {
api.get("/greet").andExpect(content().string("Hello user! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
}
}
Advanced use-cases
The most advanced tutorial demoes how to define a custom Authentication implementation to parse (and expose to java code) any private claim into things that are security related but not roles (in the sample it's grant delegation between users).
It also shows how to extend spring-security SpEL to build a DSL like:
#GetMapping("greet/on-behalf-of/{username}")
#PreAuthorize("is(#username) or isNice() or onBehalfOf(#username).can('greet')")
public String getGreetingFor(#PathVariable("username") String username) {
return ...;
}
If you are using Azure AD Oath there is a much easier way now:
http
.cors()
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(new AADJwtBearerTokenAuthenticationConverter("roles", "ROLE_"));
The ADDJwtBearerTokenAuthenticationConverter allows you to add your claim name as the first argument and what you want your role prefixed with as the second argument.
My import so you can find the library:
import com.azure.spring.aad.webapi.AADJwtBearerTokenAuthenticationConverter;
I am using Spring Cloud Contracts in projects to test microservices, everything is ok. But when I added Spring Security in the producer side, the GET return 401 status code instead of 200.
#Autowired
WebApplicationContext context;
#Before
public void setup() {
RestAssuredMockMvc.webAppContextSetup(this.context);
}
My question is:
I have to avoid Security settings in the contract tests?
If I want to consider the security configuration, how to make it work.
I successfully used a custom annotation on the base class, as documented here test-method-withsecuritycontext
#Retention(RetentionPolicy.RUNTIME)
#Inherited
#WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public #interface WithMockCustomUserDetails {
String username() default "email#example.com";
String role() default "DEFAULT_ROLE";
String password() default "123456";
}
and then
#WithMockCustomUserDetails
class AccountBase {
...
}
Two options AFAIK.
A) Use authorization header
request {
method 'POST'
urlPath '/check'
headers {
contentType(applicationJsonUtf8())
header(authorization(), "Bearer eyJhb.... ")
}
}
B)
Add #WithMockUser in my base test
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.context.WebApplicationContext;
#SpringBootTest
#WithMockUser //this will ensure a mock user will be injected to all requests
public abstract class BaseTestCloudContract {
#Autowired
private WebApplicationContext context;
#BeforeEach
public void setup() {
RestAssuredMockMvc.webAppContextSetup(context);
}
}
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.
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();
}
I need to test some protected urls, therefore I need to set up a mock security context in my tests (junit).
In particular I need perform some gets and post against my web application, using an authenticated user.
Below there is my code, I am able to create a such security context but I need to inject it in the 'MockMvc' object.
I set the authentication object in the security context and it works, the output result of 'SecurityContextHolder.getContext().getAuthentication().getPrincipal()' is chanelle.evans#616747.com but when I call the GET on /profile I have an assertion error because I am redirected to my login page, and not to /profile.
#WebAppConfiguration
#ContextConfiguration(locations = {"classpath:spring/security.xml", "classpath:spring/view.xml"})
#ActiveProfiles("default")
#RunWith(SpringJUnit4ClassRunner.class)
public class AuthenticationTest {
#Autowired
WebApplicationContext ctx;
private MockMvc mockMvc;
#Autowired
private FilterChainProxy springSecurityFilterChain;
#BeforeClass
public static void setUpBeforeClass() throws Exception {
}
#AfterClass
public static void tearDownAfterClass() throws Exception {
}
#Before
public void setUp() throws Exception {
mockMvc = MockMvcBuilders.webAppContextSetup(ctx).addFilters(springSecurityFilterChain).build();
//#formatter:off
UserDetailsLogic userDetailsLogic = null;
userDetailsLogic = ctx.getBean(UserDetailsLogic.class);
final UserDetailsImp userDetailsImp = new UserDetailsImp();
userDetailsImp.setAccountId(1001);
userDetailsImp.setUserId(8001);
userDetailsImp.setPassword("a378c92df7531df6fdf351f7ae1713f91f2dd2d45b9c6e1a8b02736ee3afec6595ff60465e9cb8da");
userDetailsImp.setUsername("chanelle.evans#616747.com");
userDetailsImp.setEmail("chanelle.evans#616747.com");
final Collection<GrantedAuthorityImplementation> authorities= new ArrayList<GrantedAuthorityImplementation>();
authorities.add(new GrantedAuthorityImplementation("ROLE_USER"));
userDetailsImp.setAuthorities(authorities);
userDetailsImp.setAccountNonExpired(true);
userDetailsImp.setAccountNonLocked(true);
userDetailsImp.setCredentialsNonExpired(true);
userDetailsImp.setEnabled(true);
final Authentication authToken = new UsernamePasswordAuthenticationToken (userDetailsImp.getUsername(), userDetailsImp.getPassword(), userDetailsImp.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
System.out.println("principal:"+SecurityContextHolder.getContext().getAuthentication().getPrincipal());
mockMvc.perform(get("/profile").principal(authToken)
.contentType(MediaType.TEXT_HTML)
.accept(MediaType.TEXT_HTML))
.andDo(print())
.andExpect(status().isOk())
.andExpect(redirectedUrl(null))
.andExpect(forwardedUrl(null));
//#formatter:on
}
I guess that I should put my authentication object inside the MockMvc object, but I do not know how
Does anyone have any idea?
This is something I wrote few days ago. I think that could be helpful (I tested the same thing against the login form, using the session for the second request) see loginUser1Ok(..))
See MvcTest.java in
m4nuv/easy-bank.
Add in pom.xml
<repository>
<id>spring-snaspho</id>
<url>http://repo.springsource.org/libs-milestone/</url>
</repository>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.0.M1</version>
</dependency>
and use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors for authorization request. See the sample usage here, and Spring.io jira ticket SEC-2592.