Skip to content

Commit

Permalink
Improve oauth glue
Browse files Browse the repository at this point in the history
Some providers e.g. Azure require the redirect URL to match exactly - including query params etc. To support that
we now store any 'next' value in the session (fs_oauth_next).

To enable better debugging a class that can be subclassed for each provider - the auth_response_error() method can be subclassed for better debugging/UX.

Convert our Github and Google pre-defined providers to the new class method.

Add support for Azure in the Oauth example code.

Change OAUTH_HANDSHAKE_ERROR to include info from authlib.

closes #912
  • Loading branch information
jwag956 committed Feb 4, 2024
1 parent f568cae commit 4d42bae
Show file tree
Hide file tree
Showing 14 changed files with 368 additions and 102 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.9"
python-version: "3.11"
- name: update pip
run: |
python -m pip install -U pip
Expand All @@ -73,7 +73,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.9"
python-version: "3.11"
- name: update pip
run: |
python -m pip install -U pip
Expand All @@ -87,7 +87,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.9"
python-version: "3.11"
- name: update pip
run: |
python -m pip install -U pip
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
default_language_version:
python: python3.9
python: python3.11
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
Expand Down
8 changes: 5 additions & 3 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ Fixes
redirect location.
- (:pr:`908`) Improve CSRF documentation and testing. Fix bug where a CSRF failure could
return an HTML page even if the request was JSON.
- (:pr:`xxx`) Chore - stop setting all config as attributes. init_app(\*\*kwargs) can only
- (:pr:`911`) Chore - stop setting all config as attributes. init_app(\*\*kwargs) can only
set forms, flags, and utility classes.
- (:pr:`xxx`) Remove deprecation of AUTO_LOGIN_AFTER_CONFIRM - it has a reasonable use case.
- (:pr:`911`) Remove deprecation of AUTO_LOGIN_AFTER_CONFIRM - it has a reasonable use case.
- (:issue:`912`) Improve oauth debugging support. Handle next propagation in a more general way.

Notes
++++++
Expand All @@ -57,7 +58,8 @@ Backwards Compatibility Concerns
- Passing in an AnonymousUser class as part of Security initialization has been removed.
- The never-public method _get_unauthorized_response has been removed.
- Social-Oauth - a new configuration variable :py:data:`SECURITY_POST_OAUTH_LOGIN_VIEW` was introduced
and it replaces :py:data:`SECURITY_POST_LOGIN_VIEW` in the oauthresponse logic.
and it replaces :py:data:`SECURITY_POST_LOGIN_VIEW` in the oauthresponse logic when
:py:data:`SECURITY_REDIRECT_BEHAVIOR` == `"spa"`.
- Two-Factor setup. Prior to this release when setting up "SMS" the `/tf-setup` endpoint could
be POSTed to w/o a phone number, and then another POST could be made to set the phone number.
This has always been confusing and added complexity to the code. Now, if "SMS" is selected, the phone number
Expand Down
10 changes: 10 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,17 @@ Utils
.. autoclass:: flask_security.SmsSenderFactory
:members: createSender

.. py:class:: OauthCbType[oauth: OAuth, token: t.Any]
This callback is called when the oauth
redirect happens. It must take the response from the provider and return
a tuple of <user_model_field_name, value> - which will be used
to look up the user in the datastore.

.. autoclass:: flask_security.OAuthGlue
:members: register_provider, register_provider_ext

.. autoclass:: flask_security.FsOAuthProvider
:members:


Expand Down
6 changes: 6 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,19 @@
("py:class", "AuthenticatorSelectionCriteria"),
("py:class", "UserVerificationRequirement"),
("py:class", "OAuth"),
("py:class", "OAuthError"),
("py:class", "authlib.integrations.flask_client.OAuth"),
("py:class", "t.Type"),
("py:class", "t.Callable"),
("py:class", "t.Tuple"),
("py:class", "t.Any"),
("py:class", "timedelta"),
]
autodoc_typehints = "description"
# autodoc_mock_imports = ["flask_sqlalchemy"]
autodoc_type_aliases = {
"CbType": "oauth_provider.CbType",
}

intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
Expand All @@ -121,6 +126,7 @@
"flask_sqlalchemy": ("https://flask-sqlalchemy.palletsprojects.com/", None),
"flask_login": ("https://flask-login.readthedocs.io/en/latest/", None),
"passlib": ("https://passlib.readthedocs.io/en/stable", None),
"authlib": ("https://docs.authlib.org/en/latest/", None),
}

# -- Options for HTML output ---------------------------------------------
Expand Down
6 changes: 5 additions & 1 deletion docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ views and features (such as two-factor authentication). Flask-Security is shippe
with support for github and google - others can be added by the application (see `loginpass`_
for many examples).

See :py:class:`flask_security.OAuthGlue`
See :py:class:`flask_security.OAuthGlue` and :py:class:`flask_security.FsOAuthProvider`

Please note - this is for authentication only, and the authenticating user must
already be a registered user in your application. Once authenticated, all further
Expand All @@ -292,6 +292,10 @@ for details. Note in particular, that you must setup and provide provider specif
information - and most importantly - XX_CLIENT_ID and XX_CLIENT_SECRET should be
specified as environment variables.

We have seen issues with some providers when `SESSION_COOKIE_SAMESITE` = "strict".
The handshake (sometimes just the first time when the user is being asked to accept your application)
fails due to the session cookie not getting sent as part of the redirect.

A very simple example of configuring social auth with Flask-Security is available
in the `examples` directory.

Expand Down
35 changes: 31 additions & 4 deletions examples/oauth/server/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Copyright 2020-2023 by J. Christopher Wagner (jwag). All rights reserved.
Copyright 2020-2024 by J. Christopher Wagner (jwag). All rights reserved.
:license: MIT, see LICENSE for more details.
A simple example of utilizing Flask-Security's oauth glue layer.
Expand All @@ -15,7 +15,7 @@
This example is designed for a browser based client.
This example uses github as the oauth provider. Before this example will work:
1) on github register a new oauth application and grap the CLIENT_ID and CLIENT_SECRET.
1) on github register a new oauth application and grab the CLIENT_ID and CLIENT_SECRET.
These must be passed in as env variables:
"GITHUB_CLIENT_ID" and "GITHUB_CLIENT_SECRET".
See: https://docs.authlib.org/en/latest/client/flask.html# for details.
Expand All @@ -30,17 +30,34 @@

import os

from flask import Flask, flash, render_template_string
from flask import Flask, flash, render_template_string, redirect
from flask_security import (
Security,
auth_required,
FsOAuthProvider,
MailUtil,
)
from flask_wtf import CSRFProtect

from models import db, user_datastore


# This is my test tenant on Azure
class AzureProvider(FsOAuthProvider):
def authlib_config(self):
return {
"api_base_url": "https://graph.microsoft.com/",
"server_metadata_url": "https://login.microsoftonline.com/"
"8ad9bf45-2e93-4043-8f46-a6420c8e9d68/v2.0/"
".well-known/openid-configuration",
"client_kwargs": {"scope": "openid email profile"},
}

def fetch_identity_cb(self, oauth, token):
profile = token["userinfo"]
return "email", profile["email"]


def _find_bool(v):
if str(v).lower() in ["true"]:
return True
Expand Down Expand Up @@ -73,6 +90,7 @@ def create_app():
app.config["SECURITY_TOTP_SECRETS"] = {
"1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B"
}
# app.config["SESSION_COOKIE_SAMESITE"] = "strict"

# As of Flask-SQLAlchemy 2.4.0 it is easy to pass in options directly to the
# underlying engine. This option makes sure that DB connections from the pool
Expand Down Expand Up @@ -109,13 +127,22 @@ def create_app():

# Setup Flask-Security
db.init_app(app)
Security(app, user_datastore, mail_util_cls=FlashMailUtil)
security = Security(app, user_datastore, mail_util_cls=FlashMailUtil)
# If configured - setup Azure - which works slightly differently -
# in particular w.r.t. the redirect URL having to match the ENTIRE
# url including query params (such as next).
if app.config.get("AZURE_CLIENT_SECRET"):
security.oauthglue.register_provider_ext(AzureProvider("azure"))

@app.route("/home")
@auth_required()
def home():
return render_template_string("Hello {{ current_user.email }}")

@app.route("/")
def root():
return redirect("/home")

return app


Expand Down
1 change: 1 addition & 0 deletions flask_security/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
)
from .mail_util import MailUtil
from .oauth_glue import OAuthGlue
from .oauth_provider import FsOAuthProvider
from .password_util import PasswordUtil
from .phone_util import PhoneUtil
from .recovery_codes import (
Expand Down
6 changes: 4 additions & 2 deletions flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,8 @@
),
"OAUTH_HANDSHAKE_ERROR": (
_(
"An error occurred while communicating with the Oauth provider. "
"An error occurred while communicating with the Oauth provider:"
" (%(exerror)s - %(exdesc)s). "
"Please try again."
),
"error",
Expand Down Expand Up @@ -1076,7 +1077,8 @@ class Security:
and signin. Defaults to :class:`WebauthnUtil`
:param mf_recovery_codes_util_cls: Class for generating, checking, encrypting
and decrypting recovery codes. Defaults to :class:`MfRecoveryCodesUtil`
:param oauth: An instance of authlib.integrations.flask_client.OAuth
:param oauth: An instance of authlib.integrations.flask_client.OAuth. If not set,
Flask-Security will create one.
.. tip::
Be sure that all your configuration values have been set PRIOR to
Expand Down
Loading

0 comments on commit 4d42bae

Please sign in to comment.