CodeX
Published in

CodeX

JWT Auth-101

How to handle authentication between different services?

Photo by Growtika Developer Marketing Agency on Unsplash

What is Authentication and Authorization?

What is JWT?

JWT Authentication in Theory

  1. The user send a request to login endpoint of auth service.
  • I use “HTTP Basic” to send a login request, you can use request body or other methods.
  • If there is no user, return error message and 401 status code.
  • If the user is exits in the system, create JWT and return the token.
  • If there is token, put the token in HTTP Authorization header (Bearer <TOKEN>).
Diagram by Author | One of the worst diagrams you have seen :)

JWT Authentication in Action

Requirements

  • flask: Web server (pip install flask)
  • pyjwt: Generating tokens (pip install pyjwt)
  • httpx: Making requests to the endpoints (pip install httpx)

Auth Service

"""
# We use the snippets below to make requests

# login post request
- httpx -m POST --auth user@user.com Passw0rd http://127.0.0.1:5000/login

# validate post request
- httpx -m POST -h "Authorization" "Bearer <TOKEN>" http://127.0.0.1:5000/validate
"""
from flask import Flask, request
import jwt
import datetime

JWT_SECRET = (
"this-is-so-secret-key-to-encode-jwt-tokens" # you should get this by using ENV
)
USERS_DB = [
{"username": "user@user.com", "password": "Passw0rd", "is_admin": True}
] # you should use a real db :)

app = Flask(__name__)


@app.route("/login", methods=["POST"])
def login():
print(request.headers) # We need to get "Authorization" header of the request
print(request.authorization) # This is automatically decode the header in Flask

# handle username and password from request header (Flask does for us, it decodes)
auth = request.authorization
if not auth:
return "missing credentials", 401

# filter users
users_list = [u for u in USERS_DB if u["username"] == auth.username]
# is there any user with the credentials
if len(users_list) != 1 or users_list[0]["password"] != auth.password:
return ("Missing credentials", 401)
else:
return createJWT(auth.username, JWT_SECRET, users_list[0]["is_admin"]), 200
httpx -m POST --auth user@user.com Passw0rd http://127.0.0.1:5000/login
Host: 127.0.0.1:5000
Content-Length: 0
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
User-Agent: python-httpx/0.23.1
Authorization: Basic dXNlckB1c2VyLmNvbTpQYXNzdzByZA==
{'username': 'user@user.com', 'password': 'Passw0rd'}
# Flask authorization header parser
def parse_authorization_header(
value: t.Optional[str],
) -> t.Optional["ds.Authorization"]:
"""Parse an HTTP basic/digest authorization header transmitted by the web
browser. The return value is either `None` if the header was invalid or
not given, otherwise an :class:`~werkzeug.datastructures.Authorization`
object.

:param value: the authorization header to parse.
:return: a :class:`~werkzeug.datastructures.Authorization` object or `None`.
"""
if not value:
return None
value = _wsgi_decoding_dance(value)
try:
auth_type, auth_info = value.split(None, 1)
auth_type = auth_type.lower()
except ValueError:
return None
if auth_type == "basic": # We used this scheme by sending request with httpx
try:
username, password = base64.b64decode(auth_info).split(b":", 1) # httpx encodes the header for us
except Exception:
return None
try:
return ds.Authorization(
"basic",
{
"username": _to_str(username, "utf-8"),
"password": _to_str(password, "utf-8"),
},
)
except UnicodeDecodeError:
return None
elif auth_type == "digest":
auth_map = parse_dict_header(auth_info)
for key in "username", "realm", "nonce", "uri", "response":
if key not in auth_map:
return None
if "qop" in auth_map:
if not auth_map.get("nc") or not auth_map.get("cnonce"):
return None
return ds.Authorization("digest", auth_map)
return None
def createJWT(username, secret, authz):
payload = {
"username": username,
"exp": datetime.datetime.now(tz=datetime.timezone.utc) # expire date
+ datetime.timedelta(days=1), # token lifetime is 1 day
"iat": datetime.datetime.utcnow(), # issued at
"admin": authz, # is user admin
}
return jwt.encode(
payload,
secret,
algorithm="HS256",
) # return JWT token
Image by author
@app.route("/validate", methods=["POST"])
def validate():
encoded_jwt = request.headers[
"Authorization"
] # handle "Authorization" header. We have to send the token in this scheme => Bearer <TOKEN>

if not encoded_jwt:
return ("missing credentials", 401)

encoded_jwt = encoded_jwt.split(" ")[1] # get the token (exclude "Bearer")

try:
decoded = jwt.decode(
encoded_jwt, JWT_SECRET, algorithms=["HS256"]
) # decode the token
except:
return ("not authorized", 403)

return (decoded, 200)
httpx -m POST -h "Authorization" "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXJAdXNlci5jb20iLCJleHAiOjE2NzA4NDY4NjIsImlhdCI6MTY3MDc2MDQ2MiwiYWRtaW4iOnRydWV9.Z3bA45Q414VsO9GYECsr9YO40ac1xD-EuMz5TeUOroA" http://127.0.0.1:5000/validate
Image by Author

Gateway Service

"""
# We use the snippets below to make requests

# login post request
- httpx -m POST --auth user@user.com Passw0rd http://127.0.0.1:8080/login

# GET Posts
- httpx -m GET -h "Authorization" "Bearer <TOKEN>" http://127.0.0.1:8080/posts

# POST Posts
- httpx -m POST -h "Authorization" "Bearer <TOKEN>" -j '{"id":1, "title": "Title Text"}' http://127.0.0.1:8080/posts
"""
from flask import Flask, request
import httpx
import json

AUTH_SERVICE_URL = "http://127.0.0.1:5000" # you should get this by using ENV
POSTS_DB = [] # to simulate inserting

app = Flask(__name__)

def auth_service_login(request):
"""Auth request to auth service

Args:
request (request): Flask request

Returns:
tuple: First item of the tuple will be the token if the request is successfull and 2nd item will be the status code
"""
auth = request.authorization
if not auth:
# if there is no "Authorization" header in the request that is coming to "gateway" service, return error
return None, ("missing credentials", 401)

basicAuth = (auth.username, auth.password)

# post request to auth service to get the token
response = httpx.post(f"{AUTH_SERVICE_URL}/login", auth=basicAuth)

if response.status_code == 200:
return response.text, None
else:
return None, (response.text, response.status_code)
def auth_service_validate_token(request):
"""Token validation in auth service

Args:
request (request): Flask request

Returns:
tuple: First item of the tuple will be the decoded token if the request is successfull and 2nd item will be the status code
"""
if not "Authorization" in request.headers:
# if there is no "Authorization" header in the request that is coming to "gateway" service, return error
return None, ("missing credentials", 401)

token = request.headers["Authorization"]

if not token:
# if there is no JWT token return error
return None, ("missing credentials", 401)

# post request to auth service to validate the token
response = httpx.post(
f"{AUTH_SERVICE_URL}/validate",
headers={"Authorization": token},
)

# auth service returns us the token and status code
if response.status_code == 200:
return response.text, None
else:
return None, (response.text, response.status_code)

@app.route("/login", methods=["POST"])
def login():
# get the token by using gateway service (we can think that the request will be redirected to the auth service)
token, err = auth_service_login(request)
if not err:
return token
else:
return err


@app.route("/posts", methods=["POST"])
def post_posts():
# firstly validate the token by using gateway service. By doing that we enforce to make the user to be logged in
token, err = auth_service_validate_token(request)
if err:
return err
else:
POSTS_DB.append(request.json)
return {"msg": "success", "data": request.json}


@app.route("/posts", methods=["GET"])
def get_posts():
# firstly validate the token by using gateway service. By doing that we enforce to make the user to be logged in
token, err = auth_service_validate_token(request)
if err:
return err

token = json.loads(token) # we get decoded token

return {"posts": POSTS_DB}

Gateway Testing

httpx -m POST --auth user@user.com Passw0rd http://127.0.0.1:8080/login
Image by author
httpx -m GET -h "Authorization" "Bearer <TOKEN>" http://127.0.0.1:8080/posts
Image by author
httpx -m POST -h "Authorization" "Bearer <TOKEN>" -j '{"id":1, "title": "Title Text"}' http://127.0.0.1:8080/posts

httpx -m POST -h "Authorization" "Bearer <TOKEN>" -j '{"id":2, "title": "Post 2"}' http://127.0.0.1:8080/posts

httpx -m POST -h "Authorization" "Bearer <TOKEN>" -j '{"id":3, "title": "Post 3"}' http://127.0.0.1:8080/posts
Image by author
httpx -m GET -h "Authorization" "Bearer <TOKEN>" http://127.0.0.1:8080/posts
Image by Author

Finally

--

--

Everything connected with Tech & Code. Follow to join our 1M+ monthly readers

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Baysan

Lifelong learner & Freelancer. I use technology that helps me. I’m currently working as a Business Intelligence & Backend Developer. mebaysan.com