Skip to main content

JWK Authentication

This guide explains how third-party integrators can use JSON Web Keys (JWK) to authenticate with the Safiri API.

What are JWKs?

JSON Web Key Sets (JWKS) are a standard way to represent cryptographic keys in JSON format. The Safiri API uses asymmetric RSA keys with the RS256 algorithm for secure token signing and verification.

Key concepts:

  • Public Key: Available at our JWKS endpoint, used by you to verify tokens we issue
  • Private Key: Kept secure on our servers, used to sign tokens
  • RS256: RSA Signature with SHA-256, an industry-standard algorithm

Discovery Endpoint

You can find our JWKS URI through the API discovery endpoint:

curl https://booking-api.safiri.app/.well-known/api-discovery

Response includes:

{
"authentication": {
"type": "Bearer",
"jwks_uri": "https://booking-api.safiri.app/api/uts/jwk"
}
}

Authentication Flow

sequenceDiagram
participant Client as Your Application
participant Safiri as Safiri API

Client->>Safiri: 1. POST /api/logon (email, password)
Safiri-->>Client: 2. JWT Token (signed with private key)
Client->>Safiri: 3. GET /api/uts/jwk
Safiri-->>Client: 4. Public Keys (JWKS)
Note over Client: 5. Verify token signature using public key
Client->>Safiri: 6. API Request with Bearer token
Safiri-->>Client: 7. API Response

Steps:

  1. Login to obtain a JWT token
  2. Fetch JWKS to get the public keys
  3. Verify the token signature (optional but recommended)
  4. Call APIs using the token in the Authorization header

Fetching Public Keys

Endpoint

GET https://booking-api.safiri.app/api/uts/jwk

Response

{
"keys": [
{
"kty": "RSA",
"kid": "ynQuzyVMfasaRJo7Aybw5xvY4sg6Rrp6e0XEdvJb0XQ",
"use": "sig",
"alg": "RS256",
"e": "AQAB",
"n": "mMra8O77Z3eX3eASPIUOyVJ4Gdul..."
}
]
}

Key Properties

PropertyDescription
ktyKey type (RSA)
kidKey ID - use this to match keys when verifying
useKey usage (sig for signature)
algAlgorithm (RS256)
eRSA public exponent
nRSA modulus

Caching Recommendations

  • Cache the JWKS for 1 hour to reduce API calls
  • Refresh the cache if token verification fails with a key mismatch
  • Always use the kid (Key ID) from the JWT header to find the matching key

Code Examples

Node.js

Using the jose library:

npm install jose
import * as jose from 'jose';

const SAFIRI_API = 'https://booking-api.safiri.app';

async function authenticate(email, password) {
// Step 1: Login to get JWT token
const loginResponse = await fetch(`${SAFIRI_API}/api/logon`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});

const { token } = await loginResponse.json();
return token;
}

async function verifyToken(token) {
// Step 2: Fetch JWKS
const JWKS = jose.createRemoteJWKSet(
new URL(`${SAFIRI_API}/api/uts/jwk`)
);

// Step 3: Verify the token
const { payload } = await jose.jwtVerify(token, JWKS, {
algorithms: ['RS256']
});

console.log('Token verified! Payload:', payload);
return payload;
}

async function searchFlights(token, searchParams) {
// Step 4: Call API with Bearer token
const response = await fetch(`${SAFIRI_API}/api/flight/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(searchParams)
});

return response.json();
}

// Usage
async function main() {
const token = await authenticate('user@example.com', 'password');
await verifyToken(token);

const flights = await searchFlights(token, {
origin: 'DAR',
destination: 'JRO',
departureDate: '2026-02-15',
passengers: { adults: 1 }
});

console.log('Flights:', flights);
}

main().catch(console.error);

Python

Using PyJWT and requests:

pip install PyJWT cryptography requests
import jwt
import requests
from jwt import PyJWKClient

SAFIRI_API = "https://booking-api.safiri.app"

def authenticate(email: str, password: str) -> str:
"""Login and get JWT token"""
response = requests.post(
f"{SAFIRI_API}/api/logon",
json={"email": email, "password": password}
)
response.raise_for_status()
return response.json()["token"]

def verify_token(token: str) -> dict:
"""Verify JWT token using JWKS"""
# Fetch JWKS and get signing key
jwks_client = PyJWKClient(f"{SAFIRI_API}/api/uts/jwk")
signing_key = jwks_client.get_signing_key_from_jwt(token)

# Verify and decode the token
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"]
)

print(f"Token verified! Payload: {payload}")
return payload

def search_flights(token: str, search_params: dict) -> dict:
"""Search for flights using the API"""
response = requests.post(
f"{SAFIRI_API}/api/flight/search",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
},
json=search_params
)
response.raise_for_status()
return response.json()

# Usage
if __name__ == "__main__":
token = authenticate("user@example.com", "password")
verify_token(token)

flights = search_flights(token, {
"origin": "DAR",
"destination": "JRO",
"departureDate": "2026-02-15",
"passengers": {"adults": 1}
})

print(f"Flights: {flights}")

PHP

Using firebase/php-jwt:

composer require firebase/php-jwt guzzlehttp/guzzle
<?php
require 'vendor/autoload.php';

use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
use GuzzleHttp\Client;

const SAFIRI_API = 'https://booking-api.safiri.app';

function authenticate(string $email, string $password): string
{
$client = new Client();

$response = $client->post(SAFIRI_API . '/api/logon', [
'json' => [
'email' => $email,
'password' => $password
]
]);

$data = json_decode($response->getBody(), true);
return $data['token'];
}

function verifyToken(string $token): object
{
$client = new Client();

// Fetch JWKS
$response = $client->get(SAFIRI_API . '/api/uts/jwk');
$jwks = json_decode($response->getBody(), true);

// Parse keys
$keys = JWK::parseKeySet($jwks);

// Decode and verify token
$decoded = JWT::decode($token, $keys);

echo "Token verified! Payload: " . json_encode($decoded) . "\n";
return $decoded;
}

function searchFlights(string $token, array $searchParams): array
{
$client = new Client();

$response = $client->post(SAFIRI_API . '/api/flight/search', [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $token
],
'json' => $searchParams
]);

return json_decode($response->getBody(), true);
}

// Usage
$token = authenticate('user@example.com', 'password');
verifyToken($token);

$flights = searchFlights($token, [
'origin' => 'DAR',
'destination' => 'JRO',
'departureDate' => '2026-02-15',
'passengers' => ['adults' => 1]
]);

print_r($flights);

Java

Using auth0/java-jwt and jwks-rsa:

<!-- pom.xml dependencies -->
<dependencies>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.22.1</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.JwkProviderBuilder;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import okhttp3.*;

import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class SafiriClient {
private static final String SAFIRI_API = "https://booking-api.safiri.app";
private static final OkHttpClient client = new OkHttpClient();
private static final Gson gson = new Gson();
private static final MediaType JSON = MediaType.get("application/json");

public static String authenticate(String email, String password) throws Exception {
JsonObject credentials = new JsonObject();
credentials.addProperty("email", email);
credentials.addProperty("password", password);

RequestBody body = RequestBody.create(credentials.toString(), JSON);
Request request = new Request.Builder()
.url(SAFIRI_API + "/api/logon")
.post(body)
.build();

try (Response response = client.newCall(request).execute()) {
JsonObject result = gson.fromJson(response.body().string(), JsonObject.class);
return result.get("token").getAsString();
}
}

public static DecodedJWT verifyToken(String token) throws Exception {
// Create JWKS provider with caching
JwkProvider provider = new JwkProviderBuilder(new URL(SAFIRI_API + "/api/uts/jwk"))
.cached(10, 1, TimeUnit.HOURS)
.build();

// Decode token to get key ID
DecodedJWT jwt = JWT.decode(token);
String keyId = jwt.getKeyId();

// Get the public key
Jwk jwk = provider.get(keyId);
RSAPublicKey publicKey = (RSAPublicKey) jwk.getPublicKey();

// Verify the token
Algorithm algorithm = Algorithm.RSA256(publicKey, null);
DecodedJWT verified = JWT.require(algorithm).build().verify(token);

System.out.println("Token verified! Subject: " + verified.getSubject());
return verified;
}

public static JsonObject searchFlights(String token, Map<String, Object> searchParams) throws Exception {
String jsonBody = gson.toJson(searchParams);
RequestBody body = RequestBody.create(jsonBody, JSON);

Request request = new Request.Builder()
.url(SAFIRI_API + "/api/flight/search")
.header("Authorization", "Bearer " + token)
.post(body)
.build();

try (Response response = client.newCall(request).execute()) {
return gson.fromJson(response.body().string(), JsonObject.class);
}
}

public static void main(String[] args) throws Exception {
String token = authenticate("user@example.com", "password");
verifyToken(token);

Map<String, Object> searchParams = Map.of(
"origin", "DAR",
"destination", "JRO",
"departureDate", "2026-02-15",
"passengers", Map.of("adults", 1)
);

JsonObject flights = searchFlights(token, searchParams);
System.out.println("Flights: " + flights);
}
}

Making API Requests

Once you have a valid JWT token, include it in all API requests:

curl -X POST https://booking-api.safiri.app/api/flight/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"origin": "DAR",
"destination": "JRO",
"departureDate": "2026-02-15",
"passengers": { "adults": 1 }
}'

Available API Endpoints

Discover all available endpoints through our API discovery:

curl https://booking-api.safiri.app/.well-known/api-discovery

Error Handling

Common JWT Errors

ErrorCauseSolution
TokenExpiredErrorToken has expiredLogin again to get a new token
JsonWebTokenErrorInvalid token formatCheck token is complete and unmodified
SignatureVerificationErrorKey mismatchRefresh JWKS cache and retry

Token Expiration

Tokens expire after 7 days. Implement token refresh logic:

async function callApiWithRefresh(apiCall, credentials) {
try {
return await apiCall();
} catch (error) {
if (error.name === 'TokenExpiredError') {
// Re-authenticate and retry
const newToken = await authenticate(credentials.email, credentials.password);
return await apiCall(newToken);
}
throw error;
}
}

Security Best Practices

  1. Never expose tokens in URLs - Always use headers
  2. Store tokens securely - Use secure storage mechanisms
  3. Validate tokens server-side - Don't trust client-only validation
  4. Implement token refresh - Handle expiration gracefully
  5. Use HTTPS only - Never transmit tokens over unencrypted connections
  6. Cache JWKS responsibly - Balance performance with key rotation needs

Next Steps