Documentation
Everything you need to integrate AuthKnox.
Your first token in three steps
AuthKnox implements the standard OAuth 2.0 Client Credentials flow. No SDKs required — any HTTP client works.
# You'll receive: client_id = "ak_eu_01HWXYZ..." client_secret = "cs_live_..." # store in secrets manager, never in code
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), )
// Content-Type: application/json { "access_token": "eyJhbGci...", "token_type": "Bearer", "expires_in": 3600, "scope": "read write" }
{
"alg": "RS256",
"kid": "authknox-eu-2024-1",
"typ": "JWT"
}
// kid selects the right key from
// JWKS — enables key rotation
// without breaking existing tokens
{
"iss": "https://auth.authknox.com",
"sub": "ak_eu_01HWXYZ...",
"aud": "https://api.yourdomain.com",
"iat": 1716892800,
"exp": 1716896400,
"jti": "01JWABCDEF...",
"scope": "read write"
}
// 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..."
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.
| 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. |
| 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
-
Register the audience in the dashboard before your first request.
Use any unique string — a URI like
https://api.yourdomain.comis conventional, or a short name likepayments-apifor internal services. Unrecognised values are rejected at issuance withinvalid_request. -
Include
audienceon every token request. AuthKnox validates it against your registered list and embeds the value verbatim as theaudclaim in the returned JWT. -
Validate
audon every inbound request. Your resource server must check thataudmatches its own identifier before trusting the token. A JWT library configured to accept any audience breaks the security model.
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.
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" }'
{
"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.
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
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
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.
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.
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.
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.