Skip to content

Commit

Permalink
Authorization: Web API protected by access token
Browse files Browse the repository at this point in the history
  • Loading branch information
rayluo committed Oct 26, 2024
1 parent 31d983c commit 7a04f29
Show file tree
Hide file tree
Showing 12 changed files with 507 additions and 32 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ you can read its HTML source code, and find the how-to instructions there.
</tr>

<tr>
<th>Web App Calls a web API</th>
<th>Your Web App Calls a Web API on behalf of the user</th>
<td colspan=4>

This library supports:
Expand All @@ -98,7 +98,22 @@ They are demonstrated by the same samples above.
</tr>

<tr>
<th>Web API Calls another web API (On-behalf-of)</th>
<th>Your Web API protected by an access token</th>
<td colspan=4>

By using this library, it will automatically emit
HTTP 401 or 403 error when the access token is absent or invalid.

* Sample written in ![Django](https://raw.githubusercontent.com/rayluo/identity/dev/docs/django.webp)(Coming soon)
* [Sample written in ![Flask](https://raw.githubusercontent.com/rayluo/identity/dev/docs/flask.webp)](https://github.com/rayluo/python-webapi-flask.git)
* Need support for more web frameworks?
[Upvote existing feature request or create a new one](https://github.com/rayluo/identity/issues)

</td>
</tr>

<tr>
<th>Your Web API Calls another web API on behalf of the user (OBO)</th>
<td colspan=4>

In roadmap.
Expand Down
18 changes: 18 additions & 0 deletions docs/app-vs-api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.. note::

Web Application (a.k.a. website) and Web API are different,
and are supported by different Identity components.
Make sure you are using the right component for your scenario.

+-------------------------+---------------------------------------------------+-------------------------------------------------------+
| Aspects | Web Application (a.k.a. website) | Web API |
+=========================+===================================================+=======================================================+
| **Definition** | A complete solution that users interact with | A back-end system that provides data (typically in |
| | directly through their browsers. | JSON format) to front-end or other system. |
+-------------------------+---------------------------------------------------+-------------------------------------------------------+
| **Functionality** | - Users interact with views (HTML user interfaces)| - Does not return views (in HTML); only provides data.|
| | and data. | - Other systems (clients) hit its endpoints. |
| | - Users sign in and establish their sessions. | - Clients presents a token to access your API. |
| | | - Each request has no session. They are stateless. |
+-------------------------+---------------------------------------------------+-------------------------------------------------------+

77 changes: 77 additions & 0 deletions docs/django-webapi.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
Identity for a Django Web API
=============================

.. include:: app-vs-api.rst

Prerequisite
------------

Create a hello world web project in Django.

You can use
`Django's own tutorial, part 1 <https://docs.djangoproject.com/en/5.0/intro/tutorial01/>`_
as a reference. What we need are basically these steps:

#. ``django-admin startproject mysite``
#. ``python manage.py migrate`` (Optinoal if your project does not use a database)
#. ``python manage.py runserver localhost:5000``

#. Now, add a new `mysite/views.py` file with an `index` view to your project.
For now, it can simply return a "hello world" page to any visitor::

from django.http import JsonResponse
def index(request):
return JsonResponse({"message": "Hello, world!"})

Configuration
-------------

#. Install dependency by ``pip install identity[django]``

#. Create an instance of the :py:class:`identity.django.Auth` object,
and assign it to a global variable inside your ``settings.py``::

import os
from identity.django import Auth
AUTH = Auth(
client_id=os.getenv('CLIENT_ID'),
...=..., # See below on how to feed in the authority url parameter
)

.. include:: auth.rst


Django Web API protected by an access token
-------------------------------------------

#. In your web project's ``views.py``, decorate some views with the
:py:func:`identity.django.ApiAuth.authorization_required` decorator::

from django.conf import settings

@settings.AUTH.authorization_required(expected_scopes={
"your_scope_1": "api://your_client_id/your_scope_1",
"your_scope_2": "api://your_client_id/your_scope_2",
})
def index(request, *, context):
claims = context['claims']
# The user is uniquely identified by claims['sub'] or claims["oid"],
# claims['tid'] and/or claims['iss'].
return JsonResponse(
{"message": f"Data for {claims['sub']}@{claims['tid']}"}
)


All of the content above are demonstrated in
`this django web app sample <https://github.com/Azure-Samples/ms-identity-python-webapi-django>`_.


API for Django web projects
---------------------------

.. autoclass:: identity.django.ApiAuth
:members:
:inherited-members:

.. automethod:: __init__

10 changes: 6 additions & 4 deletions docs/django.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
Identity for Django
===================
Identity for a Django Web App
=============================

.. include:: app-vs-api.rst

Prerequisite
------------
Expand All @@ -15,15 +17,15 @@ as a reference. What we need are basically these steps:
#. ``python manage.py runserver localhost:5000``
You must use a port matching your redirect_uri that you registered.

#. Now, add an `index` view to your project.
#. Now, add a new `mysite/views.py` file with an `index` view to your project.
For now, it can simply return a "hello world" page to any visitor::

from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world. Everyone can read this line.")

Configuration
---------------------------------
-------------

#. Install dependency by ``pip install identity[django]``

Expand Down
67 changes: 67 additions & 0 deletions docs/flask-webapi.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
Identity for a Flask Web API
============================

.. include:: app-vs-api.rst

Prerequisite
------------

Create `a hello world web project in Flask <https://flask.palletsprojects.com/en/3.0.x/quickstart/#a-minimal-application>`_.
Here we assume the project's main file is named ``app.py``.


Configuration
-------------

#. Install dependency by ``pip install identity[flask]``

#. Create an instance of the :py:class:`identity.Flask.ApiAuth` object,
and assign it to a global variable inside your ``app.py``::

import os
from flask import Flask
from identity.flask import ApiAuth

app = Flask(__name__)
auth = ApiAuth(
client_id=os.getenv('CLIENT_ID'),
...=..., # See below on how to feed in the authority url parameter
)

.. include:: auth.rst


Flask Web API protected by an access token
------------------------------------------

#. In your web project's ``app.py``, decorate some views with the
:py:func:`identity.flask.ApiAuth.authorization_required` decorator.
It will automatically put validated token claims into the ``context`` dictionary,
under the key ``claims``.
or emit an HTTP 401 or 403 response if the token is missing or invalid.

::

@app.route("/")
@auth.authorization_required(expected_scopes={
"your_scope_1": "api://your_client_id/your_scope_1",
"your_scope_2": "api://your_client_id/your_scope_2",
})
def index(*, context):
claims = context['claims']
# The user is uniquely identified by claims['sub'] or claims["oid"],
# claims['tid'] and/or claims['iss'].
return {"message": f"Data for {claims['sub']}@{claims['tid']}"}

All of the content above are demonstrated in
`this Flask web API sample <https://github.com/Azure-Samples/ms-identity-python-webapi-flask>`_.

API for Flask web API projects
------------------------------

.. autoclass:: identity.flask.ApiAuth
:members:
:inherited-members:

.. automethod:: __init__

8 changes: 5 additions & 3 deletions docs/flask.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
Identity for Flask
==================
Identity for a Flask Web App
============================

.. include:: app-vs-api.rst

Prerequisite
------------
Expand All @@ -9,7 +11,7 @@ Here we assume the project's main file is named ``app.py``.


Configuration
--------------------------------
-------------

#. Install dependency by ``pip install identity[flask]``

Expand Down
8 changes: 8 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ This Identity library is a Python authentication/authorization library that:
:hidden:

django
django-webapi
flask
flask-webapi
quart
abc
generic
Expand All @@ -60,3 +62,9 @@ This Identity library is a Python authentication/authorization library that:
Other modules in the source code are all considered as internal helpers,
which could change at anytime in the future, without prior notice.

This library is designed to be used in either a web app or a web API.
Understand the difference between the two scenarios,
before you choose the right component to build your project.

.. include:: app-vs-api.rst

18 changes: 17 additions & 1 deletion identity/django.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

from django.shortcuts import redirect, render
from django.urls import include, path, reverse
from django.http import HttpResponse

from .web import WebFrameworkAuth
from .web import WebFrameworkAuth, HttpError, ApiAuth as _ApiAuth


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -185,3 +186,18 @@ def wrapper(request, *args, **kwargs):
)
return wrapper


class ApiAuth(_ApiAuth):
def authorization_required(self, *, expected_scopes, **kwargs):
def decorator(function):
@wraps(function)
def wrapper(request, *args, **kwargs):
try:
context = self._validate(request, expected_scopes=expected_scopes)
except HttpError as e:
return HttpResponse(
e.description, status=e.status_code, headers=e.headers)
return function(request, *args, context=context, **kwargs)
return wrapper
return decorator

19 changes: 19 additions & 0 deletions identity/flask.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from typing import List, Optional # Needed in Python 3.7 & 3.8
from flask import (
Blueprint, Flask,
abort, make_response, # Used in ApiAuth
redirect, render_template, request, session, url_for,
)
from flask_session import Session
from .pallet import PalletAuth

from .web import WebFrameworkAuth, ApiAuth as _ApiAuth


class Auth(PalletAuth):
"""A long-live identity auth helper for a Flask web project."""
Expand Down Expand Up @@ -153,3 +156,19 @@ def call_an_api(*, context):
"""
return super(Auth, self).login_required(function, scopes=scopes)


class ApiAuth(_ApiAuth):
def raise_http_error(self, status_code, *, headers=None, description=None):
response = make_response(description, status_code)
response.headers.extend(headers or {})
abort(response)

def authorization_required(self, *, expected_scopes, **kwargs):
def decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
context = self._validate(request, expected_scopes=expected_scopes)
return function(*args, context=context, **kwargs)
return wrapper
return decorator

Loading

0 comments on commit 7a04f29

Please sign in to comment.