Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 32 additions & 28 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,37 @@
machine: &machine-cfg
image: ubuntu-2004:202107-02

defaults: &defaults
orbs:
python: circleci/python@2.0.3

# Jobs and Workflows
version: 2.1
jobs:
checkout:
machine:
<<: *machine-cfg
steps:
- checkout
- run:
name: Clone test script
command: git clone -b v0.0.1 --depth 1 https://github.com/auth0-samples/api-quickstarts-tests test
- persist_to_workspace:
root: ~/
paths:
- project
- test
integration-tests:
machine:
<<: *machine-cfg
environment:
AUTH0_CFG: 00-Starter-Seed
SAMPLE_PATH: 00-Starter-Seed
steps:
- attach_workspace:
at: ~/
- run:
name: Prepare environment variables
command: |
command: |
cd $AUTH0_CFG
mv .env.example .env
sed -i 's|{DOMAIN}|'$auth0_domain'|g' .env
Expand All @@ -18,7 +42,7 @@ defaults: &defaults
command: cd $AUTH0_CFG && sh exec.sh
background: true
- run:
name: Wait until server is online
name: Wait until server is online
command: |
until $(curl --silent --head --output /dev/null --fail http://localhost:3010/api/public); do
sleep 5
Expand All @@ -42,37 +66,17 @@ defaults: &defaults
- run:
name: Execute automated tests
command: cd test && npm test

# Jobs and Workflows
version: 2
jobs:
checkout:
machine:
<<: *machine-cfg
steps:
- checkout
- run:
name: Clone test script
command: git clone -b v0.0.1 --depth 1 https://github.com/auth0-samples/api-quickstarts-tests test
- persist_to_workspace:
root: ~/
paths:
- project
- test
00-Starter-Seed:
machine:
<<: *machine-cfg
environment:
AUTH0_CFG: 00-Starter-Seed
SAMPLE_PATH: 00-Starter-Seed
<<: *defaults
workflows:
version: 2
API-Tests:
jobs:
- checkout:
context: Quickstart API Tests
- 00-Starter-Seed:
- python/test:
pip-dependency-file: requirements-dev.txt
app-dir: ~/project/00-Starter-Seed
test-tool: pytest
- integration-tests:
context: Quickstart API Tests
requires:
- checkout
1 change: 1 addition & 0 deletions 00-Starter-Seed/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.env
*.iml
.idea
__pycache__
2 changes: 1 addition & 1 deletion 00-Starter-Seed/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Please check our [Quickstart](https://auth0.com/docs/quickstart/backend/python)

# Running the example

In order to run the example you need to have `python` and `pip` installed.
In order to run the example you need to have `python 3` and `pip` installed.

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_IDENTIFIER`, which is the audience of your API. You can find an example in the
Expand Down
6 changes: 6 additions & 0 deletions 00-Starter-Seed/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-r requirements.txt
black==22.1.0
flake8==4.0.1
jwcrypto==1.0
pytest==7.1.0
pytest-mock==3.7.0
9 changes: 4 additions & 5 deletions 00-Starter-Seed/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
flask
python-dotenv
python-jose
flask-cors
six
Flask==2.0.3
python-dotenv==0.19.2
Flask-Cors==3.0.10
Authlib==1.0.0
155 changes: 11 additions & 144 deletions 00-Starter-Seed/server.py
Original file line number Diff line number Diff line change
@@ -1,154 +1,26 @@
"""Python Flask API Auth0 integration example
"""

from functools import wraps
import json
from os import environ as env
from typing import Dict

from six.moves.urllib.request import urlopen

from dotenv import load_dotenv, find_dotenv
from flask import Flask, request, jsonify, _request_ctx_stack, Response
from flask import Flask, jsonify
from flask_cors import cross_origin
from jose import jwt
from authlib.integrations.flask_oauth2 import ResourceProtector
from validator import Auth0JWTBearerTokenValidator

ENV_FILE = find_dotenv()
if ENV_FILE:
load_dotenv(ENV_FILE)
AUTH0_DOMAIN = env.get("AUTH0_DOMAIN")
API_IDENTIFIER = env.get("API_IDENTIFIER")
ALGORITHMS = ["RS256"]
APP = Flask(__name__)


# Format error response and append status code.
class AuthError(Exception):
"""
An AuthError is raised whenever the authentication failed.
"""
def __init__(self, error: Dict[str, str], status_code: int):
super().__init__()
self.error = error
self.status_code = status_code


@APP.errorhandler(AuthError)
def handle_auth_error(ex: AuthError) -> Response:
"""
serializes the given AuthError as json and sets the response status code accordingly.
:param ex: an auth error
:return: json serialized ex response
"""
response = jsonify(ex.error)
response.status_code = ex.status_code
return response


def get_token_auth_header() -> str:
"""Obtains the access token from the Authorization Header
"""
auth = request.headers.get("Authorization", None)
if not auth:
raise AuthError({"code": "authorization_header_missing",
"description":
"Authorization header is expected"}, 401)

parts = auth.split()

if parts[0].lower() != "bearer":
raise AuthError({"code": "invalid_header",
"description":
"Authorization header must start with"
" Bearer"}, 401)
if len(parts) == 1:
raise AuthError({"code": "invalid_header",
"description": "Token not found"}, 401)
if len(parts) > 2:
raise AuthError({"code": "invalid_header",
"description":
"Authorization header must be"
" Bearer token"}, 401)

token = parts[1]
return token


def requires_scope(required_scope: str) -> bool:
"""Determines if the required scope is present in the access token
Args:
required_scope (str): The scope required to access the resource
"""
token = get_token_auth_header()
unverified_claims = jwt.get_unverified_claims(token)
if unverified_claims.get("scope"):
token_scopes = unverified_claims["scope"].split()
for token_scope in token_scopes:
if token_scope == required_scope:
return True
return False


def requires_auth(func):
"""Determines if the access token is valid
"""

@wraps(func)
def decorated(*args, **kwargs):
token = get_token_auth_header()
jsonurl = urlopen("https://" + AUTH0_DOMAIN + "/.well-known/jwks.json")
jwks = json.loads(jsonurl.read())
try:
unverified_header = jwt.get_unverified_header(token)
except jwt.JWTError as jwt_error:
raise AuthError({"code": "invalid_header",
"description":
"Invalid header. "
"Use an RS256 signed JWT Access Token"}, 401) from jwt_error
if unverified_header["alg"] == "HS256":
raise AuthError({"code": "invalid_header",
"description":
"Invalid header. "
"Use an RS256 signed JWT Access Token"}, 401)
rsa_key = {}
for key in jwks["keys"]:
if key["kid"] == unverified_header["kid"]:
rsa_key = {
"kty": key["kty"],
"kid": key["kid"],
"use": key["use"],
"n": key["n"],
"e": key["e"]
}
if rsa_key:
try:
payload = jwt.decode(
token,
rsa_key,
algorithms=ALGORITHMS,
audience=API_IDENTIFIER,
issuer="https://" + AUTH0_DOMAIN + "/"
)
except jwt.ExpiredSignatureError as expired_sign_error:
raise AuthError({"code": "token_expired",
"description": "token is expired"}, 401) from expired_sign_error
except jwt.JWTClaimsError as jwt_claims_error:
raise AuthError({"code": "invalid_claims",
"description":
"incorrect claims,"
" please check the audience and issuer"}, 401) from jwt_claims_error
except Exception as exc:
raise AuthError({"code": "invalid_header",
"description":
"Unable to parse authentication"
" token."}, 401) from exc

_request_ctx_stack.top.current_user = payload
return func(*args, **kwargs)
raise AuthError({"code": "invalid_header",
"description": "Unable to find appropriate key"}, 401)
require_oauth = ResourceProtector()
validator = Auth0JWTBearerTokenValidator(AUTH0_DOMAIN, API_IDENTIFIER)
require_oauth.register_token_validator(validator)

return decorated
APP = Flask(__name__)


# Controllers API
Expand All @@ -164,7 +36,7 @@ def public():
@APP.route("/api/private")
@cross_origin(headers=["Content-Type", "Authorization"])
@cross_origin(headers=["Access-Control-Allow-Origin", "http://localhost:3000"])
@requires_auth
@require_oauth(None)
def private():
"""A valid access token is required to access this route
"""
Expand All @@ -175,17 +47,12 @@ def private():
@APP.route("/api/private-scoped")
@cross_origin(headers=["Content-Type", "Authorization"])
@cross_origin(headers=["Access-Control-Allow-Origin", "http://localhost:3000"])
@requires_auth
@require_oauth("read:messages")
def private_scoped():
"""A valid access token and an appropriate scope are required to access this route
"""
if requires_scope("read:messages"):
response = "Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this."
return jsonify(message=response)
raise AuthError({
"code": "Unauthorized",
"description": "You don't have access to this resource"
}, 403)
response = "Hello from a private endpoint! You need to be authenticated and have a scope of read:messages to see this."
return jsonify(message=response)


if __name__ == "__main__":
Expand Down
Empty file.
Loading