2 layer authentication in app (username, password, token)

Hi,

How to create a 2 layer authentication in the app?

first layer:
login page > username input and password input, login button
once the username and password is valid, the login button function will bring user to the 2fa page (second layer)

second layer:
2fa page > token input and login button
once the token is valid, the login button will bring user to the home page, mean successful login

in the 2fa page, it should able to show the QR code if the user is new/ first-time user (system should able to detect and verify the user is it first time login), else, directly hide/no show the QR code for existing user.

You can use Dash’s multi-pages app feature, as well as implementing some callbacks. So when the user has finished entering their username/password and hit submit, this submit button will check for the account validity and all in the callback and if it’s true, you can tell Dash to redirect to other pages

Hi @williamwirono,

Do you know how to configure it?
Can you share the sample code?

You also implement login functionality using a single-page app. The app will have a container in which all the content goes and callbacks will control what content to show. You can store the login token and other details using a dcc.Store. The container callbacks will read this dcc.Store and see if a valid token and other login info is present before showing the site content. Because the callbacks are computed on the server, you are sure that the protected content will only render when login was successful.

def render_layout():
    return html.Div(
        id="top_level_container",
        # style={},  # put the base website styling options here
        children=[
            # put all the static content here that should persist whatever the state of the login process is. 
            # For example a page header or navbar, or the dcc.Store that will contain the authentication token
            dcc.Store("token_store", storage_type="session"),
            html.Div(
                id="main_content_container",
                children=None,
            )
        ]
    )

Load the layout in your app using app.layout = render_layout (not that there are no parentheses at the end, i.e. we pass the function handle to app.layout)

You can then create a function that builds the content layout depending on the login state:

def get_app_content(token_store):
    if <token_store has valid token>:
        return protected_layout
    elif <token is present but 2fa has not been done>:
        return 2fa_page
    else:
        return login_page

Lastly you have a callback that triggers on the token store

@dash.callback(
    Output("main_content_container", "children"),
    Input("token_store", "modified_timestamp"),
    State("token_store", "data")
)
def render_app_content(_, token_store):
    return get_app_content(token_store)

Of course you should add upon these sample to make sure that all the data that you require is present. For example, you might need extra information on whether 2fa was already done and perhaps a token of some kind related to that. But this sample should get you started at least.

@Tobs,

Yes, a single page application could work, but there are a couple of issues to think about when implementing one.

Bookmarks - You cant save a specific location easily, especially user friendly when you have lots of different layouts.

Difficulty - You’d have to work with the index match essentially and cannot use Dash’s cool pages


@beginof,

When implementing any sort of 2fa, you always have to keep in mind not allowing people to access information when halfway through the process, this also includes things mentioned in this topic:

Go towards the end with discussion about payloads getting responses, etc. making sure that user’s cannot access info even if they know the structure if they are not logged in.

Steps:

  • Allow user to log in
  • Only logged in users are able to verify
  • Once both steps are done give access to the whole site

Requirements, you cannot skip any steps.


The best way I think to work with this is to use a before_request handler from the underlying flask server, something like this:

@server.before_request
def pull_identity():
    request = flask.request
    if '/login' in request.url or '/logout' in request.url or '/static' in request.url:
        return
    else:
        try:
            if flask_login.current_user.id:
                if '/verify' in request.url or '/mfa' in request.url:
                    return
                if session['verified']:
                    return
        except:
            pass
        return flask.redirect('/login')

This punches holes for specific requests to get through unless at certain steps in the process.

As said in the other topic I mentioned, I find it better to handle the login and verification process in the flask routing itself and then just pass the verified user to the dash application.

Now, for the other steps in the process its up to how you want to do it, obviously, you’ll need a db to store the association with mfa and the user. This is necessary for a TOTP flow, with is what you are after.

For creating, I use the library pyotp:

This is the response to generate the qr code for signing up:

if info['type'] == 'authapp':
    session['temp'] = pyotp.random_base32()
    return jsonify({'data': pyotp.totp.TOTP(session['temp']).provisioning_uri(name=flask_login.current_user.id,
                                                                      issuer_name=yourwebsite)})

This is how I get the data to show in a qrcode fashion from the response generated from above:

async function Add() {
        dict = {}
        dict['command'] = 'add'
        dict['type'] = $("#type").val()
        dict['provider'] = $("#provider").val()
        dict['detail'] = $("#detail").val()
        if (dict != {}) {
            json_data = JSON.stringify(dict)
            const Url='../MFA';
            const othePram = {
            headers:{
                "content-type":"application/json; charset=UTF-8"
                },
                body:json_data,
                method:"POST"
                };

            const Response = await fetch(Url,othePram)
            .then(response=>response.json())
            .then(data=>{ return data; })

            if (!Response['data'].includes('failed')) {
                if ($("#type").val() == 'phone') {
                    $("#mfa_alert").text(Response['data'])
                    $("#qrcode_details").hide()
                } else {
                    var qrcode = new QRCode("qrcode")
                    qrcode.makeCode(Response['data'])
                    $("#qrcode_details").show()
                }
                $("#add").hide()
                $("#verify").show()
            }
        }
    }

For verifying the user, you’ll have to compare their given code with what you have stored in the db:

if pyotp.TOTP(decrypt(stored_secret_key)).verify(flask.request.form['code']):
    session['verified'] = 'true'

Something like the above.


With all of this said, you should be able to come up with your own flow, my recommendation is to add the before_request last because it can add some layer of being tricky.

You can build this flow inside of dash, but it becomes a lot more tricky to secure it completely.


Now, a lot of applications are moving away from logging in and verifying users directly and going with other things like Microsoft, Apple and Google authentication, so you might want to instead consider moving in that direction instead of trying to make your own process.

You could use a library like Flask-Dance instead.

2 Likes

It depends on how large the website/app is that you want to build. If there is only a single view (or maybe a very limited number of pages) that you want to put behind authentication, a single page app is much simpler to build. But if you are really looking at building a large app, I agree that the downsides are bigger. Though the approach you describe is not necessarily easy to implement/use:p

Haha. That’s exactly why I suggested using flask-dance. :grin: