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

Use a new helper library Identity Web #88

Merged
merged 2 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ see [Authentication Scenarios for Azure AD](https://docs.microsoft.com/en-us/azu

To run this sample, you'll need:

> - [Python 2.7+](https://www.python.org/downloads/release/python-2713/) or [Python 3+](https://www.python.org/downloads/release/python-364/)
> - [Python 3](https://www.python.org/downloads/)
> - An Azure Active Directory (Azure AD) tenant. For more information on how to get an Azure AD tenant, see [how to get an Azure AD tenant.](https://docs.microsoft.com/azure/active-directory/develop/quickstart-create-new-tenant)


Expand All @@ -47,7 +47,7 @@ From your shell or command line:
git clone https://github.com/Azure-Samples/ms-identity-python-webapp.git
```

or download and extract the repository .zip file.
or download and extract [the repository .zip file](https://github.com/Azure-Samples/ms-identity-python-webapp/archive/refs/heads/master.zip).

> Given that the name of the sample is quite long, you might want to clone it in a folder close to the root of your hard drive, to avoid file name length limitations when running on Windows.

Expand Down Expand Up @@ -138,7 +138,7 @@ In the steps below, "ClientID" is the same as "Application ID" or "AppId".
$ pip install -r requirements.txt
```

Run app.py from shell or command line. Note that the host and port values need to match what you've set up in your redirect_uri:
Run Flask on this project's directory (where `app.py` locates). Note that the host and port values need to match what you've set up in your redirect_uri:

```Shell
$ flask run --host localhost --port 5000
Expand Down
96 changes: 29 additions & 67 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
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 identity, identity.web
import requests
import app_config


Expand All @@ -17,82 +16,45 @@
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__)
auth = identity.web.Auth(
session=session,
authority=app.config.get("AUTHORITY"),
client_id=app.config["CLIENT_ID"],
client_credential=app.config["CLIENT_SECRET"],
)

@app.route("/login")
def login():
# Technically we could use empty list [] as scopes to do just sign in,
# here we choose to also collect end user consent upfront
session["flow"] = _build_auth_code_flow(scopes=app_config.SCOPE)
return render_template("login.html", auth_url=session["flow"]["auth_uri"], version=msal.__version__)
return render_template("login.html", version=identity.__version__, **auth.log_in(
scopes=app_config.SCOPE, # Have user consent scopes during log-in
redirect_uri=url_for("auth_response", _external=True), # Optional. If present, this absolute URL must match your app's redirect_uri registered in Azure Portal
))

@app.route(app_config.REDIRECT_PATH) # Its absolute URL must match your app's redirect_uri set in AAD
def authorized():
try:
cache = _load_cache()
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
session.get("flow", {}), request.args)
if "error" in result:
return render_template("auth_error.html", result=result)
session["user"] = result.get("id_token_claims")
_save_cache(cache)
except ValueError: # Usually caused by CSRF
pass # Simply ignore them
return redirect(url_for("index"))
@app.route(app_config.REDIRECT_PATH)
def auth_response():
result = auth.complete_log_in(request.args)
return render_template("auth_error.html", result=result) if "error" in result else 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))
return redirect(auth.log_out(url_for("index", _external=True)))

@app.route("/")
def index():
if not auth.get_user():
return redirect(url_for("login"))
return render_template('index.html', user=auth.get_user(), version=identity.__version__)

@app.route("/graphcall")
def graphcall():
token = _get_token_from_cache(app_config.SCOPE)
if not token:
@app.route("/call_downstream_api")
def call_downstream_api():
token = auth.get_token_for_user(app_config.SCOPE)
if "error" in token:
return redirect(url_for("login"))
graph_data = requests.get( # Use token to call downstream service
api_result = requests.get( # Use token to call downstream api
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_code_flow(authority=None, scopes=None):
return _build_msal_app(authority=authority).initiate_auth_code_flow(
scopes or [],
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_code_flow=_build_auth_code_flow) # Used in template
return render_template('display.html', result=api_result)

if __name__ == "__main__":
app.run()
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ werkzeug>=2

flask-session>=0.3.2,<0.5
requests>=2,<3
msal>=1.7,<2
identity>=0.2,<0.3

# cachelib==0.1 # Only need this if you are running Python 2
# Note: This sample does NOT directly depend on cachelib.
Expand Down
4 changes: 2 additions & 2 deletions templates/auth_error.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<head>
<meta charset="UTF-8">

{% if config.get("B2C_RESET_PASSWORD_AUTHORITY") and "AADB2C90118" in result.get("error_description") %}
{% if config.get("B2C_RESET_PASSWORD_AUTHORITY") and "AADB2C90118" in result.get("error_description") %} <!-- This will be reached when user forgot their password -->
<!-- See also https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-policies#linking-user-flows -->
<meta http-equiv="refresh" content='0;{{_build_auth_code_flow(authority=config["B2C_RESET_PASSWORD_AUTHORITY"])["auth_uri"]}}'>
<meta http-equiv="refresh" content='0;{{config.get("B2C_RESET_PASSWORD_AUTHORITY")}}?client_id={{config.get("CLIENT_ID")}}'>
{% endif %}
</head>
<body>
Expand Down
2 changes: 1 addition & 1 deletion templates/display.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</head>
<body>
<a href="javascript:window.history.go(-1)">Back</a> <!-- Displayed on top of a potentially large JSON response, so it will remain visible -->
<h1>Graph API Call Result</h1>
<h1>Result of the downstream API Call</h1>
<pre>{{ result |tojson(indent=4) }}</pre> <!-- Just a generic json viewer -->
</body>
</html>
6 changes: 3 additions & 3 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ <h1>Microsoft Identity Python Web App</h1>
<h2>Welcome {{ user.get("name") }}!</h2>

{% if config.get("ENDPOINT") %}
<li><a href='/graphcall'>Call Microsoft Graph API</a></li>
<li><a href='/call_downstream_api'>Call a downstream API</a></li>
{% endif %}

{% if config.get("B2C_PROFILE_AUTHORITY") %}
<li><a href='{{_build_auth_code_flow(authority=config["B2C_PROFILE_AUTHORITY"])["auth_uri"]}}'>Edit Profile</a></li>
<li><a href='{{config.get("B2C_PROFILE_AUTHORITY")}}?client_id={{config.get("CLIENT_ID")}}'>Edit Profile</a></li>
{% endif %}

<li><a href="/logout">Logout</a></li>
<hr>
<footer style="text-align: right">Powered by MSAL Python {{ version }}</footer>
<footer style="text-align: right">Powered by Identity Web {{ version }}</footer>
</body>
</html>

16 changes: 13 additions & 3 deletions templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@
<body>
<h1>Microsoft Identity Python Web App</h1>

<li><a href='{{ auth_url }}'>Sign In</a></li>
{% if user_code %}
<ol>
<li>To sign in, type <b>{{ user_code }}</b> into
<a href='{{ auth_uri }}' target=_blank>{{ auth_uri }}</a>
to authenticate.
</li>
<li>And then <a href="{{ url_for('auth_response') }}">proceed</a>.</li>
</ol>
{% else %}
<li><a href='{{ auth_uri }}'>Sign In</a></li>
{% endif %}

{% if config.get("B2C_RESET_PASSWORD_AUTHORITY") %}
<li><a href='{{_build_auth_code_flow(authority=config["B2C_RESET_PASSWORD_AUTHORITY"])["auth_uri"]}}'>Reset Password</a></li>
<li><a href="{{config.get('B2C_RESET_PASSWORD_AUTHORITY')}}?client_id={{config.get('CLIENT_ID')}}">Reset Password</a></li>
{% endif %}

<hr>
<footer style="text-align: right">Powered by MSAL Python {{ version }}</footer>
<footer style="text-align: right">Powered by Identity Web {{ version }}</footer>
</body>
</html>