How to use my own authentication mechanism?

Hello,

I am testing Vaadin Flow (I mostly used Vaadin 8 until now) and I like the new login screen and the security that is underneath.

My issue is that my application don’t use a database directly, but an api wrapped in a library.
This api provides a login method that will check the password provided and will return some errors if there is indeed a problem.

The problems can be more than just a wrong username or password.

So I would like to know how I can integrate this api smoothly with the security layer, and also how I can display a custom message in the login view.

I tried to implement my own PasswordEncoder, UserDetailsService and some more, but even if I indeed find a way to check the password with my own encoder, this login method of my api is not used.

I tried to add a LoginListener in the login view, but this event is only fired after the password have been verified, so some case of the errors that can occurs in my api can’t be used.

I am not sure where to search.

Thanks for your help.

Assuming you’re using Spring Security, you should check out the docs here: Security | Build a Flow UI | Tutorial | Getting Started | Vaadin Docs, especially the “Add Security Configuration” part.

I had to implement a login with Nuclos ERP backend. This consumes the Nuclos API and stores the session id within the spring authentication.

I did not use any more error messages in the client but the default. Maybe when an error is thrown it will be given to the login view or you can save the message yourself and push it the client view…

Here is my implementation:
First create your auth bean

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

public class NuclosAuthentication extends AbstractAuthenticationToken {

  private String sessionId;
  private String principal;
  private String password;

  public NuclosAuthentication(Object username, Object password, Collection<? extends GrantedAuthority> authorities) {
    super(authorities);
    this.principal = username.toString();
    this.password = password.toString();
  }

  @Override
  public String getCredentials() {
    return password;
  }

  @Override
  public String getPrincipal() {
    return principal;
  }

  public String getSessionId() {
    return sessionId;
  }

  public void setSessionId(String sessionId) {
    this.sessionId = sessionId;
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof NuclosAuthentication test)) {
      return false;
    }
    if ((this.sessionId == null) && (test.getSessionId() != null)) {
      return false;
    }
    if ((this.sessionId != null) && (test.getSessionId() == null)) {
      return false;
    }
    if ((this.sessionId != null) && (!this.sessionId.equals(test.getSessionId()))) {
      return false;
    }
    return super.equals(obj);
  }

  @Override
  public int hashCode() {
    int code = super.hashCode();
    if (this.getDetails() != null) {
      code ^= this.getSessionId().hashCode();
    }
    return code;
  }
}

Then create request and response classes for your API if needed. In my case they are called AuthenticationRequest and AuthenticationResponse. With Nuclos the requesst contains a username, password and locale. The Response contains user data from the backend and the session id (JSESSIOINID).

Then create a spring AuthenticationManager. Mine is named “wrong”. The interface AuthenticationProvider should not be required.

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;

public class NuclosAuthenticationProvider implements AuthenticationProvider, AuthenticationManager {

    private RestTemplate restTemplate = new RestTemplate();
    private String nuclosHostUrl;

    public NuclosAuthenticationProvider(String nuclosHostUrl) {
        this.nuclosHostUrl = nuclosHostUrl;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        final String username = authentication.getPrincipal().toString();
        final String password = authentication.getCredentials().toString();

        final var nuclosAuthentication = new NuclosAuthentication(username, password, null);

        try {

            // send auth to nuclos
            AuthenticationRequest authenticationRequest = new AuthenticationRequest();
            authenticationRequest.setUsername(username);
            authenticationRequest.setPassword(password);
            authenticationRequest.setLocale("en");
            AuthenticationResponse authenticationResponse = restTemplate.postForObject(nuclosHostUrl,
                    authenticationRequest, AuthenticationResponse.class);
            if (authenticationResponse != null) {
                nuclosAuthentication.setDetails(authenticationResponse);
                nuclosAuthentication.setSessionId(authenticationResponse.getSessionId());
                nuclosAuthentication.setAuthenticated(true);
            }

        } catch (Exception e) {
            throw new BadCredentialsException("Could not login.", e);
        }

        return nuclosAuthentication;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return NuclosAuthentication.class.isAssignableFrom(authentication);
    }

}

Then update your VaadinWebSecurity implementation and create the method authManager:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.web.socket.client.standard.WebSocketContainerFactoryBean;

import com.vaadin.flow.spring.security.VaadinWebSecurity;

import jakarta.annotation.PostConstruct;

@EnableWebSecurity
@Configuration
@EnableScheduling
public class SecurityConfig extends VaadinWebSecurity {

  @Value("${nuclos.api.auth}")
  private String nuclosHostUrl;
  private NuclosAuthenticationProvider auth;

  @PostConstruct
  public void init() {
    this.auth = new NuclosAuthenticationProvider(nuclosHostUrl);
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(matcher -> {
      // static resource
      matcher.requestMatchers("/img/**").permitAll();
      matcher.requestMatchers("/fonts/**").permitAll();
    });
    super.configure(http);
    setLoginView(http, "/login");
  }

  @Bean
  public AuthenticationManager authManager(HttpSecurity http) {
    return auth;
  }

}

Then later on, when consuming the backend API via spring WebClient, I add the session id to the cookies. There you may need another implementation.

    public <T> T get(String path, Class<T> returnObject, Object... uriVariables) {
        return nuclosWebClient.get().uri(path, uriVariables).cookies(this::addAuthCookies).retrieve()
                .bodyToMono(returnObject).block();
    }

    protected void addAuthCookies(MultiValueMap<String, String> cookies) {
        NuclosAuthentication auth = (NuclosAuthentication) SecurityContextHolder.getContext().getAuthentication();
        if (auth != null) {
            cookies.put(JSESSIONID, Arrays.asList(auth.getSessionId()));
            cookies.put(LOCALE, Arrays.asList("en"));
        }
    }
1 Like

Thank you so much, I yet to understand some things but this is a very nice help.

I still have issues as if I enter wrong credentials it redirects me to an empty page, and I am not sure how to code the logout action, but I should find.

Happy to help!

Did you use the LoginView from start.vaadin.com?

For logout you could do:

  • click on logout button
  • route to a LogoutView.class (that you have to create)
  • then call there in the AfterNavigation method (maybe after an short delay, so the data can be pushed to the client before the session invalidates):
UI.getCurrent().getSession().close();
VaadinRequest vaadinRequest = VaadinService.getCurrentRequest();
if (vaadinRequest != null) {
	vaadinRequest.getWrappedSession().invalidate();
}

We use this is as a non-spring solution. But I think spring has some build-in ways to do that. We offer a button in the LogoutView that routes back to the LoginView. Not the best solution but logs out the user really clean.

1 Like

I found how to make the login view display the right message when the credentials are not right, it’s in the documentation of the AuthenticationManager interface, we must throw the right exception.

I am indeed using the loginview from the start app. I will do what you suggest.

Thank you so much for your help.

Great!
I just wanted to make sure how you implemented the LoginView, but in the start app it is implemented well.

1 Like