Keycloak with vaadin and spring security - spring-security

I want to secure my vaadin application with keycloak and spring security. I try to use the "keycloak-spring-security-adapter".
My problem is that I want also unauthenticated users to use my application, but with less functionality - I do this with method security and checking which roles the current user has in the UI.
Can I configure the filter so that it ignores unauthenticated requests, but If the token is present uses it?
Thanks
Daniel

A working example of what you want can be found in the public-access branch of this github project. It does use Vaadin 8 though.
In essence, you can setup your application to be partially public, i.e. accessibly to unauthenticated user for certain parts and requires login for others, as follows:
#Configuration
#EnableWebSecurity
#EnableVaadinSharedSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, proxyTargetClass = true)
public class SecurityConfiguration extends KeycloakWebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable();
http.formLogin().disable();
http.csrf().disable();
http
.authorizeRequests()
.antMatchers("/vaadinServlet/UIDL/**").permitAll()
.antMatchers("/vaadinServlet/HEARTBEAT/**").permitAll()
.anyRequest().permitAll();
http
.logout()
.addLogoutHandler(keycloakLogoutHandler())
.logoutUrl("/sso/logout").permitAll()
.logoutSuccessUrl("/");
http
.addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class)
.addFilterBefore(keycloakAuthenticationProcessingFilter(), BasicAuthenticationFilter.class);
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
http
.sessionManagement()
.sessionAuthenticationStrategy(sessionAuthenticationStrategy());
}
...
}
The line http.anyRequest().permitAll(); is the most important where you configure the filter to just allow all requests. You could still update this to only allow public access to certain urls.
You can then use spring security annotations on methods/views/components to configure your fine-grained access control. E.g:
#SpringComponent
#Secured("ROLE_ANONYMOUS")
public class LoginOperation implements Runnable {
#Override
public void run() {
// login logic
}
}
and
#Secured("ROLE_USER")
public class LogoutOperation implements Runnable {
#Override
public void run() {
// logout logic
}
}

Related

How to properly configure spring-security with vaadin14 to handle 2 entry points - keyclaok and DB

I have a vaadin14 application that I want to enable different types of authentication mechanisms on different url paths. One is a test url, where authentication should use DB, and the other is the production url that uses keycloak.
I was able to get each authentication mechanism to work separately, but once I try to put both, I get unexpected results.
In both cases, I get login page, but the authentication doesn't work correctly. Here's my security configuration, what am I doing wrong?
#Configuration
#EnableWebSecurity
public class ApplicationSecurityConfiguration {
#Configuration
#Order(2)
public static class DBAuthConfigurationAdapter extends WebSecurityConfigurerAdapter {
private static final String LOGIN_PROCESSING_URL = "/login";
private static final String LOGIN_FAILURE_URL = "/login?error";
private static final String LOGIN_URL = "/login";
private static final String LOGOUT_SUCCESS_URL = "/login";
/**
* Require login to access internal pages and configure login form.
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
// Not using Spring CSRF here to be able to use plain HTML for the login page
http.csrf().disable()
// Register our CustomRequestCache, that saves unauthorized access attempts, so
// the user is redirected after login.
.requestCache().requestCache(new CustomRequestCache())
// Restrict access to our application.
.and().antMatcher("/test**").authorizeRequests()
// Allow all flow internal requests.
.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll()
// Allow all requests by logged in users.
.anyRequest().hasRole("USER")
// Configure the login page.
.and().formLogin().loginPage(LOGIN_URL).permitAll().loginProcessingUrl(LOGIN_PROCESSING_URL)
.failureUrl(LOGIN_FAILURE_URL)
// Configure logout
.and().logout().logoutSuccessUrl(LOGOUT_SUCCESS_URL);
}
#Bean
#Override
public UserDetailsService userDetailsService() {
Properties users = null;
try {
users = PropertiesLoaderUtils.loadAllProperties("users.properties");
return new InMemoryUserDetailsManager(users);
} catch (IOException e) {
e.printStackTrace();
}
UserDetails user =
User.withUsername("user")
.password("{noop}password")
.roles("ACTOR")
.build();
return new InMemoryUserDetailsManager(user);
}
/**
* Allows access to static resources, bypassing Spring security.
*/
#Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(
// Vaadin Flow static resources
"/VAADIN/**",
// the standard favicon URI
"/favicon.ico",
// the robots exclusion standard
"/robots.txt",
// web application manifest
"/manifest.webmanifest",
"/sw.js",
"/offline-page.html",
// icons and images
"/icons/**",
"/images/**",
// (development mode) static resources
"/frontend/**",
// (development mode) webjars
"/webjars/**",
// (development mode) H2 debugging console
"/h2-console/**",
// (production mode) static resources
"/frontend-es5/**", "/frontend-es6/**",
"/resources/**");
}
}
#Order(1)
#Configuration
#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public static class AppKeycloakSecurity extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(
AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider
= keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(
new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(
new SessionRegistryImpl());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.httpBasic().disable();
http.formLogin().disable();
http.anonymous().disable();
http.csrf().disable();
http.headers().frameOptions().disable();
http
.antMatcher("/prod**")
.authorizeRequests()
.antMatchers("/vaadinServlet/UIDL/**").permitAll()
.antMatchers("/vaadinServlet/HEARTBEAT/**").permitAll()
.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll()
.anyRequest().hasRole("actor");
http
.logout()
.addLogoutHandler(keycloakLogoutHandler())
.logoutUrl("/logout").permitAll()
.logoutSuccessUrl("/");
http
.addFilterBefore(keycloakPreAuthActionsFilter(), LogoutFilter.class);
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint());
http
.sessionManagement()
.sessionAuthenticationStrategy(sessionAuthenticationStrategy());
}
}
}
Navigating within a Vaadin UI will change the URL in your browser, but it will not necessarily create a browser request to that exact URL, effectively bypassing the access control defined by Spring security for that URL. As such, Vaadin is really not suited for the request URL-based security approach that Spring provides. For this issue alone you could take a look at my add-on Spring Boot Security for Vaadin which I specifically created to close the gap between Spring security and Vaadin.
But while creating two distinct Spring security contexts based on the URL is fairly easy, this - for the same reason - will not work well or at all with Vaadin. And that's something even my add-on couldn't help with.
Update: As combining both security contexts is an option for you, I can offer the following solution (using my add-on):
Starting from the Keycloak example, you would have to do the following:
Change WebSecurityConfig to also add your DB-based AuthenticationProvider. Adding your UserDetailsService should still be enough. Make sure to give every user a suitable role.
You have to remove this line from application.properties: codecamp.vaadin.security.standard-auth.enabled = false
This will re-enable the standard login without Keycloak via a Vaadin view.
Adapt the KeycloakRouteAccessDeniedHandler to ignore all test views that shouldn't be protected by Keycloak.
I already prepared all this in Gitlab repo and removed everything not important for the main point of this solution. See the individual commits and their diffs to also help focus in on the important bits.

Spring Boot Security Configuration OAuthSSO and ResourceServer

I have a WebApp consisting of 2 parts.
One is with a frontend (Vaadin) where i want the user to be Logged-In via OAuth2. I then Check whether the user has a certain Role or not. --> If user opens the URL he shall be redirected to the OAuthLogin automatically. --> This is working with the #EnableOAuthSso.
Second Part is the REST-API of the Application, which is found by anything under /api/*. fE. /api/devices
should give me a list if the Get-Request has a valid Bearer-Token. If the GET Request has no Bearer-Token or a wrong Role (Authority) if want to get a 403.
Now this is my configuration:
#Configuration
#EnableOAuth2Sso
public class ProdWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String ADMIN_ROLE= "role.global.admin";
private static final String READ_API_ROLE= "role.base.read.api";
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/login**", "/error**").permitAll()
.antMatchers("/*").hasAuthority(ADMIN_ROLE)
.antMatchers("/api/**").hasAnyAuthority(ADMIN_ROLE, READ_API_ROLE)
.and().logout().permitAll().logoutSuccessUrl(rootAuthUri + "/connect/endsession")
;
}
Now when opening for example /manageDevices in the Browser i get forced to be logged in via Auth-Code-Flow and everything works like as expected.
When i try to open /api/devices i also get forced to be logged in via Oauth. Even when i do send Http-Header with Authentication: Bearer xxxxx. Somehow it always forces me to the Login-Screen from my OAuth login.
application.properties these lines are defined:
base.rootauthuri=https://oauth2.mypage.ch
security.oauth2.client.clientId=client.base.parameters
security.oauth2.client.clientSecret=secret
security.oauth2.client.accessTokenUri=${base.rootauthuri}/connect/token
security.oauth2.client.userAuthorizationUri=${base.rootauthuri}/connect/authorize
security.oauth2.client.scope=openid,scope.base.parameters,role,offline_access
security.oauth2.client.clientAuthenticationScheme=form
security.oauth2.resource.userInfoUri=${base.rootauthuri}/connect/userinfo
How can i force everything under /api/* to not redirect to the AuthenticationForm but respond with 403 if no Bearer Token is sent. How can i make it to Check whether the Bearer-Token has Role "READ_API_ROLE" also.
I had the same question with SSO, I configured a ResourceServe for that:
#Configuration
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Autowired
private ResourceServerConfiguration configuration;
#PostConstruct
public void setSecurityConfigurerOrder() {
configuration.setOrder(3);
}
#Bean("resourceServerRequestMatcher")
public RequestMatcher resources() {
return new AntPathRequestMatcher("/api/**");
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/v1/**") // this is free resource
.and().authorizeRequests()
.antMatchers("/api/**").permitAll() // This is free resource for mvc calls
// Usado para paths que necessitam de token bearer
.and().requestMatchers().antMatchers("/integration/**")
.and().authorizeRequests()
.antMatchers("/integration/**").authenticated(); // this is protected resource, it's necessary token
}
}
I not configure WebSecurityConfigurerAdapter in my project;
Check this:
Spring Boot 1.3.3 #EnableResourceServer and #EnableOAuth2Sso at the same time
https://www.baeldung.com/spring-security-oauth2-enable-resource-server-vs-enable-oauth2-sso

Spring Security exclude URL on custom filter

#SuppressWarnings("SpringJavaAutowiringInspection")
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
public void configureAuthentication(AuthenticationManagerBuilder
authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService);
}
#Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
#Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/test").permitAll()
.antMatchers("/api/**").permitAll()
.anyRequest().authenticated();
httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
}
I have a custom filter that runs before Spring Security. I want to be able to exclude some URLs (like /test) from the filter and Spring Security and others to be intercepted (like /api/**).
When using postman to test localhost/test it still goes through the filter even though I have antMatchers("/test").permitAll().
How do I bypass the filter?
You can disable the Spring Security filter chain for some URLs, see WebSecurity#ignoring:
Allows adding RequestMatcher instances that should that Spring Security should ignore. Web Security provided by Spring Security (including the SecurityContext) will not be available on HttpServletRequest that match. Typically the requests that are registered should be that of only static resources. For requests that are dynamic, consider mapping the request to allow all users instead.
Example Usage:
webSecurityBuilder.ignoring()
// ignore all URLs that start with /resources/ or /static/
.antMatchers("/resources/**", "/static/**");
Therefore, you can override WebSecurityConfigurerAdapter#configure:
Override this method to configure WebSecurity. For example, if you wish to ignore certain requests.
To ignore path /test you have to add following method to your configuration:
public void configure​(WebSecurity web)
webSecurityBuilder
.ignoring()
.antMatchers("/test");
}

Spring OAuth and Boot Integration Test

What is the best way to run Spring Boot integration tests agains a OAuth Resource server configured web application.
I can think of two theoretical approaches:
Mock the security context in the resource server without acutally calling the Authorization server.
Embed the Authorization server as part of the test and redirect the authentication to it.
I was wondering how others have approach this problem.
This answer is very similar to the one provided by Ondrej, but is quite a bit simpler.
Spring Security 4 provides Test support. To use it ensure you have spring-security-test-4.0.2.RELEASE.jar (or newer version on your classpath). You will also want to ensure you are working with spring-test-4.1.0.RELEASE (or newer).
Next you can use MockMvc as the other answer indicates. However, if you setup MockMvc with the following:
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
#WebAppConfiguration
public class OAuthTests {
#Autowired
private WebApplicationContext context;
private MockMvc mvc;
#Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
// ADD THIS!!
.apply(springSecurity())
.build();
}
This makes it so
You no longer need to worry about running in stateless mode or not
It also means you do not need to use apply(springSecurity()) as indicated in the other answer.
In short, you should be able to do something like this:
#Test
#WithSecurityContext('user')
public void performOAuth() throws Exception {
...
// No need for apply(security())!!
restParcelMockMvc.perform(get("/api/some-resource"))
.andExpect(...);
}
I'd encourage you to read through the rest of the Spring Security Testing section of the reference as it provides lots of additional details including how to use custom authentication.
I use spring security 4.x #WithSecurityContext('user') annotation to create mock SecurityContext with 'user' logged in. Then when calling my REST API using MockMvc I retrieve SecurityContext and attach it to the call.
Like this:
#Test
#Transactional
#WithSecurityContext('user')
public void getAllParcels() throws Exception {
// Initialize the database
long size = parcelRepository.count();
parcelRepository.saveAndFlush(parcel);
// Get all the parcels
restParcelMockMvc.perform(get("/api/parcels").with(security()))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.[" + size + "].id").value(parcel.getId()))
.andExpect(jsonPath("$.[" + size + "].lot").value(DEFAULT_LOT))
.andExpect(jsonPath("$.[" + size + "].localName").value(DEFAULT_LOCAL_NAME));
}
where security() is static method:
public static RequestPostProcessor security() {
return SecurityMockMvcRequestPostProcessors.securityContext(SecurityContextHolder.getContext());
}
So using #WithSecurityContext('user') mock SecurityContext with authenticated user with login 'user' is created for my test method. Then in that method I retrieve this mock SecurityContext and attach it to the REST API call to make my oAuth think user is allready authenticated. It's basically the first approach you suggested in your question.
For this to work you must switch your OAuth to be statefull for the tests. Otherwise it won't work.
ie like this:
#Configuration
public class OAuth2ServerConfiguration {
#Configuration
#EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Autowired(required = false)
#Qualifier("oauth2StatelessSecurityContext")
private Boolean stateless = Boolean.TRUE; // STATEFUL switching for tests!
#Inject
private Http401UnauthorizedEntryPoint authenticationEntryPoint;
#Inject
private AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler;
#Override
public void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler)
.and()
.csrf()
.requireCsrfProtectionMatcher(new AntPathRequestMatcher("/oauth/authorize"))
.disable()
.headers()
.frameOptions().disable().and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/authenticate").permitAll()
.antMatchers("/api/register").permitAll()
.antMatchers("/api/logs/**").hasAnyAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api/**").authenticated()
.antMatchers("/metrics/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/health/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/trace/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/dump/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/shutdown/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/beans/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/configprops/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/info/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/autoconfig/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/env/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/trace/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api-docs/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/protected/**").authenticated();
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.stateless(stateless);
super.configure(resources);
}
}
...
You see my stateless property which gets injected only in tests. In normal run it uses it's default value true (so it's stateless). For tests I declare oauth2StatelessSecurityContext Bean with value false so it turns statefull for tests.
I define this configuration for tests:
#Configuration
public class OAuth2Statefull {
#Bean
#Primary
public Boolean oauth2StatelessSecurityContext() {
return Boolean.FALSE;
}
}
That's how I did it. I hope my explanation is understandable.

log access denied events with Spring Security and J2EE container authentication

I've got spring security configured as
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = false)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.jee()
.mappableRoles("ROLE1", "ROLE2");
}
}
And then #Secured annotations with roles on the rest endpoints.
Doesn't matter what I do I don't seem to be able to create a custom handler for authorization (i.e. a user logged in successfully but doesn't have the right role to access a particular endpoint) error events.
What I tried was:
An exception handler with #ExceptionHandler(value = AccessDeniedException.class) - doesn't get called. I understand that's by design, ok.
AuthenticationEntryPoint configured as
http.exceptionHandling().authenticationEntryPoint(new RestAuthenticationEntryPoint())
#Component( "restAuthenticationEntryPoint" )
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence( HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException ) throws IOException {
// logging
}
}
-doesn't get called
ApplicationListener - I can see it's getting called on context closed, so it's registered correctly but not called on authorization error.
All I need is a simple handler to log unsuccessful authorization events.
It completely slipped my mind that the allowed roles are listed in web.xml as well for j2ee container authentication to work. So any user without a least one of those roles was just being rejected by the container.
Otherwise the first, simplest, method works fine. Hopefully my mistake will help someone in the future

Resources