Multiple login option using OAuth and Username/Password

Hi,

I am trying to enable two different login options for users.

  1. The user can use their Google account using the OAuth; I plan to create a local user using an API with the information that comes back.
  2. The user can use the local user account created to log in.

When the user logs in using OAuth, the app redirects to the Home view without problem. The issue occurs when the user inputs a username and password after being redirected to the Home view, and the app automatically returns to the Login View.

Can you please send me information on how I can fix the issue?

Best regards,

Luis Laurentino

Please share your security configuration

Hi Simon, I cannot attach any files.
I do have a zip that has the java files.

Regards,

Luis.

Just paste the code of the configuration class

Here,

@EnableWebSecurity
@Configuration
public class AppSecurityConfig extends VaadinWebSecurity {
    private final Logger logger = LoggerFactory.getLogger(AppSecurityConfig.class.getSimpleName());

    @Autowired
    private UsersService userService;
    @Autowired
    private ServletContext servletContext;


    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth.requestMatchers(
                        new AntPathRequestMatcher("/", "/auth/login/**"),
                        new AntPathRequestMatcher("/public/**"),
                        new AntPathRequestMatcher("/oauth2/authorization/google"))
                .permitAll()
                .requestMatchers("/home").authenticated());
        http.oauth2Login(oauthLogin ->
                oauthLogin.loginPage("/auth/login")
                        .successHandler((request, response, authentication) -> {
                            logger.trace("Authentication success");

                            DefaultOidcUser userLogged = (DefaultOidcUser) authentication.getPrincipal();
                            AppUsers userInfo = new AppUsers()
                                    .setUserName(userLogged.getAttribute("given_name"))
                                    .setFullName(userLogged.getAttribute("name"))
                                    .setUserPassword("")
                                    .setEmail(userLogged.getAttribute("email"))
                                    .setImageLink(userLogged.getAttribute("picture"))
                                    .setRoles(new ArrayList<>(List.of("USER")));

//                            final SecurityContext context = SecurityContextHolder.createEmptyContext();
                            final SecurityContext context = SecurityContextHolder.getContext();
                            final Authentication auth = new UsernamePasswordAuthenticationToken(userInfo, userInfo.getPassword(), userInfo.getAuthorities());
                            context.setAuthentication(auth);
//                            SecurityContextHolder.setContext(context);

                            if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
                                response.sendRedirect(servletContext.getContextPath() + "/home");
                            } else {
                                throw new UsernameNotFoundException("Access denied, invalid credentials.");
                            }
                        }));
        super.configure(http);
        setLoginView(http, LoginView.class);
    }
}

Hi @SimonMartinelli,

Do you have updates on this?

Regards,

Luis Laurentino

With newer versions of Spring security I’ve run into similar issue with ‘manual authentication’ because security context has been set, but not saved.
I haven’t look too much into it, you can try the Spring docs (maybe requireExplicitSave() could be of use), but here’s what I did.

@Autowired
private SecurityContextRepository securityContextRepository;
...
SecurityContextHolder.getContext().setAuthentication(authentication);
securityContextRepository.saveContext(SecurityContextHolder.getContext(), getRequest(), getResponse());
...
private static HttpServletRequest getRequest() {
    return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
}

private static HttpServletResponse getResponse() {
    return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
}
...
@Bean
public SecurityContextRepository securityContextRepository() {
    return new DelegatingSecurityContextRepository(
            new RequestAttributeSecurityContextRepository(),
            new HttpSessionSecurityContextRepository()
    );
}

Not sure if all parts are needed for you.

Thank you Herberts.

I tested saving the security context and the behavior is the same as before.

I set the LOG as DEBUG to send you what it occurs.
After move to MainView it show a message “UI closed with a beacon request”, and it backs to LoginView.

See lines below. (Reference timestamp 2024-03-28 12:02:35.149)

Regards,

Luis

2024-03-28 12:02:14.329 [nio-8080-exec-4] DEBUG 210904 o.v.e.LoginView                          : lc - constructing base LoginView class
2024-03-28 12:02:14.341 [nio-8080-exec-4]  INFO 210904 o.v.e.LoginView                          : After navigation - login view
2024-03-28 12:02:14.351 [nio-8080-exec-4] DEBUG 210904 c.v.f.s.c.UidlWriter                     : * Creating response to client
2024-03-28 12:02:14.461 [nio-8080-exec-5] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:14.468 [nio-8080-exec-5] DEBUG 210904 c.v.f.s.c.UidlWriter                     : * Creating response to client
2024-03-28 12:02:14.653 [nio-8080-exec-1] DEBUG 210904 c.v.b.d.s.DevModeUsageStatistics         : Received client usage statistics from the browser
2024-03-28 12:02:14.653 [nio-8080-exec-1] DEBUG 210904 c.v.b.d.s.StatisticsStorage              : Reading statistics from C:\Users\LuisLaurentino\.vaadin\usage-statistics.json
2024-03-28 12:02:14.673 [nio-8080-exec-7] DEBUG 210904 c.v.b.d.s.DevModeUsageStatistics         : Received client usage statistics from the browser
2024-03-28 12:02:14.674 [nio-8080-exec-7] DEBUG 210904 c.v.b.d.s.StatisticsStorage              : Reading statistics from C:\Users\LuisLaurentino\.vaadin\usage-statistics.json
2024-03-28 12:02:14.810 [nio-8080-exec-1] DEBUG 210904 c.v.b.d.s.DevModeUsageStatistics         : Received client usage statistics from the browser
2024-03-28 12:02:14.810 [nio-8080-exec-1] DEBUG 210904 c.v.b.d.s.StatisticsStorage              : Reading statistics from C:\Users\LuisLaurentino\.vaadin\usage-statistics.json
2024-03-28 12:02:15.838 [io-8080-exec-10] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:15.849 [nio-8080-exec-2] DEBUG 210904 c.v.b.d.s.DevModeUsageStatistics         : Received client usage statistics from the browser
2024-03-28 12:02:15.849 [nio-8080-exec-2] DEBUG 210904 c.v.b.d.s.StatisticsStorage              : Reading statistics from C:\Users\LuisLaurentino\.vaadin\usage-statistics.json
2024-03-28 12:02:15.860 [nio-8080-exec-1] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:16.142 [nio-8080-exec-7] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:16.162 [nio-8080-exec-9] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:32.108 [nio-8080-exec-8] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:32.110 [nio-8080-exec-3] DEBUG 210904 c.v.f.s.s.VaadinDefaultRequestCache      : Saving request to /login
2024-03-28 12:02:35.059 [nio-8080-exec-4] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:35.077 [nio-8080-exec-8] DEBUG 210904 c.v.f.s.a.NavigationAccessControl        : Checking access for view org.vaadin.example.MainView
2024-03-28 12:02:35.078 [nio-8080-exec-8] DEBUG 210904 c.v.f.s.a.AnnotatedViewAccessChecker     : Access to view 'org.vaadin.example.MainView' with path 'home' is allowed
2024-03-28 12:02:35.080 [nio-8080-exec-8] DEBUG 210904 f.s.a.DefaultAccessCheckDecisionResolver : Access to view 'org.vaadin.example.MainView' with path 'home' allowed by 1 out of 1 navigation checkers  (0 neutral).
2024-03-28 12:02:35.080 [nio-8080-exec-8] DEBUG 210904 c.v.f.s.a.NavigationAccessControl        : Decision against 1 checker results: Access decision: ALLOW
2024-03-28 12:02:35.108 [nio-8080-exec-8] DEBUG 210904 c.v.f.s.c.UidlWriter                     : * Creating response to client
2024-03-28 12:02:35.145 [nio-8080-exec-5] DEBUG 210904 c.v.f.s.c.PushHandler                    : Could not get UI. This should never happen, except when reloading in Firefox and Chrome - see http://dev.vaadin.com/ticket/14251.
2024-03-28 12:02:35.147 [io-8080-exec-10] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:35.149 [io-8080-exec-10] DEBUG 210904 c.v.f.s.c.ServerRpcHandler               : UI closed with a beacon request
2024-03-28 12:02:35.149 [io-8080-exec-10] DEBUG 210904 c.v.f.s.c.UidlWriter                     : * Creating response to client
2024-03-28 12:02:35.151 [io-8080-exec-10] DEBUG 210904 c.v.f.s.VaadinService                    : Removing closed UI 0
2024-03-28 12:02:35.180 [nio-8080-exec-2] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:35.220 [nio-8080-exec-7] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:35.223 [nio-8080-exec-7] DEBUG 210904 c.v.f.s.c.UidlWriter                     : * Creating response to client
2024-03-28 12:02:35.223 [nio-8080-exec-7] DEBUG 210904 c.v.f.s.BootstrapHandler                 : Initial UIDL: [object Object]
2024-03-28 12:02:35.243 [io-8080-exec-10] DEBUG 210904 c.v.b.d.s.DevModeUsageStatistics         : Received client usage statistics from the browser
2024-03-28 12:02:35.243 [io-8080-exec-10] DEBUG 210904 c.v.b.d.s.StatisticsStorage              : Reading statistics from C:\Users\LuisLaurentino\.vaadin\usage-statistics.json
2024-03-28 12:02:35.509 [nio-8080-exec-2] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:35.512 [nio-8080-exec-2] DEBUG 210904 c.v.f.s.a.NavigationAccessControl        : Checking access for view org.vaadin.example.LoginView
2024-03-28 12:02:35.512 [nio-8080-exec-2] DEBUG 210904 c.v.f.s.a.NavigationAccessControl        : Allowing access for login view org.vaadin.example.LoginView
2024-03-28 12:02:35.513 [nio-8080-exec-2] DEBUG 210904 o.v.e.LoginView                          : lc - constructing base LoginView class
2024-03-28 12:02:35.514 [nio-8080-exec-2]  INFO 210904 o.v.e.LoginView                          : After navigation - login view
2024-03-28 12:02:35.514 [nio-8080-exec-2] DEBUG 210904 c.v.f.s.c.UidlWriter                     : * Creating response to client
2024-03-28 12:02:35.583 [nio-8080-exec-6] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:35.584 [nio-8080-exec-6] DEBUG 210904 c.v.f.s.c.UidlWriter                     : * Creating response to client
2024-03-28 12:02:36.998 [nio-8080-exec-9] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1
2024-03-28 12:02:37.013 [nio-8080-exec-1] DEBUG 210904 o.s.w.s.h.AbstractHandlerMapping         : Mapped to org.springframework.web.servlet.mvc.ServletForwardingController@6d65a5e1

I’m a bit confused about the original text here:

The issue occurs when the user inputs a username and password after being redirected to the Home view, and the app automatically returns to the Login View.

Do I understand correctly, that first OAuth occurs, after which user is redirected to Home view, where they can input username and password? Is this to create a “local user” or its to re-authenticate as a different user?

Either way, seems like you have only provided code for how OAuth occurs, but not how local user authentication occurs.

Yes, your understanding is correct.

  1. Allow the user to connect using the OAUTH account (Google, Azure, Custom).
  2. The first time, the user must log in using OAuth, and the service will generate a local account using the person’s information returned. The service will then send an email with the temporary username and password.
  3. Next time, the user can log in using either the local account or OAUTH (which will look up the local account, “successHandler”).

I did not send the other classes because I could not attach the files.
Below, I included the source code for all the classes.

Best regards,

Luis Laurentino

AppSecurityConfig.class

@EnableWebSecurity
@Configuration
public class AppSecurityConfig extends VaadinWebSecurity {
    private final Logger logger = LoggerFactory.getLogger(AppSecurityConfig.class.getSimpleName());

    @Autowired
    private UsersService userService;
    @Autowired
    private ServletContext servletContext;


    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth.requestMatchers(
                        new AntPathRequestMatcher("/", "/auth/login/**"),
                        new AntPathRequestMatcher("/public/**"),
                        new AntPathRequestMatcher("/oauth2/authorization/google"))
                .permitAll()
                .requestMatchers("/home").authenticated());
        http.oauth2Login(oauthLogin ->
                oauthLogin.loginPage("/auth/login")
                        .successHandler((request, response, authentication) -> {
                            logger.trace("Authentication success");

                            DefaultOidcUser userLogged = (DefaultOidcUser) authentication.getPrincipal();
                            AppUsers userInfo = new AppUsers()
                                    .setUserName(userLogged.getAttribute("given_name"))
                                    .setFullName(userLogged.getAttribute("name"))
                                    .setUserPassword("")
                                    .setEmail(userLogged.getAttribute("email"))
                                    .setImageLink(userLogged.getAttribute("picture"))
                                    .setRoles(new ArrayList<>(List.of("USER")));

//                            final SecurityContext context = SecurityContextHolder.createEmptyContext();
                            final SecurityContext context = SecurityContextHolder.getContext();
                            final Authentication auth = new UsernamePasswordAuthenticationToken(userInfo, userInfo.getPassword(), userInfo.getAuthorities());
                            context.setAuthentication(auth);
//                            SecurityContextHolder.setContext(context);

                            if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
                                response.sendRedirect(servletContext.getContextPath() + "/home");
                            } else {
                                throw new UsernameNotFoundException("Access denied, invalid credentials.");
                            }
                        }));
        super.configure(http);
        setLoginView(http, LoginView.class);
    }
}

AppUsers.class

public class AppUsers implements UserDetails, OAuth2User {

    private UUID userID;
    private String userName;
    private String fullName;
    private String email;
    private String disabledAt;
    private String token;
    private Boolean temporaryLocked;
    private String userPassword;
    private String pwdExpirationDate;
    private String imageLink;
    private List<LastConnection> connectionList;
    private List<UserGroups> userProfiles;
    private ArrayList<String> roles;

    public AppUsers(String userName, String token, String role) {
        this.userName = userName;
        this.token = token;
        if (this.roles == null)
            this.roles = new ArrayList<>();
        this.roles.add(role);
    }

    public AppUsers() {
    }

    public String getUserName() {
        return userName;
    }

    public AppUsers setUserName(String userName) {
        this.userName = userName;
        return this;
    }

    public String getToken() {
        return token;
    }

    public AppUsers setToken(String token) {
        this.token = token;
        return this;
    }

    public ArrayList<String> getRoles() {
        if (roles == null && !this.userProfiles.isEmpty()) {
            setRoles(null);
        }
        return roles;
    }

    public AppUsers setRoles(ArrayList<String> roles) {
        if (roles == null && this.userProfiles != null && !this.userProfiles.isEmpty()) {
            this.roles = new ArrayList<>();
            this.userProfiles.forEach((userProfile) -> this.roles.add(userProfile.getGroupName()));
        } else {
            this.roles = roles;
        }
        return this;
    }

    public UUID getUserID() {
        return userID;
    }

    public AppUsers setUserID(UUID userId) {
        this.userID = userId;
        return this;
    }

    public String getFullName() {
        return fullName;
    }

    public AppUsers setFullName(String fullName) {
        this.fullName = fullName;
        return this;
    }

    public String getEmail() {
        return email;
    }

    public AppUsers setEmail(String email) {
        this.email = email;
        return this;
    }

    public String getDisabledAt() {
        return disabledAt;
    }

    public AppUsers setDisabledAt(String disabledAt) {
        this.disabledAt = disabledAt;
        return this;
    }

    public Boolean getTemporaryLocked() {
        return temporaryLocked;
    }

    public AppUsers setTemporaryLocked(Boolean temporaryLocked) {
        this.temporaryLocked = temporaryLocked;
        return this;
    }

    public String getUserPassword() {
        return userPassword;
    }

    public AppUsers setUserPassword(String userPassword) {
        this.userPassword = userPassword;
        return this;
    }

    public String getPwdExpirationDate() {
        return pwdExpirationDate;
    }

    public AppUsers setPwdExpirationDate(String pwdExpirationDate) {
        this.pwdExpirationDate = pwdExpirationDate;
        return this;
    }

    public List<LastConnection> getConnectionList() {
        return connectionList;
    }

    public AppUsers setConnectionList(List<LastConnection> connectionList) {
        this.connectionList = connectionList;
        return this;
    }

    public List<UserGroups> getUserProfiles() {
        return userProfiles;
    }

    public AppUsers setUserProfiles(List<UserGroups> userProfiles) {
        this.userProfiles = userProfiles;
        return this;
    }

    public String getImageLink() {
        return imageLink;
    }

    public AppUsers setImageLink(String imageLink) {
        this.imageLink = imageLink;
        return this;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return Map.of();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthority = new ArrayList<>();
        roles.forEach(role -> grantedAuthority.add(new SimpleGrantedAuthority(role)));
        return grantedAuthority;
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getUsername() {
        return userName;
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public String getName() {
        return "";
    }

    private static class LastConnection {
        private String sourceIP;
        private String timestamp;

        public String getSourceIP() {
            return sourceIP;
        }

        public LastConnection setSourceIP(String sourceIP) {
            this.sourceIP = sourceIP;
            return this;
        }

        public String getTimestamp() {
            return timestamp;
        }

        public LastConnection setTimestamp(String timestamp) {
            this.timestamp = timestamp;
            return this;
        }
    }

    private static class UserGroups {
        private String profileID;
        private String groupID;
        private String groupName;
        private String disabledAt;

        public String getProfileID() {
            return profileID;
        }

        public UserGroups setProfileID(String profileID) {
            this.profileID = profileID;
            return this;
        }

        public String getGroupID() {
            return groupID;
        }

        public UserGroups setGroupID(String groupID) {
            this.groupID = groupID;
            return this;
        }

        public String getGroupName() {
            return groupName;
        }

        public UserGroups setGroupName(String groupName) {
            this.groupName = groupName;
            return this;
        }

        public String getDisabledAt() {
            return disabledAt;
        }

        public UserGroups setDisabledAt(String disabledAt) {
            this.disabledAt = disabledAt;
            return this;
        }
    }
}

CustomI18NProvider.class

@SpringComponent
public class CustomI18NProvider implements I18NProvider {

    private final Logger logger = LoggerFactory.getLogger(CustomI18NProvider.class.getSimpleName());
    private static final String BUNDLE_PREFIX = "i18n/translate";
    private final Locale LOCALE_EN = new Locale("en", "US");

    private List<Locale> locales = Collections
            .unmodifiableList(Arrays.asList(LOCALE_EN)); //Arrays.asList(Locale.getAvailableLocales()

    @Override
    public List<Locale> getProvidedLocales() {
        return locales;
    }

    @Override
    public String getTranslation(String key, Locale locale, Object... params) {
        if (key == null) {
            logger.warn("Got lang request for key with null value!");
            return "";
        }

        logger.info("Language :" + locale.getLanguage() + " Country :"+ locale.getCountry());

        ResourceBundle bundle;
        if (getProvidedLocales().contains(locale)) {
            bundle = ResourceBundle.getBundle(BUNDLE_PREFIX, locale);
        } else {
            bundle = ResourceBundle.getBundle(BUNDLE_PREFIX, LOCALE_EN);
        }

        String value;
        try {
            value = bundle.getString(key);
        } catch (final MissingResourceException e) {
            logger.warn("Missing resource", e);
            return "!" + locale.getLanguage() + ": " + key;
        }
        if (params.length > 0) {
            value = MessageFormat.format(value, params);
        }
        return value;
    }

}

LoginView.class

@Route("auth/login")
@RouteAlias(value="/")
@AnonymousAllowed
public class LoginView extends Composite<LoginOverlay> implements AfterNavigationObserver, BeforeEnterObserver {
    protected final Logger logger = LoggerFactory.getLogger(LoginView.class);
    private final UsersService userService;
    private final LoginI18n loginForm;
    private final ServletContext servletContext;


    /**
     * The first view presented to every user trying to access the application.
     */
    public LoginView(UsersService loginService, ServletContext servletContext) {
        this.userService = loginService;
        this.servletContext = servletContext;

        loginForm = LoginI18n.createDefault();
        getContent().addClassName("login-view");

        LoginI18n.Header loginHeader = new LoginI18n.Header();

        loginHeader.setTitle("Sample Title ");
        loginHeader.setDescription("Sample description");

        loginForm.setHeader(loginHeader);

        this.loginForm.getForm().setTitle(null);
        this.loginForm.getForm().setUsername("Username");
        this.loginForm.getForm().setPassword("Password");
        this.loginForm.getForm().setSubmit("Login");
        this.loginForm.getForm().setForgotPassword(null);

        this.loginForm.setAdditionalInformation("Please, contact your administrator if you're experiencing issues " +
                                                "logging into your account.");

        getContent().setForgotPasswordButtonVisible(false);
        getContent().setI18n(this.loginForm);

        Anchor googleUserLogin = new Anchor(servletContext.getContextPath() + "/oauth2/authorization/google",
                "Login using your google account");
        googleUserLogin.addClassName("oauth-login");
        googleUserLogin.getElement().setAttribute("router-ignore", true);
        getContent().getCustomFormArea().add(googleUserLogin);
        getContent().setOpened(true);
        getContent().getElement().setAttribute("no-autofocus", "");
        getContent().addLoginListener(this::verifyLogin);

        getContent().setOpened(true);
        getContent().setAction("login");
        logger.debug("lc - constructing base LoginView class");
    }

    private void verifyLogin(AbstractLogin.LoginEvent loginEvent) {
        try {
            SecurityContextHolder.getContext();
            String clientIP = UI.getCurrent().getSession().getBrowser().getAddress();
            var userInfo = userService.loadUser(loginEvent.getUsername(), loginEvent.getPassword(), clientIP);
            if (validateUserInformation(userInfo)) {
//                final SecurityContext context = SecurityContextHolder.createEmptyContext();
                final Authentication auth = new UsernamePasswordAuthenticationToken(userInfo, userInfo.getPassword(), userInfo.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(auth);
//                SecurityContextHolder.setContext(context);
                VaadinSession.getCurrent().getSession().setMaxInactiveInterval(300);
                UI.getCurrent().navigate(MainView.class);
                getContent().close();
            } else {
                UI.getCurrent().getPage().setLocation(servletContext.getContextPath() + "/auth/login?error");
            }
        } catch (Exception ex) {
            logger.error("Error validating user credentials.", ex);
            UI.getCurrent().getPage().setLocation(servletContext.getContextPath() + "/auth/login?error");
        }
    }

    @Override
    public void beforeEnter(BeforeEnterEvent beforeEnterEvent) {
        boolean hasError = false;

        Map<String, List<String>> parameters = beforeEnterEvent.getLocation().getQueryParameters().getParameters();
        if (parameters.isEmpty() ||
            (SecurityContextHolder.getContext().getAuthentication() instanceof AnonymousAuthenticationToken
             && parameters.containsKey("logout"))) {
            return;
        }

        if (parameters.containsKey("error")) {
            LoginI18n.ErrorMessage loginErrorMessage = loginForm.getErrorMessage();
            loginErrorMessage.setTitle("Invalid credentials");
            loginErrorMessage.setMessage("Check that you entered the correct information and try again.");
            loginForm.setErrorMessage(loginErrorMessage);
            hasError = true;

        }
//            VaadinSession.getCurrent().getSession().invalidate();
//            SecurityContextHolder.clearContext();
//            SecurityContextHolder.setContext(new SecurityContextImpl(new AnonymousAuthenticationToken("anonymousUser",
//                    "anonymousUser", Collections.singletonList(new SimpleGrantedAuthority("ROLE_ANONYMOUS")))));
//            new HttpSessionSecurityContextRepository().saveContext(SecurityContextHolder.getContext(),
//                    VaadinServletRequest.getCurrent(), VaadinServletResponse.getCurrent());
        if (hasError) {
            getContent().setI18n(loginForm);
            getContent().setError(true);
        }
    }

    private boolean validateUserInformation(AppUsers userAPIResponse) {
        if (userAPIResponse == null || userAPIResponse.getUserName() == null ||
            userAPIResponse.getUserName().isEmpty() || userAPIResponse.getAuthorities() == null ||
            userAPIResponse.getAuthorities().isEmpty()) {
            LoginI18n.ErrorMessage loginErrorMessage = loginForm.getErrorMessage();
            loginErrorMessage.setTitle("Invalid credentials");
            loginErrorMessage.setMessage("Access denied.");
            loginForm.setErrorMessage(loginErrorMessage);
            return false;
        } else if (userAPIResponse.getTemporaryLocked() != null && userAPIResponse.getTemporaryLocked()) {
            LoginI18n.ErrorMessage loginErrorMessage = loginForm.getErrorMessage();
            loginErrorMessage.setTitle("Account locked");
            loginErrorMessage.setMessage("Please contact your administrator.");
            loginForm.setErrorMessage(loginErrorMessage);
            return false;
        }
        return true;
    }

    @Override
    protected void onAttach(AttachEvent attachEvent) {
        super.onAttach(attachEvent);

    }

    @Override
    public void afterNavigation(AfterNavigationEvent afterNavigationEvent) {
        logger.info("After navigation - login view");
    }
}

MainView.class

@Route("/home")
@PermitAll
public class MainView extends VerticalLayout {

    /**
     * Construct a new Vaadin view.
     * <p>
     * Build the initial UI state for the user accessing the application.
     *
     * @param service
     *            The message service. Automatically injected Spring managed
     *            bean.
     */
    public MainView(@Autowired UsersService service) {

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        String givenName = "";
        String familyName = "";
        String picture = "";

        AppUsers principal = (AppUsers) authentication.getPrincipal();
        givenName = principal.getFullName();
        familyName = principal.getUserName();
        picture = principal.getImageLink();

        H2 header = new H2("Hello " + givenName + " (" + familyName + ")");
        Image image = new Image();
        image.setText("User Image");
        if (picture != null && !picture.isBlank() && !picture.isEmpty()) {
            image.setSrc(picture);
        }

        Button logoutButton = new Button("Logout", click -> {
            UI.getCurrent().getPage().setLocation("/auth/login");
            SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
            logoutHandler.logout(
                    VaadinServletRequest.getCurrent().getHttpServletRequest(), null,
                    null);
        });

        setAlignItems(Alignment.CENTER);
        add(header, image, logoutButton);
    }

}

UserService.class

@Service
public class UsersService implements UserDetailsService {

    @Override
    public AppUsers loadUserByUsername(String username) throws UsernameNotFoundException {
        throw new UsernameNotFoundException("Access denied, invalid credentials.");
    }

    public AppUsers loadUser(String username, String password, String ip) throws UsernameNotFoundException {
        AppUsers user = null;
        try {
            if (username.equalsIgnoreCase("admin")) {
                user = new AppUsers()
                        .setUserName("admin")
                        .setFullName("Administrator")
                        .setUserPassword("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
                        .setEmail("admin@test.net")
                        .setRoles(new ArrayList<>(List.of("ADMIN")));

            } else {
                user = new AppUsers();
                user.setUserName(username);
                user.setFullName(username);
                user.setUserPassword("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW");
                user.setRoles(new ArrayList<>(List.of("USER")));
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new UsernameNotFoundException("Access denied, invalid credentials.");
        }
        return user;
    }
}

Well, I’m not an expert on spring security, so hard to say where the problem is.
I see some commented out code that looks concerning:

//new HttpSessionSecurityContextRepository().saveContext(SecurityContextHolder.getContext(),
// VaadinServletRequest.getCurrent(), VaadinServletResponse.getCurrent());

I’m wondering if you did the same in success handler and verifyLogin(..) method, that you created new context repository instance. That doesn’t seem right to me.
Here’s what I expected the changes to look like:

@EnableWebSecurity
@Configuration
public class AppSecurityConfig extends VaadinWebSecurity {
    @Autowired
    private SecurityContextRepository securityContextRepository;

    protected void configure(HttpSecurity http) throws Exception {
        ....

        http.oauth2Login(oauthLogin ->
                oauthLogin.loginPage("/auth/login")
                        .successHandler((request, response, authentication) -> {
                             ....
                            final SecurityContext context = SecurityContextHolder.getContext();
                            final Authentication auth = new UsernamePasswordAuthenticationToken(userInfo, userInfo.getPassword(), userInfo.getAuthorities());
                            context.setAuthentication(auth);

                            securityContextRepository.saveContext(context, request, response);
                            ....
                ....
    }


    @Bean
    public SecurityContextRepository securityContextRepository() {
        return new DelegatingSecurityContextRepository(
                new RequestAttributeSecurityContextRepository(),
                new HttpSessionSecurityContextRepository()
        );
    }
}
@Route("auth/login")
@RouteAlias(value="/")
@AnonymousAllowed
public class LoginView extends Composite<LoginOverlay> implements AfterNavigationObserver, BeforeEnterObserver {
    ....
    private final SecurityContextRepository securityContextRepository;

    public LoginView(UsersService loginService, ServletContext servletContext, SecurityContextRepository securityContextRepository) {
        ....
        this.securityContextRepository = securityContextRepository;
        ....
    }

    private void verifyLogin(AbstractLogin.LoginEvent loginEvent) {
        ....
         final Authentication auth = new UsernamePasswordAuthenticationToken(userInfo, userInfo.getPassword(), userInfo.getAuthorities());
         SecurityContextHolder.getContext().setAuthentication(auth);
         ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); 
         securityContextRepository.saveContext(SecurityContextHolder.getContext(), requestAttributes.getRequest(), requestAttributes.getResponse());
        ....
    }
    ....
}

Also, strange that you have @RouteAlias(value="/") for the LoginView.
I would assume MainView would have a root path instead, the @PermitAll in theMainView annotation then ensures that user gets redirected to login page if they are not logged in.

The commented lines are different tries implemented to see if it works.

The problem is not the Security. The MainView appears on the browser and automatically returns to the login page.

The be more precise the line “UI.getCurrent().navigate(MainView.class);”

I was debugging the execution of the method navigate line by line, and the Vaadin class logs the message that I sent previously (“UI closed with a beacon request”).

The “/” on LoginView indicates that the user must log in to access all the other pages.

Regards,

Luis Laurentino

So you see the content of MainView for a bit and then it changes to login view, or do you only see a change in the URL (/home appearing in url and then switching to /auth/login) ?

You are navigating to MainView in two places, in success handler

response.sendRedirect(servletContext.getContextPath() + "/home");

and in LoginView

UI.getCurrent().navigate(MainView.class);

Maybe the fact that you change authentication in success handler, causes that issue to occur, since, you are already authenticated.

The “/” on LoginView indicates that the user must log in to access all the other pages.

It does not prevent anyone from accessing any view that is annotated with @AnonymousAllowed.

I see the content of MainView.

The code for the success handler will be used when the user connects using the OAuth configuration, and because I do not have the UI Object, I have to send the redirection from the response.

Correct, but it will redirect to login if the user just inputs the domain URL.

Hello Herberts,

I hope you’re doing well. I just wanted to inquire about the progress of resolving the issue we previously discussed. I would greatly appreciate it if you could provide me with the latest updates so we can make the necessary adjustments and ensure the correct behavior.

Best regards,

Luis Laurentino

Hi Luis,
I am unable to provide more assistance in this regard, as I don’t have as much free time anymore.
If you have access to Expert Chat you could try getting support there, if not, maybe someone else can jump in to assist here.
You can also create an example project (based on a starter) that replicates the problem and create an issue in Vaadin flow repo, so that someone in the product team can investigate.

Thank you Herberts.

Regards,

Luis Laurentino