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
- Signature is a valid RS256 signature for the key matching the header's
kidin/oauth/jwks iss == "https://accounts.camelcreatives.com"- Your
client_id∈aud exp > now(small leeway ok)- Endpoint-level authorization based on the
scopeclaim
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 actingMiddleware 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/userinfowith the bearer token: a401there 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
| Error | Likely cause |
|---|---|
kid not found | Key rotated while your JWKS cache was stale → force re-fetch and retry once |
aud mismatch | Token issued for another client — expected behavior, check which client_id you're verifying |
exp failures on every request | Server clocks skewed > leeway |
| Signature invalid, always | Someone accepted alg: none or HS256 confusion — pin RS256, as all snippets above do |