Skip to content

Updated Python Backend Quickstart #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
277 changes: 26 additions & 251 deletions quickstarts/backend/python.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,273 +3,48 @@ sidebarTitle: "Python"
title: Integrate Hanko with Python backend
---

## Get the Hanko API URL
After successful authentication, Hanko generates a session token that is stored as a cookie. Use the session token to authenticate requests to your backend. This guide demonstrates how to implement session token validation in Python to ensure that only properly authenticated users can access your application’s protected resources.

### Get the Hanko API URL

Retrieve the API URL from the [Hanko console](https://cloud.hanko.io/).

<Note>
If you are self-hosting Hanko you need to provide your own URL.
</Note>

## Hanko Authentication with JWT

Upon a successful login, Hanko sends a cookie containing a JSON Web Token ([JWT](https://datatracker.ietf.org/doc/html/rfc7519)). You can use this JWT to authenticate requests on your backend.

### Steps to Authenticate Requests

1. **Retrieve the JSON Web Key Set ([JWKS](https://datatracker.ietf.org/doc/html/rfc7517)):** The JWKS has the public keys to verify the JWT. Fetch it from the Hanko API's `.well-known/jwks.json` endpoint.

2. **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](https://fastapi.tiangolo.com/), [Django](https://www.djangoproject.com/) and [Flask](https://flask.palletsprojects.com/en/3.0.x/), based backend using the [PyJWT](https://pyjwt.readthedocs.io/en/stable/) package.

Since you will be decoding the token using the RSA digital signature algorithm, you will need to install the [cryptography](https://cryptography.io) library. You can install this library either directly or as an additional requirement within the PyJWT package.

```bash
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.

<Tabs>
<Tab title="FastAPI">
```python
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"}
```
</Tab>
<Tab title="Django">

```python yourApp/middleware.py
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
```
1. Retrieve the Session Token.

```python yourProject/settings.py
#other settings
...
2. Verify the Session token using the Hanko [Validate](/api-reference/public/session-management/validate-a-session-1) API endpoint.

MIDDLEWARE = [
...
#other middleware
...
### Example function
The following section demonstrates how to validate session tokens against the Hanko backend. The specific implementation for retrieving the session token cookie will vary depending on your framework.

'yourApp.middleware.AuthenticationMiddleware' #add this
]
...

HANKO_API_URL = 'Your Hanko API URL'

```
</Tab>
<Tab title="Flask">
```python example.py
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("/")
```python
def validate_session_token(token: str) -> Tuple[bool, Optional[str]]:
"""
Validates a session token with the Hanko API.
Returns a tuple of (is_valid: bool, error_message: Optional[str])
"""
try:
kid = jwt.get_unverified_header(jwt_cookie)["kid"]
payload = jwt.decode(
str(jwt_cookie),
public_keys[kid],
algorithms=["RS256"],
audience=AUDIENCE,
response = requests.post(
f"{HANKO_API_URL}/sessions/validate",
json={"session_token": token}
)
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
```
</Tab>
</Tabs>

<Warning>

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.
if response.status_code != 200:
return False, "Invalid token"

```python
token = token + "===="
```
validation_data = response.json()
if not validation_data.get("is_valid", False):
return False, "Invalid token"

<AccordionGroup>
<Accordion title="Tip for adding padding dynamically">
While a fixed padding like "====" can work, it might be more efficient to dynamically adjust the padding based on the actual needs of the token. Here’s how you can do it.
return True, None

```python
def add_padding(token):
padding_needed = len(token) % 4
if padding_needed:
padding_needed = 4 - padding_needed
return token + ('=' * padding_needed)
except requests.Timeout:
return False, "Authentication service timeout"
except requests.RequestException:
return False, "Authentication service unavailable"
```
This method calculates the exact amount of padding needed and adds it, ensuring that the token is correctly formatted for decoding.
</Accordion>
</AccordionGroup>

</Warning>