Over the last month, I've been working a lot with the Microsoft Graph API identity platform, which makes it possible for users to login to a website with a Microsoft 365 account. With some additional configuration, it even acts as a generic customer identity management solution.
For Python developers, msal has been the traditional package for interacting with the API. However, the author of that library recently developed a higher-level package, identity, to wrap the most common authentication flow. The ms-identity-python-webapp sample demonstrates how to use that flow in a minimal Flask web app.
Flask developers often use Blueprints and Application factories to organize large Flask apps, and many requested guidance on integrating the authentication code with a Blueprint-based application. I have developed a few Blueprint-based apps lately, so I decided to attempt the integration myself. Good news: it worked! 🎉
You can see my integration in the identity branch of flask-surveys-container-app, and look through the changes in the pull request.
To aid others in their integration, I'll walk through the notable bits.
The auth blueprint
First I added an auth
blueprint to bundle up the auth-related routes and templates.
auth/
├── __init__.py
├── routes.py
└── templates/
└── auth/
├── auth_error.html
└── login.html
The routes in the blueprint are similar to those in the original sample.
However, I added a line to store next_url
inside the current session
. By remembering that, a user can login from any page and get redirected back to that page (versus getting redirected back to the index). That's a much nicer user experience in a large app.
@bp.route("/login")
def login():
auth = current_app.config["AUTH"]
session["next_url"] = request.args.get("next_url", url_for("index"))
return render_template(
"auth/login.html",
version=identity.__version__,
**auth.log_in(
scopes=current_app.config["SCOPE"],
redirect_uri=url_for(".auth_response", _external=True)
),
)
@bp.route("/getAToken")
def auth_response():
auth = current_app.config["AUTH"]
result = auth.complete_log_in(request.args)
if "error" in result:
return render_template("auth/auth_error.html", result=result)
next_url = session.pop("next_url", url_for("index"))
return redirect(next_url)
@bp.route("/logout")
def logout():
auth = current_app.config["AUTH"]
return redirect(auth.log_out(url_for("index", _external=True)))
The templates in the auth
blueprint now extend base.html
, the only template in the root templates directory. That gives the authentication-related pages a more consistent UI with the rest of the site.
App configuration
The global app configuration happens in the root __init__.py
, mostly inside the create_app
function.
To make sure I could access the identity package's Auth
object from any blueprint, I added it to app.config
:
app.config.update(
AUTH=identity.web.Auth(
session=session,
authority=app.config.get("AUTHORITY"),
client_id=app.config["CLIENT_ID"],
client_credential=app.config["CLIENT_SECRET"],
)
)
I also used the app.context_processor
decorator to inject a user
variable into every template that gets rendered.
@app.context_processor
def inject_user():
auth = app.config["AUTH"]
user = auth.get_user()
return dict(user=user)
Finally, I registered the new auth
blueprint in the usual way:
from backend.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix="")
Requiring login
Typically, in an app with user authentication, logging in is required for some routes while optional for others. To mark routes accordingly, I defined a login_required
decorator:
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
auth = current_app.config["AUTH"]
if auth.get_user() is None:
login_url = url_for("auth.login", next_url=request.url)
return redirect(login_url)
return f(*args, **kwargs)
return decorated_function
When that decorator sees that there is no current user, it forces a redirect to the auth
blueprint's login route, and passes along the current URL as the next URL.
I then applied that decorator to any route that required login:
@bp.route("/surveys/new", methods=["GET"])
@login_required
def surveys_create_page():
return render_template("surveys/surveys_create.html")
Accessing user in templates
I want to let users know their current login status on every page in the app, and give them a way to either login or log out. To accomplish that, I added some logic to the Bootstrap nav bar in base.html:
{% if user %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="/"
id="navbarDropdown" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
{{ user.get("name")}}
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">Sign out</a>
</li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">Sign in</a>
</li>
{% endif %}
...And that's about it! Look throughthe full code or pull request for more details. Let me know if you have suggestions for a better way to architect the auth blueprint, or if you end up using this in your own app. 🤔
1 comment:
This is great, I'm currently figuring out the best way to do this. As a non-programmer it's good to find examples such as yours with an explanation instead of digging through git repos.
Question though, how would you suggest including authorisation? Eg using Azure And/Entra ID group membership or app roles to control access to routes?
I note that authZ isn't yet implemented in ms-identity-python.
Thank you.
Post a Comment