SuperAPI AI Directory

adding-api-auth

Security & auditing

View repository

Updated 10-16 04:18

About this skill

Adding API Key Auth (Scalekit)

Guardrails

  • MUST read SCALEKIT_ENVIRONMENT_URL / SCALEKIT_CLIENT_ID / SCALEKIT_CLIENT_SECRET from env vars when initializing the client; MUST NOT hardcode them.
  • MUST validate the opaque token or JWT server-side on every incoming API request; MUST NOT trust an unverified token.
  • MUST treat the plain-text token / plain_secret as returned only once at creation and store it securely (never logged or committed); use token_id (not the key itself) for list/invalidate lifecycle operations.

Before coding — pick path and stack

1. Mechanism (do not mix code from both sections in one flow):

Path When to use Sections
A. Opaque API keys Long-lived keys for partners, CI, personal access tokens; validate via Scalekit token API §§1–6 + Key rules
B. OAuth 2.0 client credentials Short-lived JWTs, scope-based M2M, JWKS validation Client Credentials section only

If the user is unclear, ask once: opaque API keys vs client-credentials JWTs.

2. Language — detect from the repo (package.json → Node, requirements.txt/pyproject.toml → Python, go.mod → Go, pom.xml/build.gradle → Java). Implement only the matching language snippets; skip other language blocks.

3. Where to edit — put client init next to existing SDK setup (or a shared lib/scalekit module); put validation middleware on the API route layer that serves protected resources (Express/FastAPI/Gin/etc. middleware directory already used by the app). Prefer extending existing auth middleware over a parallel path.

Flow overview (Path A — opaque API keys)

App creates token (org or user scoped) → Scalekit returns key + tokenId once →
Store tokenId for lifecycle; customer stores the key → Client sends Authorization: Bearer <key> →
Server validates with Scalekit → Filter data by org/user context

The plain-text API key is returned only once at creation. Scalekit never stores it.


1. Initialize the client

# Python
from scalekit import ScalekitClient
import os

scalekit_client = ScalekitClient(
    env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
    client_id=os.environ["SCALEKIT_CLIENT_ID"],
    client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
)
// Node.js
import { ScalekitClient } from '@scalekit-sdk/node';

const scalekit = new ScalekitClient(
  process.env.SCALEKIT_ENVIRONMENT_URL,
  process.env.SCALEKIT_CLIENT_ID,
  process.env.SCALEKIT_CLIENT_SECRET
);
// Go
scalekitClient := scalekit.NewScalekitClient(
  os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
  os.Getenv("SCALEKIT_CLIENT_ID"),
  os.Getenv("SCALEKIT_CLIENT_SECRET"),
)
// Java
ScalekitClient scalekitClient = new ScalekitClient(
    System.getenv("SCALEKIT_ENVIRONMENT_URL"),
    System.getenv("SCALEKIT_CLIENT_ID"),
    System.getenv("SCALEKIT_CLIENT_SECRET")
);

Required env vars: SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, SCALEKIT_CLIENT_SECRET.


2. Create a token

Organization-scoped (default)

Grants access to all resources in the organization's workspace. Use for service-to-service integrations (CI/CD, partner integrations, internal tooling).

# Python
response = scalekit_client.tokens.create_token(
    organization_id=organization_id,
    description="CI/CD pipeline token",
)
opaque_token = response.token      # show to user once; never stored by Scalekit
token_id     = response.token_id   # format: apit_xxxxx — use for lifecycle ops
// Node.js
const response = await scalekit.token.createToken(organizationId, {
  description: 'CI/CD pipeline token',
});
const opaqueToken = response.token;
const tokenId     = response.tokenId;
// Go
response, err := scalekitClient.Token().CreateToken(
  ctx, organizationId, scalekit.CreateTokenOptions{
    Description: "CI/CD pipeline token",
  },
)
opaqueToken := response.Token
tokenId     := response.TokenId
// Java
CreateTokenResponse response = scalekitClient.tokens().create(organizationId);
String opaqueToken = response.getToken();
String tokenId     = response.getTokenId();

User-scoped (optional userId)

Adds user context so your API can filter data to only that user's resources (personal access tokens, per-user audit trails, user-level rate limiting). Attach customClaims for fine-grained authz without extra DB lookups.

# Python
response = scalekit_client.tokens.create_token(
    organization_id=organization_id,
    user_id="usr_12345",
    custom_claims={"team": "engineering", "environment": "production"},
    description="Deployment service token",
)
// Node.js
const response = await scalekit.token.createToken(organizationId, {
  userId: 'usr_12345',
  customClaims: { team: 'engineering', environment: 'production' },
  description: 'Deployment service token',
});
// Go
response, err := scalekitClient.Token().CreateToken(
  ctx, organizationId, scalekit.CreateTokenOptions{
    UserId:       "usr_12345",
    CustomClaims: map[string]string{"team": "engineering", "environment": "production"},
    Description:  "Deployment service token",
  },
)
// Java
Map<String, String> claims = Map.of("team", "engineering", "environment", "production");
CreateTokenResponse response = scalekitClient.tokens().create(
    organizationId, "usr_12345", claims, null, "Deployment service token"
);

Response fields:

Field Description
token Plain-text API key. Returned only at creation.
token_id Stable ID (apit_xxxxx) for list/invalidate operations.
token_info Metadata: org, user, custom claims, timestamps.

3. Validate a token

Call this on every incoming API request. Returns org/user context; throws on invalid, expired, or revoked keys.

# Python
from scalekit import ScalekitValidateTokenFailureException

try:
    result = scalekit_client.tokens.validate_token(token=opaque_token)
    org_id   = result.token_info.organization_id
    user_id  = result.token_info.user_id          # empty for org-scoped keys
    claims   = result.token_info.custom_claims
    roles    = result.token_info.roles             # populated if RBAC is configured
    ext_org  = result.token_info.organization_external_id
except ScalekitValidateTokenFailureException:
    return 401
// Node.js
import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node';

try {
  const result = await scalekit.token.validateToken(opaqueToken);
  const { organizationId, userId, customClaims, roles, organizationExternalId } = result.tokenInfo;
} catch (error) {
  if (error instanceof ScalekitValidateTokenFailureException) return res.status(401).end();
  throw error;
}
// Go
result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken)
if errors.Is(err, scalekit.ErrTokenValidationFailed) {
  c.JSON(401, gin.H{"error": "Invalid or expired token"})
  return
}
orgId  := result.TokenInfo.OrganizationId
userId := result.TokenInfo.GetUserId()       // *string — nil for org-scoped tokens
claims := result.TokenInfo.CustomClaims
// Java
try {
    ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken);
    String orgId  = result.getTokenInfo().getOrganizationId();
    String userId = result.getTokenInfo().getUserId();
    Map<String, String> claims = result.getTokenInfo().getCustomClaimsMap();
} catch (TokenInvalidException e) {
    response.sendError(401);
}

4. List tokens

Supports pagination and optional user filter.

# Python — list with pagination
response = scalekit_client.tokens.list_tokens(
    organization_id=organization_id,
    page_size=10,
)
for token in response.tokens:
    print(token.token_id, token.description)

if response.next_page_token:
    next_page = scalekit_client.tokens.list_tokens(
        organization_id=organization_id,
        page_size=10,
        page_token=response.next_page_token,
    )

# Filter by user
user_tokens = scalekit_client.tokens.list_tokens(
    organization_id=organization_id,
    user_id="usr_12345",
)
// Node.js
const response = await scalekit.token.listTokens(organizationId, { pageSize: 10 });
if (response.nextPageToken) {
  const next = await scalekit.token.listTokens(organizationId, {
    pageSize: 10, pageToken: response.nextPageToken
  });
}
const userTokens = await scalekit.token.listTokens(organizationId, { userId: 'usr_12345' });

5. Invalidate a token

Revocation is instant — the next validation for that key fails immediately. The operation is idempotent: safe to call on already-revoked keys.

# Python — by token string or token_id
scalekit_client.tokens.invalidate_token(token=opaque_token)
# or
scalekit_client.tokens.invalidate_token(token=token_id)
// Node.js
await scalekit.token.invalidateToken(opaqueToken);  // or tokenId
// Go
_ = scalekitClient.Token().InvalidateToken(ctx, opaqueToken)  // or tokenId
// Java
scalekitClient.tokens().invalidate(opaqueToken);  // or tokenId

6. Middleware pattern (protect endpoints)

# Python — Flask decorator
from functools import wraps
from flask import request, jsonify, g
from scalekit import ScalekitValidateTokenFailureException

def authenticate_token(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        auth = request.headers.get("Authorization", "")
        if not auth.startswith("Bearer "):
            return jsonify({"error": "Missing authorization token"}), 401
        try:
            result = scalekit_client.tokens.validate_token(token=auth.split(" ", 1)[1])
            g.token_info = result.token_info
        except ScalekitValidateTokenFailureException:
            return jsonify({"error": "Invalid or expired token"}), 401
        return f(*args, **kwargs)
    return wrapper

@app.route("/api/resources")
@authenticate_token
def get_resources():
    org_id  = g.token_info.organization_id   # always present
    user_id = g.token_info.user_id            # present only for user-scoped keys
    # query DB filtered by org_id (and user_id if set)
// Node.js — Express middleware
async function authenticateToken(req, res, next) {
  const token = (req.headers.authorization || '').replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'Missing authorization token' });
  try {
    const result = await scalekit.token.validateToken(token);
    req.tokenInfo = result.tokenInfo;
    next();
  } catch (error) {
    if (error instanceof ScalekitValidateTokenFailureException)
      return res.status(401).json({ error: 'Invalid or expired token' });
    throw error;
  }
}

app.get('/api/resources', authenticateToken, (req, res) => {
  const { organizationId, userId } = req.tokenInfo;
});
// Go — Gin middleware
func AuthenticateToken(sc scalekit.Scalekit) gin.HandlerFunc {
  return func(c *gin.Context) {
    token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
    if token == "" {
      c.JSON(401, gin.H{"error": "Missing authorization token"}); c.Abort(); return
    }
    result, err := sc.Token().ValidateToken(c.Request.Context(), token)
    if err != nil {
      c.JSON(401, gin.H{"error": "Invalid or expired token"}); c.Abort(); return
    }
    c.Set("tokenInfo", result.TokenInfo)
    c.Next()
  }
}

Data filtering pattern

Key type Filter query by Example use case
Organization-scoped organizationId only All workspace contacts in a CRM
User-scoped organizationId + userId Only tasks assigned to the calling user
Custom claims Claims from customClaims map Restrict by environment, team, etc.

Key rules

  • Show token once: Display to user at creation, then discard — Scalekit cannot retrieve it.
  • Validate server-side on every request: Never trust unverified tokens; call validateToken each time.
  • Use token_id for lifecycle ops: Store token_id (not the key itself) for list/invalidate workflows.
  • Rotate safely: Create new key → update consumer → verify → invalidate old key (avoids downtime).
  • Use expiry for time-limited access: Limits blast radius if a key is compromised.
  • Never log or commit keys: Treat API keys like passwords — use encrypted secrets managers or env vars.

Client Credentials (OAuth 2.0) — Path B

Separate from Path A. Do not use opaque-token createToken / validateToken APIs here. This path registers M2M clients, issues JWTs via /oauth/token, and validates with JWKS / validate_access_token_and_get_claims.

For service-to-service (machine-to-machine) auth using JWT bearer tokens instead of opaque API keys. Use when APIs need scope-based access control, JWT validation via JWKS, or standard OAuth 2.0 client credentials flow.

Flow

Register client (your app) → Issue client_id + secret (Scalekit) →
API client fetches bearer token → Your server validates JWT + scopes

Register an API client for an organization

One organization can have multiple API clients. plain_secret is returned only once.

# Python
from scalekit.v1.clients.clients_pb2 import OrganizationClient

response = scalekit_client.m2m_client.create_organization_client(
    organization_id="<ORG_ID>",
    m2m_client=OrganizationClient(
        name="GitHub Actions Deployment Service",
        description="Deploys to production via GitHub Actions",
        scopes=["deploy:applications", "read:deployments"],  # resource:action pattern
        audience=["deployment-api.acmecorp.com"],
        custom_claims=[
            {"key": "github_repository", "value": "acmecorp/inventory-service"},
            {"key": "environment",        "value": "production_us"}
        ],
        expiry=3600  # seconds; default 3600
    )
)
client_id    = response.client.client_id
plain_secret = response.plain_secret  # store securely; not retrievable again

API client fetches a bearer token

Runs inside the API client's code, not your server:

curl -X POST "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=<API_CLIENT_ID>" \
  -d "client_secret=<API_CLIENT_SECRET>"

Response includes access_token (JWT), token_type, expires_in, and scope.

Validate the JWT on your API server

Do this on EVERY request. Never trust unverified tokens.

# Python — SDK handles JWKS automatically
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
try:
    claims = scalekit_client.validate_access_token_and_get_claims(token=token)
    # claims["scopes"] → list of granted scopes
except Exception:
    return 401  # invalid or expired
// Node.js — manual JWKS + JWT verify
import jwksClient from 'jwks-rsa';
import jwt from 'jsonwebtoken';

const jwks = jwksClient({
  jwksUri: `${process.env.SCALEKIT_ENVIRONMENT_URL}/.well-known/jwks.json`,
  cache: true
});

async function verifyToken(token) {
  const decoded = jwt.decode(token, { complete: true });
  const key     = await jwks.getSigningKey(decoded.header.kid);
  return jwt.verify(token, key.getPublicKey(), {
    algorithms: ['RS256'],
    complete: true
  }).payload;
}

Enforce scopes in middleware

# Python — Flask
def require_scope(scope):
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            token = request.headers.get("Authorization", "").removeprefix("Bearer ")
            if not token:
                return jsonify({"error": "Missing token"}), 401
            try:
                claims = scalekit_client.validate_access_token_and_get_claims(token=token)
            except Exception:
                return jsonify({"error": "Invalid token"}), 401
            if scope not in claims.get("scopes", []):
                return jsonify({"error": "Insufficient permissions"}), 403
            return f(*args, **kwargs)
        return wrapper
    return decorator
// Node.js — Express
function requireScope(scope) {
  return async (req, res, next) => {
    const token = (req.headers.authorization || '').replace('Bearer ', '');
    if (!token) return res.status(401).send('Missing token');
    try {
      const payload = await verifyToken(token);
      if (!payload.scopes?.includes(scope))
        return res.status(403).send('Insufficient permissions');
      req.tokenClaims = payload;
      next();
    } catch {
      res.status(401).send('Invalid token');
    }
  };
}

Client credentials key rules

  • plain_secret is returned once only — instruct customers to store it immediately.
  • Always validate tokens server-side before trusting claims.
  • Cache JWKS keys (avoid fetching on every request); rotate on kid mismatch.
  • Use resource:action scope naming (e.g. deployments:read, applications:create).
  • An organization_id maps to one customer; multiple API clients per org are supported.