I am developing a RestAPI with SpringBoot and Spring Security. The login end point is POST /session. Can I defer authentication to controller layer like below:
#Configuration(proxyBeanMethods = false)
#EnableWebSecurity(debug = true)
public class AppSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS).and()
.formLogin().disable()
.httpBasic().disable()
.requestCache().disable()
.logout().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST,"/session").permitAll()
.anyRequest().authenticated();
}
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
And in Controller:
#RestController
#RequestMapping("/session")
public class SessionController {
private final AuthenticationManager authenticationManager;
#Autowired
public SessionController(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#PostMapping
public ResponseEntity create(#RequestBody #Validated LoginDTO loginDTO){
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
loginDTO.getUserName().trim(), loginDTO.getPassword());
final Authentication authentication = authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
return ResponseEntity.ok().build();
}
}
Is it bad practice to do authentication like above?
There's nothing inherently wrong with using Spring MVC to handle login. The reason that Spring Security uses a filter is so that it can work in projects that aren't using Spring MVC.
That said, Spring Security's AbstractAuthenticationProcessingFilter does a few other things, too. For example, it protects against Session Fixation by changing the session id, and it changes out the CSRF token assigned to that session. If you have your controller do authentication, you'll likely want it to do those other things, too.
If you don't have a technical need to use Spring MVC, I'd still recommend relying on Spring Security to perform the login so as to not lose those other protections.
Side Note
Regardless of what you do, note that when you override the HttpSecurity method, you don't need to disable httpBasic nor formLogin as those are not turned on by default.
Related
I need to secure my REST api with token(JWT) based approach.
My application has two authentication systems
1. Azure AD (users from Azure)
2. Custom JWT Authentication (users from db)
I need to allow(authorize) users from both the systems(Azure & DB) to access api. So, I have to check a token against both the auth systems and validate if anyone of them returns true.
Authorization Implementation :
1. I used AADAuthenticationFilter(extends OncePerRequestFilter) in spring security chain to validate Azure token
(source taken from : https://github.com/Microsoft/azure-spring-boot using azure-active-directory-spring-boot-starter (0.2.0))
I implemented Custom JWTAuthenticationFilter(extends OncePerRequestFilter) in Spring security chain to validate Custom JWT Token.
I used both of them in my SecurityConfiguration class .
But Security fails if any one of the filter(AADAuthenticationFilter or JwtTokenAuthenticationFilter) from Spring security chain fails to validate.
My requirement is to allow the request if either of the token is true.
Please suggest me with the best approach!.
I tried to do below approach but not successful :
1. I tried to handle my Custom JwtTokenAuthenticationFilter on exception, Do I have any approach to pass the request to second filter?
2. If token is Success in first token filter validation then how do i skip the second filter (or remove the second filter from the stack)?
How to bypass the request to meet my requirement ?
#Configuration
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfiguration {
#Configuration
#Order(2)
#EnableOAuth2Sso
public static class ApiWebAzureSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Autowired
private AADAuthenticationFilter aadAuthFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().requestMatchers("/api/**").authenticated();
// Added AADAuthenticationFilter for Azure
http.addFilterBefore(aadAuthFilter, UsernamePasswordAuthenticationFilter.class);
}
}
#Configuration
#Order(1)
public static class ApiWebJWTSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
#Autowired
JwtTokenAuthenticationFilter jwtTokenAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/**").hasRole("USER")
.anyRequest().authenticated();
// Add our custom JWT security filter
http.exceptionHandling()
.authenticationEntryPoint(new JwtAuthenticationEntryPoint()).and()
.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
}
----------------------------
Please find my filter implementation
#Component
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
#Autowired
private JwtTokenProvider jwtTokenProvider;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Extract token from header
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
//validate the token
if (token != null && jwtTokenProvider.validateToken(token,request, response)){
Authentication auth = jwtTokenProvider.getAuthentication(token);
if (auth != null) {
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}
}
I have to allow the request if the token is validated against atleast
one of the authentication systems .
I was wondering if there is a way for authorizing incrementally with Spring Security (as mentioned here)
By default spring security provides basic profile access when using Google sign OAuth verification. That flow is correct. I would however want to request for additional scopes (Gmail Read, Calendar read etc) on certain URL endpoints.
I have already tried using the #PreAuthorize property on the endpoint along with enabling #EnableGlobalMethodSecurity(prePostEnabled = true) as in the code.
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
}
Security Configuration Class:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/error", "/privacy", "/css/**", "/images/**", "/js/**", "/fonts/**")
.permitAll().anyRequest().authenticated().and().oauth2Login().and().logout().logoutSuccessUrl("/");
http.csrf().disable();
http.headers().frameOptions().disable();
}
}
I have found a workaround. I have implemented a custom AccessDeniedHandler. Check for the exception and the URL from which it is coming from. If the URL is one where a higher scope is required I redirect the request to google authentication with extra scopes added.
This is a workaround, the real solution is still open.
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
#Value("${spring.security.oauth2.client.registration.google.client-id}")
private String clientId;
#Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
if (accessDeniedException.getMessage().equalsIgnoreCase("Insufficient scope for this resource")) {
response.sendRedirect("https://accounts.google.com/o/oauth2/v2/auth?client_id=" + clientId
+ "&response_type=code&scope=https://www.googleapis.com/auth/gmail.readonly&redirect_uri="
+ request.getServerName() + "/xyz");
}
}
}
I had been trying to make exactly what one member already did here Additional parameters in Spring Security Login, but in my case I can't make that the form authentication use the filter :
(I'm using Spring Boot 1.5.7)
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html")
.usernameParameter("username")
.passwordParameter("password").permitAll().defaultSuccessUrl("/").failureUrl("/error.html")
.and()
.logout().logoutUrl("/logout");
http.addFilterBefore(new WebAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
Pass always directly to the UserDetailsService implementation without pass through the filter. Also I had been trying using a Bean instead of 'new', but the result is the same:
http.addFilterBefore(webAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
#Bean
public WebAuthenticationFilter webAuthenticationFilter() throws Exception {
WebAuthenticationFilter auth = new WebAuthenticationFilter();
auth.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST"));
auth.setAuthenticationManager(authenticationManagerBean());
return auth;
}
My custom filter is a extends of UsernamePasswordAuthenticationFilter and in the Override of the method attemptAuthentication this method is never call:
#Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
tenant = request.getParameter("selectTenant");
System.out.println("We are here WebAuthenticationFilter");
request.getSession().setAttribute(TENANT_KEY, tenant);
return super.attemptAuthentication(request, response);
}
The only solution that worked was injecting the HttpServletRequest class into my implementation of UserDetailsService so I take the new parameter from the request here.
public class myImpleentUserDetailsService implements UserDetailsService (
#Autowired(required = false)
private HttpServletRequest request;
public UserDetail loadUserVyUsername(String username) throws UsernameNotFoundException{
String myparameter = request.getParameter("myParameter");
request.setAttribute("app-parameter", myparameter);
user = userService.findById(username, myparameter);
...
}
I have a spring-boot application with spring-security and dropwizard metrics. It uses Angularjs as a frontend. Authentication is done using separate login.html page with angularjs controller posting credentials to '/login' and after seccessful response routing to index.html (separate angularjs app). This all works quite well until I try to access dropwizard metrics. In this case I get a spring-security exception saying that user is anonymous (all other urls work fine).
My spring-security config:
#Configuration
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
#EnableWebSecurity
public class FormLoginSecurityConfigurer extends WebSecurityConfigurerAdapter {
private class AuthSuccessHandler implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_OK);
}
}
private class AuthFailureHandler implements AuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login.html", "/scripts/login/**", "/libs/**", "/styles/**", "/images/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login.html").loginProcessingUrl("/login")
.usernameParameter("username").passwordParameter("password")
.successHandler(new AuthSuccessHandler())
.failureHandler(new AuthFailureHandler())
.and().logout().logoutUrl("/logout").logoutSuccessUrl("/login.html")
.and().addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
.csrf().csrfTokenRepository(CsrfHeaderFilter.csrfTokenRepository());
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
The metrics servlet is registered in the ServletContextInitilizer:
/**
* Configuration of web application with Servlet 3.0 APIs.
*/
#Configuration
public class WebConfigurer implements ServletContextInitializer {
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
initMetrics(servletContext,
EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.ASYNC));
}
/**
* Initializes Metrics.
*/
private void initMetrics(ServletContext servletContext, EnumSet<DispatcherType> disps) {
log.debug("Initializing Metrics registries");
servletContext.setAttribute(InstrumentedFilter.REGISTRY_ATTRIBUTE,
metricRegistry);
servletContext.setAttribute(MetricsServlet.METRICS_REGISTRY,
metricRegistry);
log.debug("Registering Metrics Filter");
FilterRegistration.Dynamic metricsFilter = servletContext.addFilter("webappMetricsFilter",
new InstrumentedFilter());
metricsFilter.addMappingForUrlPatterns(disps, true, "/*");
metricsFilter.setAsyncSupported(true);
log.debug("Registering Metrics Servlet");
ServletRegistration.Dynamic metricsAdminServlet =
servletContext.addServlet("metricsServlet", new MetricsServlet());
metricsAdminServlet.addMapping("/metrics/metrics/*");
metricsAdminServlet.setAsyncSupported(true);
metricsAdminServlet.setLoadOnStartup(2);
}
}
However when I access anything under /metrics/metrics the browser prompts for basic authentication. The response has the following header WWW-Authenticate:"Basic realm="Spring"". Other resources are downloaded fine.
I'm new to this kind of applications and getting a bit frustrated :) Any help is appreciated.
Seems its all in the docs if one knows what to look for - link
The Actuator security features can be modified using external properties (management.security.*). To override the application access rules add a #Bean of type WebSecurityConfigurerAdapter and use #Order(SecurityProperties.ACCESS_OVERRIDE_ORDER) if you don’t want to override the actuator access rules, or #Order(ManagementServerProperties.ACCESS_OVERRIDE_ORDER) if you do want to override the actuator access rules.
Changed the order to ManagementServerProperties.ACCESS_OVERRIDE_ORDER and now it works.
I'm trying to implement custom stateless authentication with Spring Security by following this article
The problem I'm facing is that my custom filter is not being called by the framework, even when my SecurityConfig looks almost the same as in the previous link (a bit simpler):
#Configuration
#EnableWebMvcSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
#Qualifier("appAuthenticationProvider")
private AuthenticationProvider authenticationProvider;
#Autowired
#Qualifier("appAuthenticationFilter")
private AppAuthenticationFilter appAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable().
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.anonymous().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint());
http.addFilterBefore(appAuthenticationFilter, BasicAuthenticationFilter.class);
}
#Bean
public AuthenticationEntryPoint unauthorizedEntryPoint() {
return (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
I don't post the code for authenticationProvider and appAuthenticationFilter as the former is working fine (I can log in using /login endpoint) and the latter just implements GenericFilterBean and is not being even called.
Any help would be much appreciated!
Ok, I found the solution after I noticed that the filters were being executed when deploying the Spring Boot app, and they were not being called only when running tests. Then I found this post:
https://spring.io/blog/2014/05/23/preview-spring-security-test-web-security
I forgot to configure my mock MVC to use filters. So finally my test class for the authentication looks as follows:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = GasApplication.class)
#WebAppConfiguration
public class LoginControllerTest {
#Autowired
private WebApplicationContext context;
#Autowired
#Qualifier("appAuthenticationFilter")
private Filter appAuthenticationFilter;
private MockMvc mockMvc;
#Before
public void init() throws Exception {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.addFilter(appAuthenticationFilter, "/resource")
.build();
}
// Tests here...
}
To not autowire and set up your filter by hands as in the previous answer, you can use SecurityMockMvcConfigurers.springSecurity():
MockMvcBuilders
.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();