agentsclimarketplace

Monocloud auth fastify

Skill monocloud/agent-skills/plugins/monocloud/skills/monocloud-auth-fastify

Official MonoCloud Agent Skills repository.

Install
npx -y skills add monocloud/agent-skills --skill monocloud-auth-fastify

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when integrating MonoCloud access-token validation into a Fastify API — installing or configuring `@monocloud/backend-node/fastify`, wiring the `protectApi()` `onRequest` hook factory, validating JWT or opaque (introspection) bearer tokens, enforcing scopes/groups, attaching `claims` to `request` via `AuthenticatedFastifyRequest`, or troubleshooting `MONOCLOUD_BACKEND_*` env vars / audience / JWKS / mTLS certificate binding.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

14.5 KB, ~3.3k tokens by cl100k_base, as published. Nobody here has run it

MonoCloud Fastify API protection (@monocloud/backend-node/fastify)

Backend SDK for validating MonoCloud-issued access tokens in Fastify APIs. Same engine as the Express adapter — handles JWT signature verification (via JWKS) and opaque-token introspection automatically based on token format.

Package identity — read this first

Use: @monocloud/backend-node with the /fastify subpath. This is a single npm package that also ships /express.

This is not the same SDK as @monocloud/auth-nextjs (frontend, user sessions) or @monocloud/auth-node-core (server-side auth flows). This package is purely for API protection — validating tokens issued elsewhere, not signing users in.

If you see these symbols, they belong to a different package or an older SDK — do not use them here:

  • @fastify/jwt, fastify-jwt, fastify-auth (other libraries)
  • fastify.register(monoCloudAuth) style plugin registration (this SDK exposes a per-route onRequest hook, not a Fastify plugin)
  • Importing from @monocloud/backend-node root for Fastify hooks (use the /fastify subpath)

Installation

npm install @monocloud/backend-node

Environment variables

Required:

VariablePurpose
MONOCLOUD_BACKEND_TENANT_DOMAINMonoCloud tenant URL, e.g. https://acme.us.monocloud.com
MONOCLOUD_BACKEND_AUDIENCEExpected audience claim, e.g. https://api.example.com

Required only when validating opaque tokens (or when MONOCLOUD_BACKEND_INTROSPECT_JWT_TOKENS=true):

VariablePurpose
MONOCLOUD_BACKEND_CLIENT_IDClient used to call the introspection endpoint
MONOCLOUD_BACKEND_CLIENT_SECRETClient secret
MONOCLOUD_BACKEND_CLIENT_AUTH_METHODOne of client_secret_basic, client_secret_post (default), client_secret_jwt, private_key_jwt, tls_client_auth, self_signed_tls_client_auth, spiffe_jwt, spiffe_x509
MONOCLOUD_BACKEND_TRUST_STORE_IDSelects a specific trust store's endpoints from mtls_additional_endpoint_aliases when the client authenticates to the introspection endpoint with a mutual-TLS method (tls_client_auth, self_signed_tls_client_auth, spiffe_x509). When omitted, the default mtls_endpoint_aliases are used.

Optional tuning:

VariableDefaultPurpose
MONOCLOUD_BACKEND_INTROSPECT_JWT_TOKENSfalseIf true, skip local JWT validation and always introspect
MONOCLOUD_BACKEND_CLOCK_SKEW0Allowed clock drift (seconds)
MONOCLOUD_BACKEND_CLOCK_TOLERANCE60Extra tolerance on time-based claims (seconds)
MONOCLOUD_BACKEND_GROUPS_CLAIMgroupsClaim name that carries group memberships
MONOCLOUD_BACKEND_GROUPS_MATCH_ALLfalseIf true, all listed groups must match
MONOCLOUD_BACKEND_JWKS_CACHE_DURATIONSeconds to cache the JWKS
MONOCLOUD_BACKEND_METADATA_CACHE_DURATIONSeconds to cache the OIDC discovery doc

Basic wiring

import Fastify from "fastify";
import {
  protectApi,
  type AuthenticatedFastifyRequest,
} from "@monocloud/backend-node/fastify";

const fastify = Fastify();

// Reads MONOCLOUD_BACKEND_* env vars. Build it once and reuse.
const protect = protectApi();

// Bare protection — any valid token works
fastify.get("/api/me", { onRequest: protect() }, async (request) => {
  const { claims } = request as AuthenticatedFastifyRequest;
  return { sub: claims.sub };
});

// Scope-gated
fastify.post(
  "/api/posts",
  { onRequest: protect({ scopes: ["posts:write"] }) },
  async (request, reply) => {
    reply.code(201);
  },
);

// Group-gated
fastify.delete(
  "/api/posts/:id",
  { onRequest: protect({ groups: ["admin"] }) },
  async (request, reply) => {
    reply.code(204);
  },
);

await fastify.listen({ port: 3000 });

Two-call pattern: protectApi() builds a factory once (parses env, loads JWKS lazily); calling the factory with options returns an onRequest hook. Build the factory at startup, attach the hook per-route.

What protect(options) accepts

options (all optional):

interface ProtectOptions {
  scopes?: string[]; // require all listed scopes
  groups?: string[]; // require group membership (any-of by default)
  validateCertificateBinding?: boolean; // mTLS-bound token validation
}
  • scopes: AND semantics — the token must carry every listed scope.
  • groups: OR by default; flip with MONOCLOUD_BACKEND_GROUPS_MATCH_ALL=true (or per-client groupOptions.matchAll). Claim name comes from MONOCLOUD_BACKEND_GROUPS_CLAIM.
  • validateCertificateBinding: enforces the cnf.x5t#S256 confirmation claim against the client's TLS cert. Requires a certificateResolver (see "Advanced" below).

Client constructor options

new MonoCloudBackendNodeClient(options) accepts the backend-node option shape. Use this when you need a shared client, non-env configuration, or a custom token-claims cache:

interface MonoCloudBackendNodeClientOptions {
  tenantDomain: string;
  audience: string;
  clientId?: string;
  clientSecret?: string | Jwk;         // for spiffe_jwt, pass the SPIFFE JWT-SVID string
  clientAuthMethod?: ClientAuthMethod;
  trustStoreId?: string;               // pick a trust store's mTLS endpoint aliases (mtls_additional_endpoint_aliases)
  metadataResolver?: () => IssuerMetadata | Promise<IssuerMetadata>; // supply issuer metadata out-of-band
  jwksResolver?: () => Jwks | Promise<Jwks>;                         // supply JWKS out-of-band
  groupOptions?: { groupsClaim?: string; matchAll?: boolean };
  clockSkew?: number;
  clockTolerance?: number;
  jwksCacheDuration?: number;
  metadataCacheDuration?: number;
  introspectJwtTokens?: boolean;
  cache?: IIntrospectionCache;
  fetcher?: typeof fetch;              // (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
}

cache?: IIntrospectionCache is constructor-only; pass it in code to cache introspection results by raw token until the token expires. Only tokens validated via introspection are cached (opaque tokens, and JWTs when introspectJwtTokens is true); locally-validated JWTs are not cached.

Default responses

  • No Authorization: Bearer <token> header (and no custom tokenResolver): 401 { "message": "unauthorized" }
  • Token validation fails (signature, audience, issuer, expiry, mismatched cnf, etc.): 401 { "message": "unauthorized" }
  • Token valid but missing required scopes or groups: 403 { "message": "forbidden" }

The hook calls reply.status(...).send(...) directly on failure — done() is not invoked. Customise responses by wrapping the hook or by calling MonoCloudBackendNodeClient.validateAccessToken() from your own onRequest.

Reading the validated claims

After the hook runs, request.claims is populated. Cast the request:

import type { AuthenticatedFastifyRequest } from "@monocloud/backend-node/fastify";

fastify.get("/api/me", { onRequest: protect() }, async (request) => {
  const { claims } = request as AuthenticatedFastifyRequest;
  return claims;
});

Alternatively, declare a module augmentation to avoid casting:

import type { AccessTokenClaims } from "@monocloud/backend-node";
declare module "fastify" {
  interface FastifyRequest {
    claims?: AccessTokenClaims;
  }
}

Applying to many routes — patterns

// Apply to every route on the instance
fastify.addHook("onRequest", protect());

// Per-encapsulated-context (Fastify plugins / prefixes)
fastify.register(async (instance) => {
  instance.addHook("onRequest", protect({ scopes: ["admin"] }));
  instance.get("/admin/users", async () => {
    /* ... */
  });
});

// Different options on different routes — just attach inline as in the basic example

fastify.addHook applies to every subsequent route in that encapsulation context, so registering it inside a plugin scopes it to that plugin's routes.

Advanced: shared client, custom resolvers, caching

import {
  protectApi,
  MonoCloudBackendNodeClient,
  type IIntrospectionCache,
} from "@monocloud/backend-node/fastify";

const client = new MonoCloudBackendNodeClient({
  tenantDomain: "https://acme.us.monocloud.com",
  audience: "https://api.example.com",
  cache: redisCache, // your IIntrospectionCache implementation — caches introspection results by token until exp
  introspectJwtTokens: false,
});

const protect = protectApi(client, {
  // Pull token from somewhere other than Authorization: Bearer
  tokenResolver: async (req) =>
    (req.cookies as Record<string, string>).access_token,
  // Provide the client cert for mTLS-bound tokens (use with validateCertificateBinding)
  certificateResolver: async (req) =>
    req.headers["x-client-cert"] as string | undefined,
});

fastify.get(
  "/api/secure",
  { onRequest: protect({ validateCertificateBinding: true }) },
  async (request) => (request as AuthenticatedFastifyRequest).claims,
);

IIntrospectionCache interface (implement for Redis, in-memory, etc.) — stores introspection results only:

interface IIntrospectionCache {
  get(token: string): Promise<AccessTokenClaims | null | undefined>;
  set(
    token: string,
    claims: AccessTokenClaims,
    expiresAt: number,
  ): Promise<void>;
}

Caching is keyed on the raw token string. The exact validity check is cached.exp > now() + clockSkew - clockTolerance, i.e. a cached entry stays valid until claims.exp + clockTolerance - clockSkew < now(). With the defaults (clockSkew: 0, clockTolerance: 60) the cache will keep returning a claim for up to ~60 seconds past the token's exp. Lower clockTolerance (e.g. to 0) for strict expiry; raise it for higher hit rates at the cost of accepting slightly-expired tokens.

JWT vs. introspection — how the SDK decides

  • Three dot-separated parts (xxx.yyy.zzz) and introspectJwtTokens is false (default): the SDK validates the JWT locally using JWKS fetched from the tenant. After JWKS warms, no network call per request.
  • Otherwise (opaque tokens, or introspectJwtTokens=true): the SDK calls the OIDC introspection endpoint. Requires clientId + clientSecret (or another clientAuthMethod).

JWT tokens don't require client credentials. Opaque tokens do. MonoCloudValidationError: The clientId option must be configured to introspect access tokens on an opaque-token request means you need to add the introspection env vars.

Common pitfalls

  1. Wrong import path. Import from @monocloud/backend-node/fastify, not the root. The root only exports the framework-agnostic MonoCloudBackendNodeClient.
  2. Attaching protect instead of protect() to onRequest. The factory returns a function — you must call it to get the hook. { onRequest: protect } is wrong; { onRequest: protect() } is right.
  3. Audience mismatch. MONOCLOUD_BACKEND_AUDIENCE must exactly match the aud claim. Trailing slashes and http/https differences fail validation.
  4. Building the factory per request. protectApi() is a startup-time call — invoking it inside a handler creates a new client per request.
  5. Calling done() or reply.send() after the hook failed. The hook sends its own 401/403 — if you wrap it, check reply.sent first.
  6. Cookies but no @fastify/cookie. If you use a tokenResolver that reads cookies, register @fastify/cookie first or request.cookies is undefined.
  7. Group claim missing. If groups is set but the token doesn't carry the configured groupsClaim, requests are forbidden. Configure it in the MonoCloud dashboard or via the env var.

Onboarding checklist

  1. npm install @monocloud/backend-node.
  2. Add MONOCLOUD_BACKEND_TENANT_DOMAIN and MONOCLOUD_BACKEND_AUDIENCE to your env. For opaque tokens, also MONOCLOUD_BACKEND_CLIENT_ID + _CLIENT_SECRET.
  3. Register an API (audience) in the MonoCloud dashboard matching MONOCLOUD_BACKEND_AUDIENCE.
  4. Build the factory once: const protect = protectApi();
  5. Attach per-route: fastify.get(path, { onRequest: protect({ scopes: [...] }) }, handler);
  6. Cast request to AuthenticatedFastifyRequest inside handlers to read claims.

Related types and errors

Re-exported from @monocloud/auth-core via @monocloud/backend-node:

  • AccessTokenClaims, JwtClaims, Jwk, Jwks, IssuerMetadata, ClientAuthMethod
  • MonoCloudAuthBaseError, MonoCloudValidationError, MonoCloudOPError, MonoCloudHttpError, MonoCloudTokenError

A failed scope/group check throws MonoCloudTokenError with the message 'Token is missing required scopes' or 'Token is missing required groups' — the hook converts these to 403. Other validation failures throw MonoCloudTokenError / MonoCloudValidationError and become 401.

Deeper reference

  • references/api-surface.md — every export from @monocloud/backend-node/fastify, full type signatures, env-var → option mapping, defaults.
  • references/troubleshooting.md — symptom → cause → fix index for the most common failure modes (audience mismatch, opaque-token introspection, scope/group claims, mTLS binding, onRequest vs plugin confusion, JWKS thrash).

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.