How to avoid redirecting to login form for some URL with Spring Security? - spring

This is the Spring Security configuration of my webapp
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", LOGIN, "/webjars/**").permitAll()
.antMatchers(CONFIGURATION).hasAuthority(Authorities.AUTHORITY_SOLMAN72_EXPORT_ENABLED.getKey())
.antMatchers("/api/**").hasAuthority(Authorities.AUTHORITY_SOLMAN72_EXPORT_ENABLED.getKey())
.and()
.formLogin()
.loginPage(LOGIN)
.and()
.addFilterBefore(oAuth2ClientAuthenticationProcessingFilter, BasicAuthenticationFilter.class);
}
Currently the server is redirecting to the LOGIN page every request that does not have the right credentials.
I want to redirect to the LOGIN page only the unauthorized requests to CONFIGURATION, while the unauthorized requests to /api/** should answer with 403.
What's a good way of achieving that?

I solved my problem using an AuthenticationEntryPoint:
http
.authorizeRequests()
.antMatchers(LOGIN).permitAll()
.antMatchers("/**").hasAuthority(Authorities.AUTHORITY_SOLMAN72_EXPORT_ENABLED.getKey())
.and()
.addFilterBefore(oAuth2ClientAuthenticationProcessingFilter, BasicAuthenticationFilter.class)
.exceptionHandling().authenticationEntryPoint(unauthenticatedRequestHandler);
#Bean
UnauthenticatedRequestHandler unauthenticatedRequestHandler() {
return new UnauthenticatedRequestHandler();
}
static class UnauthenticatedRequestHandler implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
if (request.getServletPath().startsWith("/api/")) {
response.setStatus(403);
} else {
response.sendRedirect(LOGIN);
}
}
}

You could use DelegatingAuthenticationEntryPoint:
An AuthenticationEntryPoint which selects a concrete AuthenticationEntryPoint based on a RequestMatcher evaluation.
with Http403ForbiddenEntryPoint for /api/** and LoginUrlAuthenticationEntryPoint as default entry point.
#Bean
public DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint() {
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>();
entryPoints.put(new AntPathRequestMatcher("/api/**"), new Http403ForbiddenEntryPoint());
DelegatingAuthenticationEntryPoint defaultEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
defaultEntryPoint.setDefaultEntryPoint(new LoginUrlAuthenticationEntryPoint(LOGIN));
return defaultEntryPoint;
}

I went to implement dur's answer but noticed there's a ExceptionHandlingConfigurer.defaultAuthenticationEntryPointFor(...) (available from around Spring Security 3.2.x) which does effectively the same thing with much less dependent code:
http.exceptionHandling()
.defaultAuthenticationEntryPointFor(new Http403ForbiddenEntryPoint(), new AntPathRequestMatcher("/api/**"));
Moreover, I noticed specifying any defaultAuthenticationEntryPointFor() sets the first up as the default entry point.
By default, FormLoginConfigurer, OAuth2LoginConfigurer, Saml2LoginConfigurer, etc. adds their own during SecurityConfigurer.init() and, unless we've specified one, the first among those becomes the default entry point.
This may or may not be useful, but because the
AuthenticationEntryPoint provided by FormLoginConfigurer, OAuth2LoginConfigurer, Saml2LoginConfigurer, etc. avoids requests containing the header X-Requested-With: XMLHttpRequest, the entry point we've specified with defaultAuthenticationEntryPointFor() will end up being used for AJAX, regardless of what we've specified for the request matcher argument.

Related

How do I redirect to a specific uri after Google oauth using Spring Boot

I'm implementing a server using Spring Boot. After the user do an oauth login, I want the user to go redirect to a specific uri so I can let the user register or login. The Google OAuth login seems like it is working fine but it keeps going to "/" uri. I want to user to be redirected to "/api/v1/member/oauth"
This is my Spring Security setup.
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs")
.permitAll()
.anyRequest()
.permitAll()
.and()
.oauth2Login()
.defaultSuccessUrl("/api/v1/member/oauth")
.userInfoEndpoint()
.userService(customOAuth2MemberService);
}
...
This is the OAuth service that a user is directed to. (This works fine)
#Service
#RequiredArgsConstructor
public class CustomOAuth2MemberService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User;
try {
oAuth2User = delegate.loadUser(userRequest);
} catch (OAuth2AuthenticationException e) {
throw new CustomException(OAUTH_FAIL);
}
return new DefaultOAuth2User(oAuth2User.getAuthorities(), oAuth2User.getAttributes(), "sub");
}
}
I want to get the DefaultOAuth2User which is returned from the above to this uri.
#PostMapping("/api/v1/member/oauth")
public Object registerOrLogin(DefaultOAuth2User defaultOAuth2user) {
return ResponseEntity.status(200)
.body(DefaultResponseDto.builder()
.responseCode("MEMBER_LOGIN")
.build());
}
It currently is not going to this uri and is redirected to "/".
NEW: I redirected it by having .defaultSuccessUrl() but now the DefaultOAuth2User is not sent with the redirection, causing the parameter of redirected api to be null. How do I fix this problem?
Try to use
.oauth2Login()
.defaultSuccessUrl("/api/v1/member/oauth")
this should override post-authentication behavior and redirect to the desired page after successful login. Also, there is a similar method for setting redirection URL for failed authentication .failureUrl("url").
Spring-Security AbstractAuthenticationProcessingFilter class has successfulAuthentication() methos, which defines what happens when a User is successfully authenticated. You can register your success handler and put your redirect logic there.
But here is a catch, when using OAuth2.0, we need to specify redirect-uri to which user will be landed after client receives an access-token.
If you are okay with this Oauth's redirect-uri, do not alter the redirect in success handler or if you need to redirect irrespective of that, use response.sendRedirect("/social-login-sample/some-page");
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs")
.permitAll()
.anyRequest()
.permitAll()
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2MemberService)
.and()
.successHandler(
new AuthenticationSuccessHandler() {
#Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
// authentication.getName() : Principal Name
CustomOAuth2User oauthUser = (CustomOAuth2User) authentication.getPrincipal();
// Check if user is registered in your Database, if not, register new user
//userService.processAuthenticatedUser(oauthUser.getEmail());
// Get actual redirect-uri set in OAuth-Provider(Google, Facebook)
String redirectUri =
UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
.build()
.toUriString();
log.info("redirectUri: {}", redirectUri);
// Ignore redirect-uri, and send user to a different page instead...
// response.sendRedirect("/social-login-sample/some-ther-page");
}
})
}

Spring boot security, always redirects to login page, if navigate through address bar

I have a react project, and the security works fine untill I navigate within page - i.e. clicking buttons etc. But, if I refresh page, or input url directly into adress field, it always navigates to login form.
This is my security config:
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
#Configuration
#EnableWebSecurity
#RequiredArgsConstructor
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.logout()
.and()
.csrf().disable();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v2/api-docs",
"/configuration/ui",
"/swagger-resources/**",
"/configuration/security",
"/swagger-ui.html",
"/webjars/**");
web.ignoring().antMatchers("/rest/system/getVersion");
}
}
This is restAuthenticationEntryPoint -
#Slf4j
#Component
#RequiredArgsConstructor
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ResponseWrapMessage responseWrapMessage;
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.error(e.getLocalizedMessage(), e);
BaseResponse baseResponse = new BaseResponse(UNKNOWN_ERROR, e.getLocalizedMessage());
insufficientAuthenticationWrapper(baseResponse, e);
responseWrapMessage.wrap(httpServletResponse, baseResponse);
}
private void insufficientAuthenticationWrapper(BaseResponse baseResponse, AuthenticationException e) {
if (e instanceof InsufficientAuthenticationException) {
baseResponse.setContent(CREDENTIAL_NO_VALID);
}
}
}
This is accessDeniedHandler:
#Slf4j
#Component
#RequiredArgsConstructor
public class RestAccessDeniedHandler implements AccessDeniedHandler {
private final ResponseWrapMessage responseWrapMessage;
#Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.error(accessDeniedException.getLocalizedMessage(), accessDeniedException);
BaseResponse baseResponse = new BaseResponse(UNKNOWN_ERROR, accessDeniedException.getLocalizedMessage());
responseWrapMessage.wrap(response, baseResponse);
}
}
How can it be changed? I want to persist session, untill I do the logout.
UPDATE:
I see that cookie JSESSIONID is set on the logine page like -
set-cookie: JSESSIONID=9E3BD2B1CF7C69A49902DAA7E71E393E; Path=/mctm-bh; HttpOnly
And then it is sent out when I navigate pressing buttons within the page -
Cookie: JSESSIONID=9E3BD2B1CF7C69A49902DAA7E71E393E
But if I press enter in the address URL then it is NOT sent out, and hence I am redirected to login page
How can it be changed? Maybe I there is some problem with the cookie - like there is now expires attribute. But.. specification does not say that it should be additionally customized and also it is not clear how to do it.
!UPDATE2! I heard, that it is a common problem with basic authorization. If we authorise with 'Authorization: basic ...' then browser saves auth in some cash. And that cash is invalidated if we type something in browser address field. And the way out is not to use basic auth, and to migrate as an option to spring tokens solution.
And some more details could also be found here:
https://habr.com/ru/post/488388/
I suggest you check that your session tracking mechanics is working, e.g. check that you have cookies allowed in your browser, and define the tracking-mode parameter explicitly in the application.properties:
server.servlet.session.tracking-modes=COOKIE
This chooses where to store the JSESSIONID — in the cookie
Typing in the Address bar is equivalent to creating a new session/ or opening the link in a new tab. This can be one reason why it is asking for a new log-in every time you navigate through the address bar.
I heard, that it is a common problem with basic authorization. If we authorise with 'Authorization: basic ...' then browser saves auth in some cash. And that cash is invalidated if we type something in browser address field. And the way out is not to use basic auth, and to migrate as an option to spring tokens solution. And some more details could also be found here: https://habr.com/ru/post/488388/

Spring Security OAuth - how to disable login page?

I want to secure my application with Spring Security, using OAuth 2. However, I don't want the server to redirect incoming unauthorized requests, but instead to respond with HTTP 401. Is it possible?
Example: this code redirects requests to a default login page.
application.properties
spring.security.oauth2.client.registration.google.client-id=...
spring.security.oauth2.client.registration.google.client-secret=...
AuthConfig.java
#Configuration
public class AuthConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/secured/**").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login();
// https://stackoverflow.com/questions/31714585/spring-security-disable-login-page-redirect
// deos not work
// .and()
// .formLogin().successHandler((request, response, authentication) -> {});
}
}
You need to create new authentication entry point and set it in configuration.
#Configuration
public class AuthConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers("/secured/**").authenticated()
.anyRequest().permitAll()
.and()
.oauth2Login();
}
}
public class AuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint {
public AuthenticationEntryPoint() {
super("");
}
#Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(401, "Unauthorized");
}
}
You need to set oauth2Login.loginPage in your HttpSecurity config and create a controller mapping to return whatever you want. Here's a simple example.
So in your security config
http
.authorizeRequests()
.antMatchers("/noauth").permitAll()
.oauth2Login()
.loginPage("/noauth")
In a controller
#GetMapping("/noauth")
public ResponseEntity<?> noAuth() {
Map<String, String> body = new HashMap<>();
body.put("message", "unauthorized");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
}
You can pass a map or pojo to the body method.
I would like to expand on Petr's answer by explaining that apparently for the time being first of all, the default login page is shown when there are more than one OAuth2 configured providers. I would expect that Spring Boot would have a smart trick to bypass this page easily and choose the right provider automatically, basing e.g. on the existence of the provider's client ID in the original request. I found out the hard way that this is not the case. So the way to do this is.. this not very apparent trick of providing a custom handler for failures - that will REDIRECT the user to the correct OAuth2 endpoint for each provider, based on the original HTTP request URL. I tried this and it works and I spent a whole day trying all manners of other solutions - my original scenario was to pass additional parameters to OAuth2 scheme in order to be able to get them back on successful authentication - they used to do this appending Base64 encoded information to the "state" URL request parameter, but Spring Security does not allow this at the moment. So the only alternative was to call a Spring Security-protected URL with those parameters already there, so when the successful authentication happens, this URL is accessed again automatically with those parameters intact.
Related: Multiple Login endpoints Spring Security OAuth2

Use AbstractAuthenticationProcessingFilter for multiple URLs

I have the below endpoint patterns in my application
/token -- accessible to all
/rest/securedone/** -- requires authentication
/rest/securedtwo/** -- requires authentication
/rest/unsecured/** -- does not require authentication
As of now, I am able to access the /token endpoint.
But /rest/securedone/** and /rest/unsecured/** return 401 when a token(JWT) is not sent. It is my intention to secure /rest/securedone/** and that is fine /rest/unsecured/** should be accessible.
My httpSecurity config is as below:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/token").permitAll()
.antMatchers("/rest/secured/**").authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(authenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
http.headers().cacheControl();
}
and my AbstractAuthenticationProcessingFilter extended class is as below:
public class MyAuthenticationTokenFilter extends AbstractAuthenticationProcessingFilter {
private static Logger log = LoggerFactory.getLogger(MyAuthenticationTokenFilter.class);
public MyAuthenticationTokenFilter() { super("/rest/**"); }
#Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, ServletException {
//authentication handling code
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
chain.doFilter(request, response);
}
}
Can someone please help my figure out the below:
When is the MyAuthenticationTokenFilter used? For which URL will it be invoked? How come, /rest/unsecured/** is also expecting authentication? It happens even if i explicitly say .antMatchers("/rest/secured/**").permitAll().
Can I specify multiple url patterns in my super(defaultFilterProcessingUrl) call inside MyAuthenticationTokenFilter constructor? For example, if I have another url such as /api/secured/**, how can I get my MyAuthenticationTokenFilter to be invoked for /api/secured/** requests? I do not need different authentication handling so I want to re-use this filter.
When is the MyAuthenticationTokenFilter used ?
This filter is using for processing the request with client credential,it will filter the url when the
RequestMatcher match the request url, for example, in your configuration, it will handle the url that matches /rest/**, and try to convert the client credential to Authentication(e.g userInfo, role ...), it maybe throws an exception when the request with incorrect client credential.
It is different to authorizeRequests(xxx.authenticated() or xxx.permit()), authorizeRequests just check the whether the authentication has some special attributes (e.g role, scope).
By way of analogy, AbstractAuthenticationProcessingFilter just puts some cards(Authentication) into a box(SecurityContext) by different clients, authorizeRequests just check the box has the card that it needed, or it will deny the request. AbstractAuthenticationProcessingFilter
don't care who/how to use the cards, and authorizeRequests don't care where the cards come from.
Can I specify multiple url patterns in my super(defaultFilterProcessingUrl) call inside MyAuthenticationTokenFilter constructor ?
Yes, you can set the requiresAuthenticationRequestMatcher by setRequiresAuthenticationRequestMatcher, it will override the old requiresAuthenticationRequestMatcher, for example,
authenticationTokenFilter
.setRequiresAuthenticationRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/rest/secured/**")
, new AntPathRequestMatcher("/api/secured/**")
));

Problems using Spring login in REST with CORS

I am trying to implement a website using REST. My strategy to authenticate the users consist of sending a JWT token to the user in reply to a username/password combination sent via POST. The most relevant part of my security conf is shown below.
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/images/**", "/scripts/**", "/styles/**", "favicon.ico");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling()
.authenticationEntryPoint(new RESTAuthenticationEntryPoint()).and()
.formLogin()
.successHandler(authenticationSuccessHandler())
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.loginProcessingUrl("/login") //Not necesary because is the default
.permitAll().and()
.authorizeRequests()
.antMatchers("/api/getStatistics").permitAll()
.anyRequest().denyAll().and()
.addFilterBefore(new JwtTokenAuthenticationFilter(jWTTokenService()), UsernamePasswordAuthenticationFilter.class);
}
#Bean
public SavedRequestAwareAuthenticationSuccessHandler authenticationSuccessHandler() {
return new RESTAuthenticationSuccessHandler(jWTTokenService());
}
#Bean
public JWTTokenService jWTTokenService() {
return new JWTTokenServiceImpl();
}
To allow the CORS access I have written the following lines in a class extending of WebMvcConfigurerAdapter
#Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/api/**")
.allowedOrigins("*")
.allowedHeaders("Origin", "X-Requested-With", "Content-Type", "Accept")
.allowedMethods("GET", "POST", "OPTIONS")
.allowCredentials(true).maxAge(3600);
registry.addMapping("/login")
.allowedOrigins("*")
.allowedHeaders("Origin", "X-Requested-With", "Content-Type", "Accept")
.allowedMethods("POST", "OPTIONS")
.allowCredentials(true).maxAge(3600);
}
So when I make a call to /login sending the username and password it is supposed that Spring will catch the request, will process it and then will redirect to the success or failure handler.
Well, instead of that I have gotten an 403 Forbidden response during the CORS preflight. I decide to debug the program because I thought that when I wrote formLogin(), the UsernamePasswordAuthenticationFilter create a new AntPathRequestMatcher with the value ("/login", "POST").
What I found in the debug console was the following
Request 'OPTIONS /login' doesn't match 'POST /login
Of course it does not! Some hours later trying to solve the problem I discovered that everything works if I declare a empty method /login because during the preflight Spring finds the method and then send a 200OK to the client so the client then is allowed to send a POST that is captured by the UsernamePasswordAuthenticationFilter.
#Controller
public class LoginController {
#RequestMapping(value = { "/login" }, method = RequestMethod.POST)
public void dummyLogin() {
}
}
So, my question is: Should I really declare an empty method to "cheat" during the CORS preflight or it is just that I have missed something? Because it is not so elegant to declare a dummy method when you really want to delegate the job to the UsernamePasswordAuthenticationFilter...
The problem is that org.springframework.security.web.authentication.logout.LogoutFilter and org.springframework.security.web.authenticationUsernamePasswordAuthenticationFilter do not continue with the filter chain if they handled a login/logout. And since the configuration via WebSecurityConfigurerAdapter is processed later in the chain, the CorsProcessor is never applied.
I decided to keep the old solution and use a org.springframework.web.filter.CorsFilter.
It is not necessary to have empty method to make it work. The only thing you have to do is to allow OPTIONS call on the /login URL.
.antMatchers(HttpMethod.OPTIONS, "/login").permitAll()
Ex :
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling()
.authenticationEntryPoint(new RESTAuthenticationEntryPoint()).and()
.formLogin()
.successHandler(authenticationSuccessHandler())
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.loginProcessingUrl("/login") //Not necesary because is the default
.permitAll().and()
.authorizeRequests()
.antMatchers("/api/getStatistics").permitAll()
.antMatchers(HttpMethod.OPTIONS, "/login").permitAll()
.anyRequest().denyAll().and()
.addFilterBefore(new JwtTokenAuthenticationFilter(jWTTokenService()), UsernamePasswordAuthenticationFilter.class);
}

Resources