Skip to content

Commit 1492132

Browse files
committed
Remove global MSAL app instance, adjust structure.
7 files changed, 139 insertions(+), 178 deletions(-) rewrite app.py (94%) rewrite app_config.py (83%) delete mode 100644 config.py create mode 100644 requirements.txt rewrite templates/display.html (87%) delete mode 100644 templates/index.html
1 parent e1199b4 commit 1492132

7 files changed

Lines changed: 120 additions & 157 deletions

File tree

README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ endpoint: Microsoft Identity platform (formerly Azure AD v2.0)
1818
1919
### Overview
2020

21-
This sample demonstrates a Python web application that signs-in users with the Microsoft identity platform and calls the Microsoft Graph
21+
This sample demonstrates a Python web application that signs-in users with the Microsoft identity platform and calls the Microsoft Graph.
2222

2323
1. The python web application uses the Microsoft Authentication Library (MSAL) to obtain a JWT access token from the Microsoft identity platform (formerly Azure AD v2.0):
2424
2. The access token is used as a bearer token to authenticate the user when calling the Microsoft Graph.
@@ -27,16 +27,18 @@ This sample demonstrates a Python web application that signs-in users with the M
2727

2828
### Scenario
2929

30-
This sample shows how to build a Python web app that uses OAuth2 to get access to Microsoft Graph using MSAL Python. For more information about how th eprotocols work in this scenario and other scenarios, see [Authentication Scenarios for Azure AD](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-scenarios).
30+
This sample shows how to build a Python web app using Flask and MSAL Python,
31+
that signs in a user, and get access to Microsoft Graph.
32+
For more information about how the protocols work in this scenario and other scenarios,
33+
see [Authentication Scenarios for Azure AD](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-authentication-scenarios).
3134

3235
## How to run this sample
3336

3437
To run this sample, you'll need:
3538

36-
> To run this sample you will need:
3739
> - [Python 2.7+](https://www.python.org/downloads/release/python-2713/) or [Python 3+](https://www.python.org/downloads/release/python-364/)
3840
> - [Flask](http://flask.pocoo.org/), [Flask-Session](https://pythonhosted.org/Flask-Session/), [requests](https://2.python-requests.org/en/master/)
39-
> - [MSAL Python](https://github.com/AzureAD/microsoft-authentication-library-for-python)
41+
> - [MSAL Python](https://github.com/AzureAD/microsoft-authentication-library-for-python)
4042
> - 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)
4143
4244

@@ -50,7 +52,7 @@ git clone https://github.com/Azure-Samples/ms-identity-python-webapp.git
5052

5153
or download and extract the repository .zip file.
5254

53-
> Given that the name of the sample is quiet long, you might want to clone it in a folder close to the root of your hard drive, to avoid file size limitations on Windows.
55+
> 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 lenghth limitations when running on Windows.
5456
5557
### Step 2: Register the sample application with your Azure Active Directory tenant
5658

@@ -122,22 +124,20 @@ In the steps below, "ClientID" is the same as "Application ID" or "AppId".
122124

123125
#### Configure the pythonwebapp project
124126

125-
> Note: if you used the setup scripts, the changes below will have been applied for you
127+
> Note: if you used the setup scripts, the changes below may have been applied for you
126128
127129
1. Open the `app_config.py` file
128130
1. Find the app key `Enter_the_Tenant_Name_Here` and replace the existing value with your Azure AD tenant name.
129-
1. Find the app key `Enter_the_Client_Secret_Here` and replace the existing value with the key you saved during the creation of the `python-webapp` app, in the Azure portal.
131+
1. You could find the app key `Enter_the_Client_Secret_Here` and replace the existing value with the key you saved during the creation of the `python-webapp` app, in the Azure portal.
132+
But we recommend you to store the secret in environment variable, rather than in file.
130133
1. Find the app key `Enter_the_Application_Id_here` and replace the existing value with the application ID (clientId) of the `python-webapp` application copied from the Azure portal.
131134

132135

133136
### Step 4: Run the sample
134137

135-
- You will need to install MSAL Python library, Flask framework, Flask-Sessions for server side session management and requests using pip as follows:
138+
- You will need to install dependencies using pip as follows:
136139
```Shell
137-
$ pip install msal
138-
$ pip install flask
139-
$ pip install Flask-Session
140-
$ pip install requests
140+
$ pip install -r requirements.txt
141141
```
142142
- If the environment variable for Flask is already set:
143143

app.py

Lines changed: 78 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,93 @@
11
import uuid
2-
import flask
32
import requests
4-
from flask import Flask, render_template, session, request
5-
from flask_session import Session
3+
from flask import Flask, render_template, session, request, redirect, url_for
4+
from flask_session import Session # https://pythonhosted.org/Flask-Session
65
import msal
76
import app_config
87

9-
sess = Session()
10-
app = Flask(__name__)
11-
app.config.from_object('config.Config')
12-
sess.init_app(app)
13-
cache = msal.SerializableTokenCache()
14-
application = msal.ConfidentialClientApplication(
15-
app_config.CLIENT_ID, authority=app_config.AUTHORITY,
16-
client_credential=app_config.CLIENT_SECRET,
17-
token_cache=cache)
18-
19-
20-
def set_cache():
21-
if cache.has_state_changed:
22-
session[request.cookies.get("session")] = cache.serialize()
23-
24-
25-
def check_cache():
26-
# Checking token cache for accounts
27-
result = None
28-
accounts = application.get_accounts()
298

30-
# Trying to acquire token silently
31-
if accounts:
32-
result = application.acquire_token_silent(app_config.SCOPE, account=accounts[0])
33-
return result
34-
35-
36-
def get_graph_info(result):
37-
if 'access_token' not in result:
38-
return flask.redirect(flask.url_for('index'))
39-
endpoint = 'https://graph.microsoft.com/v1.0/me/'
40-
http_headers = {'Authorization': 'Bearer ' + result['access_token'],
41-
'User-Agent': 'msal-python-sample',
42-
'Accept': 'application/json',
43-
'Content-Type': 'application/json',
44-
'client-request-id': str(uuid.uuid4())}
45-
graph_data = requests.get(endpoint, headers=http_headers, stream=False).json()
46-
return graph_data
9+
app = Flask(__name__)
10+
app.config["SESSION_TYPE"] = "filesystem" # We choose to store tokens in server-side session
11+
Session(app)
4712

4813

49-
@app.route('/')
14+
@app.route("/")
5015
def index():
51-
return render_template("index.html")
52-
53-
54-
@app.route('/processing')
55-
def processing():
56-
# Initializing
57-
is_session = session.get(request.cookies.get("session"))
58-
if is_session is None:
59-
session[request.cookies.get("session")] = ''
60-
cache.deserialize(session.get(request.cookies.get("session")))
61-
return flask.redirect(flask.url_for('my_info'))
62-
63-
64-
@app.route('/my_info')
65-
def my_info():
66-
result = check_cache()
67-
if result:
68-
graph_result = get_graph_info(result)
69-
return flask.render_template('display.html', auth_result=graph_result, cond="logout")
70-
else:
71-
return flask.render_template('display.html', auth_result="You are not signed in", cond="")
72-
73-
74-
@app.route('/authenticate')
75-
def authenticate():
76-
# Call to the authorize endpoint
77-
auth_state = str(uuid.uuid4())
78-
session[(request.cookies.get("session")+'state')] = auth_state
79-
authorization_url = application.get_authorization_request_url(app_config.SCOPE, state=auth_state,
80-
redirect_uri=app_config.REDIRECT_URI)
81-
resp = flask.Response(status=307)
82-
resp.headers['location'] = authorization_url
83-
return resp
84-
85-
86-
@app.route("/getAToken")
87-
def main_logic():
88-
code = flask.request.args['code']
89-
state = flask.request.args['state']
90-
# Raising error if state does not match
91-
if state != session[(request.cookies.get("session")+'state')]:
92-
raise ValueError("State does not match")
93-
result = application.acquire_token_by_authorization_code(code, scopes=app_config.SCOPE,
94-
redirect_uri=app_config.REDIRECT_URI)
95-
# Updating cache
96-
set_cache()
97-
98-
# Using access token from result to call Microsoft Graph
99-
graph_data = get_graph_info(result)
100-
return flask.render_template('display.html', auth_result=graph_data, cond="logout")
101-
16+
if not session.get("user"):
17+
return redirect(url_for("login"))
18+
return """Welcome, %s.
19+
<li><a href='/graphcall'>Call Microsoft Graph API</a></li>
20+
<li><a href="/logout">Logout</a></li>
21+
""" % session["user"].get("name")
22+
23+
@app.route("/login")
24+
def login():
25+
session["state"] = str(uuid.uuid4())
26+
auth_url = _build_msal_app().get_authorization_request_url(
27+
app_config.SCOPE, # Technically we can use empty list [] to just sign in,
28+
# here we choose to also collect end user consent upfront
29+
state=session["state"],
30+
redirect_uri=url_for("authorized", _external=True))
31+
return "<a href='%s'>Login with Microsoft Identity</a>" % auth_url
32+
33+
@app.route("/getAToken") # Its absolute URL must match your app's redirect_uri set in AAD
34+
def authorized():
35+
if request.args['state'] != session.get("state"):
36+
return redirect(url_for("login"))
37+
cache = _load_cache()
38+
result = _build_msal_app(cache).acquire_token_by_authorization_code(
39+
request.args['code'],
40+
scopes=app_config.SCOPE, # Misspelled scope would cause an HTTP 400 error here
41+
redirect_uri=url_for("authorized", _external=True))
42+
if "error" in result:
43+
return "Login failure: %s, %s" % (
44+
result["error"], result.get("error_description"))
45+
session["user"] = result.get("id_token_claims")
46+
_save_cache(cache)
47+
return redirect(url_for("index"))
10248

10349
@app.route("/logout")
10450
def logout():
105-
# Logout
106-
accounts = application.get_accounts()
107-
application.remove_account(accounts[0])
108-
set_cache()
109-
return flask.redirect(flask.url_for('index'))
110-
51+
session["user"] = None # Mark current session as not-logged-in
52+
# session.clear() # If you prefer, this would nuke the user's token cache too
53+
return redirect(url_for("index"))
54+
55+
@app.route("/graphcall")
56+
def graphcall():
57+
token = _get_token_from_cache(app_config.SCOPE)
58+
if not token:
59+
return redirect(url_for("login"))
60+
graph_data = requests.get( # Use token to call downstream service
61+
app_config.ENDPOINT,
62+
headers={'Authorization': 'Bearer ' + token['access_token']},
63+
).json()
64+
return render_template('display.html', result=graph_data)
65+
66+
67+
def _load_cache():
68+
cache = msal.SerializableTokenCache()
69+
if session.get("token_cache"):
70+
cache.deserialize(session["token_cache"])
71+
return cache
72+
73+
def _save_cache(cache):
74+
if cache.has_state_changed:
75+
session["token_cache"] = cache.serialize()
76+
77+
def _build_msal_app(cache=None):
78+
return msal.ConfidentialClientApplication(
79+
app_config.CLIENT_ID, authority=app_config.AUTHORITY,
80+
client_credential=app_config.CLIENT_SECRET, token_cache=cache)
81+
82+
def _get_token_from_cache(scope=None):
83+
cache = _load_cache() # This web app maintains one cache per session
84+
cca = _build_msal_app(cache)
85+
accounts = cca.get_accounts()
86+
if accounts: # So all account(s) belong to the current signed-in user
87+
result = cca.acquire_token_silent(scope, account=accounts[0])
88+
_save_cache(cache)
89+
return result
11190

11291
if __name__ == "__main__":
11392
app.run()
93+

app_config.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
1-
AUTHORITY = "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here"
1+
import os
2+
3+
# This pattern is defined in Flask's documentation here
4+
# https://flask.palletsprojects.com/en/1.1.x/config/#configuring-from-environment-variables
5+
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
6+
if not CLIENT_SECRET:
7+
raise ValueError("Need to define CLIENT_SECRET environment variable")
8+
9+
AUTHORITY = "https://login.microsoftonline.com/common" # For multi-tenant app
10+
# AUTHORITY = "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here"
11+
212
CLIENT_ID = "Enter_the_Application_Id_here"
3-
CLIENT_SECRET = "Enter_the_Client_Secret_Here"
4-
SCOPE = ["https://graph.microsoft.com/User.Read"]
5-
REDIRECT_URI = "http://localhost:5000/getAToken"
13+
14+
# You can find more Microsoft Graph API endpoints from Graph Explorer
15+
# https://developer.microsoft.com/en-us/graph/graph-explorer
16+
ENDPOINT = 'https://graph.microsoft.com/v1.0/me/calendars'
17+
18+
# You can find the proper permission names from this document to form a scope
19+
# https://docs.microsoft.com/en-us/graph/permissions-reference
20+
SCOPE = ["https://graph.microsoft.com/Calendars.Read"]
21+

config.py

Lines changed: 0 additions & 10 deletions
This file was deleted.

requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Flask>=1,<2
2+
Flask-Session>=0,<1
3+
requests>=2,<3
4+
msal>=0,<2
5+

templates/display.html

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,11 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
5-
<title>Acquire Token Result </title>
65
</head>
76
<body>
8-
{% if cond %}
9-
<p1><b>Your information</b> </p1>
10-
<table>
11-
{% for key, value in auth_result.items() %}
12-
<tr>
13-
<th> {{ key }} </th>
14-
<td> {{ value }} </td>
15-
</tr>
16-
{% endfor %}
17-
</table>
18-
<form action="/logout" >
19-
<input type="submit" value=" Logout"/>
20-
</form>
21-
{% else %}
22-
<p1><b> {{auth_result}} </b> </p1>
23-
<form action="/authenticate" >
24-
<input type="submit" value=" Sign-in"/>
25-
</form>
26-
{% endif %}
7+
<h1>Graph API Call Result</h1>
8+
<pre>{{ result |tojson(indent=4) }}</pre> <!-- Just a generic json viewer -->
9+
<a href="javascript:window.history.go(-1)">Back</a>
2710
</body>
28-
</html>
11+
</html>
12+

templates/index.html

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)