Skip to content

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-keys

This 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-keys

Response ​

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 ​

FieldTypeDescription
keysarrayArray of JSON Web Keys (JWK)
keys[].kidstringKey ID - matches kid header in JWT
keys[].ktystringKey type - always RSA
keys[].usestringKey use - always sig (signature)
keys[].algstringAlgorithm - always RS256
keys[].nstringRSA modulus (base64url encoded)
keys[].estringRSA exponent (base64url encoded)

Key Rotation ​

The Identity service periodically rotates signing keys for security:

  1. New key added - A new key is added to the array
  2. Both keys active - Both old and new keys are valid during transition
  3. 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:

BenefitDescription
No latencyNo network round-trip to Identity service
No single point of failureWorks even if Identity service is temporarily unavailable
ScalabilityEach service instance handles its own verification
Serverless friendlyWorks 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 ​

ParameterTypeDescription
keyIdstringThe 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 ​

FieldTypeDescription
key_idstringKey identifier
public_keystringPEM-encoded public key
algorithmstringSigning algorithm (always RS256)
active_fromstringISO 8601 timestamp when key became active
expires_atstringISO 8601 expiration timestamp (nullable)
is_currentbooleanWhether 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;
  }
}

Changelog
DateChange
2026-03-14Accuracy fixes and added missing content.
2026-02-27Documentation corrections.
2026-01-15Initial publication.

ShopHero CommerceCore Platform