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

  1. Recover the kid from the user JWT The kid tells us which key was used to sign the JWT.

  2. 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.

  3. Find the matching JWK from the JWKS retrieved at step 2 The matching JWK is the one that has the same kid found at step 1.

  4. Verify the JWT: Use the matching JWK to verify the JWT.

The JWKS should be cached in the backend to avoid querying the endpoint every time a token needs to be validated. It is recommended to retrieve the JWKS when no JWK matches the JWT kid (step 3).

Rust-based Backend Example

Below is a sample code in rust validating the user JWT using reqwest and jsonwebtoken packages:

Cargo.toml
[dependencies]
jsonwebtoken = "9.3.0"
reqwest = { version = "0.12.4", features = ["json"]}
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
main.rs
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use reqwest;
use serde::{Deserialize, Serialize};
use std::error::Error;

const HANKO_API_URL: &str = "https://XXXXX.hanko.io";
const JWT_AUDIENCE_DOMAIN: &str = "example.com";

#[derive(Debug, Deserialize)]
struct Jwk {
    kty: String,
    kid: String,
    n: String,
    e: String,
    alg: String,
}

#[derive(Debug, Deserialize)]
struct Jwks {
    keys: Vec<Jwk>,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct EmailInfo {
    pub address: String,
    pub is_primary: bool,
    pub is_verified: bool,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct UserDecodedJwtInfo {
    pub aud: Vec<String>,
    pub email: EmailInfo,
    pub exp: i64,
    pub iat: i64,
    pub sub: String,
}

async fn fetch_jwks() -> Result<Jwks, Box<dyn Error>> {
    let response = reqwest::get(format!("{}/.well-known/jwks.json", HANKO_API_URL)).await?;
    let jwks = response.json::<Jwks>().await?;
    Ok(jwks)
}

fn get_jwt_kid(token: &str) -> Result<String, Box<dyn Error>> {
    let header = decode_header(token)?;
    header.kid.ok_or_else(|| "Missing kid in header".into())
}

fn decode_token(jwk: &Jwk, token: &str) -> Result<UserDecodedJwtInfo, Box<dyn Error>> {
    // Decode the token to return its information.
    let decoding_key = DecodingKey::from_rsa_components(&jwk.n, &jwk.e)?;

    let mut validation = Validation::new(Algorithm::RS256);
    validation.set_audience(&[JWT_AUDIENCE_DOMAIN.to_string()]);

    let token_data = decode::<UserDecodedJwtInfo>(token, &decoding_key, &validation)?;
    Ok(token_data.claims)
}

async fn validate_hanko_token(token: &str) -> Result<UserDecodedJwtInfo, Box<dyn Error>> {
    // 1) Recover the kid from the user JWT.
    let jwt_kid = get_jwt_kid(token)?;

    // 2) Retrieve the JSON Web Key Set (JWKS).
    let jwks = fetch_jwks().await?;

    // 3) Find the matching JWK from the JWKS retrieved at step 2*
    let matching_jwk = jwks.keys.iter().find(|jwk| jwk.kid == jwt_kid);

    // 4) Verify the JWT.
    match matching_jwk {
        Some(jwk) => {
            let token_data = decode_token(jwk, token)?;

            // More validations related to the decoded token...

            Ok(token_data)
        }
        None => Err(format!("No matching JWK found matching token kid: {}", jwt_kid).into()),
    }
}

#[tokio::main]
async fn main() {
    // Depending on the rust backend framework used (axum, artix-web, rocket...),
    // recover the hanko token from the request.
    let jwt_hanko_token = "some-hanko-jwt-token".to_string();

    match validate_hanko_token(&jwt_hanko_token).await {
        Ok(payload) => println!("Token is valid. Payload: {:?}", payload),
        Err(err) => eprintln!("Token validation failed: {}", err),
    }
}