This is a simple implementation of flask api with JWT(JSON WEB TOKENS). You get a token by logging in and send this token in headers for accessing every other routes.
- Implementation Features
- Usage
- Configuration
- Application registration tokens
- Loosening other routes
- Useful tools
- Inner details
- TODOS
- Rolling out custom flask decorators
- Token based auth with jwt
- Custom application registering and separate client tokens
- Http Basic username:pass authentication
- Flask blueprinting for modularization
- Flask database migration.
- Supports postgres, sql, sqlite etc dbs with sqlalchemy
- Returning diffrent http status codes
- Some common short flask tricks
- Defining same routes with diffrent methods to reduce if/else nests
- Returning python string, dicts autogenerates a json response (no need jsonify)
- Calling .app_context().push() on flask app instance. Helps a ton in intrepreter to play with db and stuff without initializing
This api has user and post tables in db. Every routes relating to ‘user’ and ‘post’ requires a token in the header which can be obtained by logging in throgh login route.
This assumes you have completed the configuration, created admin user and have its credentials. See configuration section.
NOTE: Any illegal or failed requests on any route will generate non-200 status code and return a json dict with one “message” key stating the reason
Example:
{"message": "no user found"}
ENDPOINT: “login” [GET REQ] Payload: auth(username, pass)
import requests
from requests.auth import HTTPBasicAuth
URL = "https://apiurl.com"
# fill username and password with your acc info
auth = HTTPBasicAuth("username", "password")
login_response = requests.get(URL+"/login", auth=auth)
login_response.status_code # confirm that its 200
token = login_response.json()["token"]
headers = {
"x-access-token": token
}
RESPONSE: 200
{"token": "soasloiwurpoewiurpowierupwoeirf"}
For accessing every other route, you need to specify this token in the header as value of “x-access-token”.
NOTE: This token has no time limit and will never expire, see why. For making expirable tokens have a look at here.
ENDPOINT: “user” [POST REQ] REQ: admin token
Creating a user requires admin account”s token. You send a post request to the “user” endpoint.
The payload should have username and password.
headers = {"x-access-token": token}
data = {"username": "some usename", "password": "my password"}
create_user = requests.post(URL+"/user", data=data, headers=headers
Response: 200
{
"message": "User created successfully"
}
ENDPOINT: “user” [GET REQ] REQ: admin token
Sending the request gets you all users info
headers = {"x-access-token": token}
requests.get(URL+"/user", headers=headers)
RESPONSE: 200
{"users": [
{"admin": true, "id": 1,
"password": "sha256$Sot2tcp9$671301dae8s45ad6f2fe0f583f8e60bfc90b24f045fcb791c4483711ca9c6d09",
"public_id": "e9572ee6-4b5e-45e4-a840-58a33b04b8a7",
"username": "my username"}
]
}
ENDPOINT: “user/public_id” [GET REQ] REQ: admin token
You can get public id of user by sending GET req to “user” endpoint: see above
requests.get(URL+"/user/public_id", headers=headers)
RESPONSE: 200
{"user":
{
"admin": false,
"id": 2,
"password": "sha256$f8ulwnAv$8af6f5590e8af54c8d2171cc9afc568727a8a763e8c875855f8b7d27f5dfcccd",
"public_id": "1f190b06-263s-42aa-86e9-460d0aff93d9",
"username": "my username"
}
}
ENDPOINT: “user/public_id” [PUT REQ] REQ: admin token
headers = {"x-access-token": token}
requests.put(URL+"/user/public_id", headers=headers)
RESPONSE: 200
{"message": "The user has been promoted!"}
ENDPOINT: “user/public_id” [DELETE REQ] REQ: admin token
headers = {"x-access-token": token}
requests.delete(URL+"/user/public_id", headers=headers)
RESPONSE: 200
{"message": "The user has been deleted!"}
ENDPOINT: “template” [POST REQ]
The payload should have title and url and optionally description.
headers = {"x-access-token": token}
data = {"title": "some title",
"url": "http:/test.com",
"description": "some desc",
}
requests.put(URL+"/user/public_id", headers=headers)
RESPONSE: 200
{"message": "Post created"}
ENDPOINT: “template” [GET REQ]
headers = {"x-access-token": token}
requests.get(URL+"/template", headers=headers)
RESPONSE: 200
{"templates": [
{"description": "Done",
"id": 27,
"posted": "Mon, 12 Oct 2020 04:51:27 GMT",
"title": "Test thing",
"url": "https://i.imgur.com/yYGxFJX.jpeg",
"username": "somerandomusername",
"posted": true},
{"description": null,
"id": 27,
"posted": "Mon, 12 Oct 2020 04:51:27 GMT",
"title": "Test thing",
"url": "https://i.imgur.com/yYGxFJX.jpeg",
"username": null,
"posted": false}, ]
}
Note: Sometimes user_id, description can be null.
ENDPOINT: “/” [GET REQ]
The api provides a way to get approved post (with approved propery set to true + current user’s own post) with a single api call.
requests.get(URL+"/", headers=headers)
RESPONSE: 200
{"templates": [
{"description": "Done",
"id": 27,
"posted": "Mon, 12 Oct 2020 04:51:27 GMT",
"title": "Test thing",
"url": "https://i.imgur.com/yYGxFJX.jpeg",
"username": "somerandomusername",
"posted": true}
]
}
Note: Sometimes user_id, description can be null too.
ENDPOINT: “template/template_id” [GET REQ]
You can get template id of post by sending GET req to “template” endpoint: see above
requests.get(URL+"/template/template_id", headers=headers)
RESPONSE: 200
{"template":
{"description": "Done",
"id": 27,
"posted": "Mon, 12 Oct 2020 04:51:27 GMT",
"title": "Test thing",
"url": "https://i.imgur.com/yYGxFJX.jpeg",
"user_id": "alskjdf_dfkdjf"
}
}
Note: Sometimes user_id, description can be null too.
ENDPOINT: “template/template_id” [PUT REQ]
Updating a post is same as creating it.
headers = {"x-access-token": token}
data = {"title": "some title",
"url": "http:/test.com",
"description": "some desc",
}
requests.put(URL+"/template/template_id", data=data, headers=headers)
RESPONSE: 200
{"message": "Post Updated"}
ENDPOINT: “template/template_id” [DELETE REQ]
headers = {"x-access-token": token}
requests.delete(URL+"/template/template_id", headers=headers)
RESPONSE: 200
{"message": "The post has been deleted"}
All the configs are set in the meme_api/__init__.py file.
- With Pip
$ python3 -m venv .venv $ .venv/bin/python -m pip install -r requirements.txt
- With Poetry
$ poetry install
The config SQLALCHEMY_DATABASE_URI is made from different env vars parts like HOST_NAME, HOST_PASS etc You need to set those variables Or you can just use sqlite db.
A minimal ‘.env’ config looks like
export SECRET_KEY='mysecretkey'
export SQLALCHEMY_DATABASE_URI='sqlite:///site.db'
export FLASK_APP=run.py
This same config along with example config for hosted sql (eg MYSQL) server is available in .env_eg file. Just rename, edit and source this file.
#+ .env_eg file +#
export SECRET_KEY='mysecretkey'
export SQLALCHEMY_DATABASE_URI='sqlite:///site.db'
export FLASK_APP=run.py
# For a hosted mysql/postgres server
# Note: if SQLALCHEMY_DATABASE_URI env var is present these env vars will be ignored & WONT BE USED
export DB_USERNAME='username of database'
export DB_PASS='password of database'
export DB_HOST='host address url of database'
export DB_NAME='name of db and tablename eg. mysqldb$posts'
Before initializing the database. Create a migrations folder for you db and delete the existing one
$ rm -rf ./migrations
$ python -m flask db init # makes migrations folder
Run migrate to create the tables required by the models
$ python -m flask db migrate $ python -m flask db upgrade
Once you make any changes to models you need to migrate & upgrade the database as shown above
In case you already have a database initialized(ie db schema created) through different option and want to integrate flask-migrate in it.
First: Initialize the migrations folder Note: delete existing migrations folder
$ python -m flask db init
Create another empty database table and point the database env variables to this empty table (in case of sqlite just change the ‘site.db’ name to ‘site2.db’)
$ python -m flask db migrate
Now again point to your original database column in environment vars (for sqlite just change ‘site2.db’ back to ‘site.db’)
$ python -m flask db stamp head
$ python -m flask db migrate # you should see 'no change in schema detected' message
You are all set. From now, if you make any changes to models you need to migrate & upgrade the database as shown below
$ python -m flask db migrate $ python -m flask db upgrade
Only admin users are allowed to create new accounts through api. Thus a admin user has to be manually created (or you could remove that if statement and create user acc through that route)
import uuid
from werkzeug.security import generate_password_hash
from run import app
from meme_api import db
from meme_api.models import User
app.app_context().push()
hashed_pass = generate_password_hash('secretpassword', method='sha256')
admin = User(username='admin',
password=hashed_pass,
admin=True,
public_id=str(uuid.uuid4()) )
db.session.add(admin)
db.session.commit()
The token generated by the api never expires. For preventing leaked tokens to be misued and also limit the database connections, the prod branch of this repo implements a application based registering.
A random uuid is generated and manually put into the meme_api/apps.py file. This id can now be used in headers for requesting every route.
#+ apps.py file +
registered = {
'someapp': 'generated random uuid',
'cli': 'another uuid for another app',
}
headers = {
'x-application-token': 'uuid token for application',
'x-access-token': 'user login token',
}
Every routes including login now requires above ‘x-application-token’ header for the request to be successful.
With application based authentication in place, the routes for creating new user, getting all users etc can be loosened to not require an admin token.
There are many good tools to leverage understanding of how api’s and http requests work.
- CLI tools for testing, debugging API endpoints.
- An dedicated website which provides post, delete, put etc endpoints in httpbin.org/post, /delete respectivly. Returns all the headers and data info it got in nice json format.
- Great partner tool with httpie
- Exploring, testing endpoints with diffrent kinds of requests in a friendly UI. Helps creating a test suite.
import requests
from requests.auth import HTTPBasicAuth
URL = "https://httpbin.org"
auth = HTTPBasicAuth("username", "password")
login_response = requests.post(URL+"/post", auth=auth)
print(login_response.json())
Response
{"args": {},
"data": "",
"files": {},
"form": {},
"headers": {"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
"Content-Length": "0",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.24.0",
"X-Amzn-Trace-Id": "Root=1-5f8aee35-211905107cfea23a2ad3b865"},
"json": null,
"origin": "35.229.170.146",
"url": "https://httpbin.org/post"}
What we are interested in is the Authorization header. Basically the requests transformed the username and password to base64 encoded string and passed the header.
header = {
"Authorization": "Basic " + Base64encoded(username + ":" + password)
}
So instead of passing auth arg we can also create this authorization header ourself and should get the same result
import requests
import base64
URL = "httpbin.org/post"
token = base64.b64encode(bytes("username:pass", "utf-8"))
headers = {"Authorization": f"Basic {token.decode()}"}
response = requests.get(URL, headers=headers)
print(response.json())
{"args": {},
"data": "",
"files": {},
"form": {},
"headers": {"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
"Content-Length": "0",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.24.0",
"X-Amzn-Trace-Id": "Root=1-5f8af1bb-716f15011a1b61770e118a7f"},
"json": null,
"origin": "35.229.170.146",
"url": "https://httpbin.org/post"}
Ref: stackoverflow page
- request.data : used for fallback data storage mostly empty
- request.args: the key/value pairs in the URL query string
- request.form: the key/value pairs in the body, from a HTML post form, or JavaScript request that isn’t JSON encoded
- request.files: the files in the body, which Flask keeps separate from form. HTML forms must use enctype=multipart/form-data or files will not be uploaded.
- request.values: combined args and form, preferring args if keys overlap
- request.json: parsed JSON data. The request must have the application/json content type, or use request.get_json(force=True) to ignore the content type.