Anonymous user MainView/LoginView display problem with Flow UI

Jmix version: 2.2.3
Jmix Studio plugin version: 2.2.3-241
IntelliJ IDEA 2024.1.1 (Community Edition)
Build #IC-241.15989.150, built on April 29, 2024
Runtime version: 17.0.10+1-b1207.14 x86_64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
GC: G1 Young Generation, G1 Old Generation
Kotlin: 241.15989.150-IJ
java 17.0.4 2022-07-19 LTS
Java™ SE Runtime Environment (build 17.0.4+11-LTS-179)
Java HotSpot™ 64-Bit Server VM (build 17.0.4+11-LTS-179, mixed mode, sharing)
Operating System: macOS 14.5 (23F79)
Metal Rendering: ON
File System: Case-Sensitive Journaled HFS+ (APFS)
Browser: Safari - Version 17.5 (19618.2.12.11.6)
Database: PostgreSQL 13

Hello Jmix Team

I am having problems migrating my Anonymous user functionality from Jmix 1.5.x to 2.2.x and require assistance (or maybe I am dealing with a bug).

Beginning with CUBA I implemented an anonymous MainScreen, which I then migrated to Jmix 1.5.x; it has the following functionality:

  • The MainScreen has a WorkArea containing a BrowserFrameFragment that displays a static HTML Welcome message
  • The SideMenu contains four screen menu items allowing the anonymous user to open these screens:
    Concept (normal screen without an entity containing a BrowserFrameFragment that displays a static HTML page)
    Features (normal screen without an entity containing a BrowserFrameFragment that displays a static HTML page)
    Terms & Conditions (normal screen without an entity containing a BrowserFrameFragment that displays a PDF document)
    Membership Options (edit screen with an entity (read-only) and various screen components)

Furthermore, my main requirement is that the anonymous user can choose his language (locale) in the anonymous MainScreen (actually done in the SideMenu) before they are presented with the locale choice in the normal LoginScreen. Gleb helped me with my initial CUBA implementation here:

This allows me to load language specific static resources for my Welcome message in the MainScreen and in my Concept, Features and Terms & Conditions screens.

When the user is anonymous, I set the UserIndicator visibilty in the SideMenu to false and replace it by a ComboBox for the locale selection. I also add a login button and set the logout button visibilty to false.

Screenshot 2024-05-19 at 11.15.56 Screenshot 2024-05-19 at 11.18.00

When the anonymous user selects a different locale, I reload the MainScreen with jmixApp.createTopLevelWindow().

If the anonymous user chooses to login normally, then after the login, I replace the locale ComboBox with the UserIndicator and the login button with the normal logout button in the SideMenu (code farther below).

Screenshot 2024-05-19 at 11.17.08

My 1.5.x application.properties are:

# enable anonymous access
jmix.ui.allow-anonymous-access = true
# initial screen for anonymous user
jmix.ui.initial-screen-id= nf_MainScreen
jmix.ui.login-screen-id = nf_LoginScreen
jmix.ui.main-screen-id = nf_MainScreen

jmix.ui.default-screen-id = nf_SearchInterests.browse
jmix.ui.default-screen-can-be-closed=true

jmix.ui.menu-config = com/company/nf/menu.xml
jmix.ui.composite-menu = true
jmix.ui.app-window-mode=SINGLE

Here in the 1.5 documentation it says that two MainScreens are necessary but I have only one:

https://docs.jmix.io/1.x/jmix/1.5/ui/anonymous-access-to-screens.html

Keep in mind that you’ll have two main screens in your app. The default one will be used for authenticated users and the anonymous one for anonymous:

jmix.ui.main-screen-id=MainScreen
jmix.ui.initial-screen-id=MainScreenSideMenu

My 1.5.x MainScreen code for this is:

    private boolean anonymousUser;

    @Subscribe
    public void onInit(InitEvent event) {

        Authentication authentication = currentAuthentication.getAuthentication();
        anonymousUser = (authentication instanceof AnonymousAuthenticationToken);

        initLoginButtonAndLocales();

        if (anonymousUser) {
            return;
        }


	// continue with other logic for authenticated users
    }

    private void initLoginButtonAndLocales() {

        loginButton.setVisible(anonymousUser);
        logoutButton.setVisible(!anonymousUser);
        userIndicator.setVisible(!anonymousUser);
        localeSelect.setVisible(anonymousUser);

        if (anonymousUser) {
            localeSelect.setOptionsMap(messageTools.getAvailableLocalesMap());
            localeSelect.setValue(jmixApp.getLocale());
            localeSelect.addValueChangeListener(e -> {
                Locale selectedLocale = e.getValue();

                if (selectedLocale != null) {
                    jmixApp.setLocale(selectedLocale);
                    jmixApp.createTopLevelWindow();
                }
            });
        }
    }

    @Subscribe("loginButton")
    public void onLoginButtonClick(Button.ClickEvent event) {

        String loginScreenId = uiProperties.getLoginScreenId();
        Screen loginScreen = screens.create(loginScreenId, OpenMode.ROOT);
        loginScreen.show();
    }

THE PROBLEM(S)

I am not able to reproduce the functionality of my MainView in 2.2.x. No matter what permissions or application.properties I set; either the normal LoginView is presented together with the MainView, or I receive a strange error text covering the entire browser session.

If I use the following security configuration and application.properties…

@ResourceRole(name = AnonymousUserRole.NAME, code = AnonymousUserRole.CODE, description = "Anonymous NF role for everyone arriving at the site")
public interface AnonymousUserRole {

    String NAME = "NF Anonymous User Role";
    String CODE = "nf-anonymous-user-role";

    @EntityAttributePolicy(entityClass = User.class, attributes = "*", action = EntityAttributePolicyAction.VIEW)
    @EntityPolicy(entityClass = User.class, actions = EntityPolicyAction.READ)
    void user();

    @SpecificPolicy(resources = "ui.loginToUi")
    void specify();

    @MenuPolicy(menuIds = {"nf_MainViewMenuService#showMembershipView", "ConceptView", "FeaturesView", "LegalView"})
    @ViewPolicy(viewIds = {"nf_MainViewMenuService#showMembershipView", "nf_Membership.detail", "ConceptView", "FeaturesView", "LegalView", "inputDialog", "LoginView", "MainView"})
    void screens();

    @EntityAttributePolicy(entityClass = Membership.class, attributes = "*", action = EntityAttributePolicyAction.VIEW)
    @EntityPolicy(entityClass = Membership.class, actions = EntityPolicyAction.READ)
    void membership();

    @EntityPolicy(entityClass = BaseTraitsEntity.class, actions = EntityPolicyAction.READ)
    @EntityAttributePolicy(entityClass = BaseTraitsEntity.class, attributes = "*", action = EntityAttributePolicyAction.VIEW)
    void baseTraitsEntity();
}
jmix.ui.login-view-id = LoginView
jmix.ui.main-view-id = MainView
jmix.ui.default-view-id = nf_Search.list

jmix.ui.view.validation-notification-position = TOP_CENTER
jmix.ui.view.validation-notification-type = DEFAULT

jmix.ui.menu-config = com/company/nf/menu.xml
jmix.ui.composite-menu = true

…then opening http://localhost:8080 as an anonymous user shows the MainView but, without the IFrame content (static HTML page), but it does include my Concept, Features, Term & Conditions and Membership Options menu items, and the LoginView is also displayed on top of the MainView.

However, if I select my Concept, Features, Term & Conditions menu items, those views are not displayed, but the LoginView is reloaded and the MainView WorkArea IFrame in the background is still empty.

If I choose to login, then my MainView IFrame shows the following error message instead of the contents of my nfmain.en.html file:

Could not navigate to 'nfmain.en.html'
Available routes:
<root>
add-condition
ambitions/:id (requires parameter)
availabilities/:id (requires parameter)
concept-view
contacts
contacts/:id (requires parameter)
covers
etc.

If I make the following modification to my application.properties (analog to jmix.ui.initial-screen-id= nf_MainScreen in CUBA and Jmix 1.5.x)…

jmix.ui.login-view-id = MainView

then I receive the following strange error text covering the entire view:

Screenshot 2024-05-20 at 14.30.15

I also receive this error message if I remove the LoginView from my Anonymous role permissions…

@ViewPolicy(viewIds = {"nf_MainViewMenuService#showMembershipView", "nf_Membership.detail", "ConceptView", "FeaturesView", "LegalView", "inputDialog", "MainView"})

If I revert the application.properties back to:

jmix.ui.login-view-id = LoginView

and remove the LoginView from my Anonymous role permissions… (as in the example directly above)

@ViewPolicy(viewIds = {"nf_MainViewMenuService#showMembershipView", "nf_Membership.detail", "ConceptView", "FeaturesView", "LegalView", "inputDialog", "MainView"})

…then both the MainView and the LoginView are displayed together.

And if I also remove the MainView from the Anonymous role permissions…

@ViewPolicy(viewIds = {"nf_MainViewMenuService#showMembershipView", "nf_Membership.detail", "ConceptView", "FeaturesView", "LegalView", "inputDialog"})

…then the LoginView is still displayed.

I have also unsuccessfully tried other combinations after removing the following setting:

    @SpecificPolicy(resources = "ui.loginToUi")
    void specify();

How can I stop the LoginView from displaying itself? I want to open it explicitly from my MainView with the following code:

    @Subscribe(id = "loginButton", subject = "clickListener")
    public void onLoginButtonClick(final ClickEvent<JmixButton> event) {

        viewNavigators.view(uiProperties.getLoginViewId()).navigate();
    }

Why is it not possible to set jmix.ui.login-view-id = MainView instead of LoginView ?

What can I do to migrate this functionality from 1.5.x to 2.2.x?

Thanks in advance for your support.

Best regards
Chris

1 Like

Hello Everyone

In the meantime I am using the most current versions of everything and I still have these problems:

Jmix version: 2.3.0
Jmix Studio plugin version: 2.3.0-241
IntelliJ IDEA 2024.1.4 (Community Edition)
Build #IC-241.18034.62, built on June 20, 2024

As I mentioned in my original CUBA post in Jan. 2021 and at the beginning of this topic, this is a normal use case; an Anonymous user must be able to change their locale and access anonymous views in their new locale before triggering the login view/dialog for a normal login as a registered user. Therefore, I would expect a standard solution for this situation. Can you please solve this in the next release.

Thank you in advance for your support and feedback.

Best regards
Chris

Hi!

I have prepared a small project that will help you solve your problem.
The main difference from your approach is that I used only one MainView and used a loginOverlay component instead of a LoginView.
Documentation: Login | Components | Vaadin Docs

First, you need to assign anonymous users access to the necessary view. This can be done using the @AnonymousAllowed annotation. I also added this annotation to the MainView. Here is an example of using this annotation in my PublicView:

@Route(value = "public-view", layout = MainView.class)
@ViewController("PublicView")
@ViewDescriptor("public-view.xml")
@AnonymousAllowed
public class PublicView extends StandardView {
}

Thus, these view became accessible without authorization.
In order not to write a lot of code here, I recommend that you see the attached project.

The locale is changed through the VaadinSession.getCurrent().setLocale() method.
To ensure that the locale is automatically applied to the components, I implemented the logic for reloading the MainView when the locale is changed.

    @Override
    public void localeChange(LocaleChangeEvent event) {
        if (localeInitialized) {
            UI.getCurrent().getPage().reload();
        }
    }

When you click the loginButton, an loginOverlay opens. If the credentials are entered correctly, the page will be reloaded and user will be authenticated:

    @Subscribe(id = "loginButton", subject = "clickListener")
    public void onLoginButtonClick(final ClickEvent<JmixButton> event) {
        loginOverlay.setOpened(true);
    }

    protected void onLogin(AbstractLogin.LoginEvent event) {
        try {
            loginViewSupport.authenticate(
                    AuthDetails.of(event.getUsername(), event.getPassword())
                            // select your locale here
                            .withLocale(userLocale.getValue())
                            .withRememberMe(false)
            );
            onLoginSuccess();
        } catch (final BadCredentialsException | DisabledException | LockedException | AccessDeniedException e) {
            log.warn("Login failed for user '{}': {}", event.getUsername(), e.toString());
        } finally {
            ((LoginOverlay) event.getSource()).close();
        }
    }

    protected void onLoginSuccess() {
        UI.getCurrent().getPage().reload();
    }

There is the example:
anonymous-access.zip (106.0 KB)

I hope my example will help implement your project.
Please note that this is not a complete implementation. Lots of things could be improved. This is just a base from which you can take a basic approach to create your ideal implementation.


P.S.

Also pay attention to examples of displaying elements in an iframe component:

Also, to display static resources for an anonymous user, you need to place them is the theme folder. Otherwise these resources will not available. To give access to src/main/resources you must additionally configure the security configuration.


Best regards,
Dmitriy

@d.kremnev
Hi Dmitriy

Thank you very much for your detailed feedback and tips and especially for your example project. I was able to start it and it appears to be exactly what I need. I will try and integrate it next week and will update this post with my findings.

Best regards
Chris

@d.kremnev
Hi Dmitriy

In the meantime I am using the most current versions of everything and I still have at least two problems.

Jmix version: 2.3.1
Jmix Studio plugin version: 2.3.1-241
IntelliJ IDEA 2024.1.4 (Community Edition)
Build #IC-241.18034.62, built on June 20, 2024

Problem 1

You wrote the following in your last comment…

Also, to display static resources for an anonymous user, you need to place them is the theme folder. Otherwise these resources will not available. To give access to src/main/resources you must additionally configure the security configuration.

… however, the information about the “theme” folder and the “security configuration” is insufficient for me; no matter where I put my static html source, it is not displayed when the MainView is visited by anonymous user. I have tried placing my file in the following directories and I have allowed every single element (view, dialog, etc.) in my AnonymousUserRole.java security file but nothing is displayed.

…/nf/frontend/themes/nfmain.en.html
…/nf/frontend/themes/resources/nfmain.en.html
…/nf/frontend/themes/nf/resources/nfmain.en.html
…/nf/frontend/themes/nf/main/resources/nfmain.en.html
…/nf/frontend/themes/nf/main/resources/META-INF/nfmain.en.html
…/nf/frontend/themes/nf/main/resources/META-INF/resources/nfmain.en.html
…/nf/src/main/resources/META-INF/themes/nfmain.en.html (gets automatically copied to build directory)
…/nf/src/main/resources/META-INF/resources/nfmain.en.html (gets automatically copied to build directory)

I am using mainIFrame.setSrc("nfmain.en.html") in my MainView and the static resources are displayed after a registered user logs in (without having the file in athemes directory), but it never works for an anonymous user.

I have tried this after additionally adding @AnonymousAllowed to the MainView but that has no effect. Note, I already have the following in my NfApplication.java class.

@EnableWebSecurity
public static class DefaultFlowuiSecurityConfiguration extends FlowuiVaadinWebSecurity {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.headers(headers ->
                headers.contentSecurityPolicy(secPolicy ->
                        secPolicy.policyDirectives("frame-ancestors localhost:8080")
                )
        );
    }
}

Problem 2

If I call mainIFrame.setSrc("nfmain.en.html") in my onInit() handler, then the LoginView (not the loginOverlay) is displayed on top of my MainView when an anonymous user visits the page.

@Subscribe
public void onInit(final InitEvent event) {
    mainIFrame.setSrc("nfmain.en.html");
...
...
}

If I comment it out, then only my MainView is displayed as expected, when an anonymous user visits the page. This is certainly a bug and is 100% reproducible in my application. I mentioned this problem in my initial information above but I did not know the source of the problem at that time. Unfortunately, I cannot reproduce it with the example that you gave me. Attached you will find the stack traces with (w) and without (wo) the mainIFrame.setSrc("nfmain.en.html") statement.

For the case with the mainIFrame.setSrc("nfmain.en.html") statement, I set a breakpoint at the first line of the onInit() handler of my LoginView and for the case without the mainIFrame.setSrc("nfmain.en.html") statement, I set the breakpoint to the login button handler in the MainView. Therefore the trace with the IFrame stops automatically when the LoginView is built and the trace without the IFrame has only stopped due to my manual interaction with the login button. I hope that this information is helpful to find the cause of this behavior.

Many thanks in advance for your further support and feedback.

Best regards
Chris

**
StackTrace_w_IFrame.txt (11.3 KB)
StackTrace_wo_IFrame.txt (9.9 KB)
**

Hi Chris

It would be great if you could provide a test project that reproduces the problem. It would speed up the solution to your problem.

Regards,
Dmitriy

@d.kremnev
Hi Dmitriy

I was able to reproduce the LoginView behavior in a modified version of your anonymous-access example; I have attached it to this post. TheLoginView displays itself on top of the MainView after adding the following code to the AnonymousAccessApplication.java class:

    @EnableWebSecurity
    public static class DefaultFlowuiSecurityConfiguration extends FlowuiVaadinWebSecurity {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            super.configure(http);
            http.headers(headers ->
                    headers.contentSecurityPolicy(secPolicy ->
                            secPolicy.policyDirectives("frame-ancestors localhost:8080")
                    )
            );
        }
    }

With this security configuration code the mainIFrame.setSrc("anonymous-access.html"); statement in the MainView’s onInit() handler causes the LoginView to be displayed on top of the MainView when an anonymous user visits the MainView. If the mainIFrame.setSrc("anonymous-access.html"); statement is commented out, then the LoginView is not displayed.

A login procedure started with the login button and LoginOverlay always works, no matter what the status of the mainIFrame is, and the static anonymous-access.html page is displayed after logging in if the mainIFrame was not commented out. But a login with theLoginView generates an error in the mainIFrame as mentioned in my original description at the beginning of this topic; the anonymous-access.html contents are not displayed.

I hope that this will help correct the LoginView problem and also answer my outstanding questions in my previous post regarding where exactly to place the static resources and which further permissions are required.

Thank you in advance for your support.

Best regards
Chris

anonymous-access_w_IFrame.zip (107.8 KB)

1 Like

Hi Chris!

With this configuration I was able to display the resource. :

    @Bean
    @Order(JmixSecurityFilterChainOrder.FLOWUI - 10)
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher("img/**")
                .authorizeHttpRequests(authorize ->
                        authorize.requestMatchers("img/**").permitAll()
                );
        http.headers(headers ->
                headers.contentSecurityPolicy(secPolicy ->
                        secPolicy.policyDirectives("frame-ancestors localhost:8080")
                )
        );
        return http.build();
    }

This bean should be used instead of the overridden DefaultFlowuiSecurityConfiguration.
The login page opened because the resource was not available to an anonymous user, so a redirect to the LoginView occurred.
I defined a filter chain that allows the resource and also allows it to be accessed from the same host (as in the example UI Samples :: IFrame PDF - REST).

You must place your resource in the META-INF/resources/img/ folder:
image

Then assign the resource to the new path:

 mainIFrame.setSrc("img/anonymous-access.html");

Thus, when the MainView page is loaded, a request will be sent from the IFrame component to display a static resource at the full address:
image

As I mentioned earlier, we have provided access to resources that can be accessed at /img/**, so the resource is available.

See attached project for more details: anonymous-access.zip (108.0 KB)


Placing a resource in the theme folder makes sense if this resource will be used for styling. For example, a background image for CSS property. In your case, this option is not suitable.

Best regards,
Dmitriy

@d.kremnev
Hi Dmitriy

Thank you very much for this new information and your efforts. It solves all of my current problems except for one small remaining issue; the LoginOverlay is only displayed with an English locale. Are there any plans to support the LoginOverlay internationalization in Jmix itself or are we expected to do it ourselves with loginOverlay.setI18n(LoginI18n i18n)?

Thank you in advance for your feedback.

Best regards
Chris

Hi Chris!

Thanks for the good idea. Created a task to support Jmix localization: Support i18n for LoginOverlay component · Issue #3562 · jmix-framework/jmix · GitHub

Best regards,
Dmitriy

Hello Jmix Team
I am having problems like showing blank ui or white screen and bundle processing ,This getting when i extended the version Jmix 2.2.3 to 2.3.0 or to above and along with that getting error in console "access denied to anonymous user to main View .Would you please help me out of this.

Hi everyone ,
Thanks for giving solutions from your end.I fixed the issue that i was geting above.I was not able to clean the vaadin front end.When i running this command getting build failed. fixed the error from css .After rebuild the vaadin front end .As a result getting jmix login page.
cmd: ./gradlew vaadinBuildFrontend