Search code examples
javaspring-bootspring-securityvaadin7

Spring Boot Security with Vaadin Login


I try to build an application based on Spring Boot (1.2.7.RELEASE) and Vaadin (7.6.3). My problem is that I'm not able to integrate Spring Security with Vaadin. I want a custom Vaadin built LoginScreen and Spring Security control. My project setup is as follows:

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().
                exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")).accessDeniedPage("/accessDenied")
                .and().authorizeRequests()
                .antMatchers("/VAADIN/**", "/PUSH/**", "/UIDL/**", "/login", "/login/**", "/error/**", "/accessDenied/**", "/vaadinServlet/**").permitAll()
                .antMatchers("/authorized", "/**").fullyAuthenticated();
    }
}

And here is my Vaadin login UI

 @SpringUI(path = "/login")
    @Title("LoginPage")
    @Theme("valo")
    public class LoginUI extends UI {

        TextField user;
        PasswordField password;
        Button loginButton = new Button("Login", this::loginButtonClick);
        private static final String username = "username";
        private static final String passwordValue = "test123";

        @Override
        protected void init(VaadinRequest request) {
            setSizeFull();

            user = new TextField("User:");
            user.setWidth("300px");
            user.setRequired(true);
            user.setInputPrompt("Your username");

            password = new PasswordField("Password:");
            password.setWidth("300px");
            password.setRequired(true);
            password.setValue("");
            password.setNullRepresentation("");

            VerticalLayout fields = new VerticalLayout(user, password, loginButton);
            fields.setCaption("Please login to access the application");
            fields.setSpacing(true);
            fields.setMargin(new MarginInfo(true, true, true, false));
            fields.setSizeUndefined();

            VerticalLayout uiLayout = new VerticalLayout(fields);
            uiLayout.setSizeFull();
            uiLayout.setComponentAlignment(fields, Alignment.MIDDLE_CENTER);
            setStyleName(Reindeer.LAYOUT_BLUE);
            setFocusedComponent(user);

            setContent(uiLayout);
        }

        public void loginButtonClick(Button.ClickEvent e) {
           //authorize/authenticate user
           //tell spring that my user is authenticated and dispatch to my mainUI
        }

    }

When I start my application spring redirects me to my login UI, which is fine.

But I don't know how to authenticate the user against the spring security mechanism and dispatch to my mainUI.

I'm also facing the problem with csrf tokens, if I don't disable csrf I'll get the crfs token is null exception. I found a lot of examples handling those problems but there is no solution provided with Vaadin.

Thanks for help.


Solution

  • After a week of struggle and research, I was able to get this working. It was very exhausting because there a lot of information and solutions on the internet, most of them using xml based configuration or JSP form based login, until now I couldn't find another solution without a xml config file using the Vaadin framework to create a custom login page.

    I cannot guarantee that this is best practice or the easiest solution. Moreover I didn't evaluate every part of it, the login mechanism works as far as I can see but maybe there could be some problems which I haven't discovered yet.

    Maybe it'll help someone who face the same problem so I'll post my answer here.

    First of all my securityConfig:

    @Resource(name = "authService")
    private UserDetailsService userDetailsService;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
                    http.csrf().disable().
                            exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")).accessDeniedPage("/accessDenied")
                            .and().authorizeRequests()
                            .antMatchers("/VAADIN/**", "/PUSH/**", "/UIDL/**", "/login", "/login/**", "/error/**", "/accessDenied/**", "/vaadinServlet/**").permitAll()
                            .antMatchers("/authorized", "/**").fullyAuthenticated();
                }
    
                @Bean
                public DaoAuthenticationProvider createDaoAuthenticationProvider() {
                    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    
                    provider.setUserDetailsService(userDetailsService);
                    provider.setPasswordEncoder(passwordEncoder());
                    return provider;
                }
    
                @Bean
                public BCryptPasswordEncoder passwordEncoder() {
                    return new BCryptPasswordEncoder();
                }
    

    You have to disable crsf but that's no problem since vaadin has its own crsf protection.

    Furthermore you need to permit some URIs so vaadin can access its resources: /VAADIN/** is absolutely necessary, I would also reccommend to allow /vaadinServlet/**, /PUSH/** and /HEARTBEAT/**, but it depends on what parts of Vaadin you use.

    Second my UserDetailsService:

    @Service("authService")
    public class AuthService implements UserDetailsService {
    
            @Autowired
            CustomUserRepository userRepository;
    
            @Override
            public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
                CustomUser user = userRepository.findCustomUserByUserName(userName);
    
                return user;
            }
    
    }
    

    The DaoAuthenticationProvider uses the UserDetails' loadUserByUserName method to get an object of a class which implements the UserDetails interface. Be aware that every attribute described in the UserDetailsInterface must not be null, otherwise you get a NullPointerException thrown by the DaoAuthenticationProvider later.

    I created a JPA Entity which implements the UserDetails interface:

    @Entity
    public class CustomUser implements UserDetails {
    
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            Long id;
            @ManyToMany(fetch = FetchType.EAGER)
            Collection<Authorities> authorities;
            String password;
            String userName;
            Boolean accountNonExpired;
            Boolean accountNonLocked;
            Boolean credentialsNonExpired;
            Boolean enabled;
    
            @Autowired
            @Transient
            BCryptPasswordEncoder passwordEncoder;
    
            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return authorities;
            }
    
            @Override
            public String getPassword() {
                return password;
            }
    
            @Override
            public String getUsername() {
                return userName;
            }
    
            @Override
            public boolean isAccountNonExpired() {
                return accountNonExpired;
            }
    
            @Override
            public boolean isAccountNonLocked() {
                return accountNonLocked;
            }
    
            @Override
            public boolean isCredentialsNonExpired() {
                return credentialsNonExpired;
            }
    
            @Override
            public boolean isEnabled() {
                return enabled;
            }
    
            public void setId(Long id) {
                this.id = id;
            }
    
            public void setAuthorities(Collection<Authorities> authorities) {
                this.authorities = authorities;
            }
    
            public void setPassword(String password) {
                this.password = passwordEncoder.encode(password);
            }
    
            public void setUserName(String userName) {
                this.userName = userName;
            }
    
            public void setAccountNonExpired(Boolean accountNonExpired) {
                this.accountNonExpired = accountNonExpired;
            }
    
            public void setAccountNonLocked(Boolean accountNonLocked) {
                this.accountNonLocked = accountNonLocked;
            }
    
            public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
                this.credentialsNonExpired = credentialsNonExpired;
            }
    
            public void setEnabled(Boolean enabled) {
                this.enabled = enabled;
            }
    
        }
    

    Plus the Authorities Entity:

    @Entity
    public class Authorities implements GrantedAuthority {
    
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            Long id;
    
            String authority;
    
            @Override
            public String getAuthority() {
                return authority;
            }
    
            public void setAuthority(String authority) {
                this.authority = authority;
            }
    
    }
    

    Obviously you'll have to store some user data in the database first before the authentication will work.

    In Vaadin I couldn't get it worked by using one UI with different views, so I ended up using two UIs one for login and another for the main application.

    In Vaadin I could set the URI path in the class annotation:

    @SpringUI(path = "/login")
    @Title("LoginPage")
    @Theme("valo")
    public class LoginUI extends UI {
      //...
    }
    

    With this configuration my login screen is available at localhost:port/login and my main application at localhost:port/main.

    I login the user programmatically within a button.click method in my loginUI:

    Authentication auth = new UsernamePasswordAuthenticationToken(userName.getValue(),password.getValue());
    Authentication authenticated = daoAuthenticationProvider.authenticate(auth);
                SecurityContextHolder.getContext().setAuthentication(authenticated);
    
    //redirect to main application
    getPage().setLocation("/main");
    

    I hope it helped some of you.