JWKS & Discovery

ZB ID exposes standard OpenID Connect discovery endpoints that allow services to verify JWT tokens locally. By fetching the public key from the JWKS endpoint, your service can validate token signatures without making a network call to ZB ID on every request.


GET/.well-known/jwks.json

JWKS endpoint

The JSON Web Key Set (JWKS) endpoint returns the RSA public key that ZB ID uses to sign JWT tokens. Services use this key to verify token signatures locally.

The response follows the standard JWKS format defined in RFC 7517. Each key in the keys array includes the following fields:

  • Name
    kty
    Type
    string
    Description

    Key type. Always RSA for ZB ID tokens.

  • Name
    use
    Type
    string
    Description

    Key usage. Always sig (signature verification).

  • Name
    kid
    Type
    string
    Description

    Key identifier. Use this to match the kid header in incoming JWTs to the correct public key.

  • Name
    alg
    Type
    string
    Description

    Algorithm. Always RS256 for ZB ID tokens.

  • Name
    n
    Type
    string
    Description

    The RSA modulus, Base64url-encoded.

  • Name
    e
    Type
    string
    Description

    The RSA public exponent, Base64url-encoded.

Caching

The JWKS response should be cached by your service. Refresh the cache when you encounter a JWT with a kid that does not match any key in your cached set, or on a regular interval (every 24 hours is a reasonable default). ZB ID rotates keys periodically, but the previous key remains valid during a transition window to avoid breaking existing tokens.

Request

GET
/.well-known/jwks.json
curl https://id.zb.co.zw/.well-known/jwks.json

Response

{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "kid": "zb-id-2026-01",
      "alg": "RS256",
      "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw...",
      "e": "AQAB"
    }
  ]
}
GET/.well-known/jwks.jsonTry it

Fetch the live JWKS

Public endpoint, no token needed. Returns the current signing keys used to verify ZB ID access tokens on staging.

Runs against the ZB ID STAGING sandbox (id-staging.zb.co.zw). Register a throwaway test account; never use real credentials.

Query parameters

GET/.well-known/openid-configuration

OpenID discovery

The OpenID Connect discovery endpoint returns metadata about the ZB ID authorization server. Clients and services use this document to discover endpoint URLs, supported grant types, and other configuration details automatically.

The URLs in the discovery document all sit on the production base URL (https://id.zb.co.zw), which is also the issuer identifier carried in every token's iss claim. The staging environment issues tokens with iss https://id-staging.zb.co.zw.

Response fields

  • Name
    issuer
    Type
    string
    Description

    The issuer identifier of the ZB ID authorization server (https://id.zb.co.zw). Tokens issued by ZB ID carry this value in their iss claim, so verify the iss claim against it.

  • Name
    jwks_uri
    Type
    string
    Description

    URL of the JWKS endpoint for retrieving public keys.

  • Name
    token_endpoint
    Type
    string
    Description

    URL for obtaining tokens via the OAuth token grant flow.

  • Name
    introspection_endpoint
    Type
    string
    Description

    URL for validating and inspecting tokens.

  • Name
    grant_types_supported
    Type
    array
    Description

    The OAuth grant types supported by ZB ID. Only client_credentials is advertised.

  • Name
    scopes_supported
    Type
    array
    Description

    List of scopes that can be requested during token issuance.

  • Name
    response_types_supported
    Type
    array
    Description

    Supported OAuth response types.

  • Name
    subject_types_supported
    Type
    array
    Description

    Subject identifier types supported.

  • Name
    id_token_signing_alg_values_supported
    Type
    array
    Description

    Signing algorithms supported for ID tokens and JWTs.

Request

GET
/.well-known/openid-configuration
curl https://id.zb.co.zw/.well-known/openid-configuration

Response

{
  "issuer": "https://id.zb.co.zw",
  "jwks_uri": "https://id.zb.co.zw/.well-known/jwks.json",
  "token_endpoint": "https://id.zb.co.zw/oauth/token",
  "introspection_endpoint": "https://id.zb.co.zw/oauth/introspect",
  "grant_types_supported": [
    "client_credentials"
  ],
  "response_types_supported": [
    "token"
  ],
  "subject_types_supported": [
    "public"
  ],
  "id_token_signing_alg_values_supported": [
    "RS256"
  ],
  "scopes_supported": [
    "openid",
    "profile",
    "banking:read",
    "banking:write",
    "lending:read",
    "lending:write",
    "insurance:read",
    "insurance:write",
    "wealth:read",
    "wealth:write",
    "admin:users",
    "admin:roles",
    "admin:clients"
  ]
}

Token verification guide

Local JWT verification is the most efficient way to validate tokens. Instead of calling the /oauth/introspect endpoint on every request, your service can verify the token signature using the public key from the JWKS endpoint.

Verification steps

  1. Fetch the JWKS from /.well-known/jwks.json and cache the keys.
  2. Decode the JWT header (without verifying) to extract the kid (key ID).
  3. Find the matching key in your cached JWKS by kid.
  4. Verify the signature using the RSA public key and the RS256 algorithm.
  5. Validate the claims: check exp (not expired), iss (matches ZB ID issuer), and scopes (has the required permissions).

When to use local verification vs introspection

ScenarioRecommended approach
High-traffic endpointsLocal verification (avoids network overhead)
Real-time token revocation neededIntrospection (checks current status)
Stateless microservicesLocal verification (no external dependency)
Admin or sensitive operationsIntrospection (confirms token is not revoked)
Development and debuggingIntrospection (returns full decoded claims)

For most production workloads, local verification with periodic JWKS refresh provides the best balance of performance and security. Use introspection as a fallback or for operations where you need to confirm the token has not been revoked since issuance.

Python (PyJWT + jwcrypto)

import jwt
import requests
from jwt import PyJWKClient

ZB_ID_URL = 'https://id.zb.co.zw'
ZB_ID_ISSUER = ZB_ID_URL
JWKS_URL = f'{ZB_ID_URL}/.well-known/jwks.json'

# Create a JWKS client (caches keys automatically)
jwks_client = PyJWKClient(JWKS_URL)

def verify_token(token: str) -> dict:
    """Verify a ZB ID JWT and return its claims."""
    # Get the signing key from JWKS
    signing_key = jwks_client.get_signing_key_from_jwt(token)

    # Decode and verify the token
    claims = jwt.decode(
        token,
        signing_key.key,
        algorithms=['RS256'],
        issuer=ZB_ID_ISSUER,
        options={
            'verify_exp': True,
            'verify_iss': True,
            'verify_aud': False,
        }
    )

    return claims

def require_scope(token: str, scope: str) -> dict:
    """Verify a token and check for a required scope."""
    claims = verify_token(token)
    scopes = claims.get('scopes', [])

    if scope not in scopes:
        raise PermissionError(
            f'Token missing required scope: {scope}'
        )

    return claims

# Usage
try:
    claims = require_scope(token, 'banking:read')
    user_id = claims['sub']
    kyc_tier = claims.get('kycTier')
except jwt.ExpiredSignatureError:
    print('Token has expired')
except jwt.InvalidTokenError as e:
    print(f'Invalid token: {e}')
except PermissionError as e:
    print(f'Access denied: {e}')

Node.js (jose)

import * as jose from 'jose'

const ZB_ID_URL =
  'https://id.zb.co.zw'
const ZB_ID_ISSUER = ZB_ID_URL
const JWKS_URL =
  `${ZB_ID_URL}/.well-known/jwks.json`

// Create a remote JWKS set (caches keys)
const JWKS = jose.createRemoteJWKSet(
  new URL(JWKS_URL)
)

async function verifyToken(token) {
  const { payload } = await jose.jwtVerify(
    token,
    JWKS,
    {
      issuer: ZB_ID_ISSUER,
      algorithms: ['RS256'],
    }
  )
  return payload
}

async function requireScope(token, scope) {
  const claims = await verifyToken(token)
  const scopes = claims.scopes || []

  if (!scopes.includes(scope)) {
    throw new Error(
      `Token missing required scope: ${scope}`
    )
  }

  return claims
}

// Usage in Express
async function authMiddleware(scope) {
  return async (req, res, next) => {
    const token = req.headers.authorization
      ?.replace('Bearer ', '')

    if (!token) {
      return res.status(401).json({
        result: 'failed',
        message: 'No token provided',
      })
    }

    try {
      const claims = await requireScope(
        token,
        scope
      )
      req.userId = claims.sub
      req.scopes = claims.scopes
      req.kycTier = claims.kycTier
      next()
    } catch (err) {
      if (err.code === 'ERR_JWT_EXPIRED') {
        return res.status(401).json({
          result: 'failed',
          message: 'Token has expired',
        })
      }
      return res.status(403).json({
        result: 'failed',
        message: err.message,
      })
    }
  }
}

// Apply to routes
app.get(
  '/accounts/:id',
  await authMiddleware('banking:read'),
  (req, res) => {
    // req.userId and req.scopes are available
  }
)

Was this page helpful?