Skip to content

Commit c6fdab7

Browse files
committed
Authorization: Web API protected by access token
1 parent 86fad10 commit c6fdab7

12 files changed

+498
-25
lines changed

README.md

+17-2
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ By using this library, it will automatically renew signed-in session when the ID
7070
</tr>
7171

7272
<tr>
73-
<th>Web App Calls a web API</th>
73+
<th>Your Web App Calls a Web API on behalf of the user</th>
7474
<td colspan=4>
7575

7676
This library supports:
@@ -85,7 +85,22 @@ They are demonstrated by the same samples above.
8585
</tr>
8686

8787
<tr>
88-
<th>Web API Calls another web API (On-behalf-of)</th>
88+
<th>Your Web API protected by an access token</th>
89+
<td colspan=4>
90+
91+
By using this library, it will automatically emit
92+
HTTP 401 or 403 error when the access token is absent or invalid.
93+
94+
* [Sample written in ![Django](https://raw.githubusercontent.com/rayluo/identity/dev/docs/django.webp)](https://github.com/Azure-Samples/ms-identity-python-webapi-django)
95+
* [Sample written in ![Flask](https://raw.githubusercontent.com/rayluo/identity/dev/docs/flask.webp)](https://github.com/Azure-Samples/ms-identity-python-webapi-flask)
96+
* Need support for more web frameworks?
97+
[Upvote existing feature request or create a new one](https://github.com/rayluo/identity/issues)
98+
99+
</td>
100+
</tr>
101+
102+
<tr>
103+
<th>Your Web API Calls another web API on behalf of the user (OBO)</th>
89104
<td colspan=4>
90105

91106
In roadmap.

docs/app-vs-api.rst

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.. note::
2+
3+
Web Application and Web API are different, and are supported by different Identity components.
4+
Make sure you are using the right component for your scenario.
5+
6+
+-------------------------+---------------------------------------------------+-------------------------------------------------------+
7+
| Aspect | Web Application | Web API |
8+
+=========================+===================================================+=======================================================+
9+
| **Definition** | A complete solution that users interact with | A back-end system that provides data to other systems |
10+
| | directly through their browsers. | without views. |
11+
+-------------------------+---------------------------------------------------+-------------------------------------------------------+
12+
| **Functionality** | - Users interact with views (HTML user interfaces)| - Does not return views (in HTML); only provides data.|
13+
| | and data. | - Other systems (clients) hit its endpoints. |
14+
+-------------------------+---------------------------------------------------+-------------------------------------------------------+
15+

docs/django-webapi.rst

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
Identity for a Django Web API
2+
=============================
3+
4+
.. include:: app-vs-api.rst
5+
6+
Prerequisite
7+
------------
8+
9+
Create a hello world web project in Django.
10+
11+
You can use
12+
`Django's own tutorial, part 1 <https://docs.djangoproject.com/en/5.0/intro/tutorial01/>`_
13+
as a reference. What we need are basically these steps:
14+
15+
#. ``django-admin startproject mysite``
16+
#. ``python manage.py migrate`` (Optinoal if your project does not use a database)
17+
#. ``python manage.py runserver localhost:5000``
18+
19+
#. Now, add a new `mysite/views.py` file with an `index` view to your project.
20+
For now, it can simply return a "hello world" page to any visitor::
21+
22+
from django.http import JsonResponse
23+
def index(request):
24+
return JsonResponse({"message": "Hello, world!"})
25+
26+
Configuration
27+
-------------
28+
29+
#. Install dependency by ``pip install identity[django]``
30+
31+
#. Create an instance of the :py:class:`identity.django.Auth` object,
32+
and assign it to a global variable inside your ``settings.py``::
33+
34+
import os
35+
from identity.django import Auth
36+
AUTH = Auth(
37+
client_id=os.getenv('CLIENT_ID'),
38+
...=..., # See below on how to feed in the authority url parameter
39+
)
40+
41+
.. include:: auth.rst
42+
43+
44+
Django Web API protected by an access token
45+
-------------------------------------------
46+
47+
#. In your web project's ``views.py``, decorate some views with the
48+
:py:func:`identity.django.ApiAuth.authorization_required` decorator::
49+
50+
from django.conf import settings
51+
52+
@settings.AUTH.authorization_required(expected_scopes={
53+
"your_scope_1": "api://your_client_id/your_scope_1",
54+
"your_scope_2": "api://your_client_id/your_scope_2",
55+
})
56+
def index(request, *, context):
57+
claims = context['claims']
58+
# The user is uniquely identified by claims['sub'] or claims["oid"],
59+
# claims['tid'] and/or claims['iss'].
60+
return JsonResponse(
61+
{"message": f"Data for {claims['sub']}@{claims['tid']}"}
62+
)
63+
64+
65+
All of the content above are demonstrated in
66+
`this django web app sample <https://github.com/Azure-Samples/ms-identity-python-webapi-django>`_.
67+
68+
69+
API for Django web projects
70+
---------------------------
71+
72+
.. autoclass:: identity.django.ApiAuth
73+
:members:
74+
:inherited-members:
75+
76+
.. automethod:: __init__
77+

docs/django.rst

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
Identity for Django
2-
===================
1+
Identity for a Django Web App
2+
=============================
3+
4+
.. include:: app-vs-api.rst
35

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

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

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

25-
Identity-for-Django configuration
26-
---------------------------------
27+
Configuration
28+
-------------
2729

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

docs/flask-webapi.rst

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
Identity for a Flask Web API
2+
============================
3+
4+
.. include:: app-vs-api.rst
5+
6+
Prerequisite
7+
------------
8+
9+
Create `a hello world web project in Flask <https://flask.palletsprojects.com/en/3.0.x/quickstart/#a-minimal-application>`_.
10+
Here we assume the project's main file is named ``app.py``.
11+
12+
13+
Configuration
14+
-------------
15+
16+
#. Install dependency by ``pip install identity[flask]``
17+
18+
#. Create an instance of the :py:class:`identity.Flask.ApiAuth` object,
19+
and assign it to a global variable inside your ``app.py``::
20+
21+
import os
22+
from flask import Flask
23+
from identity.flask import ApiAuth
24+
25+
app = Flask(__name__)
26+
auth = ApiAuth(
27+
client_id=os.getenv('CLIENT_ID'),
28+
...=..., # See below on how to feed in the authority url parameter
29+
)
30+
31+
.. include:: auth.rst
32+
33+
34+
Flask Web API protected by an access token
35+
------------------------------------------
36+
37+
#. In your web project's ``app.py``, decorate some views with the
38+
:py:func:`identity.flask.ApiAuth.authorization_required` decorator.
39+
It will automatically put validated token claims into the ``context`` dictionary,
40+
under the key ``claims``.
41+
or emit an HTTP 401 or 403 response if the token is missing or invalid.
42+
43+
::
44+
45+
@app.route("/")
46+
@auth.authorization_required(expected_scopes={
47+
"your_scope_1": "api://your_client_id/your_scope_1",
48+
"your_scope_2": "api://your_client_id/your_scope_2",
49+
})
50+
def index(*, context):
51+
claims = context['claims']
52+
# The user is uniquely identified by claims['sub'] or claims["oid"],
53+
# claims['tid'] and/or claims['iss'].
54+
return {"message": f"Data for {claims['sub']}@{claims['tid']}"}
55+
56+
All of the content above are demonstrated in
57+
`this Flask web API sample <https://github.com/Azure-Samples/ms-identity-python-webapi-flask>`_.
58+
59+
API for Flask web API projects
60+
------------------------------
61+
62+
.. autoclass:: identity.flask.ApiAuth
63+
:members:
64+
:inherited-members:
65+
66+
.. automethod:: __init__
67+

docs/flask.rst

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
Identity for Flask
2-
==================
1+
Identity for a Flask Web App
2+
============================
3+
4+
.. include:: app-vs-api.rst
35

46
Prerequisite
57
------------
@@ -8,8 +10,8 @@ Create `a hello world web project in Flask <https://flask.palletsprojects.com/en
810
Here we assume the project's main file is named ``app.py``.
911

1012

11-
Identity-for-Flask configuration
12-
--------------------------------
13+
Configuration
14+
-------------
1315

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

docs/index.rst

+8
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ This Identity library is a Python authentication/authorization library that:
4747
:hidden:
4848

4949
django
50+
django-webapi
5051
flask
52+
flask-webapi
5153
abc
5254
generic
5355

@@ -59,3 +61,9 @@ This Identity library is a Python authentication/authorization library that:
5961
Other modules in the source code are all considered as internal helpers,
6062
which could change at anytime in the future, without prior notice.
6163

64+
This library is designed to be used in either a web app or a web API.
65+
Understand the difference between the two scenarios,
66+
before you choose the right component to build your project.
67+
68+
.. include:: app-vs-api.rst
69+

identity/django.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
from django.shortcuts import redirect, render
88
from django.urls import include, path, reverse
9+
from django.http import HttpResponse
910

10-
from .web import WebFrameworkAuth
11+
from .web import WebFrameworkAuth, HttpError, ApiAuth as _ApiAuth
1112

1213

1314
logger = logging.getLogger(__name__)
@@ -189,3 +190,18 @@ def wrapper(request, *args, **kwargs):
189190
)
190191
return wrapper
191192

193+
194+
class ApiAuth(_ApiAuth):
195+
def authorization_required(self, *, expected_scopes, **kwargs):
196+
def decorator(function):
197+
@wraps(function)
198+
def wrapper(request, *args, **kwargs):
199+
try:
200+
context = self._validate(request, expected_scopes=expected_scopes)
201+
except HttpError as e:
202+
return HttpResponse(
203+
e.description, status=e.status_code, headers=e.headers)
204+
return function(request, *args, context=context, **kwargs)
205+
return wrapper
206+
return decorator
207+

identity/flask.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77
from flask import (
88
Blueprint, Flask,
9+
abort, make_response, # Used in ApiAuth
910
redirect, render_template, request, session, url_for,
1011
)
1112
from flask_session import Session
1213

13-
from .web import WebFrameworkAuth
14+
from .web import WebFrameworkAuth, ApiAuth as _ApiAuth
1415

1516

1617
logger = logging.getLogger(__name__)
@@ -155,3 +156,19 @@ def wrapper(*args, **kwargs):
155156
)
156157
return wrapper
157158

159+
160+
class ApiAuth(_ApiAuth):
161+
def raise_http_error(self, status_code, *, headers, description=None):
162+
response = make_response(description, status_code)
163+
response.headers.extend(headers or {})
164+
abort(response)
165+
166+
def authorization_required(self, *, expected_scopes, **kwargs):
167+
def decorator(function):
168+
@wraps(function)
169+
def wrapper(*args, **kwargs):
170+
context = self._validate(request, expected_scopes=expected_scopes)
171+
return function(*args, context=context, **kwargs)
172+
return wrapper
173+
return decorator
174+

identity/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.7.0" # Note: Perhaps update ReadTheDocs and README.md too?
1+
__version__ = "0.8.0a1" # Note: Perhaps update ReadTheDocs and README.md too?

0 commit comments

Comments
 (0)