Documentation

Everything you need to integrate AuthKnox.

AuthKnox is in early access. These docs cover the token, JWKS, and introspection endpoints. SDK clients and advanced guides are coming soon.

Your first token in three steps

AuthKnox implements the standard OAuth 2.0 Client Credentials flow. No SDKs required — any HTTP client works.

Step 01
Create an application

Register your service in the AuthKnox dashboard. You’ll receive a client_id and a client_secret. Store the secret securely — it’s only shown once.

Dashboard → Applications → New
# You'll receive:
client_id     = "ak_eu_01HWXYZ..."
client_secret = "cs_live_..."   # store in secrets manager, never in code
Step 02
Request a token

POST to the token endpoint with your credentials and an audience. AuthKnox returns a signed JWT using RS256. The private key is HSM-backed in Cloud KMS and never leaves the security boundary.

curl -X POST https://auth.authknox.com/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type":    "client_credentials",
    "client_id":     "ak_01HWXYZ...",
    "client_secret": "cs_live_...",
    "audience":      "https://api.yourdomain.com"
  }'
const res = await fetch('https://auth.authknox.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    grant_type:    'client_credentials',
    client_id:     'ak_01HWXYZ...',
    client_secret: 'cs_live_...',
    audience:      'https://api.yourdomain.com',
  }),
});
const { access_token } = await res.json();
import requests

res = requests.post(
    'https://auth.authknox.com/token',
    json={
        'grant_type':    'client_credentials',
        'client_id':     'ak_01HWXYZ...',
        'client_secret': 'cs_live_...',
        'audience':      'https://api.yourdomain.com',
    }
)
access_token = res.json()['access_token']
import (
    "bytes"
    "encoding/json"
    "net/http"
)

body, _ := json.Marshal(map[string]string{
    "grant_type":    "client_credentials",
    "client_id":     "ak_01HWXYZ...",
    "client_secret": "cs_live_...",
    "audience":      "https://api.yourdomain.com",
})
resp, err := http.Post(
    "https://auth.authknox.com/token",
    "application/json",
    bytes.NewBuffer(body),
)
Step 03
Use and verify the token

Attach the token as a Bearer in the Authorization header of every outgoing request. Verify it offline — check the signature via JWKS, then validate iss, aud, and exp. No round-trip to AuthKnox required.

200 OK — token response
// Content-Type: application/json
{
  "access_token": "eyJhbGci...",
  "token_type":   "Bearer",
  "expires_in":   3600,
  "scope":        "read write"
}
JWT header (decoded)
{
  "alg": "RS256",
  "kid": "authknox-eu-2024-1",
  "typ": "JWT"
}

// kid selects the right key from
// JWKS — enables key rotation
// without breaking existing tokens
JWT payload (decoded)
{
  "iss": "https://auth.authknox.com",
  "sub": "ak_eu_01HWXYZ...",
  "aud": "https://api.yourdomain.com",
  "iat": 1716892800,
  "exp": 1716896400,
  "jti": "01JWABCDEF...",
  "scope": "read write"
}
Using the token
// Attach the token to every outgoing request
const response = await fetch('https://api.yourdomain.com/data', {
  headers: {
    Authorization: `Bearer ${access_token}`,
  }
});
curl https://api.yourdomain.com/data \
  -H "Authorization: Bearer eyJhbGci..."
Verifying the token (on your API server)
import { createRemoteJWKSet, jwtVerify } from 'jose';

// Instantiate once at startup — jose caches the key set automatically
const JWKS = createRemoteJWKSet(
  new URL('https://auth.authknox.com/.well-known/jwks.json')
);

// Verify on every inbound request
const { payload } = await jwtVerify(token, JWKS, {
  issuer:     'https://auth.authknox.com',
  audience:   'https://api.yourdomain.com',
  algorithms: ['RS256'], // reject alg:none and unexpected algorithms (RFC 8725)
  // exp, nbf, iat validated automatically
});
// payload.sub === client_id of the caller
import jwt
from jwt import PyJWKClient

# Instantiate once at startup — PyJWKClient caches the key set
jwks_client = PyJWKClient(
    'https://auth.authknox.com/.well-known/jwks.json'
)

# Verify on every inbound request
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
    token,
    signing_key.key,
    algorithms=['RS256'],
    audience='https://api.yourdomain.com',
    issuer='https://auth.authknox.com',
    # exp validated automatically
)
# payload['sub'] == client_id of the caller
import (
    "fmt"
    "github.com/golang-jwt/jwt/v5"
    "github.com/lestrrat-go/jwx/v2/jwk"
)

// Fetch JWKS once at startup (use jwk.NewCache for auto-refresh in production)
keySet, _ := jwk.Fetch(ctx,
    "https://auth.authknox.com/.well-known/jwks.json",
)

// Verify on every inbound request
token, err := jwt.Parse(tokenString,
    func(t *jwt.Token) (interface{}, error) {
        // Reject non-RSA algorithms — prevents algorithm confusion attacks (RFC 8725)
        if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
        }
        kid, _ := t.Header["kid"].(string)
        key, found := keySet.LookupKeyID(kid)
        if !found {
            return nil, fmt.Errorf("unknown key id: %s", kid)
        }
        var pub interface{}
        if err := key.Raw(&pub); err != nil {
            return nil, err
        }
        return pub, nil
    },
    jwt.WithIssuer("https://auth.authknox.com"),
    jwt.WithAudience("https://api.yourdomain.com"),
    jwt.WithExpirationRequired(),
)
// token.Claims.(jwt.MapClaims)["sub"] == client_id of the caller

API endpoints

All endpoints are served from the canonical base URL.

POST https://auth.authknox.com/token Issue a signed JWT
GET https://auth.authknox.com/.well-known/jwks.json Public signing keys
POST https://auth.authknox.com/introspect Validate a token (RFC 7662)
POST /token — request
Content-Type header application/json or application/x-www-form-urlencoded
grant_type required Must be client_credentials
client_id required Your application’s client ID
client_secret required Your application’s client secret. Transmitted over TLS — never log this value.
audience required The intended resource server. Becomes the aud claim in the issued JWT. Any URI or short name (e.g. https://api.yourdomain.com or payments-api). Must be pre-registered in the dashboard — unknown values return invalid_request. See RFC 9068.
scope optional Space-separated list of requested scopes. Granted scopes are returned in the response and included as the scope claim in the JWT.
Error responses — RFC 6749 §5.2
400 invalid_request Missing or malformed request parameter
401 invalid_client Unknown client ID or wrong secret. Response includes WWW-Authenticate header.
400 unauthorized_client Client is not authorised to use the Client Credentials grant
400 unsupported_grant_type grant_type value is not client_credentials
400 invalid_scope Requested scope is unknown, malformed, or exceeds what the client is allowed to request

The audience parameter

AuthKnox requires an audience on every token request — it is not optional. The value maps directly to the aud claim in the issued JWT, binding the token to exactly one resource server. Resource servers check aud on every inbound request: a token issued for payments-api cannot be replayed against user-api. This follows RFC 9068 (JWT Profile for OAuth 2.0 Access Tokens).

How it works

  1. Register the audience in the dashboard before your first request. Use any unique string — a URI like https://api.yourdomain.com is conventional, or a short name like payments-api for internal services. Unrecognised values are rejected at issuance with invalid_request.
  2. Include audience on every token request. AuthKnox validates it against your registered list and embeds the value verbatim as the aud claim in the returned JWT.
  3. Validate aud on every inbound request. Your resource server must check that aud matches its own identifier before trusting the token. A JWT library configured to accept any audience breaks the security model.
Naming convention: use a URI for public-facing services (https://api.yourdomain.com) — self-documenting and consistent with RFC 9068. A short name (payments-api) is fine for internal services. AuthKnox treats the value as opaque: it only validates that it is registered, then includes it verbatim in the JWT.
request-token.sh cURL
curl -X POST https://auth.authknox.com/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type":    "client_credentials",
    "client_id":     "ak_01HWXYZ...",
    "client_secret": "cs_live_...",
    "audience":      "https://api.yourdomain.com"
  }'
JWT payload (decoded) JSON
{
  "iss": "https://auth.authknox.com",
  "sub": "ak_eu_01HWXYZ...",       // client_id of the caller
  "aud": "https://api.yourdomain.com",  // mirrors the audience param
  "iat": 1716892800,
  "exp": 1716896400,
  "jti": "01JWABCDEF..."
}

Validating aud on your resource server

Pass the expected audience to your JWT library — it handles the comparison automatically. Never skip this check: without it, a token issued for any other service would be silently accepted.

middleware.js Node.js / jose
import { createRemoteJWKSet, jwtVerify } from 'jose';

// Initialise once at startup — jose caches the key set automatically
const JWKS = createRemoteJWKSet(
  new URL('https://auth.authknox.com/.well-known/jwks.json')
);

// Validate on every inbound request
const { payload } = await jwtVerify(token, JWKS, {
  issuer:     'https://auth.authknox.com',
  audience:   'https://api.yourdomain.com',  // must match aud in the JWT
  algorithms: ['RS256'],
});
// payload.sub === client_id of the caller
middleware.go Go / golang-jwt
import (
    "fmt"
    "github.com/golang-jwt/jwt/v5"
)

// Validate on every inbound request (keySet fetched once at startup via jwk.Fetch)
parsed, err := jwt.Parse(tokenString,
    func(t *jwt.Token) (interface{}, error) {
        if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("unexpected alg: %v", t.Header["alg"])
        }
        kid, _ := t.Header["kid"].(string)
        key, found := keySet.LookupKeyID(kid)
        if !found {
            return nil, fmt.Errorf("unknown kid: %s", kid)
        }
        var pub interface{}
        if err := key.Raw(&pub); err != nil {
            return nil, err
        }
        return pub, nil
    },
    jwt.WithIssuer("https://auth.authknox.com"),
    jwt.WithAudience("https://api.yourdomain.com"),  // must match aud in the JWT
    jwt.WithExpirationRequired(),
)
// parsed.Claims.(jwt.MapClaims)["sub"] == client_id of the caller

Key concepts

RFC 6749 §4.4

Client Credentials grant

The OAuth 2.0 grant for machine-to-machine auth. No user is involved — a service proves its identity with a client_id and client_secret and receives an access token directly. No refresh tokens are issued for this grant type. Use it whenever one backend service calls another.

RFC 7519 + RFC 7518

JSON Web Tokens

A JWT is three Base64url-encoded parts separated by dots: header.payload.signature. AuthKnox signs with RS256 (RSASSA-PKCS1-v1_5 + SHA-256). The private key is HSM-protected in Cloud KMS and never enters application memory. The kid header claim maps to a key in JWKS, enabling zero-downtime key rotation.

RFC 7517 + RFC 7519 §7.2

Token verification

Fetch the public keys once from the JWKS endpoint and verify locally on every request. A correct verification checks all of: cryptographic signature (via kid), iss (must equal your region’s issuer), aud (must equal your API’s identifier), and exp (must be in the future). Skipping any one of these breaks the security model.

RFC 9068 §2.2

Audience scoping

Every token request requires an audience parameter. It maps directly to the aud claim in the issued JWT, binding the token to one specific resource server. A token for payments-api cannot be replayed against user-api — resource servers reject mismatched aud values. Audience values must be pre-registered in the dashboard; unrecognized values are rejected at issuance with invalid_request.

Token caching

Tokens are valid for expires_in seconds (3600 by default). Request a new token only when the current one is about to expire — not on every API call. Hammering the token endpoint wastes latency, risks rate limiting, and is unnecessary.

let cachedToken = null;
let tokenExpiresAt = 0;

async function getAccessToken() {
  // Re-use the cached token if it has more than 60 s of life left
  if (cachedToken && Date.now() < tokenExpiresAt - 60_000) {
    return cachedToken;
  }
  const res = await fetch('https://auth.authknox.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type:    'client_credentials',
      client_id:     process.env.AUTHKNOX_CLIENT_ID,
      client_secret: process.env.AUTHKNOX_CLIENT_SECRET,
    })
  });
  if (!res.ok) throw new Error(`Token request failed: ${res.status}`);
  const { access_token, expires_in } = await res.json();
  cachedToken    = access_token;
  tokenExpiresAt = Date.now() + expires_in * 1000;
  return cachedToken;
}
import os, time, requests

_token    = None
_expires  = 0

def get_access_token():
    global _token, _expires
    # Re-use if more than 60 s of life remains
    if _token and time.time() < _expires - 60:
        return _token
    res = requests.post(
        'https://auth.authknox.com/token',
        data={
            'grant_type':    'client_credentials',
            'client_id':     os.environ['AUTHKNOX_CLIENT_ID'],
            'client_secret': os.environ['AUTHKNOX_CLIENT_SECRET'],
        },
        timeout=5,
    )
    res.raise_for_status()
    data     = res.json()
    _token   = data['access_token']
    _expires = time.time() + data['expires_in']
    return _token
type TokenCache struct {
    mu        sync.Mutex
    token     string
    expiresAt time.Time
}

func (c *TokenCache) Get(ctx context.Context) (string, error) {
    c.mu.Lock()
    defer c.mu.Unlock()

    // Re-use if more than 60 s of life remains
    if c.token != "" && time.Until(c.expiresAt) > 60*time.Second {
        return c.token, nil
    }
    resp, err := http.PostForm("https://auth.authknox.com/token",
        url.Values{
            "grant_type":    {"client_credentials"},
            "client_id":     {os.Getenv("AUTHKNOX_CLIENT_ID")},
            "client_secret": {os.Getenv("AUTHKNOX_CLIENT_SECRET")},
        })
    // ... parse response, set c.token and c.expiresAt
}

Read credentials from environment variables or a secrets manager — never hardcode them.

Ready to get your first token?

Free to start. No credit card required. Your first 10,000 tokens are on us.