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:
- Login to obtain a JWT token
- Fetch JWKS to get the public keys
- Verify the token signature (optional but recommended)
- 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
| Property | Description |
|---|---|
kty | Key type (RSA) |
kid | Key ID - use this to match keys when verifying |
use | Key usage (sig for signature) |
alg | Algorithm (RS256) |
e | RSA public exponent |
n | RSA 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
| Error | Cause | Solution |
|---|---|---|
TokenExpiredError | Token has expired | Login again to get a new token |
JsonWebTokenError | Invalid token format | Check token is complete and unmodified |
SignatureVerificationError | Key mismatch | Refresh 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
- Never expose tokens in URLs - Always use headers
- Store tokens securely - Use secure storage mechanisms
- Validate tokens server-side - Don't trust client-only validation
- Implement token refresh - Handle expiration gracefully
- Use HTTPS only - Never transmit tokens over unencrypted connections
- Cache JWKS responsibly - Balance performance with key rotation needs
Next Steps
- Authentication Basics - Login and token usage
- Flight Booking API - Search and book flights
- Parcel Delivery API - Request deliveries
- Bus Booking API - Search bus trips