Skip to content

Commit d4b2f2e

Browse files
committed
Proof of Concept
Identity supports B2C Pivot to standalone Identity Web
1 parent 0cc2f1e commit d4b2f2e

File tree

5 files changed

+43
-64
lines changed

5 files changed

+43
-64
lines changed

app.py

Lines changed: 25 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import uuid
21
import requests
32
from flask import Flask, render_template, session, request, redirect, url_for
43
from flask_session import Session # https://pythonhosted.org/Flask-Session
5-
import msal
4+
from werkzeug.exceptions import Unauthorized, Forbidden
5+
from identity import __version__
6+
from identity.web import Web, LifespanValidator
67
import app_config
78

89

@@ -17,43 +18,43 @@
1718
from werkzeug.middleware.proxy_fix import ProxyFix
1819
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
1920

21+
web = Web(
22+
session=session,
23+
authority=app.config.get("AUTHORITY"),
24+
client_id=app.config["CLIENT_ID"],
25+
client_credential=app.config["CLIENT_SECRET"],
26+
redirect_uri="http://localhost:5000" + app.config["REDIRECT_PATH"], # It must match your redirect_uri
27+
validators=[LifespanValidator(seconds=3600, on_error=Unauthorized("Login expired"))],
28+
)
29+
2030
@app.route("/")
2131
def index():
22-
if not session.get("user"):
32+
if not web.get_user():
2333
return redirect(url_for("login"))
24-
return render_template('index.html', user=session["user"], version=msal.__version__)
34+
return render_template('index.html', user=web.get_user(), version=__version__)
2535

2636
@app.route("/login")
2737
def login():
28-
# Technically we could use empty list [] as scopes to do just sign in,
29-
# here we choose to also collect end user consent upfront
30-
session["flow"] = _build_auth_code_flow(scopes=app_config.SCOPE)
31-
return render_template("login.html", auth_url=session["flow"]["auth_uri"], version=msal.__version__)
38+
return render_template("login.html", version=__version__, **web.start_auth(scopes=app_config.SCOPE))
39+
40+
@app.errorhandler(Unauthorized)
41+
def handler(error):
42+
return redirect(url_for("login"))
3243

3344
@app.route(app_config.REDIRECT_PATH) # Its absolute URL must match your app's redirect_uri set in AAD
34-
def authorized():
35-
try:
36-
cache = _load_cache()
37-
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
38-
session.get("flow", {}), request.args)
39-
if "error" in result:
40-
return render_template("auth_error.html", result=result)
41-
session["user"] = result.get("id_token_claims")
42-
_save_cache(cache)
43-
except ValueError: # Usually caused by CSRF
44-
pass # Simply ignore them
45+
def auth_response():
46+
result = web.complete_auth(request.args)
47+
if "error" in result:
48+
return render_template("auth_error.html", result=result)
4549
return redirect(url_for("index"))
4650

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

5455
@app.route("/graphcall")
5556
def graphcall():
56-
token = _get_token_from_cache(app_config.SCOPE)
57+
token = web.get_token(app_config.SCOPE)
5758
if not token:
5859
return redirect(url_for("login"))
5960
graph_data = requests.get( # Use token to call downstream service
@@ -62,38 +63,6 @@ def graphcall():
6263
).json()
6364
return render_template('display.html', result=graph_data)
6465

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, authority=None):
77-
return msal.ConfidentialClientApplication(
78-
app_config.CLIENT_ID, authority=authority or app_config.AUTHORITY,
79-
client_credential=app_config.CLIENT_SECRET, token_cache=cache)
80-
81-
def _build_auth_code_flow(authority=None, scopes=None):
82-
return _build_msal_app(authority=authority).initiate_auth_code_flow(
83-
scopes or [],
84-
redirect_uri=url_for("authorized", _external=True))
85-
86-
def _get_token_from_cache(scope=None):
87-
cache = _load_cache() # This web app maintains one cache per session
88-
cca = _build_msal_app(cache=cache)
89-
accounts = cca.get_accounts()
90-
if accounts: # So all account(s) belong to the current signed-in user
91-
result = cca.acquire_token_silent(scope, account=accounts[0])
92-
_save_cache(cache)
93-
return result
94-
95-
app.jinja_env.globals.update(_build_auth_code_flow=_build_auth_code_flow) # Used in template
96-
9766
if __name__ == "__main__":
9867
app.run()
9968

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ werkzeug>=2
1212

1313
flask-session>=0.3.2,<0.5
1414
requests>=2,<3
15-
msal>=1.7,<2
15+
identity>=0.1,<0.2
1616

1717
# cachelib==0.1 # Only need this if you are running Python 2
1818
# Note: This sample does NOT directly depend on cachelib.

templates/auth_error.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
<head>
44
<meta charset="UTF-8">
55

6-
{% if config.get("B2C_RESET_PASSWORD_AUTHORITY") and "AADB2C90118" in result.get("error_description") %}
6+
{% if config.get("B2C_RESET_PASSWORD_AUTHORITY") and "AADB2C90118" in result.get("error_description") %} <!-- This will be reached when user forgot their password -->
77
<!-- See also https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-policies#linking-user-flows -->
8-
<meta http-equiv="refresh" content='0;{{_build_auth_code_flow(authority=config["B2C_RESET_PASSWORD_AUTHORITY"])["auth_uri"]}}'>
8+
<meta http-equiv="refresh" content='0;{{config.get("B2C_RESET_PASSWORD_AUTHORITY")}}?client_id={{config.get("CLIENT_ID")}}'>
99
{% endif %}
1010
</head>
1111
<body>

templates/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ <h2>Welcome {{ user.get("name") }}!</h2>
1212
{% endif %}
1313

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

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

templates/login.html

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,24 @@
66
<body>
77
<h1>Microsoft Identity Python Web App</h1>
88

9-
<li><a href='{{ auth_url }}'>Sign In</a></li>
9+
{% if user_code %}
10+
<ol>
11+
<li>To sign in, type <b>{{ user_code }}</b> into
12+
<a href='{{ auth_uri }}' target=_blank>{{ auth_uri }}</a>
13+
to authenticate.
14+
</li>
15+
<li>And then <a href="{{ url_for('auth_response') }}">proceed</a>.</li>
16+
</ol>
17+
{% else %}
18+
<li><a href='{{ auth_uri }}'>Sign In</a></li>
19+
{% endif %}
1020

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

1525
<hr>
16-
<footer style="text-align: right">Powered by MSAL Python {{ version }}</footer>
26+
<footer style="text-align: right">Powered by Identity Web {{ version }}</footer>
1727
</body>
1828
</html>
1929

0 commit comments

Comments
 (0)