Integrations
Backend Token Verification

Backend Token Verification

Your backend receives Authorization: Bearer <JWT> from clients. Verify it locally against the JWKS — no network call to Camel Accounts per request, no introspection endpoint involved.

The checklist

  1. Signature is a valid RS256 signature for the key matching the header's kid in /oauth/jwks
  2. iss == "https://accounts.camelcreatives.com"
  3. Your client_idaud
  4. exp > now (small leeway ok)
  5. Endpoint-level authorization based on the scope claim

Go

Using golang-jwt/v5 + keyfunc (JWKS handling + caching + kid routing in one):

package auth
 
import (
	"errors"
	"strings"
	"time"
 
	"github.com/MicahParks/keyfunc/v3"
	"github.com/golang-jwt/jwt/v5"
)
 
var errUnauthorized = errors.New("invalid access token")
 
const issuer = "https://accounts.camelcreatives.com"
 
// NewVerifier fetches /oauth/jwks once and refreshes it automatically,
// routing keys by the token header's kid.
func NewVerifier() (keyfunc.Keyfunc, error) {
	return keyfunc.NewDefault([]string{issuer + "/oauth/jwks"})
}
 
type Identity struct {
	UserID string   // sub claim
	Scopes []string // parsed from the scope claim
}
 
func Verify(kf keyfunc.Keyfunc, audience, raw string) (Identity, error) {
	tok, err := jwt.ParseWithClaims(raw, jwt.MapClaims{}, kf.Keyfunc,
		jwt.WithValidMethods([]string{"RS256"}), // never accept anything else
		jwt.WithIssuer(issuer),
		jwt.WithAudience(audience),
		jwt.WithLeeway(30*time.Second),
	)
	if err != nil || !tok.Valid {
		return Identity{}, errUnauthorized
	}
 
	claims := tok.Claims.(jwt.MapClaims)
	sub, _ := claims["sub"].(string)
	scopeStr, _ := claims["scope"].(string)
	return Identity{UserID: sub, Scopes: strings.Fields(scopeStr)}, nil
}

Minimal inline version of the same idea:

kf, _ := keyfunc.NewDefault([]string{
	"https://accounts.camelcreatives.com/oauth/jwks",
})
 
tok, err := jwt.ParseWithClaims(raw, jwt.MapClaims{}, kf.Keyfunc,
	jwt.WithValidMethods([]string{"RS256"}),
	jwt.WithIssuer("https://accounts.camelcreatives.com"),
	jwt.WithAudience("bajeti-web-x7k2p9"),
	jwt.WithLeeway(30*time.Second),
)
if err != nil || !tok.Valid {
	http.Error(w, "unauthorized", http.StatusUnauthorized)
	return
}
claims := tok.Claims.(jwt.MapClaims)
userID := claims["sub"].(string)
scope := claims["scope"].(string) // space-separated — check before acting

Middleware wiring:

kf, _ := auth.NewVerifier()
 
func requireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		raw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
		id, err := auth.Verify(kf, "bajeti-web-x7k2p9", raw)
		if err != nil {
			http.Error(w, "unauthorized", 401)
			return
		}
		ctx := context.WithValue(r.Context(), identityKey{}, id)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

Python

import jwt
from jwt import PyJWKClient
 
JWKS_URL = "https://accounts.camelcreatives.com/oauth/jwks"
ISSUER   = "https://accounts.camelcreatives.com"
AUDIENCE = "your-app-web"
 
jwks_client = PyJWKClient(JWKS_URL, cache_keys=True, lifespan=300)
 
def verify(token: str) -> dict:
    signing_key = jwks_client.get_signing_key_from_jwt(token)  # routes by kid
    return jwt.decode(
        token,
        signing_key.key,
        algorithms=["RS256"],
        issuer=ISSUER,
        audience=AUDIENCE,
        leeway=30,
    )
 
# Flask example
@app.route("/api/orders")
def orders():
    auth = request.headers.get("Authorization", "")
    if not auth.startswith("Bearer "):
        abort(401)
    try:
        claims = verify(auth.removeprefix("Bearer "))
    except jwt.PyJWTError:
        abort(401)
    user_id = claims["sub"]
    ...

Node.js

import { createRemoteJWKSet, jwtVerify } from "jose";
 
const JWKS = createRemoteJWKSet(
  new URL("https://accounts.camelcreatives.com/oauth/jwks")
);
 
export async function verify(token: string) {
  const { payload } = await jwtVerify(token, JWKS, {
    algorithms: ["RS256"],
    issuer: "https://accounts.camelcreatives.com",
    audience: "your-app-web",
    clockTolerance: 30,
  });
  return payload; // sub, scope, client_id, exp…
}
// Express middleware
app.use("/api", async (req, res, next) => {
  const raw = req.headers.authorization?.replace(/^Bearer /, "");
  if (!raw) return res.sendStatus(401);
  try {
    req.identity = await verify(raw);
    next();
  } catch {
    res.sendStatus(401);
  }
});

Scope enforcement example

The scope claim is space-separated; check it per endpoint:

func hasScope(identity Identity, want string) bool {
	return slices.Contains(identity.Scopes, want)
}
def has_scope(scope: str, want: str) -> bool:
    return want in scope.split()

Handling revocation

Verification alone can't see mid-life revocations (a revoked token still verifies cryptographically until exp). Options:

  • Accept ≤15 min staleness — fine for most apps; revocation (connection revoke, client delete, theft detection) still kills refresh capability instantly.
  • Strict mode — on sensitive endpoints, also call GET /oauth/userinfo with the bearer token: a 401 there means the token was revoked. Cache positives briefly.
curl -s "$ISSUER/oauth/userinfo" -H "Authorization: Bearer $TOKEN" -o /dev/null -w "%{http_code}\n"

Common failure causes

ErrorLikely cause
kid not foundKey rotated while your JWKS cache was stale → force re-fetch and retry once
aud mismatchToken issued for another client — expected behavior, check which client_id you're verifying
exp failures on every requestServer clocks skewed > leeway
Signature invalid, alwaysSomeone accepted alg: none or HS256 confusion — pin RS256, as all snippets above do