I'm looking for a way to implement a Maintenance Mode in my Spring app.
While the app is in Maintenance Mode only users role = MAINTENANCE should be allowed to log in. Everyone else gets redirected to login page.
Right now I just built a Filter:
#Component
public class MaintenanceFilter extends GenericFilterBean {
#Autowired SettingStore settings;
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if(settingStore.get(MaintenanceMode.KEY).isEnabled()) {
HttpServletResponse res = (HttpServletResponse) response;
res.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
} else {
chain.doFilter(request, response);
}
}
}
And added it using:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// omitted other stuff
.addFilterAfter(maintenanceFilter, SwitchUserFilter.class);
}
Because as far as I figured out SwitchUserFilter should be the last filter in Spring Security's filter chain.
Now every request gets canceled with a 503 response. Though there's no way to access the login page.
If I add a redirect to the Filter, this will result in an infinite loop, because access to login page is also denied.
Additionally I can't figure out a nice way to get the current users roles. Or should I just go with SecurityContextHolder ?
I'm looking for a way to redirect every user to the login page (maybe with a query param ?maintenance=true) and every user with role = MAINTENANCE can use the application.
So the Filter / Interceptor should behave like:
if(maintenance.isEnabled()) {
if(currentUser.hasRole(MAINTENANCE)) {
// this filter does nothing
} else {
redirectTo(loginPage?maintenance=true);
}
}
I now found two similar solutions which work, but the place where I inject the code doesn't look that nice.
For both I add a custom RequestMatcher, which get's #Autowired and checks if Maintenance Mode is enabled or not.
Solution 1:
#Component
public class MaintenanceRequestMatcher implements RequestMatcher {
#Autowired SettingStore settingStore;
#Override
public boolean matches(HttpServletRequest request) {
return settingStore.get(MaintenanceMode.KEY).isEnabled()
}
}
And in my Security Config:
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired MaintenanceRequestMatcher maintenanceRequestMatcher;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.requestMatchers(maintenanceRequestMatcher).hasAuthority("MY_ROLE")
.anyRequest().authenticated()
// ...
}
Solution 2:
Very similar, but uses HttpServletRequest.isUserInRole(...):
#Component
public class MaintenanceRequestMatcher implements RequestMatcher {
#Autowired SettingStore settingStore;
#Override
public boolean matches(HttpServletRequest request) {
return settingStore.get(MaintenanceMode.KEY).isEnabled() && !request.isUserInRole("MY_ROLE");
}
}
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired MaintenanceRequestMatcher maintenanceRequestMatcher;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.requestMatchers(maintenanceRequestMatcher).denyAll()
.anyRequest().authenticated()
// ...
}
This will perform a denyAll() if Maintenance Mode is enabled and the current user does not have MY_ROLE.
The only disadvantage is, that I cannot set a custom response. I'd prefer to return a 503 Service Unavailable. Maybe someone can figure out how to do this.
It's kinda of a chicken or egg dilemma, you want to show unauthorized users a "we're in maintenance mode ..." message, while allow authorized users to login, but you don't know if they are authorized until they log in. Ideally it would be nice to have this in some sort of filter, but I found in practice it was easier for me to solve a similar issue by putting the logic after login, like in the UserDetailsService.
Here's how I solved it on a project. When I'm in maintenance mode, I set a flag for the view to show the "we're in maintenance mode .." message, in a global header or on the login page. So users, regardless of who they are know it's maintenance mode. Login should work as normal.
After user is authenticated, and in my custom UserDetailsService, where their user details are loaded with their roles, I do the following:
// if we're in maintenance mode and does not have correct role
if(maintenance.isEnabled() && !loadedUser.hasRole(MAINTENANCE)) {
throw new UnauthorizedException(..)
}
// else continue as normal
It's not pretty, but it was simple to understand (which I think is good for security configuration stuff) and it works.
Update:
With you solution I'd have to destroy everyone's session, else a user
which was logged in before maintenance mode was enabled, is still able
to work with the system
On our project we don't allow any users to be logged in while in maintenance mode. An admin, kicks off a task which enables "maintenance..." msg, with a count down, then at the end, we expire everyone's session using SessionRegistry.
I was a similar situation and found this answer is helpful. I followed the second approach and also managed to return custom response.
Here is what I have done to return the custom response.
1- Define a controller method that returns the needed custom response.
#RestController
public class CustomAccessDeniedController {
#GetMapping("/access-denied")
public String getAccessDeniedResponse() {
return "my-access-denied-page";
}
}
2- In your security context, you should permit this URL to be accessible.
http.authorizeRequests().antMatchers("/access-denied").permitAll()
3- Create a custom access denied exception handler
#Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
#Autowired
private SettingStore settingStore;
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
if(settingStore.get(MaintenanceMode.KEY).isEnabled()) {
response.sendRedirect(request.getContextPath() + "/access-denied");
}
}
}
4- Register the custom access denier exception handler in the security config
#Autowired
private CustomAccessDeniedHandler accessDeniedHandler;
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
Related
I would like to know if in spring oauth2 is possible get a new pair tokens (access token and refresh token) just using another refresh token, without the basic authentication (without clientId and clientSecret, is there any way?
For exemple:
WITH BASIC AUTH
curl -u clientId:clientSecret -X POST 'http://myapplication.oauth2/accounts/oauth/token?grant_type=refresh_token&client_id=<CLIENT_ID>&refresh_token=' -v
WITHOUT BASIC AUTH
curl -u -X POST 'http://myapplication.oauth2/accounts/oauth/token?grant_type=refresh_token&client_id=<CLIENT_ID>&refresh_token=' -v
I note that sprint BasicAuthenticationFilter in spring uses validation bellow, maybe override this filter and make the authentication just with refresh token.
String header = request.getHeader("Authorization");
if (header == null || !header.toLowerCase().startsWith("basic ")) {
chain.doFilter(request, response);
return;
}
The short answer is no. The class used to manage the Spring Oauth 2 endpoints is the following one:
#FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint
Both requests, I mean, get access token and refresh one use the same endpoint with different parameters. And the method to manage those ones is:
#RequestMapping(
value = {"/oauth/token"},
method = {RequestMethod.POST}
)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, #RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
} else {
String clientId = this.getClientId(principal);
...
As you can see, a Principal object is required (in this case provided by the Basic Authentication).
Even, if you configure the security of your project to permit that url without checking authentication, you will achieve to "enter" in above method but you will receive an InsufficientAuthenticationException because no Authentication instance has been provided.
Why custom authentication will not work
1. Create a custom AuthenticationProvider will not work because the method postAccessToken is invoked before. So you will receive an InsufficientAuthenticationException.
2. Create a OncePerRequestFilter and configure it to execute before process the current request:
#Override
protected void configure(HttpSecurity http) throws Exception {
http...
.anyRequest().authenticated()
.and()
.addFilterBefore(myCustomFilter, UsernamePasswordAuthenticationFilter.class);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(POST, "/accounts/oauth/**");
}
with a code "similar to":
#Component
public class CustomAuthenticationFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
...
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("existingUser",
"passwordOfExistingUser",
Collections.emptyList()));
...
filterChain.doFilter(request, response);
}
The problem with this approach is the principal in TokenEndpoint comes from the HttpServletRequest not from Spring context, as you can see debugging BasicAuthenticationFilter class.
In your custom filter you can try, using reflection, set a value in userPrincipal property but, as you can verify, request has several "internal request properties" and that could be a "too tricky option".
In summary, Oauth standard needs user/pass to access to the resources, if you want to workaround in almost of provided endpoints maybe that project is not what you are looking for.
Workaround to include your own object in Spring Principal
I do not recommend that but if you still want to go ahead with this approach, there is a way to include your own value inside the principal parameter received by TokenEndpoint class.
It is important to take into account BasicAuthorizationFilter will be still executed, however you will be able to override the Spring principal object by your own one.
For this, we can reuse the previous CustomAuthenticationFilter but now your have to include the filters you need, I mean, allowed urls, parameters, etc You are going to "open the doors", so be careful about what you allow and not.
The difference in this case is, instead of add the configuration in our class that extends WebSecurityConfigurerAdapter we are going to do it in:
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private CustomAuthenticationFilter customAuthenticationFilter;
...
#Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security.checkTokenAccess("isAuthenticated()");
security.addTokenEndpointAuthenticationFilter(customAuthenticationFilter);
}
...
I have the following spring boot 2.0 config but I am still getting the basic auth login screen. I DO NOT want to disable all spring security like almost every post on the internet suggests. I only want to stop the form login page and basic auth so I can use my own.
I have seen all the suggestions with permitAll and exclude = {SecurityAutoConfiguration.class} and a few others that I can't remember anymore. Those are not what I want. I want to use spring security but I wan my config not Spring Boots. Yes I know many people are going to say this is a duplicate but I disagree because all the other answers are to disable spring security completely and not just stop the stupid login page.
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true)
public class CustomSecurity extends WebSecurityConfigurerAdapter {
private final RememberMeServices rememberMeService;
private final AuthenticationProvider customAuthProvider;
#Value("${server.session.cookie.secure:true}")
private boolean useSecureCookie;
#Inject
public CustomSecurity(RememberMeServices rememberMeService, AuthenticationProvider customAuthProvider) {
super(true);
this.rememberMeService = rememberMeService;
this.bouncerAuthProvider = bouncerAuthProvider;
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/**").antMatchers("/webjars/**").antMatchers("/swagger-resources/**")
.antMatchers("/swagger-ui.html");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable().formLogin().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).headers().frameOptions().disable();
http.authenticationProvider(customAuthProvider).authorizeRequests().antMatchers("/health").permitAll()
.anyRequest().authenticated();
http.rememberMe().rememberMeServices(rememberMeService).useSecureCookie(useSecureCookie);
http.exceptionHandling().authenticationEntryPoint(new ForbiddenEntryPoint());
}
}
If you want to redirect to your own login page, i can show your sample code and configuration
remove the http.httpBasic().disable().formLogin().disable();, you should set your own login page to redirect instead of disable form login
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/my_login").permitAll().and().authorizeRequests().anyRequest().authenticated();
http.formLogin().loginPage("/my_login");
}
then create your own LoginController
#Controller
public class LoginController {
#RequestMapping("/my_login")
public ModelAndView myLogin() {
return new ModelAndView("login");
}
}
you can specified the login with thymeleaf view resolver
I am making an API with Spring boot + Spring Security. I am having issues with Spring Security "letting" any request go through.
What is happening is that I have two endpoints:
users/register/app
users/recoverpasswordbyemail
And I am testing them with Postman, the thing is that if I call one of those endpoints for the first time after the app started without an Authoritation header or a wrong one, it won't let me in and gives a 401 error. However, If I call the other method or the same method again with a correct Authoritation header and then call one of them without the Header again, it'll let me pass. I do not think it's suppossed to work without an auth header.
I am not sure if it is because I have already done some kind of "log in" when I put the correct auth header or if it's a Postman's issue. But I want that every time a call to one of those endpoints a check is done for the user.
This is my Spring config file:
#Configuration #EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private UserServiceImpl userService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/users/register/app", "/users/recoverpasswordbyemail")
.hasAnyRole("ADMIN", "SADMIN")
.and().httpBasic().authenticationEntryPoint(new NotAuthorizedExceptionMapper())
.and().sessionManagement().disable();
}
#Override
public void configure(AuthenticationManagerBuilder builder) throws Exception {
builder.userDetailsService(userService);
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Also every time this happens, the user doesn't get first if exists (however it does when I have a wrong auth header) or whatever as I have in my UsersService:
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("Hi");
User user = userRepository.findById(username).get();
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.getRole());
}
This is the URL I use to call the endpoints:
http://localhost:8680/ulises/usersserver/users/register/app
where /ulises/usersserver/ is the servlet context
Has anyone a clue of why this can be? I researched quite a lot but saw nothing that could solve it.
Thank.
Okay, it turns out that a session was being created despite of sessions being disabled. I don't understand why this is happening but using
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
solved my problem.
I am building a restful web service with an Angular front end. If the users log in with wrong credentials, the Chrome browser automatically shows a login popup. I want to prevent this by responding to code 403 instead of 401.
i have review [this question][1] and it does not help with my use case. I have also tried different solutions found on google, but in all cases is stays giving 401 with the login popup.
With the newest Spring versions, the configuration to handle authentication has changed. For this reason most tutorials do not work anymore. here is the working configuration:
SecurityConfig.java:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
//....
//.and()
.httpBasic().authenticationEntryPoint(authenticationEntryPoint)
.and().csrf().disable();
}
}
CustomAuthenticationAntryPoint.java:
#Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final Logger log = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.debug("Pre-authenticated entry point called. Rejecting access");
httpServletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Bad credentials");
}
}
How in Spring Security can I set a RememberMe cookie url path, that differs from the context path?
Supposing my website's homepage url is (url rewrite):
https://www.mysuperspecialdomain.com
And that my login page has a url like this:
https://www.mysuperspecialdomain.com/shop/account/login
After succesful login the RememberMe cookie has the path /shop (visible in the browser, e.g. Chrome). This is the project's context path.
This leads to the situation, that when I'm going to my homepage, RememberMe is not logging in. Only when I navigate to a url, that starts with https://www.myspecialdomain.com/shop it's doing it.
If you use Spring Security 4.1.0 or higher, you can configure the cookie domain, see RememberMeConfigurer#rememberMeCookieDomain:
The domain name within which the remember me cookie is visible.
but you can't change the context path.
So you have to implement your own RememberMeServices (you could create a sub class of an existing one) and add it with RememberMeConfigurer#rememberMeServices to your security configuration.
I've found a solution to my own question - manipulation of the path of the RememberMe-cookie can be done via an HttpServletResponseWrapper. This is the solution (based on this answer https://stackoverflow.com/a/7047298/7095884):
Define an HttpServletResponseWrapper:
public class RememberMeCookieResponseWrapper extends HttpServletResponseWrapper {
public RememberMeCookieResponseWrapper(HttpServletResponse response) {
super(response);
}
#Override
public void addCookie(Cookie cookie) {
if (cookie.getName().equals("shop")) {
cookie.setPath("/");
}
super.addCookie(cookie);
}
}
Define a filter, that wraps the servlet response with the just defined wrapper:
public class RememberMeCookieFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (response instanceof HttpServletResponse) {
HttpServletResponse newResponse =
new RememberMeCookieResponseWrapper((HttpServletResponse)response);
chain.doFilter(request, newResponse);
}
}
}
Add this filter to the Spring Filter Chain in front of the authentication part:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new RememberMeCookieFilter(), UsernamePasswordAuthenticationFilter.class)
...