Azure-Samples / ms-identity-python-webapp

A Python web application calling Microsoft graph that is secured using the Microsoft identity platform
MIT License
284 stars 135 forks source link

My app always redirect to login page even though it retrieves the code and status successfully from 'getAToken' method. #35

Closed ragehegy closed 4 years ago

ragehegy commented 4 years ago

My app always loads the index file with login button after successful authentication. Terminal shows the getAToken called with the code and status strings, but it always redirects to the index. i just clonned the repo and changed the config parameters as per my registered app. Also the line app.jinja_env.globals.update(_build_auth_url=_build_auth_url) shows an error 'Method 'jinja_env' has no 'globals' member' Full app.py

import uuid
import requests
from flask import Flask, render_template, session, request, redirect, url_for
from flask_session import Session  # https://pythonhosted.org/Flask-Session
import msal
import app_config

app = Flask(__name__)
app.config.from_object(app_config)
Session(app)

from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

@app.route("/")
def index():
    if not session.get("user"):
        return redirect(url_for("login"))
    return render_template('index.html', user=session["user"], version=msal.__version__)

@app.route("/login")
def login():
    session["state"] = str(uuid.uuid4())
    auth_url = _build_auth_url(scopes=app_config.SCOPE, state=session["state"])
    return render_template("login.html", auth_url=auth_url, version=msal.__version__)

@app.route(app_config.REDIRECT_PATH)  # Its absolute URL must match your app's redirect_uri set in AAD
def authorized():
    if request.args.get('state') != session.get("state"):
        return redirect(url_for("index"))  # No-OP. Goes back to Index page
    if "error" in request.args:  # Authentication/Authorization failure
        return render_template("auth_error.html", result=request.args)
    if request.args.get('code'):
        cache = _load_cache()
        result = _build_msal_app(cache=cache).acquire_token_by_authorization_code(
            request.args['code'],
            scopes=app_config.SCOPE,  # Misspelled scope would cause an HTTP 400 error here
            redirect_uri=url_for("authorized", _external=True))
        if "error" in result:
            return render_template("auth_error.html", result=result)
        session["user"] = result.get("id_token_claims")
        _save_cache(cache)
    return redirect(url_for("index"))

@app.route("/logout")
def logout():
    session.clear()  # Wipe out user and its token cache from session
    return redirect(  # Also logout from your tenant's web session
        app_config.AUTHORITY + "/oauth2/v2.0/logout" +
        "?post_logout_redirect_uri=" + url_for("index", _external=True))

@app.route("/graphcall")
def graphcall():
    token = _get_token_from_cache(app_config.SCOPE)
    if not token:
        return redirect(url_for("login"))
    graph_data = requests.get(  # Use token to call downstream service
        app_config.ENDPOINT,
        headers={'Authorization': 'Bearer ' + token['access_token']},
        ).json()
    return render_template('display.html', result=graph_data)

def _load_cache():
    cache = msal.SerializableTokenCache()
    if session.get("token_cache"):
        cache.deserialize(session["token_cache"])
    return cache

def _save_cache(cache):
    if cache.has_state_changed:
        session["token_cache"] = cache.serialize()

def _build_msal_app(cache=None, authority=None):
    return msal.ConfidentialClientApplication(
        app_config.CLIENT_ID, authority=authority or app_config.AUTHORITY,
        client_credential=app_config.CLIENT_SECRET, token_cache=cache)

def _build_auth_url(authority=None, scopes=None, state=None):
    return _build_msal_app(authority=authority).get_authorization_request_url(
        scopes or [],
        state=state or str(uuid.uuid4()),
        redirect_uri=url_for("authorized", _external=True))

def _get_token_from_cache(scope=None):
    cache = _load_cache()  # This web app maintains one cache per session
    cca = _build_msal_app(cache=cache)
    accounts = cca.get_accounts()
    if accounts:  # So all account(s) belong to the current signed-in user
        result = cca.acquire_token_silent(scope, account=accounts[0])
        _save_cache(cache)
        return result

app.jinja_env.globals.update(_build_auth_url=_build_auth_url)  # Used in template

if __name__ == "__main__":
    app.run()
rayluo commented 4 years ago

Also the line app.jinja_env.globals.update(_build_auth_url=_build_auth_url) shows an error 'Method 'jinja_env' has no 'globals' member'

This sounds like a dependency environment issue. What is the pip list output of your environment? In particular, what is the version numbers for our direct dependencies here?

For example, my environment currently contains:

$ pip list
...
Flask              1.1.2
Flask-Session      0.3.2
...
msal               1.1.0
...
requests           2.22.0
...
Werkzeug           1.0.1
...

You do NOT have to use exact same versions as I do, but please let us know if you believe your particular combination does not work, so that we can investigate.

ragehegy commented 4 years ago

Thanks for fast reply @rayluo. I already had my own combination which worked fine for a regular flask applications. I tried your combination since mine didn't work. The following error while trying to install flask-oauthlib: ERROR: flask-oauthlib 0.9.5 has requirement oauthlib!=2.0.3,!=2.0.4,!=2.0.5,<3.0.0,>=1.1.2, but you'll have oauthlib 3.1.0 which is incompatible. Alternatively, I reinstalled the oauthlib with version 2.1.0 and got another error related to requests-oauthlib: ERROR: requests-oauthlib 1.3.0 has requirement oauthlib>=3.0.0, but you'll have oauthlib 2.1.0 which is incompatible. And still have the jinja_env error.

ragehegy commented 4 years ago

here's how my app requirements.txt looks like atm:

Flask>=1,<2 Flask-Session>=0,<1 requests>=2,<3 msal>=0,<2

ragehegy commented 4 years ago

Capture terminal output

ragehegy commented 4 years ago

tracing this lines

if request.args.get('state') != session.get("state"):
        return redirect(url_for("index"))

shows that the session object doesn't persist the state value after clicking login. It's always 'none', while the request.args.get('state') does have the original state session value.

ragehegy commented 4 years ago

Now it's working thanks to https://stackoverflow.com/questions/61602426/python-and-azuread-with-flask-ms-identity-python-webapp-example-only-logging-i i installed the flask-session from https://github.com/rayluo/flask-session and it finally got to work!

rayluo commented 4 years ago

Glad that you manage to make it work.

With regarding to the flask-session, you did an amazing research job on finding my workaround :-) But that shouldn't be necessary. An indirect upstream module, cachelib, released a relevant bugfix at about the same time you posted this issue. So now this sample can probably work out-of-box, if you deploy it in a freshly-installed environment.

Lastly, I happen to notice you committed your client_secret (among many temporary sessions which contains tokens in themselves) into your project repo. It is highly recommended that you remove them from repo, change your client_secret.

ragehegy commented 4 years ago

Lastly, I happen to notice you committed your client_secret (among many temporary sessions which contains tokens in themselves) into your project repo. It is highly recommended that you remove them from repo, change your client_secret.

My bad! Thanks for this tip. and thanks for your efforts, appreciate it.