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"));
}
}