diff --git a/README.md b/README.md index cb3937d..dbc509a 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,22 @@ By using this library, it will automatically renew signed-in session when the ID * [Sample written in ![Django](https://raw.githubusercontent.com/rayluo/identity/dev/docs/django.webp)](https://github.com/Azure-Samples/ms-identity-python-webapp-django) * [Sample written in ![Flask](https://raw.githubusercontent.com/rayluo/identity/dev/docs/flask.webp)](https://github.com/Azure-Samples/ms-identity-python-webapp) +* [Sample written in ![Quart](https://raw.githubusercontent.com/rayluo/identity/dev/docs/quart.webp)](https://github.com/rayluo/python-webapp-quart) * Need support for more web frameworks? [Upvote existing feature request or create a new one](https://github.com/rayluo/identity/issues) + + + + + How to customize the login page + + +The default login page will typically redirect users to your Identity Provider, +so you don't have to customize it. +But if the default login page is shown in your browser, +you can read its HTML source code, and find the how-to instructions there. + @@ -90,18 +103,6 @@ They are demonstrated by the same samples above. In roadmap. - - - - - How to customize the login page - - -The default login page will typically redirect users to your Identity Provider, -so you don't have to customize it. -But if the default login page is shown in your browser, -you can read its HTML source code, and find the how-to instructions there. - @@ -134,6 +135,7 @@ Choose the package declaration that matches your web framework: * Django: `pip install identity[django]` * Flask: `pip install identity[flask]` +* Quart: `pip install identity[quart]` ## Versions diff --git a/docs/flask.rst b/docs/flask.rst index f3b90bb..014a777 100644 --- a/docs/flask.rst +++ b/docs/flask.rst @@ -73,19 +73,19 @@ Web app that logs in users and calls a web API on their behalf #. Decorate your token-consuming views using the same :py:func:`identity.flask.Auth.login_required` decorator, - this time with a parameter ``scopes=["your_scope_1", "your_scope_2"]``. + this time with a parameter ``scopes=["your_scope_1", "your_scope_2"]``. Then, inside your view, the token will be readily available via ``context['access_token']``. For example:: @app.route("/call_api") - @auth.login_required(scopes=os.getenv("SCOPE", "").split()) + @auth.login_required(scopes=["your_scope_1", "your_scope_2"]) def call_api(*, context): api_result = requests.get( # Use access token to call a web api "https://your_api.example.com", headers={'Authorization': 'Bearer ' + context['access_token']}, timeout=30, - ).json() # Here we assume the response format is json + ) ... All of the content above are demonstrated in diff --git a/docs/quart.rst b/docs/quart.rst index 5234309..afc86a3 100644 --- a/docs/quart.rst +++ b/docs/quart.rst @@ -35,20 +35,20 @@ Configuration #. Setup session management with the `Quart-session `_ package, which currently supports either Redis or MongoDB backing stores. To use Redis as the session store, you should first install the package with the extra dependency:: - + pip install quart-session[redis] #. Then add configuration to ``app.py`` pointing to your Redis instance:: app.config['SESSION_TYPE'] = 'redis' app.config['SESSION_URI'] = 'redis://localhost:6379' - + Sign In and Sign Out ---------------------------------- #. In your web project's ``app.py``, decorate some views with the - :py:func:`identity.flask.Auth.login_required` decorator. + :py:func:`identity.quart.Auth.login_required` decorator. It will automatically trigger sign-in. :: @app.route("/") @@ -63,6 +63,31 @@ Sign In and Sign Out Logout +Web app that logs in users and calls a web API on their behalf +-------------------------------------------------------------- + +#. Decorate your token-consuming views using the same + :py:func:`identity.quart.Auth.login_required` decorator, + this time with a parameter ``scopes=["your_scope_1", "your_scope_2"]``. + + Then, inside your view, the token will be readily available via + ``context['access_token']``. For example:: + + @app.route("/call_api") + @auth.login_required(scopes=["your_scope_1", "your_scope_2"]) + async def call_api(*, context): + async with httpx.AsyncClient() as client: + api_result = await client.get( # Use access token to call a web api + os.getenv("ENDPOINT"), + headers={'Authorization': 'Bearer ' + context['access_token']}, + ) + return await render_template('display.html', result=api_result) + + +All of the content above are demonstrated in +`this Quart web app sample `_. + + API reference -------------------------- diff --git a/docs/quart.webp b/docs/quart.webp new file mode 100644 index 0000000..71b322f Binary files /dev/null and b/docs/quart.webp differ diff --git a/identity/django.py b/identity/django.py index f37ca5d..701f961 100644 --- a/identity/django.py +++ b/identity/django.py @@ -154,7 +154,7 @@ def my_view2(request, *, context): "https://example.com/endpoint", headers={'Authorization': 'Bearer ' + context['access_token']}, timeout=30, - ).json() # Here we assume the response format is json + ) ... """ # With or without brackets. Inspired by https://stackoverflow.com/a/39335652/728675 diff --git a/identity/flask.py b/identity/flask.py index cb6f299..cc7cf30 100644 --- a/identity/flask.py +++ b/identity/flask.py @@ -38,7 +38,6 @@ def __init__(self, app: Flask, *args, **kwargs): # Manually register the routes, since we cannot use @app or @bp on methods if self._redirect_uri: redirect_path = urlparse(self._redirect_uri).path - #bp.route(redirect_path or "/auth_response")(self.auth_response) bp.route(redirect_path)(self.auth_response) bp.route( f"{os.path.dirname(redirect_path)}/logout" # Use it in template by url_for("identity.logout") @@ -107,12 +106,14 @@ def login_required( # Named after Django's login_required Usage:: - @settings.AUTH.login_required - def my_view(request, *, context): - return render(request, 'index.html', dict( + @app.route("/") + @auth.login_required + def index(*, context): + return render_template( + 'index.html', user=context["user"], # User is guaranteed to be present # because we decorated this view with @login_required - )) + ) :param list[str] scopes: A list of scopes that your app will need to use. @@ -121,13 +122,14 @@ def my_view(request, *, context): Usage:: - @settings.AUTH.login_required(scopes=["scope1", "scope2"]) - def my_view2(request, *, context): + @app.route("/call_api") + @auth.login_required(scopes=["scope1", "scope2"]) + def call_an_api(*, context): api_result = requests.get( # Use access token to call an api "https://example.com/endpoint", headers={'Authorization': 'Bearer ' + context['access_token']}, timeout=30, - ).json() # Here we assume the response format is json + ) ... """ # With or without brackets. Inspired by https://stackoverflow.com/a/39335652/728675 diff --git a/identity/quart.py b/identity/quart.py index c0870f8..28d98b3 100644 --- a/identity/quart.py +++ b/identity/quart.py @@ -38,7 +38,6 @@ def __init__(self, app: Quart, *args, **kwargs): # Manually register the routes, since we cannot use @app or @bp on methods if self._redirect_uri: redirect_path = urlparse(self._redirect_uri).path - #bp.route(redirect_path or "/auth_response")(self.auth_response) bp.route(redirect_path)(self.auth_response) bp.route( f"{os.path.dirname(redirect_path)}/logout" # Use it in template by url_for("identity.logout") @@ -107,12 +106,14 @@ def login_required( # Named after Django's login_required Usage:: - @settings.AUTH.login_required - def my_view(request, *, context): - return render(request, 'index.html', dict( + @app.route("/") + @auth.login_required + async def index(*, context): + return await render_template( + 'index.html', user=context["user"], # User is guaranteed to be present # because we decorated this view with @login_required - )) + ) :param list[str] scopes: A list of scopes that your app will need to use. @@ -121,14 +122,16 @@ def my_view(request, *, context): Usage:: - @settings.AUTH.login_required(scopes=["scope1", "scope2"]) - def my_view2(request, *, context): - api_result = requests.get( # Use access token to call an api - "https://example.com/endpoint", - headers={'Authorization': 'Bearer ' + context['access_token']}, - timeout=30, - ).json() # Here we assume the response format is json - ... + @app.route("/call_api") + @auth.login_required(scopes=["scope1", "scope2"]) + async def call_api(*, context): + async with httpx.AsyncClient() as client: + api_result = await client.get( # Use access token to call a web api + os.getenv("ENDPOINT"), + headers={'Authorization': 'Bearer ' + context['access_token']}, + ) + return await render_template('display.html', result=api_result) + """ # With or without brackets. Inspired by https://stackoverflow.com/a/39335652/728675