Spring Security - fallback to basic authentication if Kerberos fails - spring-boot

How can I enable fallback to basic auth if Kerberos authentication fails (e.g. client is not on the domain)? With the configuration below no browser authentication window appears and the following exceptions are thrown:
org.springframework.security.authentication.BadCredentialsException: Kerberos validation not successful
org.ietf.jgss.GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)
Relevant part of my WebSecurityConfigurerAdapter implementation:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(spnegoEntryPoint())
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.logout()
.permitAll()
.and()
.addFilterBefore(
spnegoAuthenticationProcessingFilter(),
BasicAuthenticationFilter.class);
}
#Bean
public SpnegoEntryPoint spnegoEntryPoint() {
return new SpnegoEntryPoint("/");
}
#Bean
public SpnegoAuthenticationProcessingFilter spnegoAuthenticationProcessingFilter() {
SpnegoAuthenticationProcessingFilter filter = new SpnegoAuthenticationProcessingFilter();
try {
filter.setAuthenticationManager(authenticationManagerBean());
} catch (Exception e) {
log.error("Failed to set AuthenticationManager on SpnegoAuthenticationProcessingFilter.", e);
}
return filter;
}

Related

Migrating from WebSecurityConfigurerAdapter to SecurityFilterChain

Here is my working security conf before migration :
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/auth/**")
.antMatchers("/swagger-ui/**")
.antMatchers("/swagger-ui.html")
.antMatchers("/swagger-resources/**")
.antMatchers("/v2/api-docs/**")
.antMatchers("/v3/api-docs/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedPortalRoleConverter);
http
.csrf().disable()
.cors()
.and()
.exceptionHandling()
.authenticationEntryPoint(new AuthenticationFallbackEntryPoint())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer()
.jwt().jwtAuthenticationConverter(jwtAuthenticationConverter);
}
And here is my Security chain config after migration :
#Bean
#Order(1)
public SecurityFilterChain ignorePathsSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.antMatchers(
"/auth/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**")
.permitAll());
return http.build();
}
#Bean
#Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, GrantedPortalRoleConverter grantedPortalRoleConverter) throws Exception {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedPortalRoleConverter);
http
.csrf().disable()
.cors(Customizer.withDefaults())
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(new AuthenticationFallbackEntryPoint()))
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer(configurer -> configurer.jwt().jwtAuthenticationConverter(jwtAuthenticationConverter));
return http.build();
}
With the original conf, when I call a random non existing path :
#Test
void should_not_authenticate_or_return_not_found() throws Exception {
logger.info("should_not_authenticate_or_return_not_found");
mvc.perform(get("/toto/tata"))
.andExpect(status().isUnauthorized());
}
I get :
15:44:00.230 [main] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Failed to authorize filter invocation [GET /toto/tata] with attributes [authenticated]
With the new conf, I'm just getting HTTP 404, what am I missing here please ? I can't see any difference and debug logs don't show much.
Here is the first line of log missing using the non working conf :
16:24:58.651 [main] DEBUG o.s.s.w.a.e.ExpressionBasedFilterInvocationSecurityMetadataSource - Adding web access control expression [authenticated] for any request
But in both logs, I can see (2 lines of this for the new conf since there are 2 security chains) :
o.s.s.web.DefaultSecurityFilterChain - Will secure any request with (...)
Explanation
When you have multiple SecurityFilterChains, you have to specify a request matcher, otherwise all requests will be processed by the first SecurityFilterChain, annotated with #Order(1), and never reach the second SecurityFilterChain, annotated with #Order(2).
In the code you shared above, this means configuring .requestMatchers() in ignorePathsSecurityFilterChain:
#Bean
#Order(1)
public SecurityFilterChain ignorePathsSecurityFilterChain(HttpSecurity http) throws Exception {
http
.requestMatchers(requests -> requests // add this block
.antMatchers(
"/auth/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**")
)
.authorizeHttpRequests(authorize -> authorize
.antMatchers(
"/auth/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**")
.permitAll());
return http.build();
}
This means that only the requests matching /auth/**, /swagger-ui/** etc will be processed by ignorePathsSecurityFilterChain, while the rest of the requests will move on to defaultSecurityFilterChain.
To understand the difference between requestMatchers and authorizeHttpRequests you can check out this StackOverflow question.
Solution
An even better option is to combine the SecurityFilterChains into a single one. I don't see any reason why you would separate them in this case.
The resulting configuration would be:
#Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, GrantedPortalRoleConverter grantedPortalRoleConverter) throws Exception {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedPortalRoleConverter);
http
.authorizeHttpRequests(authorize -> authorize
.antMatchers(
"/auth/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**")
.permitAll()
.anyRequest().authenticated()
)
.csrf().disable()
.cors(Customizer.withDefaults())
.exceptionHandling(configurer -> configurer.authenticationEntryPoint(new AuthenticationFallbackEntryPoint()))
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(configurer -> configurer.jwt().jwtAuthenticationConverter(jwtAuthenticationConverter));
return http.build();
}
Alternative
Alternatively you can use a WebSecurityCustomizer to ignore certain endpoints:
#Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers(
"/auth/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**");
}
Then you would use defaultSecurityFilterChain as your only SecurityFilterChain.

Configure public access, JWT authentication and HTTP basic authentication depending on paths in Spring Security

A Spring Boot application provides some REST endpoints with different authentication mechanisms. I'm trying to setup the security configuration according to the following requirements:
By default, all endpoints shall be "restricted", that is, if any endpoint is hit for which no specific rule exists, then it must be forbidden.
All endpoints starting with /services/** shall be secured with a JWT token.
All endpoints starting with /api/** shall be secured with HTTP basic authentication.
Any endpoint defined in a RESOURCE_WHITELIST shall be public, that is, they are accessible without any authentication. Even if rules #2 or #3 would apply.
This is what I came up with so far but it does not match the above requirements. Could you help me with this?
#Configuration
#RequiredArgsConstructor
public static class ApiSecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String[] RESOURCE_WHITELIST = {
"/services/login",
"/services/reset-password",
"/metrics",
"/api/notification"
};
private final JwtRequestFilter jwtRequestFilter;
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("some-username")
.password(passwordEncoder().encode("some-api-password"))
.roles("api-role");
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors()
.and()
.csrf().disable()
.formLogin().disable()
// apply JWT authentication
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.mvcMatchers(RESOURCE_WHITELIST).permitAll()
.mvcMatchers("/services/**").authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// apply HTTP Basic Authentication
.and()
.authorizeRequests()
.mvcMatchers("/api/**")
.hasRole(API_USER_ROLE)
.and()
.httpBasic()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}

Why the character ';' is added the end of request in OAuth2

There are Authorization Server(UAA) and Resource Server and Gateway applications that have been working correctly. The scenario for authentication is authorization_code. In the first time after authentication, the end of request is added ;jesessionid=[value], so its result is exception from HttpFirewall of Gateway application, because of having ';' in the request.
My question is that what is it and why jessionid is added the end of request? and how is it adaptable with HttpFirewall.
I have found a way around but I know it has some risks. It is like this:
#Bean
public HttpFirewall allowUrlEncodedSlashHttpFirwall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowSemicolon(true);
return firewall;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.headers().cacheControl().disable()
.and()
.headers()
.cacheControl()
.disable()
.frameOptions()
.sameOrigin()
.and()
.httpBasic().disable()
.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.mvcMatchers("/uaa/**", "/login**", "/favicon.ico", "/error**").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.permitAll();
}
#Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.httpFirewall(allowUrlEncodedSlashHttpFirwall());
}
As above configuration, the ; is skipped but it is not right and it has some risks.
What is the correct way and config to solve this problem?

Spring Security custom authentication failure handler redirect with parameter

I have a problem with Spring Security authentication failure handler redirect with parameter.
In security config when I use
failureUrl("/login.html?error=true")
it works. But when I use custom authentication failure handler (as shown below), it always returns: url/login.html
getRedirectStrategy().sendRedirect(request, response, "/login.html?error=true");
or
response.sendRedirect(request.getContextPath() + "/login.html?error=true");
I don't know whats wrong. Why does it not show the parameter ?error=true?
Info: I am using Spring + JSF + Hibernate + Spring Security
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.usernameParameter("j_username")
.passwordParameter("j_password")
.loginProcessingUrl("/j_spring_security_check")
.failureHandler(customAuthenticationFailureHandler)// .failureUrl("/login.html?error=true")//.successHandler(authSuccsessHandler)
.defaultSuccessUrl("/dashboard.html")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.logoutSuccessUrl("/")
.permitAll()
.and()
.exceptionHandling()
.accessDeniedPage("/access.html")
.and()
.headers()
.defaultsDisabled()
.frameOptions()
.sameOrigin()
.cacheControl();
http
.csrf().disable();
}
This is custom authentication failure handler:
#Component
public class CustomAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
getRedirectStrategy().sendRedirect(request, response, "/login.html?error=true");
}
}
I will change parameter for some cases.
You didn't allow anonymous access to URL /login.html?error=true, so you are redirected to the login page (/login.html).
AbstractAuthenticationFilterConfigurer#permitAll allows access (for anyone) to failure URL but not for custom failure handler:
Ensures the urls for failureUrl(String) as well as for the HttpSecurityBuilder, the getLoginPage() and getLoginProcessingUrl() are granted access to any user.
You have to allow access explicitly with AbstractRequestMatcherRegistry#antMatchers:
Maps a List of AntPathRequestMatcher instances that do not care which HttpMethod is used.
and ExpressionUrlAuthorizationConfigurer.AuthorizedUrl#permitAll:
Specify that URLs are allowed by anyone.
You don't have to allow the exact URL /login.html?error=true, because AntPathRequestMatcher ignores the query string:
Matcher which compares a pre-defined ant-style pattern against the URL ( servletPath + pathInfo) of an HttpServletRequest. The query string of the URL is ignored and matching is case-insensitive or case-sensitive depending on the arguments passed into the constructor.
Your modified configuration:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login.html")
.usernameParameter("j_username")
.passwordParameter("j_password")
.loginProcessingUrl("/j_spring_security_check")
.failureHandler(customAuthenticationFailureHandler)// .failureUrl("/login.html?error=true")//.successHandler(authSuccsessHandler)
.defaultSuccessUrl("/dashboard.html")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.logoutSuccessUrl("/")
.permitAll()
.and()
.exceptionHandling()
.accessDeniedPage("/access.html")
.and()
.headers()
.defaultsDisabled()
.frameOptions()
.sameOrigin()
.cacheControl();
http
.csrf().disable();
}
In the case of OAuth token failure, I am getting below response, which is inconsistent with app response style.
{
"error": "invalid_token",
"error_description": "Invalid access token: 4cbc6f1c-4d47-44bd-89bc-92a8c86d88dbsdfsdfs"
}
I just wanted to use common response object for the consistency.
Following approach worked for me.
Build your resource server with your custom entry-point object
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(new CustomOAuth2AuthenticationEntryPoint());
}
and here is your custom entry point
public class CustomOAuth2AuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint{
public CustomOAuth2AuthenticationEntryPoint() {
super.setExceptionTranslator(new CustomOAuth2WebResponseExceptionTranslator());
}
}
here is your custom WebResponseExceptionTranslator, In my case I have just used a replica of DefaultWebResponseExceptionTranslator and rewritten handleOAuth2Exception method.
CustomOAuth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator<Response> {
....
.....
private ResponseEntity<Response> handleOAuth2Exception(OAuth2Exception e) throws IOException {
int status = e.getHttpErrorCode();
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
}
ResponseEntity<Response> response =new ResponseEntity<>(new Response().message(e.getMessage()).status(StatusEnum.ERROR)
.errorType(e.getClass().getName()), HttpStatus.UNAUTHORIZED);
return response;
}
Result looks like
{
"status": "error",
"message": "Invalid access token: 4cbc6f1c-4d47-44bd-89bc-92a8c86d88dbsdfsdfs",
"error_type": "org.springframework.security.oauth2.common.exceptions.InvalidTokenException"
}

How to handle access denied exception using Websocket and Spring security

I am using spring security along with Websocket, I have the following method,
#PreAuthorize("hasAuthority('ROLE_CREATE_USER')")
#MessageMapping("/cashDeposit")
public void cashDeposit(CashDepositRequest cashDepositRequest) {
And I have websocket security configuration. Now the problem is, when access is denied, it is throwing an exception,
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.1.1.RELEASE.jar:4.1.1.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-4.1.1.RELEASE.jar:4.1.1.RELEASE]
And I searched through Stackoverflow and I found most of the questions are related to Servlets (HTTPServletRequest/Response) and not to Websocket.
Please let me know, how to handle AccessDeniedException for websocket and how to send a websocket message back to the user saying 403 forbidden.
UPDATE
My security config
#Configuration
public class WebSocketSecurityConfig extends
AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.simpMessageDestMatchers("/topic/**").permitAll()
.anyMessage().authenticated();
}
#Override
protected boolean sameOriginDisabled() {
//disable CSRF for websockets for now...
return true;
}
}
My security config for rest services,
#Override
protected void configure(HttpSecurity http) throws Exception {
http .csrf().disable()
.headers().addHeaderWriter( new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)).and()
.authorizeRequests()
.antMatchers("/index.html").permitAll()
.antMatchers("/**.js").permitAll()
.antMatchers("/**.css").permitAll()
.anyRequest().authenticated().and()
.authenticationProvider(authenticationProvider())
.exceptionHandling()
.authenticationEntryPoint(entryPoint)
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.permitAll()
.logoutRequestMatcher(new AntPathRequestMatcher("/login", "DELETE"))
.logoutSuccessHandler(logoutSuccessHandler)
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.and()
.sessionManagement()
.enableSessionUrlRewriting(true)
.maximumSessions(1);
}
You can try something like this:
#MessageExceptionHandler
#SendToUser(destinations="/queue/errors", broadcast=false)
public ApplicationError handleException(AccessDeniedException exception) {
// ...
return appError;
}
source: https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-user-destination

Resources