Skip to content

Commit 980f2bc

Browse files
authored
Merge pull request #2 from Azure-Samples/app2
Pyton Web App with sign-in, calling MS Graph API, and sign-out
2 parents e1199b4 + 6e8e882 commit 980f2bc

File tree

7 files changed

+137
-169
lines changed

7 files changed

+137
-169
lines changed

README.md

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
---
22
page_type: sample
3-
languages: python
4-
product: azure-active-directory
5-
services: active-directory
6-
platforms: python
7-
author: abpati
8-
level: 300
9-
client: Python Web Application
10-
service: Microsoft Graph
11-
endpoint: Microsoft Identity platform (formerly Azure AD v2.0)
3+
languages:
4+
- python
5+
- powershell
6+
- html
7+
products:
8+
- azure-active-directory
9+
description: "This sample demonstrates a Java web application calling a Microsoft Graph that is secured using Azure Active Directory."
10+
urlFragment: ms-identity-python-webapp
1211
---
1312
# Integrating Microsoft Identity Platform with a Python web application
1413

@@ -18,7 +17,7 @@ endpoint: Microsoft Identity platform (formerly Azure AD v2.0)
1817
1918
### Overview
2019

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

2322
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):
2423
2. The access token is used as a bearer token to authenticate the user when calling the Microsoft Graph.
@@ -27,16 +26,16 @@ This sample demonstrates a Python web application that signs-in users with the M
2726

2827
### Scenario
2928

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

3234
## How to run this sample
3335

3436
To run this sample, you'll need:
3537

36-
> To run this sample you will need:
3738
> - [Python 2.7+](https://www.python.org/downloads/release/python-2713/) or [Python 3+](https://www.python.org/downloads/release/python-364/)
38-
> - [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)
4039
> - 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)
4140
4241

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

5150
or download and extract the repository .zip file.
5251

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.
52+
> 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.
5453
5554
### Step 2: Register the sample application with your Azure Active Directory tenant
5655

@@ -122,37 +121,27 @@ In the steps below, "ClientID" is the same as "Application ID" or "AppId".
122121

123122
#### Configure the pythonwebapp project
124123

125-
> Note: if you used the setup scripts, the changes below will have been applied for you
124+
> Note: if you used the setup scripts, the changes below may have been applied for you
126125
127126
1. Open the `app_config.py` file
128127
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.
128+
1. You saved your application secret during the creation of the `python-webapp` app in the Azure portal.
129+
Now you can set the secret in environment variable `CLIENT_SECRET`,
130+
and then adjust `app_config.py` to pick it up.
130131
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.
131132

132133

133134
### Step 4: Run the sample
134135

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

144141
Run app.py from shell or command line:
145142
```Shell
146143
$ python app.py
147144
```
148-
- If the environment variable for Flask is not set:
149-
150-
Type the following commands on shell or command line by navigating to the project directory:
151-
```Shell
152-
$ export FLASK_APP=app.py
153-
$ export FLASK_DEBUG=1
154-
$ flask run
155-
```
156145

157146
## Community Help and Support
158147

app.py

Lines changed: 77 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,92 @@
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.from_object(app_config)
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 render_template('index.html', user=session["user"])
19+
20+
@app.route("/login")
21+
def login():
22+
session["state"] = str(uuid.uuid4())
23+
auth_url = _build_msal_app().get_authorization_request_url(
24+
app_config.SCOPE, # Technically we can use empty list [] to just sign in,
25+
# here we choose to also collect end user consent upfront
26+
state=session["state"],
27+
redirect_uri=url_for("authorized", _external=True))
28+
return "<a href='%s'>Login with Microsoft Identity</a>" % auth_url
29+
30+
@app.route("/getAToken") # Its absolute URL must match your app's redirect_uri set in AAD
31+
def authorized():
32+
if request.args['state'] != session.get("state"):
33+
return redirect(url_for("login"))
34+
cache = _load_cache()
35+
result = _build_msal_app(cache).acquire_token_by_authorization_code(
36+
request.args['code'],
37+
scopes=app_config.SCOPE, # Misspelled scope would cause an HTTP 400 error here
38+
redirect_uri=url_for("authorized", _external=True))
39+
if "error" in result:
40+
return "Login failure: %s, %s" % (
41+
result["error"], result.get("error_description"))
42+
session["user"] = result.get("id_token_claims")
43+
_save_cache(cache)
44+
return redirect(url_for("index"))
10245

10346
@app.route("/logout")
10447
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-
48+
session["user"] = None # Log out from this app from its session
49+
# session.clear() # If you prefer, this would nuke the user's token cache too
50+
return redirect( # Also need to logout from Microsoft Identity platform
51+
"https://login.microsoftonline.com/common/oauth2/v2.0/logout"
52+
"?post_logout_redirect_uri=" + url_for("index", _external=True))
53+
54+
@app.route("/graphcall")
55+
def graphcall():
56+
token = _get_token_from_cache(app_config.SCOPE)
57+
if not token:
58+
return redirect(url_for("login"))
59+
graph_data = requests.get( # Use token to call downstream service
60+
app_config.ENDPOINT,
61+
headers={'Authorization': 'Bearer ' + token['access_token']},
62+
).json()
63+
return render_template('display.html', result=graph_data)
64+
65+
66+
def _load_cache():
67+
cache = msal.SerializableTokenCache()
68+
if session.get("token_cache"):
69+
cache.deserialize(session["token_cache"])
70+
return cache
71+
72+
def _save_cache(cache):
73+
if cache.has_state_changed:
74+
session["token_cache"] = cache.serialize()
75+
76+
def _build_msal_app(cache=None):
77+
return msal.ConfidentialClientApplication(
78+
app_config.CLIENT_ID, authority=app_config.AUTHORITY,
79+
client_credential=app_config.CLIENT_SECRET, token_cache=cache)
80+
81+
def _get_token_from_cache(scope=None):
82+
cache = _load_cache() # This web app maintains one cache per session
83+
cca = _build_msal_app(cache)
84+
accounts = cca.get_accounts()
85+
if accounts: # So all account(s) belong to the current signed-in user
86+
result = cca.acquire_token_silent(scope, account=accounts[0])
87+
_save_cache(cache)
88+
return result
11189

11290
if __name__ == "__main__":
11391
app.run()
92+

app_config.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
1-
AUTHORITY = "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here"
1+
import os
2+
3+
CLIENT_SECRET = "Enter_the_Client_Secret_Here" # Our Quickstart uses this placeholder
4+
# In your production app, we recommend you to use other ways to store your secret,
5+
# such as KeyVault, or environment variable as described in Flask's documentation here
6+
# https://flask.palletsprojects.com/en/1.1.x/config/#configuring-from-environment-variables
7+
# CLIENT_SECRET = os.getenv("CLIENT_SECRET")
8+
# if not CLIENT_SECRET:
9+
# raise ValueError("Need to define CLIENT_SECRET environment variable")
10+
11+
AUTHORITY = "https://login.microsoftonline.com/common" # For multi-tenant app
12+
# AUTHORITY = "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here"
13+
214
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"
15+
16+
# You can find more Microsoft Graph API endpoints from Graph Explorer
17+
# https://developer.microsoft.com/en-us/graph/graph-explorer
18+
ENDPOINT = 'https://graph.microsoft.com/v1.0/users' # This resource requires no admin consent
19+
20+
# You can find the proper permission names from this document
21+
# https://docs.microsoft.com/en-us/graph/permissions-reference
22+
SCOPE = ["User.ReadBasic.All"]
23+
24+
SESSION_TYPE = "filesystem" # So token cache will be stored in server-side session
25+

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: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
5-
<title>Title</title>
65
</head>
76
<body>
8-
<form action="/processing" >
9-
<input type="submit" value="Get my information from graph"/>
10-
</form>
7+
<h1>Microsoft Identity Python Web App</h1>
8+
Welcome {{ user.get("name") }}!
9+
<li><a href='/graphcall'>Call Microsoft Graph API</a></li>
10+
<li><a href="/logout">Logout</a></li>
1111
</body>
12-
</html>
12+
</html>
13+

0 commit comments

Comments
 (0)