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