display spring security authentication object when SessionCreationPolicy.STATELESS - spring

#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
MyUserDetailsService myUserDetailsService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/index").permitAll();
http.authorizeRequests().antMatchers("/main").permitAll();
http.formLogin().loginPage("/login").permitAll().successHandler(successHandler());
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // Session is STATELESS
}
I set spring security sessionpolicy to STATLESS
because I'm using JWT so that STATLESS would be better
but STATELESS cause one problem
it's impossible to dispaly authentication object in thymeleaf
<h1>[[${#authentication }]]</h1>
if I changed session policy I could display authentication object
but but that's not what i want
in short
can i use authentication object with thymeleaf when spring's session policy is STATELESS

Form based log in requires a session, so marking as stateless would mean that the user is not available. Likely you can see the page because it is marked as permitAll which means no user is necessary to see it.
To fix this, you can switch to a form of authentication that is stateless too (i.e. it is included in every request). For example:
// #formatter:off
http
.authorizeRequests()
.mvcMatchers("/index", "/main").permitAll()
.and()
.httpBasic()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// #formatter:on
I'm also not sure about the syntax the themleaf template is using. For me, I use something like this:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Test</title>
</head>
<body>
<h1 th:text="${authentication?.name}"></h1>
</body>
</html>
Then I use the following to expose the Authentication as a model attribute:
#Controller
public class IndexController {
#GetMapping("/")
String index() {
return "index";
}
#ModelAttribute
Authentication authentication(Authentication authentication) {
return authentication;
}
}
I have a test that validates it works
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoApplicationTests {
#Autowired
TestRestTemplate rest;
#Test
void indexWhenAnonymous() throws Exception{
ResponseEntity<String> result = rest.exchange(RequestEntity.get(URI.create("/")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).doesNotContain("user");
}
#Test
void indexWhenAuthenticated() throws Exception{
ResponseEntity<String> result = rest.exchange(RequestEntity.get(URI.create("/")).headers(h -> h.setBasicAuth("user", "password")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).contains("user");
}
}
You can find the complete sample at https://github.com/rwinch/spring-security-sample/tree/display-auth-stateless-thymeleaf which allows log in with the username user and password password.

Related

Spring Boot webservice (REST) - How to change JUnit 5 tests from basic authentication to OAuth2 (Keycloak)

I have a Spring Boot webservice with REST controllers and with basic authentication (username and password).
On this base I developed JUnit 5 test.
Now I switch to OAuth2, currently trying the Resource Owner Password Credentials grant type.
What do I need to change on my JUnit 5 tests to run now with OAuth2?
Of course, before running my new tests with OAuth2 I have to start first Keycloak, afterwards the tests.
Following is my setup for the current basic authentication and the new OAuth2.
BASIC AUTHENTICATION (old implementation)
On my webservice side the web security config class looks like following:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/articles/**").hasRole("ADMIN")
// More antMatchers...
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.formLogin().disable();
}
#Bean
#Override
public UserDetailsService userDetailsService() {
UserDetails admin = User
.withUsername("admin")
.password("{noop}" + "admin123")
.roles("ADMIN")
.build();
// More users...
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
userDetailsManager.createUser(admin);
...
return userDetailsManager;
}
}
For the JUnit 5 tests I allways use the user admin, for example
#SpringBootTest
#AutoConfigureMockMvc
#WithUserDetails(value = "admin")
#TestInstance(Lifecycle.PER_CLASS)
public class MyRestControllerMockMvcTest {
#Autowired
private MockMvc mockMvc;
#BeforeAll
public void init(ApplicationContext appContext) throws Exception {
TestUtils.setupSecurityContext(appContext);
// some initialization
}
#AfterAll
public void cleanup(ApplicationContext appContext) throws Exception {
TestUtils.setupSecurityContext(appContext);
// some cleanup
}
#Test
public void getSomeInformationFromMyRestController() throws Exception {
MvcResult mvcResult = TestUtils.performGet(mockMvc, "...REST controller endpoint...", status().isOk());
MockHttpServletResponse response = mvcResult.getResponse();
ObjectMapper objectMapper = new ObjectMapper();
... = objectMapper.readValue(response.getContentAsString(), ...);
assertNotNull(...);
}
}
public class TestUtils {
public static void setupSecurityContext(ApplicationContext appContext) {
UserDetailsService uds = (UserDetailsService) appContext.getBean("userDetailsService");
UserDetails userDetails = uds.loadUserByUsername ("admin");
Authentication authToken = new UsernamePasswordAuthenticationToken (userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
}
public static MvcResult performGet(MockMvc mockMvc, String endpoint, ResultMatcher status) throws Exception {
MvcResult mvcResult = mockMvc.perform(get(endpoint))
.andDo(print())
.andExpect(status)
.andReturn();
return mvcResult;
}
}
Looking right now on the test setup in #BeforeAll and #AfterAll I'm not sure all of a sudden if I have to do
TestUtils.setupSecurityContext(appContext);
because now I use
#WithUserDetails(value = "admin")
#TestInstance(Lifecycle.PER_CLASS)
on the class. Just curious if the tests would still run without TestUtils.setupSecurityContext(appContext);, will try.
OAUTH2 (new implementation, replacing basic authentication above)
application.properties
...
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8183/auth/realms/myrealm/protocol/openid-connect/certs
With OAuth2 I changed the web security config class in my webservice (resource server) as following:
#EnableWebSecurity
public class WebSecurityConfig {
#Bean
SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/articles/**").hasRole("ADMIN")
// More antMatchers...
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter())
;
return httpSecurity.build();
}
private JwtAuthenticationConverter jwtAuthenticationConverter() {
final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new MyRoleConverter());
return jwtAuthenticationConverter;
}
public class MyRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
#Override
public Collection<GrantedAuthority> convert(final Jwt jwt) {
jwt.getClaims().get("realm_access");
// Create roles
return ...;
}
}
}
My users are now defined in Keycloak.
Keycloak is configured to use Resource Owner Password Credentials.
#jzheaux is right (sure, he's spring-security team member...).
Changes will occure in your security configuration but the test won't change ... for the most part: You'll probably want to have an Authentication of the right type in your test security-context.
If your new security configuration populates security-context with JwtAuthenticationToken, it would be nice to have JwtAuthenticationToken in test security-context too. #WithUserDetails(value = "admin") won't build JwtAuthenticationToken.
You should have a look at this lib I wrote and specifically at #WithMockJwtAuth. Usage is demonstrated there:
#Test
#WithMockJwtAuth(authorities = "ROLE_AUTHORIZED_PERSONNEL", claims = #OpenIdClaims(sub = "Ch4mpy"))
public void greetJwtCh4mpy() throws Exception {
api.get("/greet").andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
}
P.S.
You'll find in this same git repo samples for other kind of Authentication better adapted to OIDC than JwtAuthenticationToken like KeycloakAuthenticationToken (written by Keycloak team for Keycloak exclusively) or OidcAuthentication (written by myself for any OpenID Connect complient authorization server), along with #WithMockKeycloakAuth and #WithMockOidcAuth

How to redirect, with Spring Security, a logged user to his front page and a non logged one to a different one

I'm doing a Spring web-application project as a final project for school. We use SpringBoot with Hibernate and an H2 database. In order to authenticate and authorize, we use Spring Security. We also use the model View-Controller with repositories, services, and controllers.
I have an initial front page (with the URL "/") that is shown when you open the project. Once you log-in, you're redirected to another front page with the URL ("/portadaUsuario")(I'm from Spain so there's a lot of names in Spanish). But, if you somehow end up in the ("/") URL after you've logged in, you're shown the same front page that is shown to non-logged users with the sign-up and log-in options, which is obviously wrong.
I know that I can show different HTML elements with spring security tags and elements, but I've already built my project around having two different front-pages and I would like to keep it that way if possible. If it isn't achievable, please let me know how should I proceed to show different elements in the same HTML file.
Here are the methods of my WellcomeController
#Controller
public class PortadaController {
#GetMapping({"/"})
public String mostrarPortada() {
return "portada";
}
#GetMapping("/portadaUser")
public String mostrarPortadaUsuario() {
return "/usuario/portada";
}
}
Here are the methods that authenticate the users
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/css/**","/js/**","/webjars/**", "/h2-console/**", "/", "/newUser/**").permitAll()
.antMatchers("/admin/**").hasAnyRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(customSuccessHandler)
.defaultSuccessUrl("/portadaUser")
.and()
.logout()
.logoutUrl("/logout")
.permitAll()
.logoutSuccessUrl("/")
.and()
.exceptionHandling()
.accessDeniedPage("/acceso-denegado");
http.csrf().disable();
http.headers().frameOptions().disable();
}
What I want is the application detecting when a logged user is requesting the "/" URL, and for it to be redirected to /portadaUser instead.
Assuming, you have two pages, one for signed in (signed-in-home) and another for not signed in(not-signed-in-home), you code something like this -
#GetMapping("/")
public String index(Principal principal, Model model) {
return principal != null ? "signed-in-home" : "not-signed-in-home";
}
Read more about principal here- https://www.baeldung.com/get-user-in-spring-security
Tested in jsp + Spring 4.3.13 + Spring Security 4.2.11
1.checked in jsp
"<%# taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>"
<script>
<sec:authorize access="hasRole('ADMIN')">
location.replace('/yourpage')
</sec:authorize>
</script>
2. checked in controller
SpringSecurityUtil.class
public class SpringSecurityUtil {
public static boolean hasRole(String role) {
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) SecurityContextHolder.getContext().getAuthentication().getAuthorities();
boolean hasRole = false;
for (GrantedAuthority authority : authorities) {
if(authority.getAuthority().equals(role)) {
hasRole = true;
break;
}
}
return hasRole;
}
}
HomeController.class
#Controller
public class HomeController {
#RequestMapping(value = "/", method = RequestMethod.GET)
public String home(HttpServletResponse resp) {
if(SpringSecurityUtil.hasRole("ADMIN"))
//if has role send your page
else
//if has not role send your page
}
}
3. add antMatchers in your Spring Security Config class.
antMatchers("/").hasAnyRole("ADMIN")
4. use #PreAuthority
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled=true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
}
HomeController.class
#Controller
public class HomeController {
#PreAuthorize("hasAuthority('ADMIN')")
#RequestMapping(value = "/", method = RequestMethod.GET)
public String home() {
//If this code starts, you have ADMIN privileges.
}
}
see more Spring Security Method Security

How to validate user roles from request header with spring security configuration

I have added spring security with taglibs for role based content loading in my application . The authentication process will be taken care by external service and after successful authentication , the external service will add user detail and role detail in the request header. In my application I have to get the roles from request header and need to validate the roles with spring security configure method.
Help me on how to validate the roles .
Spring security Class:
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/console")
.access("hasRole('MASTER ADMIN') or hasRole('ADMIN') or hasRole('DEV') or hasRole('QA')")
.and()
.formLogin()
.defaultSuccessUrl("/console", true)
.and().logout().logoutSuccessUrl("/login");
}
}
Controller Class:
#RequestMapping(value = "/login")
public ModelAndView validateLogin(final HttpServletRequest request, final HttpServletResponse response) {
final ModelAndView modelView = new ModelAndView();
final String loginUserId = request.getParameter("USER");
final String companyCode = request.getHeader("ROLE");
final String firstName = request.getHeader("FIRSTNAME");
final String lastName = request.getHeader("LASTNAME");
//** some code to validate the role details with spring security configure method ie (has access method) and return the default success url based on the role.

Custom login page in Spring Security 5 using oauth2 returns null

I am developing custom login page in Spring Security 5 using oauth2. So I have customized settings:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login().loginPage("/login/oauth2").permitAll();
}
and creating controller with #RequestMapping("/login/oauth2"):
#GetMapping("/")
public String index() {
return "index";
}
#GetMapping("/login/oauth2")
public String login(Model model, OAuth2AuthenticationToken authentication) {
return "login";
}
The login.html page is a regular form which redirect to login method from controller:
<h1>Logowanie</h1>
<a>ZALOGUJ</a>
<a class="btn" href="/login/oauth2/code/google">Login</a>
With this configuration OAuth2AuthenticationToken authentication is null and therefore authentication can't be applied. With default Spring Security 5 configuration everything works fine. The example on which I based is described here: https://docs.spring.io/spring-security/site/docs/5.0.0.RELEASE/reference/htmlsingle/#oauth2login-advanced-login-page; section 31.1 OAuth 2.0 Login Page.
In my app to work I had to create my custom WebMvc configuration:
#Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
Then in WebSecurityConfig:
#Override
protected void configure(final HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login")
.permitAll()
.and().csrf().disable()
.logout().permitAll();
}
I think in this case You don't need custom controller.
I wrote a blog post about silent token refresh in implicit flow, but there You will find full working app with custom login page:
https://regulargeek.blogspot.com/2018/05/angular-5-springboot-2-oauth2-and.html

denyAll() redirect to login url but I need another url

This code blocked properly every request which is finished on XHTML but I would like redirect the request to url like "/spring/denied" not "/spring/login" which is setted on method formLogic()
http
.formLogin()
.loginPage("/spring/login")
.loginProcessingUrl("/spring/loginProcess")
.defaultSuccessUrl("/spring/main")
.failureUrl("/spring/login?login_error=1")
.and()
.logout()
.logoutUrl("/spring/logout")
.logoutSuccessUrl("/spring/logoutSuccess")
.and()
.authorizeRequests().antMatchers("/spring/**/*.xhtml").denyAll()
.and()
// Disable CSRF (won't work with JSF) but ensure last HTTP POST request is saved
// See https://jira.springsource.org/browse/SEC-2498
.csrf().disable()
.requestCache()
.requestCache(new HttpSessionRequestCache());
So, I think there will be theses possible scenes:
Someone intent to access to any real XHTML file directly (/main/index.xhtml): Behavior: Request blocked and redirected to denied url, if someone wish it , he must interact using right flow definition (p.e. /main, /groups....)
Someone intent to access to secured url without right permissions or anonymous (p.e. /admin, /authenticate...): Behavior: Spring security intercept request and redirect to login url
Some intent to access to secured url with right permissions (p.e /admin, /authenticate....): Behavior: Spring security grants access and spring web flow make its task redirecting properly
Someone intent to access unknown url (p.e. /ImAUnluckyGuyAndThisUrlIsUnreal): Behavior: Spring webflow intercept request and redirect to last flow known.
Using XML configuration above cases are right. Furthermore I used spring webflow 2.3 instead 2.4.0RC1 and Annotations configurations
Case 1: Adding this code on web.xml, I don't know how replace fot annotations configurations
<security-constraint>
<display-name>Restrict direct access to XHTML access</display-name>
<web-resource-collection>
<web-resource-name>XHTML</web-resource-name>
<url-pattern>*.xhtml</url-pattern>
</web-resource-collection>
<auth-constraint />
</security-constraint>
Case 4: Adding this code on a abstract flow definition, I don't know if doesn't work on Spring webflow 2.4.0RC1 or it's a annotations configuration problem.
<global-transitions>
<transition on-exception="org.springframework.webflow.engine.NoMatchingTransitionException" to="handlingViewState">
<evaluate expression="handlingBean.handle(flowExecutionException)"> </evaluate>
</transition>
</global-transitions>
Case 2 and 3: These are not problematic. if user is authenticated and doesn't got permissions is redirected using .exceptionHandling().accessDeniedPage("/spring/denied") or user is anonymous is redirected to loginPage()
Webflow configuration
#Bean
public FlowExecutor flowExecutor() {
return getFlowExecutorBuilder(flowRegistry())
.addFlowExecutionListener(new FlowFacesContextLifecycleListener())
.addFlowExecutionListener(new SecurityFlowExecutionListener())
.build();
}
#Bean
public FlowDefinitionRegistry flowRegistry() {
return getFlowDefinitionRegistryBuilder(flowBuilderServices())
.setBasePath("/WEB-INF/flows")
.addFlowLocationPattern("/**/*-flow.xml")
.build();
}
#Bean
public FlowBuilderServices flowBuilderServices() {
return getFlowBuilderServicesBuilder().setDevelopmentMode(true).build();
}
}
MVC configuration
#Autowired
private WebFlowConfig webFlowConfig;
#Bean
public FlowHandlerMapping flowHandlerMapping() {
FlowHandlerMapping mapping = new FlowHandlerMapping();
mapping.setOrder(1);
mapping.setFlowRegistry(this.webFlowConfig.flowRegistry());
/* If no flow matches, map the path to a view, e.g. "/intro" maps to a view named "intro" */
mapping.setDefaultHandler(new UrlFilenameViewController());
return mapping;
}
#Bean
public FlowHandlerAdapter flowHandlerAdapter() {
JsfFlowHandlerAdapter adapter = new JsfFlowHandlerAdapter();
adapter.setFlowExecutor(this.webFlowConfig.flowExecutor());
return adapter;
}
#Bean
public UrlBasedViewResolver faceletsViewResolver() {
UrlBasedViewResolver resolver = new UrlBasedViewResolver();
resolver.setViewClass(JsfView.class);
resolver.setPrefix("/WEB-INF/");
resolver.setSuffix(".xhtml");
return resolver;
}
#Bean
public SimpleControllerHandlerAdapter simpleControllerHandlerAdapter() {
return new SimpleControllerHandlerAdapter();
}
What you need is multiple <http> [HttpSecurity] configuration and you need to provide a custom AuthenticationEntryPoint implementation.
Below is the HttpSecurity configuration for case 1. (I hope you can come-up with a configuration for case 2 & 3 and you can use the one you already have.)
#Configuration
#Order(1)
public static class XHTMLAccessDenyWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/spring/**/*.xhtml")
.exceptionHandling().authenticationEntryPoint(new AccessDenyEntryPoint()).and()
.authorizeRequests().antMatchers("/spring/**/*.xhtml").denyAll();
}
}
Note: The order of the above security configuration should be higher than the security configuration for case 2 & 3; therefore, #Order is used.
Custom AuthenticationEntryPoint implementation would simply redirect the request to /spring/deny page as below
public class AccessDenyEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
request.getRequestDispatcher("/spring/denied").forward(request, response);
}
}

Resources