Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

logout from basic auth #51

Open
zachspar opened this issue Aug 2, 2018 · 5 comments
Open

logout from basic auth #51

zachspar opened this issue Aug 2, 2018 · 5 comments

Comments

@zachspar
Copy link

zachspar commented Aug 2, 2018

Is there any way to logout from basic auth in these examples? The only way I can possibly think is if you did a redirect with a 401 status code. I'm just looking for a simple way to circumvent this problem for now.

I've looked at the docs on https://dash.plot.ly/authentication and understand that one of the limitations of dash_auth.BasicAuth is that users cannot logout of applications, but there must be a way to use flask routing to return a 401 unauthorized.

If anyone has figured out a way around the problem, I'd love to hear some suggestions!

@eric-burel
Copy link

eric-burel commented Dec 19, 2019

Here you go for a dirty hack:

// hack to logout
// Note: all files on assets are loaded automatically
function logout() {

    // To invalidate a basic auth login:
    //
    // 	1. Call this logout function.
    //	2. It makes a GET request to an URL with false Basic Auth credentials
    //	3. The URL returns a 401 Unauthorized
    // 	4. Forward to some "you-are-logged-out"-page
    // 	5. Done, the Basic Auth header is invalid now
    alert("You will be logged out, please click 'Cancel' once on the next alert to log in again.")

    fetch('/', {
        method: "GET",
        headers: { "Authorization": "Basic " + btoa("logout:logout") }
    })
    window.location.reload()

    return false;
}

// Function to watch for an element appaerance in a React context
function waitForMutation(parentNode, isMatchFunc, handlerFunc, observeSubtree, disconnectAfterMatch) {
    var defaultIfUndefined = function (val, defaultVal) {
        return (typeof val === "undefined") ? defaultVal : val;
    };

    observeSubtree = defaultIfUndefined(observeSubtree, false);
    disconnectAfterMatch = defaultIfUndefined(disconnectAfterMatch, false);

    var observer = new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {
            if (mutation.addedNodes) {
                for (var i = 0; i < mutation.addedNodes.length; i++) {
                    var node = mutation.addedNodes[i];
                    if (isMatchFunc(node)) {
                        handlerFunc(node);
                        if (disconnectAfterMatch) observer.disconnect();
                    };
                }
            }
        });
    });

    observer.observe(parentNode, {
        childList: true,
        attributes: false,
        characterData: false,
        subtree: observeSubtree
    });
}

// Example
waitForMutation(
    // parentNode: Root node to observe. If the mutation you're looking for
    // might not occur directly below parentNode, pass 'true' to the
    // observeSubtree parameter.
    document.body,
    // isMatchFunc: Function to identify a match. If it returns true,
    // handlerFunc will run.
    // MutationObserver only fires once per mutation, not once for every node
    // inside the mutation. If the element we're looking for is a child of
    // the newly-added element, we need to use something like
    // node.querySelector() to find it.
    function (node) {
        return (node.querySelector && node.querySelector(".logout-button") !== null);
    },
    // handlerFunc: Handler.
    function (node) {
        button = node.querySelector('.logout-button')
        button.onclick = function () {
            logout()
        }
    },
    // observeSubtree
    true,
    // disconnectAfterMatch: If this is true the hanlerFunc will only run on
    // the first time that isMatchFunc returns true. If it's false, the handler
    // will continue to fire on matches.
    false);

What it does:
1- it defines a logout function, which simply send an HTTP request with a wrong Basic token. This is sufficient to log you out.
2 - the rest is an absolute overkill to be able to add an onclick callback to a button of .logout-button classname. Basically in React you can't just wait for Dom content to be loaded as usual, so you have to use a MutationObserver. See https://stackoverflow.com/a/31489875/5513532.

Problems:

  • UX is terrible, because the logout request will open a login prompt that can never be valid. I don't know why it does that, I suspect that it send the HTTP query with the fake identifiers again and again. Hence the first warning message... Then on the second reload it's ok.
  • The button onclick thing is ridiculous lol , as Dash's Button is actually a component meant to reload graphs, not an usual HTML/React button, so it has no onclick callback (or more precisely it's probably already used by Plotly internally to trigger callbacks so you can't add your own callback).

There is no solution for the "UX" issue, but the onclick thing can be solved hopefully. The right solution would be to write a custom React component to have an actual Button with an onClick callback.

@eric-burel
Copy link

  • Regarding the Logout button:

I've written a Button component with onClick ability.

Big big caveat: I am not yet sure that it's safe, at all! See this code review question.

Code is here: https://github.com/lbke/dash-standard-button

Again not sure it's safe yet!

  • Regarding the Logout process:

A better process would be to install BasicAuth a bit differently. You would need to have at least one insecure route on /login. Currently Dash does not allow such whitelisting according to the source of auth.py.
So basically you would set it up as in any Flask application, it's probably not very difficult.

Then, in the frontend, you can create a Dash component displaying a login form, in which you would store the login token on your own, in the sessionStorage. Then you can easily write a logout button that just dumps the token from the storage.

Basically the logout issue is strongly related to the fact that we rely on browser magic. Yet, usually, browsers magic is complete crap (I mean, not being able to log out without closing the browser is an open door to social engineering...).

However note that this behaviour ties your login process to your frontend, while Plotly default behaviour is standard, and as far as I understand the code, it's secure.

@raghavendrajain
Copy link

raghavendrajain commented Apr 29, 2020

@eric-burel Thank you for the detailed instructions and code for the button. Is it possible for you to kindly tell us where to put the code for logout ? If you could make a Git repo for that too, it would be most useful. Thank you so much!

@eric-burel
Copy link

Hi, the logout call will go into the button onclick callback. You may add the logout function as a script in your app (you probably have a way to serve static files in dash, using a "public" folder + a script tag).

I won't provide a repo because that's more a hack, but the code I've posted should be sufficient.

@raghavendrajain
Copy link

Okay @eric-burel Thank you for the instructions. I will try.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants