Search code examples
airflowflask-securityflask-appbuilder

How to skip extra 'sign in with' page in Flask App Builder / Airflow


I have started using Apache Airflow (Built upon FAB) and enabled authentication through OAuth (as shown in FAB Security). I have ONLY one OAUTH_PROVIDER (azure).

FYI, Airflow version 2.1.4

When I launch my airflow home page, it used to open the direct login page of my OAUTH_PROVIDER.

Airflow 2.1.4 Login page

NOW, the real problem started when I upgraded my airflow to 2.2.4 AND configured the same OAUTH (Azure) provider.

When I launch my airflow home page, a page coming like this

Airflow 2.2.4 Login page

After clicking the button "Sign In with azure", the user login page comes.

Compared to the older airflow, the latest version got an extra page.

Why is matter to me?

We are rendering airflow in a web app and this extra "sign in with" page does not look good.

Please provide some info on SKIPPING that extra interaction.


Solution

  • After upgrading Airflow, Flask Appbuilder version was probably also bumped and that caused change in behavior. Basically:

    • Flask Appbuilder < 3.4.0 made it possible to have automatic sign-in when just one oauth provider was configured
    • Flask Appbuilder >= 3.4.0 changed the behavior and made it impossible to achieve this. This is the PR (with justification) that removed this functionality: https://github.com/dpgaspar/Flask-AppBuilder/pull/1707

    If you want previous behavior to be used in your deployment, this is what most likely would work (although I didn't test it):

    1. Prepare custom view class, let's call it CustomAuth0AuthView - it would bring back old behavior on login method. (I used v4.1.3 as reference code and modified it slightly).

    webserver_config.py:

    from flask_appbuilder.security.views import AuthOAuthView
    
    
    class CustomAuthOAuthView(AuthOAuthView):
        @expose("/login/")
        @expose("/login/<provider>")
        @expose("/login/<provider>/<register>")
        def login(self, provider: Optional[str] = None) -> WerkzeugResponse:
            log.debug("Provider: {0}".format(provider))
            if g.user is not None and g.user.is_authenticated:
                log.debug("Already authenticated {0}".format(g.user))
                return redirect(self.appbuilder.get_url_for_index)
    
            if provider is None:
                if len(self.appbuilder.sm.oauth_providers) > 1:
                    return self.render_template(
                        self.login_template,
                        providers=self.appbuilder.sm.oauth_providers,
                        title=self.title,
                        appbuilder=self.appbuilder,
                    )
                else:
                    provider = self.appbuilder.sm.oauth_providers[0]["name"]
    
            log.debug("Going to call authorize for: {0}".format(provider))
            state = jwt.encode(
                request.args.to_dict(flat=False),
                self.appbuilder.app.config["SECRET_KEY"],
                algorithm="HS256",
            )
            try:
                if provider == "twitter":
                    return self.appbuilder.sm.oauth_remotes[provider].authorize_redirect(
                        redirect_uri=url_for(
                            ".oauth_authorized",
                            provider=provider,
                            _external=True,
                            state=state,
                        )
                    )
                else:
                    return self.appbuilder.sm.oauth_remotes[provider].authorize_redirect(
                        redirect_uri=url_for(
                            ".oauth_authorized", provider=provider, _external=True
                        ),
                        state=state.decode("ascii") if isinstance(state, bytes) else state,
                    )
            except Exception as e:
                log.error("Error on OAuth authorize: {0}".format(e))
                flash(as_unicode(self.invalid_login_message), "warning")
                return redirect(self.appbuilder.get_url_for_index)
    
    1. Prepare custom security manager. Let's call it CustomSecurityManager. It would use your custom view to handle login.

    webserver_config.py:

    from airflow.www.security import AirflowSecurityManager
    
    
    class CustomSecurityManager(AirflowSecurityManager):
        authoidview = CustomAuthOAuthView
    
    1. Configure Airflow to use your CustomSecurityManager

    webserver_config.py:

    SECURITY_MANAGER_CLASS = CustomSecurityManager
    

    More on this: