A demo app with simple REST APIs that uses Auth0 and SciTokens to authorize access to the API endpoints
The app provides 3 different REST APIs that allow standard HTTP method requests GET
, POST
, PUT
, DELETE
. Each API is built with different authorization scopes:
/expenses
is a public endpoint that requires no authorization./incomes
is a private endpoint authorized with Auth0 tokens with static scopes./properties
is a private endpoint authorized with SciTokens tokens with dynamic scopes.
Diagram source: https://swimlanes.io/u/kmFDYxo9g
This demo is derived from https://github.com/auth0-blog/flask-restful-apis.
Diagram source: https://swimlanes.io/u/ZMHpFBMfZ
You can clone this repository by using:
$ git clone [email protected]:SciAuth/rest-demo.git
First and foremost, make sure that your local machine is installed with Python 3, Pip, and Flask. If you are not sure, check to see if everything is set up as expected:
$ python3 --version
# Python 3.9.10
$ pip3 --version
# pip 22.0.3
$ flask --version
# Python 3.9.10
# Flask 2.0.3
# Werkzeug
If you have never developed a Flask application before, high chance you haven't had it yet. The installation is quite simple
# you might need to use pip3 instead of pip
$ pip install Flask
To manage our project's dependencies (with specific versions) without messing up with the system's global packages, we are going to use pipenv
instead of pip
.
Pipenv is a dependency manager that isolates projects on private environments, allowing packages to be installed per project
$ pip install pipenv
The prerequisites for this app are located on Pipfile
and Pipfile.lock
.
The
Pipfile
specifies packages requirements for a Python application or library to development and execution.
The
Pipfile.lock
specifies the version of the packages present in Pipfile to be used. It eliminates the risk of automatically upgrading packages that depend upon each other and break of the project dependency tree.
You can manually install each of them or run:
$ pipenv install --dev
To run this app on your local server, direct to the cashman-flask-project
folder to facilitate the start up of our application:
$ cd cashman-flask-project
$ chmod +x bootstrap.sh
$ ./bootstrap.sh
This will first defines the main script (index.py
) to be executed by Flask, then activate a virtual environment by pipenv
that locates exact versions of our dependencies, then run our Flask app.
If the script is working properly, you should expect to see a result similar to this:
# * Serving Flask app './cashman/index.py' (lazy loading)
# * Environment: production
# WARNING: This is a development server. Do not use it in a production deployment.
# Use a production WSGI server instead.
# * Debug mode: off
# * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
To test our APIs, we can use curl command-line tool via terminal. You can also use Postman – a GUI platform for building and testing APIs.
This is a public endpoint, thus to test all 4 HTTP methods are working properly, we simple invoke curl
calls.
GET
$ curl --request GET \
--url http://127.0.0.1:5000/expenses/linh
Response
[{"amount":20,"description":"salad"}]
POST
$ curl -X POST -H "Content-Type: application/json" -d '{
"amount": 10,
"description": "ticket"
}' http://127.0.0.1:5000/expenses/linh
Response
There is no terminal output for this command. If you call GET again, you should expect
[{"amount":20,"description":"salad"},{"amount":10,"description":"ticket"}]
PUT
$ curl -X PUT -H "Content-Type: application/json" -d '[{"amount": 10,
"description": "ticket"}, {"amount": 50, "description": "dinner"}]' http://127.0.0.1:5000/expenses/linh
Response
There is no terminal output for this command. If you call GET again, you should expect
[{"amount":20,"description":"salad"},{"amount":50,"description":"dinner"}]
DELETE
$ curl -X DELETE -H "Content-Type: application/json" -d '{
"amount": 20,
"description": "salad"
}' http://127.0.0.1:5000/expenses/linh
Response
There is no terminal output for this command. If you call GET again, you should expect
[{"amount":50,"description":"dinner"}]
To test this endpoint, we first need to ask Auth0 for authorized tokens by issuing the following API call:
$ curl --request POST \
--url https://dev-7jgsf20u.us.auth0.com/oauth/token \
--header 'content-type: application/json' \
--data '{"client_id":"MXzwZEYbHMFlJZWpfNjSFWttW0xq16JT","client_secret":"DjItEcgIhsFK6ma0rr3dgc-cMcuH1nMfRVSl181VNU3eiMh5_SlV9XcPwIJqb7c5","audience":"https://cashman/api","grant_type":"client_credentials"}'
To lear more about how to add and configure Auth0 authorization to a Python API built with Flask, follow this tutorial on Auth0 website.
We previously allowed these following scopes from our Auth0 dashboard:
Thus, if the above request is successful, you should expect to get something similar to this response:
{
"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im85Z2FhelRrM0tHVzJvU1g5VlpaaiJ9.eyJpc3MiOiJodHRwczovL2Rldi03amdzZjIwdS51cy5hdXRoMC5jb20vIiwic3ViIjoiTVh6d1pFWWJITUZsSlpXcGZOalNGV3R0VzB4cTE2SlRAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vY2FzaG1hbi9hcGkiLCJpYXQiOjE2NDgxMTI1MTgsImV4cCI6MTY0ODE5ODkxOCwiYXpwIjoiTVh6d1pFWWJITUZsSlpXcGZOalNGV3R0VzB4cTE2SlQiLCJzY29wZSI6InJlYWQ6aW5jb21lcyB3cml0ZTppbmNvbWVzIGRlbGV0ZTppbmNvbWVzIHVwZGF0ZTppbmNvbWVzIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.oxnL0kRhhczd8utKvYjjRw79mxdGKjKE_qviyR44Au8AFzVB-J4tu5CFXWzRoWajsKc1fPha32pt2s3v39bu5OUmmSLS_Zd4V4SvxmBvzHYdhJk2QY37GpHpgaPGwZst6M6YjMh0XxChFHxab_GxbAe__H5ZK8UBUCD09LuJWS-IjF4Wa5pl1vJP-dAkOf5aTCA9kqxZTTbcnDbJXOi2QAW5RTGY-dLQQAQDHU04EtFyBB4tIHqJBAX2mtdSpFnAxiIJOgopk3yaDItPHl_--5c_4uZq1lZ81e76I5tyFt0jJllJij8QSQribeHWp5CAzjhJe0v-MrZXO6WbUnJZ6g",
"scope":"read:incomes write:incomes delete:incomes update:incomes"
"expires_in":86400,
"token_type":"Bearer"}
To inspect this token, you can decode it at jwt.io:
You can now extract the access_token
property from the response to make to create a bearer token
with an Authorization Header
in your request to obtain authorized access to your API.
GET
$ curl --request GET \
--url http://127.0.0.1:5000/incomes/linh \
--header 'authorization: Bearer <access_token>'
Response
[{"amount":1000,"description":"stock interest"}]
POST
$ curl --request POST \
--url http://127.0.0.1:5000/incomes/linh \
-H "Content-Type: application/json" -d '{
"amount": 50,
"description": "lottery"
}' \
--header 'authorization: Bearer <access_token>'
Response
There is no terminal output for this command. If you call GET again, you should expect
[{"amount":1000,"description":"stock interest"},{"amount":50,"description":"lottery"}]
PUT
$ curl --request PUT \
--url http://127.0.0.1:5000/incomes/linh \
-H "Content-Type: application/json" -d '[{"amount": 50,
"description": "lottery"}, {"amount": 1000, "description": "salary"}]' \
--header 'authorization: Bearer <access_token>'
Response
There is no terminal output for this command. If you call GET again, you should expect
[{"amount":1000,"description":"stock interest"},{"amount":1000,"description":"salary"}]
DELETE
$ curl --request DELETE \
--url http://127.0.0.1:5000/incomes/linh \
-H "Content-Type: application/json" -d '{
"amount": 1000,
"description": "salary"
}' \
--header 'authorization: Bearer <access_token>'
Response
There is no terminal output for this command. If you call GET again, you should expect
[{"amount":1000,"description":"stock interest"}]
To test this endpoint, we first need to ask SciTokens for authorized tokens for our request. Go to https://demo.scitokens.org/ to generate your sample SciTokens. You can edit the payload of the SciToken on the left to serve your tests, such as adding new scope. You can also add multiple scopes for the tokens, separated by a single space.
For example the below token is used to authorize permission for all methods on /properties/linh
endpoint. However, this token shouldn't work if we use it to request /properties/yolanda
.
You now can copy the encoded token on the right for your API call.
GET
$ curl --request GET \
--url http://127.0.0.1:5000/properties/linh \
--header 'authorization: Bearer <your_sample_token>'
Response
[{"amount":5000,"description":"truck"},{"amount":70000,"description":"condo"}]
POST
$ curl --request POST \
--url http://127.0.0.1:5000/properties/linh \
-H "Content-Type: application/json" -d '{
"amount": 10000,
"description": "land"
}' \
--header 'authorization: Bearer <your_sample_token>'
Response
There is no terminal output for this command. If you call GET again, you should expect
[{"amount":5000,"description":"truck"},{"amount":70000,"description":"condo"},{"amount":10000,"description":"land"}]
PUT
$ curl --request PUT \
--url http://127.0.0.1:5000/properties/linh \
-H "Content-Type: application/json" -d '[{"amount": 10000,
"description": "land"}, {"amount": 8000, "description": "house"}]' \
--header 'authorization: Bearer <your_sample_token>'
Response
There is no terminal output for this command. If you call GET again, you should expect
[{"amount":5000,"description":"truck"},{"amount":70000,"description":"condo"},{"amount":8000,"description":"house"}]
DELETE
$ curl --request DELETE \
--url http://127.0.0.1:5000/properties/linh \
-H "Content-Type: application/json" -d '{
"amount": 8000,
"description": "house"
}' \
--header 'authorization: Bearer <your_sample_token>'
Response
There is no terminal output for this command. If you call GET again, you should expect
[{"amount":5000,"description":"truck"},{"amount":70000,"description":"condo"}]
Testing Validation
Since the token allows delete permissions on /properties/linh
but not /properties/yolanda
, you will see a Validation incorrect
error if you try to DELETE /properties/yolanda
. For example:
$ curl --request DELETE \
--url http://127.0.0.1:5000/properties/yolanda \
-H "Content-Type: application/json" -d '{
"amount": 100000,
"description": "house"
}' \
--header 'authorization: Bearer <your_sample_token>'
Response
Validation incorrect: Validator rejected value of 'read:/properties write:/properties update:/properties delete:/properties/linh' for claim 'scope'
You can modify the scope
value in the token from https://demo.scitokens.org/ to test permissions for different methods and /properties
paths.
Testing Expiration
If the exp
value in your token is too old, you will see an error response like this:
Unable to deserialize: %Signature has expired
By default, the tokens from https://demo.scitokens.org/ are valid for 10 minutes, so you may see this error if you use the same token for over 10 minutes. In that case, you can get a new token from https://demo.scitokens.org/ and try again.
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
Please make sure to update tests as appropriate.
Linh Tang - SciAuth Fellow Spring 2022
Yolanda Jiang - SciAuth Fellow Spring 2022