Skip to content

Commit 50cf3dd

Browse files
author
EC2 Default User
committed
Integrate JWT Auth into API
partially fixes #2063 This will allow us to add permissions checks to our API calls. Currently it does not require a valid JWT, but if one exists will store the "sub" field such that the user's id can be checked against our user table to establish permissions.
1 parent 89f516b commit 50cf3dd

File tree

5 files changed

+76
-0
lines changed

5 files changed

+76
-0
lines changed

changelog_entry.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- bump: patch
2+
changes:
3+
added:
4+
- API now attempts to parse a the bearer token if one is provided and logs success/fail.

policyengine_api/api.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
import flask
88
import yaml
99
from flask_caching import Cache
10+
from authlib.integrations.flask_oauth2 import ResourceProtector
11+
from policyengine_api.validator import Auth0JWTBearerTokenValidator
1012
from policyengine_api.utils import make_cache_key
1113
from .constants import VERSION
14+
import policyengine_api.auth_context as auth_context
1215

1316
# from werkzeug.middleware.profiler import ProfilerMiddleware
1417

@@ -42,6 +45,13 @@
4245

4346
app = application = flask.Flask(__name__)
4447

48+
## as per https://auth0.com/docs/quickstart/backend/python/interactive
49+
require_auth = ResourceProtector()
50+
validator = Auth0JWTBearerTokenValidator()
51+
require_auth.register_token_validator(validator)
52+
53+
auth_context.configure(app, require_auth=require_auth)
54+
4555
app.config.from_mapping(
4656
{
4757
"CACHE_TYPE": "RedisCache",

policyengine_api/auth_context.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from flask import Flask, g
2+
from werkzeug.local import LocalProxy
3+
from authlib.integrations.flask_oauth2 import ResourceProtector
4+
5+
6+
def configure(app: Flask, require_auth: ResourceProtector):
7+
"""
8+
Configure the application to attempt to get and validate a bearer token.
9+
If there is a token and it's valid the user id is added to the request context
10+
which can be accessed via get_user_id
11+
Otherwise, the request is accepted but get_user_id returns None
12+
13+
This supports our current auth model where only user-specific actions are restricted and
14+
then only to allow the user
15+
"""
16+
17+
# If the user is authenticated then get the user id from the token
18+
# And add it to the flask request context.
19+
@app.before_request
20+
def get_user():
21+
try:
22+
token = require_auth.acquire_token()
23+
print(f"Validated JWT for sub {g.authlib_server_oauth2_token.sub}")
24+
except Exception as ex:
25+
print(f"Unable to parse a valid bearer token from request: {ex}")
26+
27+
28+
def get_user() -> None | str:
29+
# I didn't see this documented anywhere, but if you look at the source code
30+
# the validator stores the token in the flask global context under this name.
31+
if "authlib_server_oauth2_token" not in g:
32+
print("authlib_server_oauth2_token is not in the flask global context. Please make sure you called 'configure' on the app")
33+
return None
34+
if "sub" not in g.authlib_server_oauth2_token:
35+
print("ERROR: authlib_server_oauth2_token does not contain a sub field. The JWT validator should force this to be true.")
36+
return None
37+
return g.authlib_server_oauth2_token.sub

policyengine_api/validator.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# As defined by https://auth0.com/docs/quickstart/backend/python/interactive
2+
import json
3+
from urllib.request import urlopen
4+
5+
from authlib.oauth2.rfc7523 import JWTBearerTokenValidator
6+
from authlib.jose.rfc7517.jwk import JsonWebKey
7+
8+
9+
class Auth0JWTBearerTokenValidator(JWTBearerTokenValidator):
10+
def __init__(
11+
self,
12+
audience="https://api.policyengine.org/",
13+
):
14+
issuer = "https://policyengine.uk.auth0.com/"
15+
jsonurl = urlopen(f"https://policyengine.uk.auth0.com/.well-known/jwks.json")
16+
public_key = JsonWebKey.import_key_set(json.loads(jsonurl.read()))
17+
super(Auth0JWTBearerTokenValidator, self).__init__(public_key)
18+
self.claims_options = {
19+
"exp": {"essential": True},
20+
"aud": {"essential": True, "value": audience},
21+
"iss": {"essential": True, "value": issuer},
22+
#Provides the user id as we currently use it.
23+
"sub": {"essential": True},
24+
}

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"streamlit",
3333
"werkzeug",
3434
"Flask-Caching>=2,<3",
35+
"Authlib",
3536
],
3637
extras_require={
3738
"dev": [

0 commit comments

Comments
 (0)