Black Lives Matter. Please consider donating to Black Girls Code today.

Dash-okta-auth and dash-google-auth userinfo

Hi everyone,
I’m still going in circles wit authentication inside my dash application.Although I can make both dash-okta-auth and dash-google-auth work out of the box (after proper configuration of the authentication provider endpoints) I cannot get user information (id of whoever is logged on).
Although I am not a proper developer, I went as far as making a local copy of dash-okta-auth (a close derivative of dash-google-auth) in my project to do some minor debugging – interestingly enough, inside the class OktaOAuth(Auth), the code

        try:
            resp = okta.get("/oauth2/default/v1/userinfo")
            assert resp.ok, resp.text

always raises the exception, which is probably why I can’t find the userinfo anywhere in the OktaOAuth object.

On the other hand, I tried to circunvent the lack of information by interrogating the Okta endpoints directly. For example, a GET request on

URL = “https://dev-XXXXXX.okta.com/oauth2/default/v1/keys?client_id=xxxxxxxxxx

works just fine, but another request that would, in principle, provide the information I need would proably require almost mimicking what dash-okta-auth is doing.

So, I’m almost stuck on this matter, but I need to find a solution for this urgently.
Any help out there? Someone that has a profound knowledge on authentication?
Thanks.

did you try this one?

Personally I register my Dash app on another main Flask app and protect the routes to the Dash app. That way I can use any Flask Auth solution available, without worrying about actual Dash compatibility.

I used this setup: https://github.com/okomarov/dash_on_flask

Hi, Mark, thank you so much for all the help.

I installed the newest dash-okta-oauth2 and it works fine, but I still don’t know where to extract the user info from – I’ve written an email to the package developer, but no answer so far. Meanwhile, I also followed your advise and started looking for a solution using flask-dance, since my app already is a Dash app inside a Flask server. I tried the following simplified code, adapted from the flask-dance repository, which runs, but the unfortunately the google login window is never called (using incognito). I also read the blogs you recommended about dash-inside-flask solutions, but they seem quite convoluted.

import os
from flask import Flask, redirect, url_for
from flask_dance.contrib.google import make_google_blueprint, google
import dash
import dash_html_components as html


server = Flask(__name__)


server.config.update({
    'GOOGLE_OAUTH_CLIENT_ID': 'XXXXXXXXXXXXXXXXX',
    'GOOGLE_OAUTH_CLIENT_SECRET': 'XXXXXXXXXXXXXXXXXX'
})

server.secret_key = os.environ.get('FLASK_SECRET_KEY', 'whatever you want it to be 12345')
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

external_stylesheets = []
app = dash.Dash(__name__, server=server, url_base_pathname='/', external_stylesheets=external_stylesheets,
                meta_tags=[{'name': 'viewport', 'content': 'width=device-width, initial-scale=1'}])
app.config['suppress_callback_exceptions']=True

google_bp = make_google_blueprint(scope=["profile", "email", "openid"])
server.register_blueprint(google_bp, url_prefix="/login")

app.layout = html.Div(
    children=[
        html.P('Hello world')
    ],
)

@server.route("/")
def index():
    print('this message never shows up ...')
    if not google.authorized:
        return redirect(url_for("google.login"))
    resp = google.get("/oauth2/v1/userinfo")
    assert resp.ok, resp.text
    return "You are {email} on Google".format(email=resp.json()["email"])


if __name__ == '__main__':
    app.run_server(debug=True)

Do I need to construct a proper Flask folder structure for the declared routes before running this code? What am I missing? My idea of using Dash is not having to deal with Flask too much.

I feel like I’m going into a rabbit’s hole by exploring these flask and flask-dance solutions, when all I want is the user info from the OktaAuth or GoogleAuth objects.
Thanks.

Hi @florescorreia,

yes, a proper folder structure would be required I think yes. I didn’t try to combine flask-dance with dash yet, so I can only do guess work here. You can strip down the example in that blog post to your needs.

This would be the relevant flask part. Exchange the permissions.admin(dashapp.server.view_functions[view_func]). This must be adapted to the flask-dance solution, ideally by just exchanging the permissions function here. Check the blog post again, once you have set that up, you can forget about flask and continue working in dash. I’m not too experienced tho, there might be better solutions, but this is the way I would go now.

Hope it helps.

First file

# your_app_folder/dashapp.py
from app.app import create_app

server = create_app()

Second file

# your_app_folder/app/app.py
import dash
from flask import Flask
from app.infrastructure import permissions

def create_app():
    # Create Flask server app
    server = Flask(__name__)
    register_dashapps(server) 
    setup_db(server) #your database storage solution

    return server


########### REGISTER DASHAPPS ###########

def register_dashapps(app):
'''abc is my dashapp folder, I chose to build a multipage app 
with a sidebar from this example: https://dash-bootstrap-components.opensource.faculty.ai/examples/simple-sidebar/'''
    from app.abc.sidebar import layout
    from app.abc.callbacks import register_callbacks


    # Meta tags for viewport responsiveness
    meta_viewport = {"name": "viewport", "content": "width=device-width, initial-scale=1, shrink-to-fit=no"}

    # Create dash app passing our server
    abc = dash.Dash(__name__,
                    server=app,
                    url_base_pathname='/dashboard/',
                    assets_folder=get_root_path(__name__) + '/abc/assets/',
                    meta_tags=[meta_viewport],
                    )

    abc.config.suppress_callback_exceptions = True
    # Push an application context so we can use Flask's 'current_app'
    with app.app_context():
        abc.title = 'abc'
        abc.layout = layout
        register_callbacks(abc)
        _protect_dashviews(abc) # this is the important function


def _protect_dashviews(dashapp):
''' permissions.admin is the relevant part for you, this is my own permissions function, 
but in the example blog post, this is done with a flask-login solution and an additional register_extensions(server) function and you could 
probably exchange the flask-login part in that with flask-dance here'''

    # Method to protect dash views/routes
    for view_func in dashapp.server.view_functions:
        if view_func.startswith(dashapp.config.url_base_pathname):
            dashapp.server.view_functions[view_func] = permissions.admin(dashapp.server.view_functions[view_func])

Hi @florescorreia,

The userinfo details are set in the flask session object, and you can refer to them as shown in the is_authorized method in https://github.com/nicohein/dash-okta-oauth2/blob/master/dash_okta_oauth2/okta_oauth.py.

The full list of attributes parsed from the /userinfo call is:

  • user_name
  • user_nickname
  • user_given_name
  • user_middle_name
  • user_family_name
  • user_profile
  • user_zoneinfo
  • user_updated_at
  • user_locale
  • user_email
  • user_email_verified
  • user_address
  • user_preferred_username
  • user_phone_number

If you need other attributes, I am happy to push an update.

You can see an example, using these attributes in https://github.com/nicohein/dash-okta-oauth2/blob/master/app.py line 66:

return f"Welcome, {session['user_given_name']} {session['user_family_name']}!"

Hope, this helps.

Thank you @nicohein for your fast reply.
Now I see, the problem with the old dash-okta-auth is that it doesn’t register the atributes from the /userinfo call in the SecureCookieSession object. I ran your solution with the old package and that info is nowhere to be found in variable session.

But, now, contrary to what I thought, I cannot make your package work, because there are differences in the URIs/redirects between your package and the old one. My app URIs at okta are registered as

https://xxxxxxxx/login/okta/authorized

The old package sends the redirect to the login page at

https://dev-xxxxxx.okta.com/login/login.htm?fromURI=/oauth2/v1/authorize/redirect?okta_key=XXXXXXX

which has been working for a long time now.

Your package is sending it to

https://dev-xxxxxx.okta.com/authorize?response_type=code&client_id=xxxxxxxxxxxxx&redirect_uri=http%3A%2F%2F127.0.0.1%3A8050%2Flogin%2Fokta%2Fauthorized&scope=openid+email+profile&state=xxxxxxxxxxxxxxxxxxx

Yours seems to be the correct one but – for some reason I cannot understand – the old one is working and the new one throws a 404 (page not found) error.

I have been going back and forth with the new one and the old one, and it is always the same – one works, the other one does not.

Should I register another URI at Okta that works with your package? Any advice?
Thanks.

Hi @florescorreia,

sorry for the delay in my responses. Please try to set the OKTA_BASE_URL like with /oauth2/default/v1 endpoint. E.g. OKTA_BASE_URL=https://dev-xxxxxxx.okta.com/oauth2/default/v1.

You should also be able to find the correct URL for your Developer account on the Okta admin page under API > Authorization Server.

Hope this solves the issue. Otherwise please try to run app.py example after cloning the https://github.com/nicohein/dash-okta-oauth2 repository and provide the logs generated.

Best
Nico

@nicohein, thank you, thank you so much. Case solved. It is working like a charm!

As per your last post, I changed the variable value into

‘OKTA_BASE_URL’: ‘https://dev-xxxxxx.okta.com/oauth2/default/v1

and it just worked.

So, for all of you out there still strugling to make Okta authentication work with added userinfo, here goes my code that puts everything together, thanks to @nicohein and dash-okta-oauth2 :

from flask import Flask, session
import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output, State
from dash_okta_oauth2 import OktaOAuth

server = Flask(__name__)

server.config.update({
    'OKTA_OAUTH_CLIENT_ID': xxxxxxxxxxxxxxxxxxxxxxx',
    'OKTA_OAUTH_CLIENT_SECRET': 'xxxxxxxxxxxxxxxxxxxxxxxxx',
    'OKTA_BASE_URL': 'https://dev-xxxxxx.okta.com/oauth2/default/v1',
})

server.secret_key = os.environ.get('FLASK_SECRET_KEY', 'xxxxxxxxxxxxxxxxxxxxxxxxxxx')
# clean this for production
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

external_stylesheets = []
app = dash.Dash(__name__, server=server, url_base_pathname='/', external_stylesheets=external_stylesheets,
                meta_tags=[{'name': 'viewport', 'content': 'width=device-width, initial-scale=1'}])
app.config['suppress_callback_exceptions']=True
app.title = xxxxxxxxxxxxx'

auth = OktaOAuth(app)

@server.route('/')
def MyDashApp():
    return app.index()

@app.callback(
    [Output('xxxxxxx', 'children')],
    [Input('yyyyyyy', 'children')],
)
def your_callback (ch):
    # get the fields of interest from session
    profile = session['user_profile']
    # use them for some purpose
    return []

Glad it has been solved!