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-dev.safiri.app/.well-known/api-discovery
Response includes:
{
"authentication": {
"type": "Bearer",
"jwks_uri": "https://booking-api-dev.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-dev.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-dev.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, {
originLocationCode: 'JRO',
destinationLocationCode: 'DAR',
departureDate: '2027-02-15',
adults: 1,
currencyCode: 'USD'
});
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-dev.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, {
"originLocationCode": "JRO",
"destinationLocationCode": "DAR",
"departureDate": "2027-02-15",
"adults": 1,
"currencyCode": "USD"
})
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-dev.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, [
'originLocationCode' => 'JRO',
'destinationLocationCode' => 'DAR',
'departureDate' => '2027-02-15',
'adults' => 1,
'currencyCode' => 'USD'
]);
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-dev.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(
"originLocationCode", "JRO",
"destinationLocationCode", "DAR",
"departureDate", "2027-02-15",
"adults", 1,
"currencyCode", "USD"
);
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-dev.safiri.app/api/flight/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-d '{
"originLocationCode": "JRO",
"destinationLocationCode": "DAR",
"departureDate": "2027-02-15",
"adults": 1,
"currencyCode": "USD"
}'
Available API Endpoints
Discover all available endpoints through our API discovery:
curl https://booking-api-dev.safiri.app/.well-known/api-discovery
Error Handling
Common JWT Errors
| Error | Cause | Solution |
|---|---|---|
| Unauthorized response | Token is missing or no longer accepted | Log in 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 validity
Tokens issued by POST /api/logon currently do not include an expiry. Keep the
token secure and be prepared to log in again if an API response says the token
is invalid or unauthorized. Do not place the token in a URL or application log.
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
- Handle rejected tokens - Log in again when the API no longer accepts a token
- 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