Skip to content

Commit b613a24

Browse files
authored
Merge pull request #15 from auth0-samples/update-python-flask-api
Update python flask api
2 parents f9bea71 + 298aa24 commit b613a24

8 files changed

Lines changed: 114 additions & 46 deletions

File tree

00-Starter-Seed/.dockerignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.env
2+
.env.example
3+
.gitignore
4+
README.md
5+
exec.sh
6+
exec.ps1
7+
Procfile

00-Starter-Seed/.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
.env
1+
.env
2+
*.iml
3+
.idea

00-Starter-Seed/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM python:3
2+
3+
WORKDIR /home/app
4+
5+
#If we add the requirements and install dependencies first, docker can use cache if requirements don't change
6+
ADD requirements.txt /home/app
7+
RUN pip install -r requirements.txt
8+
9+
ADD . /home/app
10+
CMD python server.py
11+
12+
EXPOSE 3001

00-Starter-Seed/README.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
# Auth0 + Python + Flask API Seed
2-
This is the seed project you need to use if you're going to create a Python + Flask API. If you just want to create a Regular Python WebApp, please check [this other seed project](https://github.com/auth0-samples/auth0-python-web-app/tree/master/00-Starter-Seed)
2+
This is the seed project you need to use if you're going to create a Python + Flask API.
3+
If you just want to create a Regular Python WebApp, please
4+
check [this project](https://github.com/auth0-samples/auth0-python-web-app/tree/master/01-Login)
5+
6+
Please check our [Quickstart](https://auth0.com/docs/quickstart/backend/python) to better understand this sample.
37

48
# Running the example
59
In order to run the example you need to have `python` and `pip` installed.
610

7-
You also need to set your Auth0 Domain and the API's audience as environment variables with the following names respectively: `AUTH0_DOMAIN` and `API_ID`, which is the audience of your API. You can find an example in the `env.example` file.
11+
You also need to set your Auth0 Domain and the API's audience as environment variables with the following names
12+
respectively: `AUTH0_DOMAIN` and `API_ID`, which is the audience of your API. You can find an example in the
13+
`env.example` file.
814

9-
For that, if you just create a file named `.env` in the directory and set the values like the following, the app will just work:
15+
For that, if you just create a file named `.env` in the directory and set the values like the following,
16+
the app will just work:
1017

1118
```bash
1219
# .env file
@@ -20,4 +27,21 @@ Once you've set those 2 enviroment variables:
2027
2. Start the server with `python server.py`
2128
3. Try calling [http://localhost:3001/ping](http://localhost:3001/ping)
2229

23-
You can then try to do a GET to [http://localhost:3001/secured/ping](http://localhost:3001/secured/ping) which will throw an error if you don't send an access token signed with RS256 with the appropriate issuer and audience in the Authorization header. You can also try to do a GET to [http://localhost:3001/secured/private/ping](http://localhost:3001/secured/private/ping) which will throw an error if you don't send an access token with the scope `read:agenda` signed with RS256 with the appropriate issuer and audience in the Authorization header.
30+
# Testing the API
31+
32+
You can then try to do a GET to [http://localhost:3001/secured/ping](http://localhost:3001/secured/ping) which will
33+
throw an error if you don't send an access token signed with RS256 with the appropriate issuer and audience in the
34+
Authorization header.
35+
36+
You can also try to do a GET to
37+
[http://localhost:3001/secured/private/ping](http://localhost:3001/secured/private/ping) which will throw an error if
38+
you don't send an access token with the scope `read:agenda` signed with RS256 with the appropriate issuer and audience
39+
in the Authorization header.
40+
41+
# Running the example with Docker
42+
43+
In order to run the sample with [Docker](https://www.docker.com/) you need to add the `AUTH0_DOMAIN` and `API_ID`
44+
to the `.env` filed as explained [previously](#running-the-example) and then
45+
46+
1. Execute in command line `sh exec.sh` to run the Docker in Linux, or `.\exec.ps1` to run the Docker in Windows.
47+
2. Try calling [http://localhost:3001/ping](http://localhost:3001/ping)

00-Starter-Seed/exec.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
docker build -t auth0-python-api .
2+
docker run --env-file .env -p 3001:3001 -it auth0-python-api

00-Starter-Seed/exec.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env bash
2+
docker build -t auth0-python-api .
3+
docker run --env-file .env -p 3001:3001 -it auth0-python-api

00-Starter-Seed/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ flask
22
python-dotenv
33
python-jose
44
flask-cors
5+
six

00-Starter-Seed/server.py

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,66 @@
33

44
from functools import wraps
55
import json
6-
from os import environ as env, path
7-
import urllib
6+
from os import environ as env
7+
from six.moves.urllib.request import urlopen
88

9-
from dotenv import load_dotenv
9+
from dotenv import load_dotenv, find_dotenv
1010
from flask import Flask, request, jsonify, _app_ctx_stack
1111
from flask_cors import cross_origin
1212
from jose import jwt
1313

14-
load_dotenv(path.join(path.dirname(__file__), ".env"))
15-
AUTH0_DOMAIN = env["AUTH0_DOMAIN"]
16-
API_AUDIENCE = env["API_ID"]
14+
ENV_FILE = find_dotenv()
15+
if ENV_FILE:
16+
load_dotenv(ENV_FILE)
17+
AUTH0_DOMAIN = env.get("AUTH0_DOMAIN")
18+
API_AUDIENCE = env.get("API_ID")
1719
ALGORITHMS = ["RS256"]
1820
APP = Flask(__name__)
1921

2022

2123
# Format error response and append status code.
22-
def handle_error(error, status_code):
23-
"""Handles the errors
24-
"""
25-
resp = jsonify(error)
26-
resp.status_code = status_code
27-
return resp
24+
class AuthError(Exception):
25+
def __init__(self, error, status_code):
26+
self.error = error
27+
self.status_code = status_code
28+
29+
30+
@APP.errorhandler(AuthError)
31+
def handle_auth_error(ex):
32+
response = jsonify(ex.error)
33+
response.status_code = ex.status_code
34+
return response
35+
2836

2937
def get_token_auth_header():
3038
"""Obtains the access token from the Authorization Header
3139
"""
3240
auth = request.headers.get("Authorization", None)
3341
if not auth:
34-
return handle_error({"code": "authorization_header_missing",
35-
"description":
36-
"Authorization header is expected"}, 401)
42+
raise AuthError({"code": "authorization_header_missing",
43+
"description":
44+
"Authorization header is expected"}, 401)
3745

3846
parts = auth.split()
3947

4048
if parts[0].lower() != "bearer":
41-
return handle_error({"code": "invalid_header",
42-
"description":
43-
"Authorization header must start with"
44-
"Bearer"}, 401)
49+
raise AuthError({"code": "invalid_header",
50+
"description":
51+
"Authorization header must start with"
52+
" Bearer"}, 401)
4553
elif len(parts) == 1:
46-
return handle_error({"code": "invalid_header",
47-
"description": "Token not found"}, 401)
54+
raise AuthError({"code": "invalid_header",
55+
"description": "Token not found"}, 401)
4856
elif len(parts) > 2:
49-
return handle_error({"code": "invalid_header",
50-
"description": "Authorization header must be"
51-
"Bearer token"}, 401)
57+
raise AuthError({"code": "invalid_header",
58+
"description":
59+
"Authorization header must be"
60+
" Bearer token"}, 401)
5261

5362
token = parts[1]
5463
return token
5564

65+
5666
def requires_scope(required_scope):
5767
"""Determines if the required scope is present in the access token
5868
Args:
@@ -66,24 +76,27 @@ def requires_scope(required_scope):
6676
return True
6777
return False
6878

79+
6980
def requires_auth(f):
7081
"""Determines if the access token is valid
7182
"""
7283
@wraps(f)
7384
def decorated(*args, **kwargs):
7485
token = get_token_auth_header()
75-
jsonurl = urllib.urlopen("https://"+AUTH0_DOMAIN+"/.well-known/jwks.json")
86+
jsonurl = urlopen("https://"+AUTH0_DOMAIN+"/.well-known/jwks.json")
7687
jwks = json.loads(jsonurl.read())
7788
try:
7889
unverified_header = jwt.get_unverified_header(token)
7990
except jwt.JWTError:
80-
return handle_error({"code": "invalid_header",
81-
"description": "Invalid header. "
82-
"Use an RS256 signed JWT Access Token"}, 401)
91+
raise AuthError({"code": "invalid_header",
92+
"description":
93+
"Invalid header. "
94+
"Use an RS256 signed JWT Access Token"}, 401)
8395
if unverified_header["alg"] == "HS256":
84-
return handle_error({"code": "invalid_header",
85-
"description": "Invalid header. "
86-
"Use an RS256 signed JWT Access Token"}, 401)
96+
raise AuthError({"code": "invalid_header",
97+
"description":
98+
"Invalid header. "
99+
"Use an RS256 signed JWT Access Token"}, 401)
87100
rsa_key = {}
88101
for key in jwks["keys"]:
89102
if key["kid"] == unverified_header["kid"]:
@@ -104,23 +117,26 @@ def decorated(*args, **kwargs):
104117
issuer="https://"+AUTH0_DOMAIN+"/"
105118
)
106119
except jwt.ExpiredSignatureError:
107-
return handle_error({"code": "token_expired",
108-
"description": "token is expired"}, 401)
120+
raise AuthError({"code": "token_expired",
121+
"description": "token is expired"}, 401)
109122
except jwt.JWTClaimsError:
110-
return handle_error({"code": "invalid_claims",
111-
"description": "incorrect claims,"
112-
" please check the audience and issuer"}, 401)
123+
raise AuthError({"code": "invalid_claims",
124+
"description":
125+
"incorrect claims,"
126+
" please check the audience and issuer"}, 401)
113127
except Exception:
114-
return handle_error({"code": "invalid_header",
115-
"description": "Unable to parse authentication"
116-
" token."}, 400)
128+
raise AuthError({"code": "invalid_header",
129+
"description":
130+
"Unable to parse authentication"
131+
" token."}, 400)
117132

118133
_app_ctx_stack.top.current_user = payload
119134
return f(*args, **kwargs)
120-
return handle_error({"code": "invalid_header",
121-
"description": "Unable to find appropriate key"}, 400)
135+
raise AuthError({"code": "invalid_header",
136+
"description": "Unable to find appropriate key"}, 400)
122137
return decorated
123138

139+
124140
# Controllers API
125141
@APP.route("/ping")
126142
@cross_origin(headers=["Content-Type", "Authorization"])
@@ -139,6 +155,7 @@ def secured_ping():
139155
"""
140156
return "All good. You only get this message if you're authenticated"
141157

158+
142159
@APP.route("/secured/private/ping")
143160
@cross_origin(headers=["Content-Type", "Authorization"])
144161
@cross_origin(headers=["Access-Control-Allow-Origin", "*"])

0 commit comments

Comments
 (0)