Get the Hanko API URL
Retrieve the API URL from the Hanko console.
If you are self-hosting Hanko you need to provide your own URL.
Hanko Authentication with JWT
Upon a successful login, Hanko sends a cookie containing a JSON Web Token (JWT). You can use this JWT to authenticate requests on your backend.
Steps to Authenticate Requests
-
Retrieve the JSON Web Key Set (JWKS): The JWKS has the public keys to verify the JWT. Fetch it from the Hanko API’s .well-known/jwks.json
endpoint.
-
Verify the JWT: Use the JWKS to verify the JWT.
Python-based Backend Examples
Below we show examples of using a custom middleware in FastAPI, Django and Flask, based backend using the PyJWT package.
Since you will be decoding the token using the RSA digital signature algorithm, you will need to install the cryptography library. You can install this library either directly or as an additional requirement within the PyJWT package.
pip install pyjwt[crypto]
The pyjwt[crypto]
format is recommended in requirements files in projects using PyJWT, as a separate cryptography requirement line may later be mistaken for an unused requirement and removed.
from typing import Any
import os
import ssl
import jwt
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
HANKO_API_URL = os.environ.get("HANKO_API_URL")
def deny():
return JSONResponse(content={"error": "Unauthorized"}, status_code=401)
def extract_token_from_header(header: str) -> str:
parts = header.split()
return parts[1] if len(parts) == 2 and parts[0].lower() == "bearer" else None
app = FastAPI()
@app.middleware("http")
async def auth(request: Request, call_next: Any):
authorization = request.headers.get("authorization")
if not authorization:
return deny()
token = extract_token_from_header(authorization)
if not token:
return deny()
try:
# Disable SSL certificate verification while in development.
# Don't forget to remove this when in prod.
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
jwks_client = jwt.PyJWKClient(
HANKO_API_URL + "/.well-known/jwks.json", ssl_context=ssl_context
)
signing_key = jwks_client.get_signing_key_from_jwt(token)
data = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="localhost",
)
if not data:
return deny()
return await call_next(request)
except (jwt.DecodeError, Exception) as e:
print(f"Authentication error: {e}")
return deny()
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/protected")
async def protected():
return {"message": "Hello World"}
from typing import Any
import os
import ssl
import jwt
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
HANKO_API_URL = os.environ.get("HANKO_API_URL")
def deny():
return JSONResponse(content={"error": "Unauthorized"}, status_code=401)
def extract_token_from_header(header: str) -> str:
parts = header.split()
return parts[1] if len(parts) == 2 and parts[0].lower() == "bearer" else None
app = FastAPI()
@app.middleware("http")
async def auth(request: Request, call_next: Any):
authorization = request.headers.get("authorization")
if not authorization:
return deny()
token = extract_token_from_header(authorization)
if not token:
return deny()
try:
# Disable SSL certificate verification while in development.
# Don't forget to remove this when in prod.
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
jwks_client = jwt.PyJWKClient(
HANKO_API_URL + "/.well-known/jwks.json", ssl_context=ssl_context
)
signing_key = jwks_client.get_signing_key_from_jwt(token)
data = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="localhost",
)
if not data:
return deny()
return await call_next(request)
except (jwt.DecodeError, Exception) as e:
print(f"Authentication error: {e}")
return deny()
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/protected")
async def protected():
return {"message": "Hello World"}
import ssl
import jwt
from django.conf import settings
from django.http import JsonResponse
class AuthenticationMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __deny(self):
return JsonResponse({"error": "Unauthorized"},safe=False)
def __extract_token_from_header(self,header: str) -> str:
parts = header.split()
return parts[1] if len(parts) == 2 and parts[0].lower() == "bearer" else None
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
authorization = request.headers.get("authorization")
if not authorization:
return self.__deny()
token = self.__extract_token_from_header(authorization)
if not token:
return self.__deny()
try:
# Disable SSL certificate verification while in development. Don't forget to remove this when in prod
ssl_context = ssl.create_default_context()
if settings.DEBUG:
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
jwks_client = jwt.PyJWKClient(
settings.HANKO_API_URL + "/.well-known/jwks.json",
ssl_context=ssl_context
)
signing_key = jwks_client.get_signing_key_from_jwt(token)
data = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience="localhost",
)
if not data:
return self.__deny()
except (jwt.DecodeError, Exception) as e:
print(f"Authentication error: {e}")
return self.__deny()
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
return response
#other settings
...
MIDDLEWARE = [
...
#other middleware
...
'yourApp.middleware.AuthenticationMiddleware' #add this
]
...
HANKO_API_URL = 'Your Hanko API URL'
from flask import Flask, render_template, request, jsonify, redirect
import jwt #upm package(pyjwt)
import requests
from pprint import pprint
app = Flask(__name__)
#constants
API_URL = "HANKO_URL_HERE" #change this to your url from cloud.hanko.io
AUDIENCE = "localhost" # change this to the domain you're hosting on, and make sure it matches the URL on cloud.hanko.io
# Retrieve the JWKS from the Hanko API
jwks_url = f"{API_URL}/.well-known/jwks.json"
jwks_response = requests.get(jwks_url)
jwks_data = jwks_response.json()
public_keys = {}
for jwk in jwks_data["keys"]:
kid = jwk["kid"]
public_keys[kid] = jwt.algorithms.RSAAlgorithm.from_jwk(jwk)
@app.route("/protected")
def protected():
# Retrieve the JWT from the cookie
jwt_cookie = request.cookies.get("hanko")
# print(jwt_cookie)
if not jwt_cookie: #check that the cookie exists
return redirect("/")
try:
kid = jwt.get_unverified_header(jwt_cookie)["kid"]
payload = jwt.decode(
str(jwt_cookie),
public_keys[kid],
algorithms=["RS256"],
audience=AUDIENCE,
)
pprint(payload)
except Exception as e:
# The JWT is invalid
print(e)
return jsonify({"message": "unauthorised"})
return jsonify({"message": "authorised"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80)
#80 is the default port for http, so you won't need to specify the port in the browser
#open http://localhost in a browser to see it in action
While using PyJWT to decode JWTs, you might encounter errors such as binascii.Error: Incorrect padding
and jwt.exceptions.DecodeError: Invalid crypto padding
. These errors often occur due to insufficient base64
padding at the end of the token.
To resolve this, you might need to manually add the necessary padding to the token before attempting to decode it. You can read the token as a string and append the required padding characters to ensure successful decoding.