Appearance
Public Keys
The Identity service provides public keys for verifying JWT token signatures. This enables services to validate tokens locally without making network calls to the Identity service.
Endpoint
GET /api/public-keysThis endpoint is publicly accessible and does not require authentication. It is intentionally unversioned as a stable infrastructure endpoint.
Request
bash
curl https://identity.retailsuccessplatform.com/api/public-keysResponse
json
{
"keys": [
{
"kid": "key_2024_001",
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e": "AQAB"
}
]
}Response Fields
| Field | Type | Description |
|---|---|---|
keys | array | Array of JSON Web Keys (JWK) |
keys[].kid | string | Key ID - matches kid header in JWT |
keys[].kty | string | Key type - always RSA |
keys[].use | string | Key use - always sig (signature) |
keys[].alg | string | Algorithm - always RS256 |
keys[].n | string | RSA modulus (base64url encoded) |
keys[].e | string | RSA exponent (base64url encoded) |
Key Rotation
The Identity service periodically rotates signing keys for security:
- New key added - A new key is added to the array
- Both keys active - Both old and new keys are valid during transition
- Old key removed - After transition period, old key is removed
Cache Keys Appropriately
Cache public keys for 1 hour maximum. This ensures you pick up new keys during rotation while avoiding excessive requests.
Using Public Keys
JavaScript (jose library)
javascript
import * as jose from 'jose';
// Fetch and cache public keys
let cachedKeys = null;
let cacheExpiry = 0;
async function getPublicKeys() {
if (cachedKeys && Date.now() < cacheExpiry) {
return cachedKeys;
}
const response = await fetch('https://identity.retailsuccessplatform.com/api/public-keys');
const { keys } = await response.json();
cachedKeys = keys;
cacheExpiry = Date.now() + 3600000; // Cache for 1 hour
return keys;
}
// Verify a token
async function verifyToken(token) {
const keys = await getPublicKeys();
// Decode header to get key ID
const header = jose.decodeProtectedHeader(token);
const key = keys.find(k => k.kid === header.kid) || keys[0];
// Import the key
const publicKey = await jose.importJWK(key, 'RS256');
// Verify and decode
const { payload } = await jose.jwtVerify(token, publicKey, {
issuer: 'https://app.retailsuccessplatform.com',
});
return payload;
}
// Usage
try {
const claims = await verifyToken(accessToken);
console.log('Token valid, scopes:', claims.scopes);
} catch (error) {
console.error('Token verification failed:', error.message);
}PHP (firebase/php-jwt)
php
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
class TokenVerifier
{
private array $cachedKeys = [];
private int $cacheExpiry = 0;
public function getPublicKeys(): array
{
if ($this->cachedKeys && time() < $this->cacheExpiry) {
return $this->cachedKeys;
}
$response = file_get_contents('https://identity.retailsuccessplatform.com/api/public-keys');
$data = json_decode($response, true);
$this->cachedKeys = JWK::parseKeySet($data);
$this->cacheExpiry = time() + 3600; // Cache for 1 hour
return $this->cachedKeys;
}
public function verifyToken(string $token): object
{
$keys = $this->getPublicKeys();
return JWT::decode($token, $keys);
}
}
// Usage
$verifier = new TokenVerifier();
try {
$claims = $verifier->verifyToken($accessToken);
echo "Token valid, scopes: " . implode(', ', $claims->scopes);
} catch (Exception $e) {
echo "Token verification failed: " . $e->getMessage();
}Python (PyJWT)
python
import jwt
import requests
from datetime import datetime, timedelta
class TokenVerifier:
def __init__(self):
self.cached_keys = None
self.cache_expiry = None
def get_public_keys(self):
if self.cached_keys and datetime.now() < self.cache_expiry:
return self.cached_keys
response = requests.get('https://identity.retailsuccessplatform.com/api/public-keys')
data = response.json()
self.cached_keys = data['keys']
self.cache_expiry = datetime.now() + timedelta(hours=1)
return self.cached_keys
def verify_token(self, token: str) -> dict:
keys = self.get_public_keys()
# Get the key ID from the token header
header = jwt.get_unverified_header(token)
kid = header.get('kid')
# Find the matching key
key = next((k for k in keys if k['kid'] == kid), keys[0])
# Build the public key from JWK
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
# Verify and decode
return jwt.decode(
token,
public_key,
algorithms=['RS256'],
issuer='https://app.retailsuccessplatform.com'
)
# Usage
verifier = TokenVerifier()
try:
claims = verifier.verify_token(access_token)
print(f"Token valid, scopes: {claims['scopes']}")
except jwt.InvalidTokenError as e:
print(f"Token verification failed: {e}")Local Verification Benefits
Verifying tokens locally (using public keys) instead of calling the Identity service offers:
| Benefit | Description |
|---|---|
| No latency | No network round-trip to Identity service |
| No single point of failure | Works even if Identity service is temporarily unavailable |
| Scalability | Each service instance handles its own verification |
| Serverless friendly | Works in Lambda/serverless environments |
Get Individual Key
Retrieve a specific public key by its key ID. Useful when you already know the kid from a JWT header.
GET /api/public-keys/{keyId}This endpoint is publicly accessible and does not require authentication.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
keyId | string | The key ID (matches kid in JWT headers) |
Response (200 OK)
json
{
"key_id": "key_2024_001",
"public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgk...\n-----END PUBLIC KEY-----",
"algorithm": "RS256",
"active_from": "2024-01-01T00:00:00+00:00",
"expires_at": "2025-01-01T00:00:00+00:00",
"is_current": true
}Response Fields
| Field | Type | Description |
|---|---|---|
key_id | string | Key identifier |
public_key | string | PEM-encoded public key |
algorithm | string | Signing algorithm (always RS256) |
active_from | string | ISO 8601 timestamp when key became active |
expires_at | string | ISO 8601 expiration timestamp (nullable) |
is_current | boolean | Whether this is the current signing key |
Error Response (404)
json
{
"error": "Key not found or expired"
}JWKS Endpoint
Not Available
A standard /.well-known/jwks.json endpoint is not currently available for general JWT verification. Use the GET /api/public-keys endpoint above to retrieve keys in JWK format for token verification.
Caching Strategy
javascript
const KEY_CACHE_TTL = 3600000; // 1 hour in milliseconds
class PublicKeyCache {
constructor() {
this.keys = null;
this.expiry = 0;
this.refreshPromise = null;
}
async getKeys() {
// Return cached keys if valid
if (this.keys && Date.now() < this.expiry) {
return this.keys;
}
// Prevent multiple concurrent refreshes
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = this.fetchKeys();
const keys = await this.refreshPromise;
this.refreshPromise = null;
return keys;
}
async fetchKeys() {
const response = await fetch(
'https://identity.retailsuccessplatform.com/api/public-keys'
);
const { keys } = await response.json();
this.keys = keys;
this.expiry = Date.now() + KEY_CACHE_TTL;
return keys;
}
}Related
- JWT Structure - Understanding token contents
- OAuth Token Endpoint - How to obtain tokens
Changelog
| Date | Change |
|---|---|
| 2026-03-14 | Accuracy fixes and added missing content. |
| 2026-02-27 | Documentation corrections. |
| 2026-01-15 | Initial publication. |