Advice on converting an extended login screen from CUBA --> Jmix...?

Working down the numerous compile-stoppers in our migrated app, I’ve finally come to ExtLoginScreen and it seems there were major, major changes between CUBA and Jmix. In CUBA, I overrode numerous methods from the LoginScreen superclass.

I have (all annotated @Override):

public void login()
protected void doLogin()
protected void doLogin(Credentials credentials) throws LoginException
protected void doRememberMeLogin()

All redlined on the @Override with the message “does not override method from its superclass.” And the ending call to super.method() is redlined with an error saying the method doesn’t exist.

It seems these methods are not a part of the LoginScreen in Jmix?

I need advice on how to convert this - we have a custom-implemented 2FA system and such, so we need to extend the login screen and handle that.

After playing with this in a test project, I learned in Jmix apparently one doesn’t make an ExtLoginScreen to extend the login screen…one just modifies the login screen itself. Cool. I still need advice on how to interrupt the login process. In CUBA it was simple; just throw a LoginException and you’re good.

Obviously, it’s not that simple in Jmix. I have, in LoginScreen.java:

        try {
            loginScreenSupport.authenticate(
                    AuthDetails.of(username, password)
                            .withLocale(localesField.getValue())
                            .withRememberMe(rememberMeCheckBox.isChecked()), this);
            log.info(">> Login succeeded; checking 2FA");
            if (fake2FA == null || !fake2FA.equals("0000")) {
                log.info(">> 2FA failed.");
                notifications.create(Notifications.NotificationType.ERROR)
                        .withCaption(messages.getMessage(getClass(), "loginFailed"))
                        .withDescription(messages.getMessage(getClass(), "badCredentials"))
                        .show();
                //currentAuthentication.getAuthentication().setAuthenticated(false);
                throw new BadCredentialsException("2FA");
            }
        } catch (BadCredentialsException | DisabledException | LockedException e) {
            log.info("Login failed", e);
            notifications.create(Notifications.NotificationType.ERROR)
                    .withCaption(messages.getMessage(getClass(), "loginFailed"))
                    .withDescription(messages.getMessage(getClass(), "badCredentials"))
                    .show();
        }

Most of that obviously is the original code from the controller; I’ve just added the “fake 2FA” stuff. currentAuthentication.getAuthentication.setAuthenticated(false) doesn’t work; that causes an endless exception loop. Throwing the BadCredentialsException doesn’t work either.

I need to know how to invalidate/logout the just-authenticated session if the 2FA fails.

After some more experimenting, I’ve come up with this:

        try {
            // use Spring's authenticationManager to check the user/pw, but not do the Jmix login stuff...
            Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));

            log.info(">> Authed -> {}; checking 2FA", authentication.getName());

            if (fake2FA == null || !fake2FA.equals("0000")) {
                log.info(">> 2FA failed.");
                throw new BadCredentialsException("2FA");
            }

            log.info(">>> 2FA succeeded, doing real login");

            // this is inefficient because it will re-do the Spring auth, etc, but, not sure of another way.
            loginScreenSupport.authenticate(
                    AuthDetails.of(username, password)
                            .withLocale(localesField.getValue())
                            .withRememberMe(rememberMeCheckBox.isChecked()), this);
        } catch (BadCredentialsException | DisabledException | LockedException e) {
            log.info("Login failed", e);
            notifications.create(Notifications.NotificationType.ERROR)
                    .withCaption(messages.getMessage(getClass(), "loginFailed"))
                    .withDescription(messages.getMessage(getClass(), "badCredentials"))
                    .show();
        }

I first use Spring’s authenticationManager to see if the user/pass are ok, then check the (fake; this is a test project!) 2FA, and if that’s ok, then I use the Jmix loginScreenSupport.authenticate(...) call to actually login to the Jmix app. This (at least seems to!) works as I’d expect.

Obviously, though, loginScreenSupport does the Spring authenticate again which is inefficient, but I’m not sure of another way.

Does anyone have advice as to another way to accomplish this?

Any advice as to whether or not the above solution is an ok one, or is there a better/more usually-used way to do this?

Hi,
Your solution is not optimal but more or less fine. More beatiful implementation will require to dig into the Spring Security internals.

Jmix’s authentication is based on Spring Security mechanisms. They are pretty complex.
You can read here, and this is a long read: Servlet Authentication Architecture :: Spring Security

I suppose, ideally, the second step of the two-factor authentication should be implemented with a separate, specially implemented AuthenticationProvider.
This provider should receive special object as a token - containing desired user login and one-time password.
The provider checks one-time password, verifies that it belongs to the desired user, and finishes the authentication procedure by returning the successful Authentication object.

The example of AuthenticationProvider implementation is org.springframework.security.authentication.dao.DaoAuthenticationProvider.
This is the implementation of “login by username and password” used by standard Jmix authentication.

1 Like

Fair enough. Maybe just go with this for right now, and make it better later. I still have not got the app to the fully compiling stage post-migration from CUBA, so getting things working, if not optimal, is important.

@albudarov - so, after reading/watching a lot of info on Spring security it definitely seems like a custom AuthenticationProvider is the way to go (at least eventually) - but the built-in one is already doing most of the work - the username/password part. Can anyone give a high-level overview on implementing a custom AuthenticationProvider that delegates the user/password part to the built-in one, but takes other actions (eg, 2FA), and install said provider to be used in place of the existing one?

Even just a skeleton implementation that delegates to the existing/built-in auth (and showing how to “install” the custom provider) would be a huge help.

Heck, even a pointer to where to get started on installing a custom AuthenticationProvider would be a huge help at this point. Jumping around in the Jmix source, I can’t figure out where it installs its implementation!

All the usual Spring tutorials have you implementing a WebSecurityConfigurerAdapter and a UserDetailsService and such, but Jmix doesn’t seem to do that. There is SecurityConfigurers and such… but they don’t seem to actually do anything. O_o

Just some pointers or a very skeleton implementation with info on how to wire it in would be much appreciated!

Seems like from other posts, @gorbunkov might be the one to ask about this? :smiley:

You can try to extend the StandardSecurityConfiguration (which actually extends WebSecurityConfigurerAdapter) from Jmix so that it overrides its void configure(AuthenticationManagerBuilder auth). In this method register your own AuthenticationProvider instead of standard DaoAuthenticationProvider.

@Configuration
@EnableWebSecurity
public class MySecurityConfiguration extends StandardSecurityConfiguration {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private PreAuthenticationChecks preAuthenticationChecks;

    @Autowired
    private PostAuthenticationChecks postAuthenticationChecks;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new SystemAuthenticationProvider(userRepository));
        auth.authenticationProvider(new SubstitutedUserAuthenticationProvider(userRepository));

        MyAuthenticationProvider daoAuthenticationProvider = new MyAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userRepository);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        daoAuthenticationProvider.setPreAuthenticationChecks(preAuthenticationChecks);
        daoAuthenticationProvider.setPostAuthenticationChecks(postAuthenticationChecks);

        auth.authenticationProvider(daoAuthenticationProvider);
    }
}
public class MyAuthenticationProvider extends DaoAuthenticationProvider {

    private static final Logger log = LoggerFactory.getLogger(MyAuthenticationProvider.class);

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        log.info("Custom authentication");
        return super.authenticate(authentication);
    }
}

A sample that demonstrates this: jmix-custom-auth-provider-sample.zip (84.8 KB)

I’ll have a look at the sample - thank you!

That gave me what I needed!

I needed to additionally:

  1. Extend UsernamePasswordAuthenticationToken (to hold 2FA value to pass to auth provider)
  2. Create a TwofaAuthDetails (to hold an AuthDetails and the 2FA String), because AuthDetails isn’t extendable (only has a private constructor)
  3. Extend LoginScreenSupport (to create 2FA-holding tokens and pass them up the chain)
  4. Change the login screen to use the extended LoginScreenSupport and pass in the 2FA code, etc.
2 Likes