flow icon indicating copy to clipboard operation
flow copied to clipboard

Cannot navigate to secured View when not logged for OAuth2

Open svein-loken opened this issue 1 year ago • 11 comments

Description of the bug

When using RouterLink and Anchor to a secured @Route with @PermitAll an error page is displayed when not logged in.

Could not navigate to '' Available routes:

Expected behavior

A login page should be show. Azure call it user flow: https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview.

Adding router-ignore makes it work. I have added router-ignore to the /logout link which is not a Vaadin route.

Minimal reproducible example

It is the same behavior for com.azure.spring:spring-cloud-azure-starter-active-directory-b2c:4.3.0 and org.springframework.boot:spring-boot-starter-oauth2-client:2.6.7. For azure b2c the config is:

@EnableWebSecurity
public class SecurityConfig extends VaadinWebSecurityConfigurerAdapter {

    private final AadB2cOidcLoginConfigurer configurer;

    public SecurityConfig(AadB2cOidcLoginConfigurer configurer) {
        this.configurer = configurer;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.apply(configurer);
    }
}
@Route("vaadin-hello")
@PermitAll()
public class VaadinHelloView extends VerticalLayout {
    public VaadinHelloView() {
        add(new H1("Hello from VAADIN"));

Versions

Vaadin: 23.2.0.alpha3 Flow: 23.2.0.alpha2 Java: JetBrains s.r.o. 17.0.2 OS: amd64 Windows 10 10.0 Browser: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36 Live reload: Java active (HotswapAgent): Front end active IntelliJ Tomcat

svein-loken avatar Aug 02 '22 19:08 svein-loken

That is expected. Any link since v23 that has no router-ignore attribute is handled by the Vaadin Router. It's documented in the Migration Guide.

knoobie avatar Aug 02 '22 20:08 knoobie

That is expected. Any link since v23 that has no router-ignore attribute is handled by the Vaadin Router. It's documented in the Migration Guide.

It is a router link I am talking about. A add(new RouterLink("RouterLink to VaadinHelloView", VaadinHelloView.class)); to @Route("vaadin-hello") shown above

svein-loken avatar Aug 02 '22 20:08 svein-loken

This sounds like you have the logic to show a login form based on the URL that is requested on the HTTP level, rather than also being based on Vaadin views that bypass HTTP URLs because of how Vaadin uses a SPA architecture.

What you need is to also call setLoginView in your VaadinWebSecurityConfigurerAdapter, as shown in https://vaadin.com/docs/latest/security/enabling-security/#security-configuration-class.

For more information about the difference between HTTP-level URLs and Vaadin view URLs, you can read my blog post: https://vaadin.com/blog/the-dangers-of-using-the-wrong-abstraction-for-vaadin-access-control

Legioth avatar Aug 03 '22 07:08 Legioth

What you need is to also call setLoginView in your VaadinWebSecurityConfigurerAdapter, as shown in https://vaadin.com/docs/latest/security/enabling-security/#security-configuration-class.

Not sure how to do this. I do not have a login view, it is handled by azure user-flow.

image

svein-loken avatar Aug 03 '22 07:08 svein-loken

There is most likely a "login URL" managed by the Spring Security integration. In our example with Google login, it's /oauth2/authorization/google but I'm not directly familiar with what it would be in your case.

If you cannot find the corresponding URL for your case, then you might also do a workaround through adding a dummy login view on the Vaadin side that only triggers are page reload (using ui.getPage().reload()). When the page is reloaded, it will cause a HTTP-level request to a forbidden URL which will be intercepted by the regular Spring Security integration and in that way forward the user to the actual login page. The drawback of this approach is that it might cause an additional redirect for the user compared to if they could directly be sent to the right location.

Legioth avatar Aug 03 '22 08:08 Legioth

I can use

setLoginView(http,  "/oauth2/authorization/B2C_1_signupsignin1");

but I am not redirected back to the link I clicked.

Tried the ui.getPage().reload(), did not work for me.

svein-loken avatar Aug 03 '22 08:08 svein-loken

Found a solution for this to work after many hours. After login the page-link I clicked is shown.

image

It is a ugly hack. I think this should be solved in the Vaadin framework.

svein-loken avatar Aug 09 '22 13:08 svein-loken

@svein-loken Keep in mind that this usage of executeJs leaves you open for attacks. You should convert this to the usage shown here: https://vaadin.com/docs/latest/security/advanced-topics/vulnerabilities/#running-custom-javascript

knoobie avatar Aug 09 '22 13:08 knoobie

@knoobie Thanks! I updated my post.

svein-loken avatar Aug 09 '22 14:08 svein-loken

I investigated this issue with more detail. If I create a Vaadin application from start.vaadin.com with two views, one that is public and the other one secured, and if I add a link from the public page to the secured page, when the user clicks on the link, they are redirected to the login page, and after entering the credentials then they are redirected to the secured page. This works just fine and you can test it with this project: secured-app.zip

If I modify that project just a little bit to add support for oauth2 authentication (I'm attaching also this project: secured-app-oauth2.zip, for testing it, you have to follow the step 1 of this tutorial and then modify the application.properties as explained in step 3) then the behavior is as follows: when the user clicks on the link they are shown a page that says:

Could not navigate to 'hello'
Available routes:
<root>
hello
login
This detailed message is only shown when running in development mode.

This is a wrong behavior, the server is not allowing to navigate to that page, but is not redirecting to the login page like in the plain login example. This, of course, can be avoided by using router-ignore as explained by @knoobie earlier, but it is not a perfect solution (you have to remember to do that to every single link in public pages that target internal pages and if you forgot to do that it is hard for a new developer to realize what the problem is). The server side should figure out about the issue and send a redirect to the configured login page. If you just refresh the "Could not navigate to 'hello'" page, it will redirect you to the google login page.

mlopezFC avatar Aug 11 '22 23:08 mlopezFC

Tried @mlopezFC solution to handle RouteNotFoundError, but then it hides normal navigation error. My best solution is so far:

image

svein-loken avatar Aug 12 '22 09:08 svein-loken

If I modify that project just a little bit to add support for oauth2 authentication (I'm attaching also this project: secured-app-oauth2.zip

So if you take this app and tell Vaadin about where the login view is, using setLoginView(http, "/oauth2/authorization/google"); then it will redirect to the login view also when navigating

Artur- avatar Sep 14 '22 08:09 Artur-

If I modify that project just a little bit to add support for oauth2 authentication (I'm attaching also this project: secured-app-oauth2.zip

So if you take this app and tell Vaadin about where the login view is, using setLoginView(http, "/oauth2/authorization/google"); then it will redirect to the login view also when navigating

This is something we tried initially - but the hole point of this issue is that user will not see the selected page.

svein-loken avatar Sep 14 '22 09:09 svein-loken

Related issues:

  • #14528. This probably is the root cause here

  • #14520. More of a cosmetic issue

Then it seems like the success handler is applied by default only formLogin which has no shared configuration with oauth2Login so you would need something like

        oauth2Login.successHandler(getVaadinSavedRequestAwareAuthenticationSuccessHandler(http));

    private VaadinSavedRequestAwareAuthenticationSuccessHandler getVaadinSavedRequestAwareAuthenticationSuccessHandler(
            HttpSecurity http) {
        VaadinSavedRequestAwareAuthenticationSuccessHandler vaadinSavedRequestAwareAuthenticationSuccessHandler = new VaadinSavedRequestAwareAuthenticationSuccessHandler();
        vaadinSavedRequestAwareAuthenticationSuccessHandler
                .setDefaultTargetUrl(applyUrlMapping(""));
        RequestCache requestCache = http.getSharedObject(RequestCache.class);
        if (requestCache != null) {
            vaadinSavedRequestAwareAuthenticationSuccessHandler
                    .setRequestCache(requestCache);
        }
        return vaadinSavedRequestAwareAuthenticationSuccessHandler;
    }


to get the same configuration

Artur- avatar Sep 14 '22 12:09 Artur-

Tried but it is not working for me

@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends VaadinWebSecurityConfigurerAdapter {

    public static final String LOGOUT_URL = "/";

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    private final AadB2cOidcLoginConfigurer configurer;

    public SecurityConfiguration(AadB2cOidcLoginConfigurer configurer) {
        this.configurer = configurer;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.apply(configurer);
        super.configure(http);
        setLoginView(http, "/oauth2/authorization/B2C_1_signupsignin1");
        http.oauth2Login().successHandler(getVaadinSavedRequestAwareAuthenticationSuccessHandler(http));
    }

    private VaadinSavedRequestAwareAuthenticationSuccessHandler getVaadinSavedRequestAwareAuthenticationSuccessHandler(HttpSecurity http) {
        VaadinSavedRequestAwareAuthenticationSuccessHandler vaadinSavedRequestAwareAuthenticationSuccessHandler
            = new VaadinSavedRequestAwareAuthenticationSuccessHandler();
        vaadinSavedRequestAwareAuthenticationSuccessHandler.setDefaultTargetUrl(applyUrlMapping(""));
        RequestCache requestCache = http.getSharedObject(RequestCache.class);
        if (requestCache != null) {
            vaadinSavedRequestAwareAuthenticationSuccessHandler.setRequestCache(requestCache);
        }
        return vaadinSavedRequestAwareAuthenticationSuccessHandler;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
        web.ignoring().antMatchers("/images/*.png");
    }
}

svein-loken avatar Sep 14 '22 21:09 svein-loken

What about looking at my solution https://github.com/vaadin/flow/issues/14253#issuecomment-1212918821? Maybe introduce a method setExternalLogin(true)?

svein-loken avatar Sep 14 '22 21:09 svein-loken

I would recommend you to create an Azure Active Directory B2C tenant for testing https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant. Then it will also work for Azure AD for company/B2B. It is free up to 50,000 MAU https://azure.microsoft.com/en-us/pricing/details/active-directory/external-identities/

svein-loken avatar Sep 15 '22 06:09 svein-loken

This ticket/PR has been released with Vaadin 23.3.0.alpha1 and is also targeting the upcoming stable 23.3.0 version.

vaadin-bot avatar Oct 03 '22 09:10 vaadin-bot

This ticket/PR has been released with Vaadin 23.2.3.

vaadin-bot avatar Oct 03 '22 12:10 vaadin-bot

Tried both 23.3.0.alpha1 and 23.2.3 but it does not work. Created a sample: https://github.com/sveine/vaadin-azure-ad-b2c-not-working-example

It is pretty easy to set up and it is free:

https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/configure-spring-boot-starter-java-app-with-azure-active-directory-b2c-oidc

sveine avatar Oct 04 '22 08:10 sveine

Thanks for testing @sveine , I re-open the ticket, we'll retest it with your example and get back to you.

mshabarov avatar Oct 04 '22 08:10 mshabarov

@sveine can you please try call setOAuth2LoginPage(HttpSecurity http, String oauth2LoginPage) in yourcustom SecurityConfig class, to inform ViewAccessChecker about the correct login path ?

mshabarov avatar Oct 04 '22 11:10 mshabarov

@sveine can you please try call setOAuth2LoginPage(HttpSecurity http, String oauth2LoginPage) in yourcustom SecurityConfig class, to inform ViewAccessChecker about the correct login path ?

I don't have a login page. It is defined by the flow, which works with "router-ignore". I tried this anyway:

Try to use setOAuth2LoginPage(). Now /logout does not work https://github.com/sveine/vaadin-azure-ad-b2c-not-working-example/commit/754a1c8cb9d25d1c79e6226bf93d05897d8984f2

sveine avatar Oct 04 '22 12:10 sveine

@sveine I tried to run the application with azure AD B2C configured as documented in the link you provided and both links to the secured view works as expected (redirect to the login form or show the view if user is authenticated).

For logout, by default, spring LoginFilter processes only POST requests on the logout URL, if CSRF is configured (and VaadinWebSecurity does it). To make it work, you can do it programmatically with a button as explained in https://vaadin.com/docs/latest/security/enabling-security/#log-out-capability, or, if you want to have a direct link (GET request) you should provide a logoutRequestMatcher to LogoutConfigurer, for example

http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"));

Please let me know if you still experience issues or if I missed something.

mcollovati avatar Oct 05 '22 06:10 mcollovati

The logoutRequestMatcher() and /logout works, but I am not getting the logout flow from Azure. I think the user should get this page as it is a "standard". Maybe Azure is clearing up something also. Is there any configuration that allow the logout flow to be shown on /logout?

The logout button is not working well. Try running my last check in. It is in VaadinHelloView()

In the end I think the SecurityConfig class configuration is to magical. Initially a link with router-ignore did work with the simple configuration in my first commit. It is similar to this: https://github.com/Azure-Samples/azure-spring-boot-samples/blob/spring-cloud-azure_v4.4.0/aad/spring-cloud-azure-starter-active-directory-b2c/aad-b2c-web-application/src/main/java/com/azure/spring/sample/aad/b2c/security/WebSecurityConfiguration.java

sveine avatar Oct 05 '22 08:10 sveine

Sorry, what do you mean by "not getting the logout flow from Azure"? What do you expect to see after pressing the logout link? I couldn't see anything related to logout in the links you provided.

mcollovati avatar Oct 05 '22 09:10 mcollovati

I think he is talking about something like RP-Initiated logout from the Oauth Spec - which is not part of flow and has to be implemented by himself.

knoobie avatar Oct 05 '22 09:10 knoobie

Thanks, @knoobie. I think the same but asked to be sure

mcollovati avatar Oct 05 '22 09:10 mcollovati

Check out rev 194d0c8285ff3be8dce7e14dd3e5e45670583970, "Not working in starter flow 23.2.3", click in the link "RouterLink to VaadinHelloView (router-ignore)", then logout. You will se this page: image

sveine avatar Oct 05 '22 09:10 sveine

This is to be expected - see https://stackoverflow.com/a/58199008/1662997

knoobie avatar Oct 05 '22 09:10 knoobie