From saaskit
Implements machine-to-machine authentication using Scalekit — either long-lived opaque API keys (org or user scoped) or OAuth 2.0 client credentials for service-to-service auth. Use when adding API key auth, building key management, or implementing client credentials flows.
How this skill is triggered — by the user, by Claude, or both
Slash command
/saaskit:adding-api-authThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
```
Your app creates token (org or user scoped) → Scalekit returns key + tokenId →
Customer stores key → API client sends Bearer key → Your server validates →
Scalekit returns org/user context → Filter data accordingly
The plain-text API key is returned only once at creation. Scalekit never stores it.
# 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.
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();
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. |
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);
}
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' });
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
# 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()
}
}
| 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. |
token once: Display to user at creation, then discard — Scalekit cannot retrieve it.validateToken each time.token_id for lifecycle ops: Store token_id (not the key itself) for list/invalidate workflows.expiry for time-limited access: Limits blast radius if a key is compromised.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.
Register client (your app) → Issue client_id + secret (Scalekit) →
API client fetches bearer token → Your server validates JWT + scopes
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
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.
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;
}
# 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');
}
};
}
plain_secret is returned once only — instruct customers to store it immediately.kid mismatch.resource:action scope naming (e.g. deployments:read, applications:create).organization_id maps to one customer; multiple API clients per org are supported.npx claudepluginhub scalekit-inc/authstack --plugin saaskitBlocks Edit/Write/Bash actions until Claude investigates importers, data schemas, and user instructions. Improves output quality by forcing concrete facts before edits.