Add authentication and authorization capabilities for MCP servers, AI agents and SaaS apps
Guides users through adding OAuth 2.1 authorization to MCP servers using Scalekit — configures discovery endpoints, sets up token validation middleware, and enables scope-based tool authorization. Use when setting up MCP servers, implementing authentication for AI hosts like Claude Desktop, Cursor, or VS Code, or when users mention MCP security, OAuth, or Scalekit integration.
# Adding OAuth 2.1 Authorization to MCP Servers
## Prerequisite: HTTP transport
MCP OAuth requires **Streamable HTTP** transport. Stdio does not support OAuth.
**Node.js:** Use `StreamableHTTPServerTransport` from `@modelcontextprotocol/sdk/server/streamableHttp.js`
**Python:** Use `mcp.streamable_http_app(path="/mcp")` and run with `uvicorn module:app`
If currently using stdio, migrate to HTTP first. See [MCP Transport Docs](https://spec.modelcontextprotocol.io/specification/architecture/#transports).
## Setup workflow
Copy this checklist and track progress:
```
MCP OAuth Setup:
- [ ] Step 1: Install Scalekit SDK
- [ ] Step 2: Register MCP server in Scalekit dashboard
- [ ] Step 3: Implement discovery endpoint
- [ ] Step 4: Add token validation middleware
- [ ] Step 5: (Optional) Add scope-based authorization
- [ ] Step 6: Test with AI hosts
```
## Step 1: Install Scalekit SDK
**Node.js:**
```bash
npm install @scalekit-sdk/node
```
**Python:**
```bash
pip install scalekit-sdk-python
```
Get credentials from [Scalekit dashboard](https://app.scalekit.com/) after creating an account.
## Step 2: Register MCP server
In Scalekit dashboard:
1. Go to **MCP servers** → **Add MCP server**
2. Provide a descriptive **name** (appears on consent page)
3. Enable **dynamic client registration** (allows automatic MCP host registration)
4. Enable **Client ID Metadata Document (CIMD)** (fetches client metadata automatically)
5. Click **Save**
**Advanced settings** (optional):
- **Server URL**: Your MCP server identifier (e.g., `https://mcp.yourapp.com`)
- **Access token lifetime**: 300-3600 seconds recommended
- **Scopes**: Define permissions like `todo:read`, `todo:write`
**Important**: Restart your MCP server after toggling DCR or CIMD settings.
## Step 3: Implement discovery endpoint
Create `/.well-known/oauth-protected-resource` endpoint. Copy metadata JSON from **Dashboard > MCP Servers > Your server > Metadata JSON**.
**Node.js (Express):**
```javascript
app.get('/.well-known/oauth-protected-resource', (req, res) => {
res.json({
"authorization_servers": [
"https://<SCALEKIT_ENVIRONMENT_URL>/resources/<YOUR_RESOURCE_ID>"
],
"bearer_methods_supported": ["header"],
"resource": "https://mcp.yourapp.com",
"resource_documentation": "https://mcp.yourapp.com/docs",
"scopes_supported": ["todo:read", "todo:write"]
});
});
```
**Python (FastAPI):**
```python
@app.get("/.well-known/oauth-protected-resource")
async def get_oauth_protected_resource():
return {
"authorization_servers": [
"https://<SCALEKIT_ENVIRONMENT_URL>/resources/<YOUR_RESOURCE_ID>"
],
"bearer_methods_supported": ["header"],
"resource": "https://mcp.yourapp.com",
"resource_documentation": "https://mcp.yourapp.com/docs",
"scopes_supported": ["todo:read", "todo:write"]
}
```
Replace placeholders with actual values from Scalekit dashboard.
## Step 4: Add token validation middleware
### Initialize Scalekit client
**Node.js:**
```javascript
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
);
const RESOURCE_ID = 'https://your-mcp-server.com'; // Or autogenerated ID from dashboard
const METADATA_ENDPOINT = 'https://your-mcp-server.com/.well-known/oauth-protected-resource';
export const WWWHeader = {
HeaderKey: 'WWW-Authenticate',
HeaderValue: `Bearer realm="OAuth", resource_metadata="${METADATA_ENDPOINT}"`
};
```
**Python:**
```python
from scalekit import ScalekitClient
import os
scalekit_client = ScalekitClient(
env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
)
RESOURCE_ID = "https://your-mcp-server.com"
METADATA_ENDPOINT = "https://your-mcp-server.com/.well-known/oauth-protected-resource"
WWW_HEADER = {
"WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{METADATA_ENDPOINT}"'
}
```
### Implement authentication middleware
**Node.js:**
```javascript
export async function authMiddleware(req, res, next) {
try {
// Allow public access to well-known endpoints
if (req.path.includes('.well-known')) {
return next();
}
// Extract Bearer token
const authHeader = req.headers['authorization'];
const token = authHeader?.startsWith('Bearer ')
? authHeader.split('Bearer ')[1]?.trim()
: null;
if (!token) {
throw new Error('Missing or invalid Bearer token');
}
// Validate token against resource audience
await scalekit.validateToken(token, {
audience: [RESOURCE_ID]
});
next();
} catch (err) {
return res
.status(401)
.set(WWWHeader.HeaderKey, WWWHeader.HeaderValue)
.end();
}
}
// Apply to all MCP endpoints
app.use('/', authMiddleware);
```
**Python:**
```python
from scalekit.common.scalekit import TokenValidationOptions
from fastapi import Request, HTTPException, status
async def auth_middleware(request: Request, call_next):
# Allow public access to well-known endpoints
if request.url.path.startswith("/.well-known"):
return await call_next(request)
# Extract Bearer token
auth_header = request.headers.get("Authorization", "")
token = None
if auth_header.startswith("Bearer "):
token = auth_header.split("Bearer ")[1].strip()
if not token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
headers=WWW_HEADER
)
# Validate token
try:
options = TokenValidationOptions(
issuer=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
audience=[RESOURCE_ID]
)
scalekit_client.validate_access_token_and_get_claims(token, options=options)
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
headers=WWW_HEADER
)
return await call_next(request)
# Apply to all MCP endpoints
app.middleware("http")(auth_middleware)
```
## Step 5: Scope-based tool authorization (Optional)
Add fine-grained access control at the tool execution level:
**Node.js:**
```javascript
try {
await scalekit.validateToken(token, {
audience: [RESOURCE_ID],
requiredScopes: [scope] // e.g., 'todo:write'
});
} catch(error) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scope: ${scope}`,
scope: scope
});
}
```
**Python:**
```python
try:
scalekit_client.validate_access_token(
token,
options=TokenValidationOptions(
audience=[RESOURCE_ID],
required_scopes=[scope]
)
)
except Exception:
return {
"error": "insufficient_scope",
"error_description": f"Required scope: {scope}",
"scope": scope
}
```
## Step 6: Verify and deploy
### Verify your integration
Before testing with AI hosts, the coding agent will scan your project to determine
the right URL to verify against. It will look for:
- `RESOURCE_ID` or `resource` values in your code or `.env`
- The host/domain used in `/.well-known/oauth-protected-resource`
- Any deployed base URL in environment config (`SERVER_URL`, `PUBLIC_URL`, etc.)
If no URL is found, you'll be asked:
> "What is your MCP server base URL?
> (e.g., `https://mcp.yourapp.com` or `https://mcp.yourapp.com/mcp`)"
Once the URL is known, run these three checks:
**Check 1 – Confirm 401 without token:**
```bash
curl -i <your-mcp-url>
```
Expected: `HTTP/1.1 401 Unauthorized`
**Check 2 – Confirm WWW-Authenticate header:**
The response must include:
```
WWW-Authenticate: Bearer realm="OAuth", resource_metadata="https://<your-domain>/.well-known/oauth-protected-resource"
```
This is what triggers the MCP client's OAuth flow. A plain 401 without this header
will cause AI hosts (Claude Desktop, Cursor, VS Code) to fail silently.
**Check 3 – Confirm metadata endpoint is reachable:**
```bash
curl https://<your-domain>/.well-known/oauth-protected-resource
```
Expected: JSON with `resource`, `authorization_servers`, and `scopes_supported`.
After verification passes, test with Claude Desktop, Cursor, and VS Code. Ensure invalid tokens get 401, and scope-based authorization (if implemented) rejects insufficient scopes.
## Framework-specific references
- FastMCP (Python, simplest): [fastmcp-reference.md](fastmcp-reference.md)
- Express.js (Node.js): [express-reference.md](express-reference.md)
- FastAPI + FastMCP (Python, custom middleware): [fastapi-reference.md](fastapi-reference.md)
## Common issues
**Token validation fails**:
- Verify RESOURCE_ID matches Server URL in dashboard
- Check environment variables are set correctly
- Ensure token hasn't expired
**Discovery endpoint not found**:
- Verify endpoint path is exactly `/.well-known/oauth-protected-resource`
- Check endpoint is publicly accessible (not protected by auth middleware)
**Scope validation errors**:
- Verify scopes in dashboard match those in code
- Check token includes required scopes
- Ensure scope strings match exactly (case-sensitive)Implements server-side RBAC and permission checks by validating and decoding Scalekit access tokens, extracting roles/permissions, and enforcing them with middleware/decorators at route boundaries. Use when adding role-based access control, protecting routes or endpoints, building auth middleware, or checking JWT permissions with Scalekit tokens.
# Implementing access control (Scalekit SaaSKit)
## When to use
After authentication is working and the app must authorize access to routes/actions by inspecting the user's access token for `roles` and `permissions`.
## Workflow
1. Validate the access token (expiry, issuer/audience as applicable) and then decode it to extract `sub`, `oid`, `roles`, and `permissions`.
2. Attach a normalized auth context to the request (e.g., `req.user = { id, organizationId, roles, permissions }`) so downstream handlers can authorize consistently.
3. Enforce authorization at route boundaries using (a) role checks for broad access patterns and (b) permission checks for fine-grained actions (often `resource:action`).
4. Combine checks when needed (examples: "admin bypass", "resource ownership", time-based restrictions for sensitive operations).
5. Never rely on client-side authorization alone; enforce roles/permissions server-side.
## Reference implementation
### Node.js (Express-style middleware)
Validate+extract, then RBAC/PBAC guards.
```js
// validate + extract
const validateAndExtractAuth = async (req, res, next) => {
try {
const accessToken = decrypt(req.cookies.accessToken); // if encrypted
const tokenData = await scalekit.validateAccessTokenAndGetClaims(accessToken);
if (!tokenData) return res.status(401).json({ error: "Unauthorized" });
req.user = {
id: tokenData.sub,
organizationId: tokenData.oid,
roles: tokenData.roles || [],
permissions: tokenData.permissions || []
};
next();
} catch {
return res.status(401).json({ error: "Authentication failed" });
}
};
// RBAC
const hasRole = (user, role) => user.roles?.includes(role);
const requireRole = (role) => (req, res, next) =>
hasRole(req.user, role) ? next() : res.status(403).json({ error: `Access denied. Required role: ${role}` });
// PBAC
const hasPermission = (user, perm) => user.permissions?.includes(perm);
const requirePermission = (perm) => (req, res, next) =>
hasPermission(req.user, perm) ? next() : res.status(403).json({ error: `Access denied. Required permission: ${perm}` });
// usage
app.get("/api/projects", validateAndExtractAuth, requirePermission("projects:read"), handler);
app.get("/api/admin/users", validateAndExtractAuth, requireRole("admin"), handler);
```
### Python (decorator pattern)
Validate+extract, then RBAC/PBAC decorators.
```py
from functools import wraps
def validate_and_extract_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
access_token = decrypt(request.cookies.get("accessToken"))
try:
token_data = scalekit_client.validate_access_token_and_get_claims(access_token)
except Exception:
return jsonify({"error": "Invalid or expired token"}), 401
request.user = {
"id": token_data.get("sub"),
"organization_id": token_data.get("oid"),
"roles": token_data.get("roles", []),
"permissions": token_data.get("permissions", []),
}
return f(*args, **kwargs)
return decorated
def require_role(role):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
if role not in getattr(request, "user", {}).get("roles", []):
return jsonify({"error": f"Access denied. Required role: {role}"}), 403
return f(*args, **kwargs)
return decorated
return decorator
def require_permission(permission):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
if permission not in getattr(request, "user", {}).get("permissions", []):
return jsonify({"error": f"Access denied. Required permission: {permission}"}), 403
return f(*args, **kwargs)
return decorated
return decorator
```
## Verification
After implementing, test these cases:
```bash
# Test with a valid token that has the required role
curl -H "Cookie: accessToken=<valid_admin_token>" http://localhost:3000/api/admin/users
# Expected: 200
# Test with a token missing the required role
curl -H "Cookie: accessToken=<valid_member_token>" http://localhost:3000/api/admin/users
# Expected: 403 {"error": "Access denied. Required role: admin"}
# Test with an expired/invalid token
curl -H "Cookie: accessToken=expired_token" http://localhost:3000/api/projects
# Expected: 401 {"error": "Invalid or expired token"}
```
If 403 isn't returned for unauthorized users, check that the middleware chain order is correct: `validateAndExtractAuth` must run before `requireRole`/`requirePermission`.
## Patterns
- Roles for broad tiers (admin/manager/member), permissions for granular actions (`projects:create`, `tasks:assign`)
- Admin bypass: admins skip permission checks for operational tasks
- Resource ownership: user can edit only their own resource unless role-elevated
## Checklist
- [ ] Token validated before decoding claims
- [ ] `roles` and `permissions` normalized as arrays in request context
- [ ] Every protected route uses `requireRole(...)` and/or `requirePermission(...)` at the boundary
- [ ] Permission names follow `resource:action` convention
- [ ] Server-side checks are authoritative; client-side checks are UX onlyImplements enterprise SSO and authentication flows using Scalekit, including modular SSO (SAML/OIDC), IdP-initiated login, and admin portal for self-serve configuration. Use when adding SSO, integrating identity providers like Okta or Azure AD, or embedding the Scalekit admin portal.
# Implement Modular SSO
## Quick Start
**Choose your authentication mode:**
- **Modular SSO**: You manage users and sessions (covered here)
- **SaaSKit (Full-Stack Auth)**: Scalekit manages users and sessions (built-in SSO)
This skill covers Modular SSO for applications with existing user management.
**Key concept — `organization_id`**: SSO in Scalekit is scoped to an organization. Pass `organization_id` (or the user's email domain) in the authorization URL to route the user to their identity provider (Okta, Azure AD, Google Workspace, etc.). Without it, Scalekit cannot determine which IdP to use.
## Implementation Workflow
Copy this checklist and track progress:
```
Authentication Integration Progress:
- [ ] Step 1: Configure Modular Auth mode
- [ ] Step 2: Install and configure Scalekit SDK
- [ ] Step 3: Implement authorization URL generation
- [ ] Step 4: Handle IdP-initiated SSO (RECOMMENDED)
- [ ] Step 5: Process authentication callback
- [ ] Step 6: Validate tokens and extract user profile
- [ ] Step 7: Test SSO integration
- [ ] Step 8: Set up customer onboarding flow
```
## Step 1: Configure Modular Auth Mode
**Action**: Configure environment for Modular SSO:
1. Navigate to Dashboard > Authentication > General
2. Under "Full-Stack Auth" section, click "Disable Full-Stack Auth"
**Result**: System ready for modular integration.
## Step 2: Install and Configure SDK
### Installation
Choose the SDK for the project's tech stack:
**Node.js:**
```bash
npm install @scalekit-sdk/node
```
**Python:**
```bash
pip install scalekit-sdk-python
```
**Go:**
```bash
go get github.com/scalekit-inc/scalekit-sdk-go/v2
```
**Java:**
```xml
<dependency>
<groupId>com.scalekit</groupId>
<artifactId>scalekit-sdk-java</artifactId>
</dependency>
```
### Environment Configuration
Add these credentials to `.env` file (fetch from Dashboard > Developers > Settings > API credentials):
```env
SCALEKIT_ENVIRONMENT_URL=<environment-url>
SCALEKIT_CLIENT_ID=<client-id>
SCALEKIT_CLIENT_SECRET=<client-secret>
```
---
## Step 3: Generate Authorization URL
Create authorization URL to redirect users to their identity provider.
### SSO Connection Selectors (Priority Order)
Use ONE of these identifiers (evaluated in precedence order):
1. **connectionId** (highest) - Direct SSO connection reference
2. **organizationId** - Routes to organization's active SSO
3. **loginHint** - Extracts domain from email to find connection
### Implementation Pattern
**Node.js:**
```javascript
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENVIRONMENT_URL,
process.env.SCALEKIT_CLIENT_ID,
process.env.SCALEKIT_CLIENT_SECRET
);
const options = {
organizationId: 'org_XXXXX', // OR
connectionId: 'conn_15696105471768821', // OR
loginHint: 'user@example.com'
};
const authUrl = scalekit.getAuthorizationUrl(
'https://yourapp.com/auth/callback',
options
);
// Redirect user to authUrl
```
**Python:**
```python
from scalekit import ScalekitClient, AuthorizationUrlOptions
scalekit = ScalekitClient(
os.getenv('SCALEKIT_ENVIRONMENT_URL'),
os.getenv('SCALEKIT_CLIENT_ID'),
os.getenv('SCALEKIT_CLIENT_SECRET')
)
options = AuthorizationUrlOptions()
options.organization_id = 'org_XXXXX'
auth_url = scalekit.get_authorization_url(
redirect_uri='https://yourapp.com/auth/callback',
options=options
)
```
**Direct URL (no SDK):**
```
<SCALEKIT_ENVIRONMENT_URL>/oauth/authorize?
response_type=code&
client_id=<CLIENT_ID>&
redirect_uri=<CALLBACK_URL>&
scope=openid profile email&
organization_id=<ORG_ID>
```
## Step 4: Handle IdP-Initiated SSO
**CRITICAL**: Implement this to support users who start login from their identity provider portal.
### Why This Matters
IdP-initiated SSO converts potentially insecure flows into secure SP-initiated flows, protecting against SAML assertion theft and replay attacks.
### Configuration Required
1. Set initiate login endpoint: Dashboard > Authentication > Redirects
2. Configure endpoint: `https://yourapp.com/login`
### Implementation
**Node.js:**
```javascript
app.get('/login', async (req, res) => {
const { idp_initiated_login, error, error_description } = req.query;
if (error) {
return res.status(400).json({ message: error_description });
}
if (idp_initiated_login) {
// Decode JWT to extract connection details
const claims = await scalekit.getIdpInitiatedLoginClaims(idp_initiated_login);
const options = {
connectionId: claims.connection_id,
organizationId: claims.organization_id,
loginHint: claims.login_hint,
state: claims.relay_state
};
const authUrl = scalekit.getAuthorizationUrl(
'https://yourapp.com/auth/callback',
options
);
return res.redirect(authUrl);
}
// Handle normal login flow
});
```
**Python:**
```python
@app.route('/login')
async def handle_login():
idp_initiated_login = request.args.get('idp_initiated_login')
error = request.args.get('error')
if error:
return {'error': request.args.get('error_description')}, 400
if idp_initiated_login:
claims = await scalekit.get_idp_initiated_login_claims(idp_initiated_login)
options = AuthorizationUrlOptions()
options.connection_id = claims.get('connection_id')
options.organization_id = claims.get('organization_id')
options.state = claims.get('relay_state')
auth_url = scalekit.get_authorization_url(
redirect_uri='https://yourapp.com/auth/callback',
options=options
)
return redirect(auth_url)
```
---
## Step 5: Process Authentication Callback
Handle the callback after successful IdP authentication.
### Callback Endpoint Setup
1. Create endpoint: `/auth/callback`
2. Register in Dashboard > Authentication > Redirect URLs > Allowed Callback URLs
### Implementation
**Node.js:**
```javascript
app.get('/auth/callback', async (req, res) => {
const { code, error, error_description } = req.query;
if (error) {
return res.status(400).json({ error: error_description });
}
try {
// Exchange code for user profile and tokens
const result = await scalekit.authenticateWithCode(
code,
'https://yourapp.com/auth/callback'
);
// Extract user information
const userEmail = result.user.email;
const userName = result.user.givenName + ' ' + result.user.familyName;
const userId = result.user.id;
// Create session for authenticated user
req.session.user = {
id: userId,
email: userEmail,
name: userName
};
res.redirect('/dashboard');
} catch (err) {
res.status(500).json({ error: 'Authentication failed' });
}
});
```
**Python:**
```python
@app.route('/auth/callback')
async def auth_callback():
code = request.args.get('code')
error = request.args.get('error')
if error:
return {'error': request.args.get('error_description')}, 400
result = scalekit.authenticate_with_code(
code,
'https://yourapp.com/auth/callback'
)
# Create session
session['user'] = {
'id': result.user.id,
'email': result.user.email,
'name': f"{result.user.given_name} {result.user.family_name}"
}
return redirect('/dashboard')
```
---
## Step 6: Validate Tokens
**ALWAYS** validate tokens before trusting claims.
**Node.js:**
```javascript
// Validate ID token
const idTokenClaims = await scalekit.validateToken(result.idToken);
// Validate access token
const accessTokenClaims = await scalekit.validateToken(result.accessToken);
```
**Python:**
```python
id_token_claims = scalekit.validate_access_token_and_get_claims(result['id_token'])
access_token_claims = scalekit.validate_access_token_and_get_claims(result['access_token'])
```
### Token Structure
**ID Token includes:**
- `email`: User's email address
- `given_name`, `family_name`: User's name
- `sub`: Unique user identifier (format: `connectionId;userId`)
- `oid`: Organization ID
- `amr`: Authentication method (SSO connection ID)
**Access Token includes:**
- `sub`: User identifier
- `exp`: Expiration timestamp
- `client_id`: Your application client ID
## Step 7: Test SSO Integration
Use the built-in IdP Simulator for comprehensive testing.
### Test Organization Setup
Your environment includes pre-configured test organization with domains:
- `@example.com`
- `@example.org`
### Testing Workflow
1. **Find test organization**: Dashboard > Organizations
2. **Use test selector**: Pass one of these in authorization URL:
- Email with `@example.com` domain
- Test organization's connection ID
- Organization ID
3. **Simulate SSO flow**: IdP Simulator appears (mimics customer's IdP)
4. **Complete authentication**: Enter test credentials
5. **Verify callback**: Check user profile received correctly
### Test Scenarios
Test ALL three scenarios:
1. **SP-initiated SSO**: User starts login from your app
2. **IdP-initiated SSO**: User starts from IdP portal
3. **Domain-based routing**: User enters email, auto-routes to IdP
---
## Step 8: Customer Onboarding
Enable SSO for enterprise customers through self-service Admin Portal.
### Quick Onboarding
**Create organization**: Dashboard > Organizations > New Organization
**Generate portal link** (Node.js):
```javascript
const portalLink = await scalekit.organization.generatePortalLink(
'org_32656XXXXXX0438'
);
// Share this link with customer's IT admin
console.log('Admin Portal:', portalLink.location);
```
**Share link**: Send to customer's IT administrator via email/Slack
**Share setup guide**: Include the Scalekit [SSO setup guide](https://docs.scalekit.com/guides/integrations/sso-integrations/) — provider-specific steps for Okta, Azure AD, Google Workspace, and others.
### Embedded Portal (Advanced)
Embed Admin Portal in your app for seamless experience:
```javascript
// Backend: Generate portal link
const portalLink = await scalekit.organization.generatePortalLink(orgId);
res.json({ portalUrl: portalLink.location });
```
```html
<!-- Frontend: Embed in iframe -->
<iframe
src="${portalUrl}"
width="100%"
height="600"
frameborder="0"
allow="clipboard-write">
</iframe>
```
## Advanced Patterns
### Pre-Check SSO Availability
Prevent failed redirects by checking SSO configuration before redirecting:
**Node.js:**
```javascript
const domain = email.split('@')[1].toLowerCase();
const connections = await scalekit.connections.listConnectionsByDomain({
domain
});
if (connections.length > 0) {
// SSO available - redirect to IdP
const authUrl = scalekit.getAuthorizationUrl(redirectUri, {
domainHint: domain
});
return res.redirect(authUrl);
} else {
// No SSO - show password login
return showPasswordLogin();
}
```
### Domain Verification
Enable seamless routing by verifying customer domains:
1. Customer verifies domain (e.g., `@megacorp.org`) in Admin Portal
2. Users sign in without organization selection
3. Scalekit auto-routes based on email domain
### Session Management Best Practices
**Set secure session configuration:**
```javascript
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Prevent XSS
maxAge: 86400000, // 24 hours
sameSite: 'lax' // CSRF protection
}
}));
```
**Implement session refresh:**
```javascript
// Check token expiration
if (Date.now() / 1000 > accessTokenClaims.exp) {
// Redirect to re-authentication
return res.redirect('/login');
}
```
## Integration with Existing Auth Systems
### Auth0 Integration
Configure Scalekit as Custom Social Connection in Auth0:
1. Auth0 Dashboard > Authentication > Social > Create Connection
2. Use Scalekit OAuth2 endpoints
3. Map Scalekit user attributes to Auth0 profile
### Firebase Integration
Add Scalekit as Custom Auth Provider:
1. Use Firebase Custom Token generation
2. Exchange Scalekit tokens for Firebase tokens
3. Maintain session with Firebase SDK
### AWS Cognito Integration
Configure Scalekit as SAML Identity Provider:
1. Cognito User Pool > Identity Providers > SAML
2. Use Scalekit metadata URL
3. Map attributes to Cognito user attributes
## Security Checklist
Before production deployment, verify:
- [ ] Environment variables stored securely (never in code)
- [ ] HTTPS enforced on all endpoints
- [ ] Tokens validated before trusting claims
- [ ] Session cookies use `secure` and `httpOnly` flags
- [ ] CSRF protection enabled
- [ ] Callback URLs registered in Scalekit dashboard
- [ ] Error messages don't expose sensitive information
- [ ] Rate limiting implemented on auth endpoints
- [ ] Logging configured (without exposing tokens)
## Troubleshooting
### "Invalid redirect_uri" Error
**Cause**: Callback URL not registered in dashboard
**Fix**: Add URL to Dashboard > Authentication > Redirect URLs
### "Organization not found" Error
**Cause**: Invalid organization ID or user doesn't belong to organization
**Fix**: Verify organization ID and user's email domain
### IdP-Initiated SSO Not Working
**Cause**: Initiate login URL not configured
**Fix**: Set URL in Dashboard > Authentication > Redirects
### Token Validation Fails
**Cause**: Token expired or invalid signature
**Fix**: Check token expiration and environment URL configuration
## Common Patterns
### Multi-Tenant Architecture
```javascript
// Determine organization from subdomain
const subdomain = req.hostname.split('.');
const organization = await getOrganizationBySubdomain(subdomain);
const authUrl = scalekit.getAuthorizationUrl(redirectUri, {
organizationId: organization.scalekitOrgId
});
```
### Step-Up Authentication
```javascript
// Require re-authentication for sensitive operations
if (requiresStepUp && !session.recentAuth) {
return res.redirect('/auth/step-up');
}
```
### Logout Implementation
```javascript
app.post('/logout', (req, res) => {
req.session.destroy();
res.redirect('/');
});
```
## Reference
**Scalekit Dashboard**: [https://app.scalekit.com](https://app.scalekit.com)
**Connection Selector Precedence**: connectionId > organizationId > loginHint
**Token Expiration**: ID tokens expire in 15 minutes, access tokens in 5 minutes (configurable in dashboard)
**Admin Portal Events**: Listen for `sso.enabled`, `sso.disabled`, `session.expired`
**Support**: [docs.scalekit.com](https://docs.scalekit.com)
## Implementation Notes
**Always validate tokens**: Never trust token claims without validation
**Handle errors gracefully**: Show user-friendly messages, log details internally
**Test all scenarios**: SP-initiated, IdP-initiated, and domain-based routing
**Enable domain verification**: Provides best user experience
**Use progressive enhancement**: Start with basic SSO, add advanced features iteratively
**Monitor authentication flows**: Track success rates and common failure points
## When to switch skills
- Use `implementing-saaskit` for the base auth flow that SSO builds on.
- Use `implementing-scim-provisioning` for automated user provisioning alongside SSO.
- Use `production-readiness-saaskit` to validate SSO configuration before launch.Implements Scalekit SaaSKit authentication in a Next.js App Router project using @scalekit-sdk/node. Use when adding auth routes, protecting pages, managing sessions, or checking permissions in Next.js with Scalekit.
# Scalekit Auth — Next.js App Router
Reference repo: [scalekit-inc/scalekit-nextjs-auth-example](https://github.com/scalekit-inc/scalekit-nextjs-auth-example)
## Project structure
```
app/api/auth/
├── login/route.ts # GET — generates auth URL + sets CSRF state
├── callback/route.ts # GET — exchanges code, sets session cookie
├── logout/route.ts # POST — clears session, returns Scalekit logout URL
├── refresh/route.ts # POST — refreshes access token, updates session
└── validate/route.ts # Token validation endpoint
lib/
├── scalekit.ts # Singleton ScalekitClient + default scopes
├── cookies.ts # Session read/write/clear + OAuth state helpers
└── auth.ts # isAuthenticated(), getCurrentUser(), hasPermission()
```
## Environment variables
```env
SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com
SCALEKIT_CLIENT_ID=your-client-id
SCALEKIT_CLIENT_SECRET=your-client-secret
SCALEKIT_REDIRECT_URI=http://localhost:3000/auth/callback
NEXT_PUBLIC_APP_URL=http://localhost:3000
SCALEKIT_SCOPES=openid profile email offline_access # optional, space-separated
```
`SCALEKIT_REDIRECT_URI` must exactly match the allowed callback URL in the Scalekit dashboard.
## SDK client (`lib/scalekit.ts`)
Singleton pattern — always use `getScalekitClient()`, never instantiate directly. Throws if env vars are missing.
```ts
import { getScalekitClient, getDefaultScopes } from '@/lib/scalekit';
const client = getScalekitClient();
```
## Session shape (`lib/cookies.ts`)
Session stored as JSON in a single `scalekit_session` HttpOnly cookie:
```ts
interface SessionData {
user: { sub, email, name, given_name, family_name, preferred_username };
tokens: { access_token, refresh_token, id_token, expires_at, expires_in };
roles?: string[];
permissions?: string[];
}
```
Key helpers:
- `getSession()` — returns `SessionData | null`
- `setSession(data)` — writes HttpOnly cookie; expires = token `expires_at`
- `clearSession()` — deletes cookie (call on logout)
- `isTokenExpired(session)` — returns true if token expires within **5 minutes**
- `getOAuthState()` / `setOAuthState(state)` — CSRF state cookie, 10-min TTL
- Cookie config: `httpOnly: true`, `secure` in production, `sameSite: 'lax'`, `path: '/'`
## Auth flow
### Login (`app/api/auth/login/route.ts` — GET)
```ts
const state = crypto.randomBytes(32).toString('base64url');
await setOAuthState(state);
const authUrl = client.getAuthorizationUrl(redirectUri, { state, scopes: getDefaultScopes() });
return NextResponse.json({ authUrl });
```
### Callback (`app/api/auth/callback/route.ts` — GET)
1. Validate `state` param against stored `oauth_state` cookie → redirect to `/error` on mismatch
2. `clearOAuthState()`
3. `client.authenticateWithCode(code, redirectUri)` → `authResponse`
4. `client.validateToken(authResponse.accessToken)` → extract `roles`, `permissions`
- Permission claims checked in order: `permissions` → `https://scalekit.com/permissions` → `scalekit:permissions`
5. Name resolution priority: `user.name` → `claims.name` → `givenName + familyName` → `email` → `preferred_username` → `'User'`
6. `setSession({ user, tokens, roles, permissions })`
7. Redirect to `/dashboard`
### Logout (`app/api/auth/logout/route.ts` — POST)
```ts
const logoutUrl = client.getLogoutUrl({
idTokenHint: session.tokens.id_token,
postLogoutRedirectUri: process.env.NEXT_PUBLIC_APP_URL,
});
await clearSession();
return NextResponse.json({ logoutUrl });
// Client receives logoutUrl and redirects
```
### Token refresh (`app/api/auth/refresh/route.ts` — POST)
```ts
const refreshResponse = await client.refreshAccessToken(session.tokens.refresh_token);
// Decode exp from JWT using jose.decodeJwt(); fallback to 3600s if missing
await setSession({ ...session, tokens: { ...session.tokens, access_token, refresh_token, expires_at, expires_in } });
```
## Auth utilities (`lib/auth.ts`)
```ts
isAuthenticated() // → boolean (session exists)
getCurrentUser() // → session.user | null
getAccessToken() // → access_token string | null
hasPermission('read:data') // → validates token, checks permission claim
```
## Protecting routes
For Server Components, call auth helpers directly:
```ts
import { isAuthenticated, getCurrentUser } from '@/lib/auth';
import { redirect } from 'next/navigation';
const authenticated = await isAuthenticated();
if (!authenticated) redirect('/login');
const user = await getCurrentUser();
```
For permission-gated pages:
```ts
import { hasPermission } from '@/lib/auth';
const allowed = await hasPermission('org:admin');
if (!allowed) redirect('/permission-denied');
```
## Route map
| Route | Auth required |
|---|---|
| `/` | No |
| `/login` | No |
| `/auth/callback` | No |
| `/dashboard` | Yes |
| `/sessions` | Yes |
| `/organization/settings` | Yes + permission |
| `/permission-denied` | No |
| `/error` | No |
## Dependencies
```bash
npm install @scalekit-sdk/node jose date-fns js-cookie
```
## Tactics
### Edge middleware for route protection
Add `middleware.ts` at the project root to enforce auth before any Server Component renders:
```ts
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const PROTECTED_PATHS = ['/dashboard', '/sessions', '/organization']
export function middleware(request: NextRequest) {
const session = request.cookies.get('scalekit_session')
const isProtected = PROTECTED_PATHS.some(p => request.nextUrl.pathname.startsWith(p))
if (isProtected && !session) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('next', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
return NextResponse.next()
}
export const config = {
matcher: ['/((?!_next|api|favicon).*)'],
}
```
Server Components should still call `isAuthenticated()` as a second layer.
### Triggering login from a Client Component
`/api/auth/login` returns `{ authUrl }` — never navigate there with `router.push`. OAuth requires a full page navigation:
```ts
const { authUrl } = await fetch('/api/auth/login').then(r => r.json())
window.location.href = authUrl // full navigation, not client-side route change
```
### OIDC logout from the client
Logout returns `{ logoutUrl }` — the client must navigate to it:
```ts
const { logoutUrl } = await fetch('/api/auth/logout', { method: 'POST' }).then(r => r.json())
window.location.href = logoutUrl // navigates to Scalekit end-session endpoint
```
Local session is already cleared; this step revokes the IdP session so the user isn't silently re-authenticated on next login.
### Deep link preservation
In the login page, read `?next` from search params and carry it through the state:
```ts
// app/login/page.tsx
const next = searchParams.get('next') || '/dashboard'
// Pass next to /api/auth/login as a query param, store in session before redirect
// In /api/auth/callback: redirect to stored next URL after setSession()
```
Validate `next` on the server: only allow relative paths (`/...`) to prevent open redirect.
### SameSite=Lax — never Strict
The `scalekit_session` and `oauth_state` cookies must use `sameSite: 'lax'`. The OAuth callback is a cross-site redirect from Scalekit back to your app — `'strict'` drops the cookie on that redirect, causing a CSRF state mismatch error every time.
### Cache-Control: no-store on protected pages
Without this, the browser back button after logout serves a cached authenticated page:
```ts
// In a protected route handler or layout
export const dynamic = 'force-dynamic'
// Or explicitly in a route handler:
return new Response(html, {
headers: { 'Cache-Control': 'no-store' },
})
```
### Token refresh race condition across tabs
Multiple browser tabs can simultaneously trigger token refresh with the same refresh token — most IdPs reject the second attempt. Mitigation: set a short-lived `refresh_in_progress` flag in the session before calling the refresh endpoint, and check it at the start of the refresh route to skip concurrent calls.Implements Scalekit SaaSKit authentication in Python web frameworks (Django, FastAPI, or Flask) using scalekit-sdk-python. Use when adding auth to a Django, FastAPI, or Flask project, or when the user mentions Python web authentication with Scalekit.
# SaaSKit Auth — Python
Implements Scalekit authentication in Django, FastAPI, or Flask using `scalekit-sdk-python`.
## Framework detection
Before generating code, detect which framework is in use:
1. Check for `django` in `requirements.txt` / `pyproject.toml` → Django
2. Check for `fastapi` → FastAPI
3. Check for `flask` → Flask
4. If unclear, ask the user.
## Quick setup
```bash
pip install scalekit-sdk-python python-dotenv
```
```python
import os
from dotenv import load_dotenv
from scalekit import ScalekitClient
load_dotenv()
sc = ScalekitClient(
env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
)
```
## Framework routing
Each framework has different patterns for routes, middleware, and session storage:
| Framework | Auth middleware | Session store | Reference |
|---|---|---|---|
| Django | Custom middleware class | Django sessions (DB/cache) | [django-reference.md](django-reference.md) |
| FastAPI | Dependency injection | Server-side or JWT | [fastapi-reference.md](fastapi-reference.md) |
| Flask | `@login_required` decorator | Flask-Session | [flask-reference.md](flask-reference.md) |
## Default workflow (FastAPI example)
```python
import os, secrets
from fastapi import FastAPI, Request, Response
from fastapi.responses import RedirectResponse
from dotenv import load_dotenv
from scalekit import ScalekitClient
load_dotenv()
app = FastAPI()
REDIRECT_URI = os.getenv("SCALEKIT_REDIRECT_URI", "http://localhost:8000/auth/callback")
sc = ScalekitClient(
env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
)
@app.get("/auth/login")
def login(response: Response):
state = secrets.token_urlsafe(32)
from scalekit.common.scalekit import AuthorizationUrlOptions
options = AuthorizationUrlOptions()
options.state = state
response = RedirectResponse(sc.get_authorization_url(REDIRECT_URI, options))
response.set_cookie("oauth_state", state, httponly=True, samesite="lax", secure=True)
return response
@app.get("/auth/callback")
def callback(request: Request, code: str, state: str):
stored = request.cookies.get("oauth_state")
if not stored or stored != state:
return Response("CSRF mismatch", status_code=403)
result = sc.authenticate_with_code(code, REDIRECT_URI)
# Store result.user and tokens in your session mechanism
response = RedirectResponse("/dashboard")
response.delete_cookie("oauth_state")
return response
@app.get("/auth/logout")
def logout(request: Request):
from scalekit.common.scalekit import LogoutUrlOptions
logout_url = sc.get_logout_url(options=LogoutUrlOptions(post_logout_redirect_uri="http://localhost:8000"))
# Clear your session here
return RedirectResponse(logout_url)
```
If `authenticate_with_code` raises an exception, verify the redirect URI matches the dashboard exactly.
For Django and Flask patterns, see the framework-specific references linked in the table above.
## Deep reference
- Auth flows: [docs.scalekit.com/authenticate/fsa/quickstart](https://docs.scalekit.com/authenticate/fsa/quickstart/)
- Sessions: [docs.scalekit.com/authenticate/fsa/sessions](https://docs.scalekit.com/authenticate/fsa/sessions/)
## When to switch skills
- Use `implementing-saaskit` for the general (non-Python-specific) integration guide.
- Use `managing-saaskit-sessions` for advanced session handling.
- Use `implementing-access-control` for RBAC after auth is working.Implements Scalekit SaaSKit authentication (sign-up, login, logout, sessions) using JWT tokens across Node.js, Python, Go, Java, or PHP. Use when building or integrating user authentication with Scalekit, setting up OAuth callbacks, token refresh, or session handling.
# Scalekit SaaSKit (Full-Stack Authentication)
## Setup
Install the SDK and set credentials in `.env`:
```sh
SCALEKIT_ENVIRONMENT_URL=<your-environment-url>
SCALEKIT_CLIENT_ID=<your-client-id>
SCALEKIT_CLIENT_SECRET=<your-client-secret>
SCALEKIT_REDIRECT_URI=<your-callback-url> # e.g. https://yourapp.com/auth/callback
```
> `SCALEKIT_REDIRECT_URI` must exactly match the callback URL registered in the Scalekit dashboard under Allowed Redirect URIs.
## Auth flow
### 1. Redirect to login
Generate an authorization URL and redirect the user:
```js
// Node.js
const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, {
scopes: ['openid', 'profile', 'email', 'offline_access']
});
res.redirect(authorizationUrl);
```
> `redirectUri` must exactly match the allowed callback URL registered in the Scalekit dashboard.
### 2. Handle the callback
Exchange the authorization code for tokens:
```js
// Node.js
const { user, idToken, accessToken, refreshToken } =
await scalekit.authenticateWithCode(code, redirectUri);
```
| Token | Purpose |
|---|---|
| `idToken` | Full user profile (sub, oid, email, name, exp) |
| `accessToken` | Roles + permissions; expires in 5 min (configurable) |
| `refreshToken` | Long-lived; use to renew access tokens |
### 3. Create the session
Store tokens in HttpOnly cookies:
```js
// Node.js
res.cookie('accessToken', authResult.accessToken, {
maxAge: (authResult.expiresIn - 60) * 1000,
httpOnly: true, secure: true, path: '/api', sameSite: 'lax'
});
res.cookie('refreshToken', authResult.refreshToken, {
httpOnly: true, secure: true, path: '/auth/refresh', sameSite: 'lax'
});
```
**Token validation middleware pattern:**
1. Read `accessToken` cookie → decrypt → `scalekit.validateAccessToken(token)`
2. If invalid → `scalekit.refreshAccessToken(refreshToken)` → update cookies
3. If refresh fails → log out the user
### 4. Log out
Clear session data, then redirect to Scalekit's logout endpoint:
```js
// Node.js
clearSessionData();
const logoutUrl = scalekit.getLogoutUrl({ idTokenHint, postLogoutRedirectUri });
res.redirect(logoutUrl); // One-time use URL; expires after logout
```
## Cross-language reference
All SDK methods follow the same pattern across languages with minor naming conventions:
| Operation | Node.js | Python | Go | Java |
|---|---|---|---|---|
| Auth URL | `getAuthorizationUrl` | `get_authorization_url` | `GetAuthorizationUrl` | `getAuthorizationUrl` |
| Exchange code | `authenticateWithCode` | `authenticate_with_code` | `AuthenticateWithCode` | `authenticateWithCode` |
| Validate token | `validateAccessToken` | `validate_access_token` | `ValidateAccessToken` | `validateAccessToken` |
| Refresh token | `refreshAccessToken` | `refresh_access_token` | `RefreshAccessToken` | `refreshToken` |
| Logout URL | `getLogoutUrl` | `get_logout_url` | `GetLogoutUrl` | `getLogoutUrl` |
## What this unlocks
One integration enables: Magic Link & OTP, social sign-ins, enterprise SSO, workspaces, MCP authentication, SCIM provisioning, and user management.
## Framework-specific references
- Python (Django/FastAPI/Flask): use `implementing-saaskit-python` skill
- Next.js: use `implementing-saaskit-nextjs` skill
- Go (Gin): see [go-reference.md](go-reference.md)
- Spring Boot: see [springboot-reference.md](springboot-reference.md)
- Laravel: see [laravel-reference.md](laravel-reference.md)
## Deep reference
- Auth flows: [docs.scalekit.com/authenticate/fsa/quickstart](https://docs.scalekit.com/authenticate/fsa/quickstart/)
- Sessions: [docs.scalekit.com/authenticate/fsa/sessions](https://docs.scalekit.com/authenticate/fsa/sessions/)
- Access control: [docs.scalekit.com/authenticate/fsa/access-control](https://docs.scalekit.com/authenticate/fsa/access-control/)
## When to switch skills
- Use `managing-saaskit-sessions` for token storage, refresh middleware, and session auditing.
- Use `implementing-access-control` for RBAC and permission enforcement.
- Use `implementing-modular-sso` for enterprise SSO on top of SaaSKit.
- Use `migrating-to-saaskit` when replacing an existing auth system.
- Use `production-readiness-saaskit` before going live.Sets up SCIM endpoints, handles directory webhook events, maps user attributes, and manages group memberships using Scalekit's Directory API. Use when the user asks to add SCIM, directory sync, user provisioning, deprovisioning, or lifecycle management to their application.
# SCIM Provisioning with Scalekit
Adds automated user lifecycle management (create, update, deactivate) via Scalekit's Directory API and real-time webhooks.
## Workflow
Copy and track progress:
```
SCIM Implementation Progress:
- [ ] Step 1: Detect stack and install SDK
- [ ] Step 2: Configure environment credentials
- [ ] Step 3: Initialize Scalekit client
- [ ] Step 4: Add Directory API sync (polling/on-demand)
- [ ] Step 5: Add webhook endpoint (real-time)
- [ ] Step 6: Register webhook in Scalekit dashboard
- [ ] Step 7: Map directory events to local user operations
- [ ] Step 8: Validate end-to-end
```
---
## Step 1: Detect stack and install SDK
Detect the project's language/framework from existing files (`package.json`, `requirements.txt`, `go.mod`, `pom.xml`) and install accordingly:
| Stack | Install command |
|-------|----------------|
| Node.js | `npm install @scalekit-sdk/node` |
| Python | `pip install scalekit-sdk-python` |
| Go | `go get github.com/scalekit-inc/scalekit-sdk-go/v2` |
| Java | Add `com.scalekit:scalekit-sdk-java` to `pom.xml` or `build.gradle` |
---
## Step 2: Environment credentials
Add to `.env` (never hardcode):
```shell
SCALEKIT_ENVIRONMENT_URL='https://<your-env>.scalekit.com'
SCALEKIT_CLIENT_ID='<CLIENT_ID>'
SCALEKIT_CLIENT_SECRET='<CLIENT_SECRET>'
SCALEKIT_WEBHOOK_SECRET='<WEBHOOK_SECRET>'
```
Credentials are found in **Dashboard > Developers > Settings > API Credentials**.
Webhook secret is found in **Dashboard > Webhooks** after registering an endpoint.
---
## Step 3: Initialize the Scalekit client
Insert initialization near the app's startup or service layer — match the project's existing patterns (singleton, DI, module export, etc.).
**Node.js:**
```javascript
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
);
```
**Python:**
```python
from scalekit import ScalekitClient
scalekit_client = ScalekitClient(
env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
)
```
For Go and Java patterns, see the [Scalekit SDK documentation](https://docs.scalekit.com/apis).
---
## Step 4: Directory API (on-demand sync)
Use for scheduled jobs, onboarding flows, or bulk imports. Integrate into existing user service/repository layer — do not create a parallel user management path.
**Fetch users and sync:**
```javascript
// Node.js
// Note: returns the first directory; multi-directory orgs need an explicit directory ID.
const { directories } = await scalekit.directory.listDirectories(orgId);
const directory = directories[0];
const { users } = await scalekit.directory.listDirectoryUsers(orgId, directory.id);
for (const user of users) {
await upsertUser({ email: user.email, name: user.name, orgId });
}
```
```python
# Python
# Note: returns the first directory; multi-directory orgs need an explicit directory ID.
directory = scalekit_client.directory.list_directories(organization_id=org_id).directories[0]
users = scalekit_client.directory.list_directory_users(org_id, directory.id)
for user in users:
upsert_user(email=user.email, name=user.name, org_id=org_id)
```
**Group sync for RBAC:**
```javascript
const { groups } = await scalekit.directory.listDirectoryGroups(orgId, directory.id);
for (const group of groups) {
await syncGroupPermissions(group.id, group.name);
}
```
Plug `upsertUser` / `syncGroupPermissions` into the project's **existing** user/role management functions — identify them by searching for `createUser`, `updateUser`, or equivalent patterns in the codebase.
---
## Step 5: Webhook endpoint (real-time provisioning)
Add a new route to the existing HTTP server/router. Match the framework pattern already in use (Express, FastAPI, Spring Boot, net/http, etc.).
**ALWAYS verify the signature before processing. Return 400 on failure.**
**Node.js (Express):** mount the route with `express.raw({ type: 'application/json' })` so `req.body` is the raw `Buffer` — signature verification must run on the exact bytes that were signed.
```javascript
app.post('/webhooks/scalekit', express.raw({ type: 'application/json' }), async (req, res) => {
const ok = await scalekit.verifyWebhookPayload(
process.env.SCALEKIT_WEBHOOK_SECRET,
req.headers,
req.body
);
if (!ok) return res.status(401).end();
const { type, data } = JSON.parse(req.body.toString('utf8'));
try {
await handleDirectoryEvent(type, data);
res.status(201).json({ status: 'processed' });
} catch (err) {
res.status(500).json({ error: 'Processing failed' });
}
});
```
**Python (FastAPI):** read the raw body BEFORE parsing JSON so the bytes match exactly what Scalekit signed.
```python
@app.post("/webhooks/scalekit")
async def scalekit_webhook(request: Request):
raw_body = await request.body()
valid = scalekit_client.verify_webhook_payload(
secret=os.getenv("SCALEKIT_WEBHOOK_SECRET"),
headers=dict(request.headers),
payload=raw_body,
)
if not valid:
raise HTTPException(status_code=401, detail="Invalid signature")
body = json.loads(raw_body)
await handle_directory_event(body.get("type"), body.get("data", {}))
return JSONResponse(status_code=201, content={"status": "processed"})
```
For Go and Java, see the [Scalekit SDK documentation](https://docs.scalekit.com/apis).
---
## Step 6: Event handler
Create a single dispatcher that routes to existing user operations. Map events to the project's **existing** create/update/deactivate functions:
```javascript
async function handleDirectoryEvent(type, data) {
switch (type) {
case 'organization.directory.user_created':
return createUser(data.email, data.name, data.organization_id);
case 'organization.directory.user_updated':
return updateUser(data.email, data.name);
case 'organization.directory.user_deleted':
return deactivateUser(data.email); // prefer deactivate over hard delete
case 'organization.directory.group_created':
case 'organization.directory.group_updated':
return syncGroup(data);
default:
console.log(`Unhandled event: ${type}`);
}
}
```
**Prefer deactivation over deletion** for `user_deleted` events unless the project explicitly hard-deletes users.
---
## Step 7: Dashboard registration checklist
After deploying the webhook endpoint:
1. Go to **Dashboard > Webhooks > +Add Endpoint**
2. Enter the public HTTPS URL: `https://your-app.com/webhooks/scalekit`
3. Subscribe to events:
- `organization.directory.user_created`
- `organization.directory.user_updated`
- `organization.directory.user_deleted`
- `organization.directory.group_created`
- `organization.directory.group_updated`
4. Copy the webhook secret into `SCALEKIT_WEBHOOK_SECRET`
5. Share the [SCIM setup guide](https://docs.scalekit.com/guides/integrations/scim-integrations/) with the customer's IT admin for their IdP-specific directory sync steps.
---
## Guardrails
- **Never hardcode credentials** — always `process.env` / `os.getenv` / `System.getenv`
- **Idempotent operations** — `upsertUser` must handle duplicate events safely
- **Return 2xx quickly** — offload heavy processing to a queue if needed; Scalekit retries on non-2xx with exponential backoff (up to 8 attempts over ~10 hours)
- **Validate signatures** — every webhook request, every time
- **Deactivate, don't delete** — unless codebase explicitly hard-deletes users
---
## Customer self-serve SCIM setup (admin portal)
Let customers configure directory sync via an embedded admin portal. Generate a single-use portal link server-side, embed it in an iframe, and handle `SCIM_CONFIGURED` and `SESSION_EXPIRED` postMessage events.
```javascript
// Server: generate link (single-use, regenerate on each page load)
const { location } = await scalekit.organization.generatePortalLink(organizationId);
// Client: embed in iframe
// <iframe src="{{ portalLink }}" width="100%" height="600px" allow="clipboard-write"></iframe>
```
Register your app domain in **Dashboard > Developers > API Configuration > Redirect URIs** or the iframe will be blocked.
For no-code onboarding: **Dashboard > Organizations** → select org → **Generate link** → share URL directly. Also share [SCIM setup guides](https://docs.scalekit.com/guides/integrations/scim-integrations/) for IdP-specific steps.
---
## Reference
- Full Go/Java SDK examples → [Scalekit SDK documentation](https://docs.scalekit.com/apis)
- Webhook event payload schemas → [Scalekit webhook events](https://docs.scalekit.com/directory/scim/quickstart/)
- RBAC group-to-role mapping patterns → [Role based access control](https://docs.scalekit.com/authenticate/fsa/rbac/)Manages Scalekit SaaSKit user sessions by securely storing tokens, validating access tokens on requests, refreshing tokens in middleware, and revoking sessions via Scalekit APIs. Use when building session persistence, implementing login/logout, managing cookies, handling JWT tokens, fixing session expiry, or auditing session security in a Scalekit web app.
# SaaSKit Session Management
## Inputs to collect (ask before coding)
- App type: traditional server-rendered web app, SPA, mobile app, or hybrid.
- Framework: Express/Fastify/Next (Node), Flask/Django/FastAPI (Python), Gin/Fiber (Go), Spring Boot (Java), etc.
- Token storage plan:
- Cookie names (examples used below: `accessToken`, `refreshToken`, `idToken`).
- Cookie attributes actually used in the repo (Path, Domain, Secure, HttpOnly, SameSite).
- Encryption approach already present (KMS, libsodium, AES-GCM, framework session store), or whether the app needs one introduced.
- Scalekit SDK/client availability and the exact methods used (validate, refresh, sessions list/revoke).
## Non-negotiable security rules (defaults)
- Store access and refresh tokens separately.
- Use HttpOnly cookies for tokens in traditional web apps to reduce XSS exposure.
- Use `Secure` in production (HTTPS-only) and set `SameSite` to `Lax` (required for OAuth callback redirects to work correctly; `Strict` breaks auth flows).
- Scope cookies with `Path` to reduce exposure:
- Access token cookie: scope to `/api` (or your protected routes) when possible.
- Refresh token cookie: scope to the refresh endpoint only (example `/auth/refresh`).
- Rotate refresh tokens on each refresh if your Scalekit flow supports it (token rotation helps detect theft).
## Workflow (implementation sequence)
1. Implement “store tokens” at login completion.
2. Implement “verify + refresh” middleware that runs on every protected request.
3. Implement a dedicated refresh endpoint (recommended even if middleware calls refresh internally).
4. Add logout and remote session revocation if the product needs “sign out this device / sign out all devices”.
5. Add a test checklist (cookie flags, refresh flow, failure modes).
## 1) Store session tokens securely
### Cookie-based approach (traditional web apps)
Use encryption-in-cookie as an extra layer, then store:
- Access token in an HttpOnly cookie with short TTL.
- Refresh token in a separate HttpOnly cookie, ideally scoped to the refresh route.
- ID token in a place that remains available at runtime if needed for logout flows (cookie or local storage depending on your logout design).
#### Node.js (Express)
```js
import cookieParser from "cookie-parser";
app.use(cookieParser());
// Example after successful authentication:
const { accessToken, expiresIn, refreshToken, idToken } = authResult;
// Encrypt before storing (implementation is app-specific)
const encAccess = encrypt(accessToken);
const encRefresh = encrypt(refreshToken);
// Access token: short-lived, cookie scoped
res.cookie("accessToken", encAccess, {
maxAge: (expiresIn - 60) * 1000, // clock-skew buffer
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/api",
});
// Refresh token: separate cookie, scoped to refresh endpoint
res.cookie("refreshToken", encRefresh, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/auth/refresh",
});
// Optional: ID token for logout (only if your logout needs it)
if (idToken) {
res.cookie("idToken", idToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
});
}
```
#### Python (Flask)
```py
from flask import make_response
import os
# auth_result: access_token, expires_in, refresh_token, id_token (optional)
enc_access = encrypt(auth_result.access_token)
enc_refresh = encrypt(auth_result.refresh_token)
resp = make_response()
resp.set_cookie(
"accessToken",
enc_access,
max_age=auth_result.expires_in - 60,
httponly=True,
secure=os.environ.get("FLASK_ENV") == "production",
samesite="Lax",
path="/api",
)
resp.set_cookie(
"refreshToken",
enc_refresh,
httponly=True,
secure=os.environ.get("FLASK_ENV") == "production",
samesite="Lax",
path="/auth/refresh",
)
if getattr(auth_result, "id_token", None):
resp.set_cookie(
"idToken",
auth_result.id_token,
httponly=True,
secure=os.environ.get("FLASK_ENV") == "production",
samesite="Lax",
path="/",
)
```
#### Go (Gin)
```go
// accessToken, refreshToken, expiresIn come from your auth completion result
encAccess := encrypt(accessToken)
encRefresh := encrypt(refreshToken)
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("accessToken", encAccess, expiresIn-60, "/api", "", isProd(), true)
c.SetCookie("refreshToken", encRefresh, 0, "/auth/refresh", "", isProd(), true)
// Optional
if idToken != "" {
c.SetCookie("idToken", idToken, 0, "/", "", isProd(), true)
}
```
#### Java (Spring)
```java
// Encrypt tokens before storing (implementation is app-specific)
String encAccess = encrypt(authResult.getAccessToken());
String encRefresh = encrypt(authResult.getRefreshToken());
Cookie access = new Cookie("accessToken", encAccess);
access.setMaxAge(authResult.getExpiresIn() - 60);
access.setHttpOnly(true);
access.setSecure(isProd());
access.setPath("/api");
response.addCookie(access);
// Ensure SameSite is applied (implementation depends on your framework version)
Cookie refresh = new Cookie("refreshToken", encRefresh);
refresh.setHttpOnly(true);
refresh.setSecure(isProd());
refresh.setPath("/auth/refresh");
response.addCookie(refresh);
```
### SPA/mobile note (reduce CSRF exposure)
For SPAs and mobile apps, prefer:
- Access token stored in memory and sent via `Authorization: Bearer <token>`.
- Refresh token stored in an HttpOnly cookie or secure device storage (platform dependent).
If using cookies in a browser SPA, configure CSRF protections explicitly.
## 2) Validate access token on every request (and refresh transparently)
### Behavior
- If access token is valid: proceed.
- If access token is expired and refresh token exists: refresh, rotate, rewrite cookies/headers, proceed.
- If refresh fails: return 401 and force re-login.
### Node.js middleware (Express-style)
```js
export async function verifySession(req, res, next) {
const accessCookie = req.cookies?.accessToken;
const refreshCookie = req.cookies?.refreshToken;
if (!accessCookie) return res.status(401).json({ error: "Authentication required" });
try {
const accessToken = decrypt(accessCookie);
const isValid = await scalekit.validateAccessToken(accessToken);
if (isValid) return next();
// Not valid -> attempt refresh
if (!refreshCookie) {
return res.status(401).json({ error: "Session expired. Please sign in again." });
}
const refreshToken = decrypt(refreshCookie);
const authResult = await scalekit.refreshAccessToken(refreshToken);
res.cookie("accessToken", encrypt(authResult.accessToken), {
maxAge: (authResult.expiresIn - 60) * 1000,
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/api",
});
res.cookie("refreshToken", encrypt(authResult.refreshToken), {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/auth/refresh",
});
return next();
} catch (e) {
return res.status(401).json({ error: "Authentication failed" });
}
}
```
### Python decorator (Flask)
```py
from functools import wraps
from flask import request, jsonify, make_response
def verify_session(f):
@wraps(f)
def inner(*args, **kwargs):
access_cookie = request.cookies.get("accessToken")
refresh_cookie = request.cookies.get("refreshToken")
if not access_cookie:
return jsonify({"error": "Authentication required"}), 401
try:
access_token = decrypt(access_cookie)
is_valid = scalekit_client.validate_access_token(access_token)
if is_valid:
return f(*args, **kwargs)
if not refresh_cookie:
return jsonify({"error": "Session expired. Please sign in again."}), 401
refresh_token = decrypt(refresh_cookie)
auth_result = scalekit_client.refresh_access_token(refresh_token)
resp = make_response(f(*args, **kwargs))
resp.set_cookie("accessToken", encrypt(auth_result.access_token),
max_age=auth_result.expires_in - 60, httponly=True,
secure=is_prod(), samesite="Lax", path="/api")
resp.set_cookie("refreshToken", encrypt(auth_result.refresh_token),
httponly=True, secure=is_prod(), samesite="Lax", path="/auth/refresh")
return resp
except Exception:
return jsonify({"error": "Authentication failed"}), 401
return inner
```
### Go middleware (Gin)
```go
func VerifySession() gin.HandlerFunc {
return func(c *gin.Context) {
accessCookie, err := c.Cookie("accessToken")
if err != nil || accessCookie == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error":"Authentication required"})
c.Abort()
return
}
accessToken := decrypt(accessCookie)
isValid, err := scalekitClient.ValidateAccessToken(accessToken)
if err == nil && isValid {
c.Next()
return
}
refreshCookie, err := c.Cookie("refreshToken")
if err != nil || refreshCookie == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error":"Session expired. Please sign in again."})
c.Abort()
return
}
refreshToken := decrypt(refreshCookie)
authResult, err := scalekitClient.RefreshAccessToken(refreshToken)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error":"Session expired. Please sign in again."})
c.Abort()
return
}
c.SetSameSite(http.SameSiteLaxMode)
c.SetCookie("accessToken", encrypt(authResult.AccessToken), authResult.ExpiresIn-60, "/api", "", isProd(), true)
c.SetCookie("refreshToken", encrypt(authResult.RefreshToken), 0, "/auth/refresh", "", isProd(), true)
c.Next()
}
}
```
### Java interceptor (Spring)
Implement `HandlerInterceptor#preHandle` (or a filter) to:
- Read cookies.
- Decrypt and validate access token.
- Refresh when invalid and refresh token exists.
- Rewrite cookies and allow the request to proceed.
Return 401 on failure.
## 3) Configure session security and duration (dashboard-driven)
Session behavior should be adjustable without code changes (typical policy knobs):
- Absolute session timeout (max total session time).
- Idle session timeout (logout after inactivity).
- Access token lifetime (drives refresh frequency).
## 4) Manage sessions remotely (API/SDK)
Use Scalekit session APIs to implement:
- “View active sessions” in account settings.
- “Sign out this device” (revoke a single session).
- “Sign out all devices” (revoke all sessions for a user).
### Example (Node.js)
```js
const sessionDetails = await scalekit.session.getSession("ses_1234567890123456");
const userSessions = await scalekit.session.getUserSessions("usr_1234567890123456", {
pageSize: 10,
filter: { status: ["ACTIVE"] }
});
await scalekit.session.revokeSession("ses_1234567890123456");
await scalekit.session.revokeAllUserSessions("usr_1234567890123456");
```
## Testing checklist (must pass)
- Cookies are `HttpOnly`, `Secure` (in prod), and `SameSite` is set intentionally.
- Cookie `Path` scoping works: refresh token cookie is only sent to `/auth/refresh`.
- Protected routes reject missing/invalid access token with 401.
- Expired access token triggers refresh and continues the request without user interaction.
- Refresh failure forces re-login (401) and does not loop.
- Multi-device: remote revoke invalidates the targeted session(s) as expected.
## Common failure modes
- Cookie deletion/overwrite doesn’t work due to mismatched Path/Domain.
- Refresh token accidentally sent to all endpoints (missing `Path=/auth/refresh`).
- Middleware refreshes but does not rotate tokens (misses theft detection benefits).
- SPA stores access token in localStorage (higher XSS risk) when memory storage was feasible.
## When to switch skills
- Use `implementing-saaskit` for the initial auth setup that produces the tokens.
- Use `implementing-access-control` for RBAC checks on the validated session.
- Use `production-readiness-saaskit` to audit session security before launch.Audits the existing auth system, exports users and orgs, imports them into Scalekit via SDK, configures redirects and roles, and deploys with a gradual rollout behind a feature flag. Use when a user mentions migrating, switching, or moving away from their current auth provider (Auth0, Firebase, Cognito, custom).
# Scalekit Auth Migration Planner
Guides an incremental, reversible migration from an existing auth system to Scalekit. Follow these phases in order—do not skip phases.
## Migration checklist
Copy and track progress:
```
Migration Progress:
- [ ] Phase 1: Audit and export existing auth data
- [ ] Phase 2: Import organizations and users into Scalekit
- [ ] Phase 3: Configure redirects and roles
- [ ] Phase 4: Update application code
- [ ] Phase 5: Deploy and monitor
```
---
## Phase 1: Audit and export
Conduct a code audit covering:
- Sign-up/login flows, session middleware, token validation
- RBAC logic, email verification, logout/session termination
Export the following data:
- User records (email, name, `email_verified`)
- Org/tenant structure
- Role assignments and permissions
- SSO/IdP provider configs
**Backup checklist before proceeding:**
- [ ] Export a sample JWT or session cookie (understand current format)
- [ ] Set up a feature flag to roll back to old auth system
- [ ] Document rollback procedure
Minimum user schema:
| Field | Required |
|---|---|
| `email` | Required |
| `first_name` | Optional |
| `last_name` | Optional |
| `email_verified` | Optional (defaults `false`) |
See [AUDIT-CHECKLIST.md](AUDIT-CHECKLIST.md) for full code audit patterns.
---
## Phase 2: Import organizations and users
`external_id` is critical—store original PKs here to preserve system-to-system mappings.
**Step 1: Create organizations first**
Node.js example:
```javascript
const result = await scalekit.organization.createOrganization(
org.display_name,
{ externalId: org.external_id, metadata: org.metadata }
);
```
**Step 2: Create users within organizations**
```javascript
const { user } = await scalekit.user.createUserAndMembership("org_scalekit_id", {
email: "user@example.com",
externalId: "usr_987",
userProfile: { firstName: "John", lastName: "Doe" },
});
```
**Rules:**
- Set `sendInvitationEmail: false` during import to skip invite emails; membership auto-activates and email is marked verified
- Batch imports in parallel; respect Scalekit rate limits
- Validate `external_id` mappings match source data exactly
For language-specific samples (Python, Go, Java, cURL): See [IMPORT-SAMPLES.md](IMPORT-SAMPLES.md).
---
## Phase 3: Configure redirects and roles
**Redirects:**
- Register callback URLs in **Settings → Redirects** in Scalekit dashboard
- Add post-logout URLs to control destination after logout
**Roles:**
- Define roles under **User Management → Roles** or via SDK
- During user import, include `roles` array inside the `membership` object
- Verify role claims are readable from the token after login
---
## Phase 4: Update application code
**Session middleware:** Replace legacy JWT validation with Scalekit SDK or JWKS endpoint.
Verify:
- [ ] Access tokens accepted on all protected routes
- [ ] Refresh token renewal works seamlessly
- [ ] `roles` claim from Scalekit tokens used for RBAC checks
**Login page:** Update logo, colors, copy, and legal links in Scalekit dashboard under Branding.
**Secondary flows to update:**
- Email verification prompt
- Logout redirect destination
---
## Phase 5: Deploy and monitor
**Pre-deployment:**
- [ ] Test login with a subset of migrated users
- [ ] Verify session creation, validation, and expiry
- [ ] Confirm role-based access works end-to-end
**Deployment sequence:**
1. Deploy updated application code
2. Enable feature flag → route traffic to Scalekit
3. Start at 5–10% of users; expand after stability confirmed
4. Monitor auth success rates and error logs
5. Keep rollback plan active for first 48 hours
**Post-deployment verification:**
```bash
# Verify token endpoint works with Scalekit credentials
curl -s -o /dev/null -w "%{http_code}" -X POST "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
-d "client_id=$SCALEKIT_CLIENT_ID&client_secret=$SCALEKIT_CLIENT_SECRET&grant_type=client_credentials"
# Expected: 200
# Verify a migrated user can be looked up
curl -s -H "Authorization: Bearer $TOKEN" "$SCALEKIT_ENVIRONMENT_URL/api/v1/organizations/<org_id>/users?email=migrated-user@example.com" | jq .
# Should return the user with correct external_id
```
Monitor: auth error rates, session creation/validation, SSO connection health, user-reported issues.
---
## Troubleshooting
| Symptom | Fix |
|---|---|
| Users can't log in | Verify callback URLs registered; check `external_id` mappings; ensure emails match exactly |
| Session validation fails | Switch JWT validation to Scalekit JWKS endpoint; verify token expiry/refresh logic |
| SSO not working | Confirm org has SSO enabled; verify IdP config; test IdP-initiated login |
> **Note:** Password migration support is coming. If required, contact Scalekit's Solutions team.Validates SSO configuration, checks SCIM provisioning, audits token security, and verifies MCP auth flows for Scalekit SaaSKit implementations before production launch. Use when going live, launching to production, or doing a pre-launch review.
# SaaSKit Production Readiness
Work through in order — skip sections that don't apply. Earlier sections are blockers for later ones.
## Quick checks
```bash
# Confirm production credentials are set (not dev/staging)
echo $SCALEKIT_ENVIRONMENT_URL # should be https://<subdomain>.scalekit.com (not .scalekit.dev)
echo $SCALEKIT_CLIENT_ID # should be set
echo $SCALEKIT_CLIENT_SECRET # should be set
```
- [ ] HTTPS enforced; CORS restricted to your domains only
- [ ] All credentials in environment variables — `grep -r "sks_" src/` returns nothing
- [ ] Webhook secret in env vars (if using webhooks)
## Core auth flows
- [ ] Redirect URLs in code match dashboard exactly
- [ ] `state` parameter validated in callbacks (CSRF)
- [ ] Tokens stored with `httpOnly: true`, `secure: true`, `sameSite: 'lax'`
- [ ] Token refresh working; logout calls `getLogoutUrl()` with `idTokenHint`
**Verify with curl:**
```bash
# Test token endpoint reachability
curl -s -o /dev/null -w "%{http_code}" -X POST "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
-d "client_id=$SCALEKIT_CLIENT_ID&client_secret=$SCALEKIT_CLIENT_SECRET&grant_type=client_credentials"
# Expected: 200
```
**Test each enabled auth method:** email/password, magic links, social logins, passkeys. For each: complete the full sign-up → login → logout cycle.
**If a flow fails:** check redirect URI mismatch first (most common), then verify `state` cookie is `sameSite: 'lax'` (not `'strict'`).
## SSO (if applicable)
- [ ] SSO tested with target IdPs (Okta, Azure AD, Google Workspace)
- [ ] SP-initiated and IdP-initiated flows both working
- [ ] Admin portal configured for self-serve SSO setup
- [ ] JIT provisioning: domains registered, default roles set
**Verify SSO round-trip:**
```bash
# Generate an auth URL with organization_id to trigger SSO
node -e "
const { ScalekitClient } = require('@scalekit-sdk/node');
const sc = new ScalekitClient(process.env.SCALEKIT_ENVIRONMENT_URL, process.env.SCALEKIT_CLIENT_ID, process.env.SCALEKIT_CLIENT_SECRET);
console.log(sc.getAuthorizationUrl(process.env.SCALEKIT_REDIRECT_URI, { organizationId: '<org_id>' }));
"
# Open the URL — should redirect to the IdP login page
```
Test with: new users, existing users, deactivated users.
## SCIM provisioning (if applicable)
- [ ] Webhook endpoints verify signature before processing
- [ ] User provisioning, deprovisioning, and profile updates tested
- [ ] Deactivation preferred over hard deletion for `user_deleted` events
- [ ] Endpoint returns 2xx quickly — offload heavy processing to a queue
**Verify webhook signature validation:**
```typescript
// In your webhook handler — this MUST be present
const isValid = scalekit.verifyWebhookPayload(
process.env.SCALEKIT_WEBHOOK_SECRET!,
req.headers,
req.body.toString()
);
if (!isValid) return res.sendStatus(401);
```
**If webhooks aren't arriving:** check that the endpoint URL in the dashboard is publicly reachable and returns 2xx.
## MCP authentication (if applicable)
- [ ] Resource metadata published at `/.well-known/oauth-protected-resource`
- [ ] Scopes enforced per tool
- [ ] Client reconnection after token expiry working
```bash
# Verify well-known endpoint is reachable
curl -s https://your-mcp-server.com/.well-known/oauth-protected-resource | jq .
# Should return JSON with resource, authorization_servers, scopes_supported
```
## RBAC (if applicable)
- [ ] Roles and permissions defined; default roles set for new users
- [ ] Permission enforcement verified at API endpoints
```typescript
// Verify token contains expected claims
const claims = await scalekit.validateToken(accessToken);
console.log('roles:', claims.roles); // should list assigned roles
console.log('permissions:', claims.permissions); // should list granted permissions
```
**If permissions are empty:** check that roles are assigned to the user in the dashboard and that the role has permissions attached.
## Network / firewall
Enterprise VPN customers must whitelist: `<your-env>.scalekit.com`, `cdn.scalekit.com`, `fonts.googleapis.com`.
## Monitoring
- [ ] Auth logs monitoring active; alerts for suspicious activity
- [ ] Webhook error tracking configured
- [ ] Incident response runbook written; rollback plan ready (feature flag)
- **Key metrics:** login success/failure rate, token refresh frequency, webhook delivery rate, SSO completion rate
## Final smoke test
Run the full cycle in staging with production credentials before flipping DNS:
1. Sign up / log in → verify session cookies are set with `httpOnly`, `secure`, `sameSite`
2. Access a protected route → verify the access token is validated and the request succeeds
3. Wait for access token to expire (or force expiry) → verify token refresh works and the session is maintained
4. Log out → verify cookies are cleared and re-visiting login prompts credentials again
5. If SSO enabled: trigger SSO login → verify callback completes and user session is created
6. If SCIM: trigger a directory sync event → verify user appears
7. If MCP: connect a client → verify tool execution succeedsUse when a user asks to generate, review, validate, or fix any code snippet that uses Scalekit APIs or SDKs. Generates illustration-quality snippets and reviews existing code to catch wrong method names, missing parameters, security anti-patterns, and broken auth flows. Covers all four SDKs (Node, Python, Go, Java), raw REST API calls, and both product suites — SaaSKit (SSO, login, sessions, RBAC, SCIM) and AgentKit (connections, tool calling, MCP auth). Use when the user says review my Scalekit code, generate a Scalekit example, validate this auth flow, check my SDK usage, fix my Scalekit integration, or write a code sample for docs.
# Scalekit Code Doctor
**Before doing anything else**, read the reference files:
- `references/REFERENCE.md` — Every correct SDK method signature and REST endpoint
- `references/COMMON-MISTAKES.md` — Known anti-patterns with wrong → right corrections
- `references/EXAMPLE-REPOS.md` — GitHub repos with working examples by framework
Never hallucinate a method name, parameter, or import — if it's not in the reference, verify against live sources before using it.
## Step 1 — Detect mode
**Generate mode** — User describes what they want but has no code yet.
**Review mode** — User provides existing code for validation.
If unclear, ask: "Do you want me to generate a fresh code example, or review existing code?"
## Step 2 — Identify context
| Language | Package | Import |
|----------|---------|--------|
| Node.js / TypeScript | `@scalekit-sdk/node` | `import { ScalekitClient } from '@scalekit-sdk/node'` |
| Python | `scalekit-sdk-python` | `from scalekit import ScalekitClient` |
| Go | `scalekit-sdk-go` | `import scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2"` |
| Java | `scalekit-sdk-java` | `import com.scalekit.ScalekitClient;` |
Product area: **SaaSKit** (SSO, login, sessions, RBAC, SCIM) or **AgentKit** (connections, tool calling, MCP auth).
## Step 3 — Generate mode
Output should be illustration-ready: self-contained, essential path only, correct imports, framework-idiomatic, 1–2 pages max.
**Correct SaaSKit login+callback example (Node.js/Express):**
```typescript
import { ScalekitClient } from '@scalekit-sdk/node';
import crypto from 'crypto';
const scalekit = new ScalekitClient(
process.env.SCALEKIT_ENVIRONMENT_URL!,
process.env.SCALEKIT_CLIENT_ID!,
process.env.SCALEKIT_CLIENT_SECRET!
);
const REDIRECT_URI = 'https://yourapp.com/auth/callback';
// Login — generate auth URL with CSRF state
app.get('/auth/login', (req, res) => {
const state = crypto.randomBytes(32).toString('base64url');
res.cookie('oauth_state', state, { httpOnly: true, sameSite: 'lax', secure: true });
const authUrl = scalekit.getAuthorizationUrl(REDIRECT_URI, { state });
res.redirect(authUrl);
});
// Callback — validate state, exchange code, store session
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== req.cookies.oauth_state) return res.status(403).send('CSRF mismatch');
const result = await scalekit.authenticateWithCode(code as string, REDIRECT_URI);
req.session.user = { id: result.user.id, email: result.user.email };
req.session.idToken = result.idToken;
res.redirect('/dashboard');
});
// Logout — clear local + end IdP session
app.post('/auth/logout', (req, res) => {
const logoutUrl = scalekit.getLogoutUrl({
idTokenHint: req.session.idToken,
postLogoutRedirectUri: 'https://yourapp.com',
});
req.session.destroy(() => res.redirect(logoutUrl));
});
```
**Mandatory checks before outputting generated code** — cross-reference every SDK call against `references/REFERENCE.md`:
- [ ] Method names exist for the target SDK
- [ ] Parameters match in name, order, and type
- [ ] Import path is exactly correct
- [ ] Environment variable names follow Scalekit conventions
## Step 4 — Review mode
Check these categories in order:
**1. SDK correctness** — Every method name, parameter, import, and return type matches `references/REFERENCE.md`.
**2. Auth flow completeness** — Login has a callback. Callback validates `state`. Logout calls `getLogoutUrl()`. Token refresh exists if `offline_access` is used. IdP-initiated login handled if applicable.
**3. Security** — Cookies: `httpOnly`, `secure`, `sameSite: 'lax'`. State: cryptographically random. Redirects: only relative paths. Secrets: from env vars. Webhooks: signature verified before processing.
**4. Environment** — `SCALEKIT_ENVIRONMENT_URL`, `SCALEKIT_CLIENT_ID`, `SCALEKIT_CLIENT_SECRET`. Redirect URI matches dashboard. Domain format: `https://<subdomain>.scalekit.com`.
**5. Best practices** — Client is singleton. Error handling uses typed exceptions. `window.location.href` for OAuth redirects (not `router.push`).
**Output for each finding:** What's wrong → Why it matters → Corrected code.
## Step 5 — Unknown methods
Resolution order when a method isn't in `references/REFERENCE.md`:
| Priority | Source |
|----------|--------|
| 1 | Embedded `references/REFERENCE.md` |
| 2 | Live SDK reference: `https://raw.githubusercontent.com/scalekit-inc/scalekit-sdk-{node,python,go,java}/main/REFERENCE.md` |
| 3 | REST API: `https://docs.scalekit.com/apis` |
| 4 | State explicitly: "This method could not be verified." |
Never output code containing an unverified method call.
## Documentation
| Resource | URL |
|----------|-----|
| REST API reference | `https://docs.scalekit.com/apis` |
| LLM doc index | `https://docs.scalekit.com/llms.txt` |
| SaaSKit docs | `https://docs.scalekit.com/_llms-txt/saaskit-complete.txt` |
| AgentKit docs | `https://docs.scalekit.com/_llms-txt/agentkit.txt` |
| MCP Auth docs | `https://docs.scalekit.com/_llms-txt/mcp-authentication.txt` |
For framework-specific example repos, see `references/EXAMPLE-REPOS.md`.|
# Self-Hosted Scalekit Deployment
**Before doing anything else**, load the reference files for the detailed procedures:
- `references/quickstart.md` — Evaluation deployment (bundled databases)
- `references/production-deployment.md` — Full production steps, setup script, and portal flow
- `references/configuration.md` — values.yaml field reference and examples
- `references/troubleshooting.md` — Diagnostics and fixes
- `references/upgrades.md` — Upgrades and maintenance
Use this skill when the user needs to stand up their own Scalekit instance on Kubernetes (Helm chart from the distribution portal) instead of using Scalekit Cloud.
## When to use self-hosted
Self-hosted Scalekit runs the full platform (auth service + dashboard) on the user's own Kubernetes cluster via Helm.
Common reasons:
- Data residency requirements
- Network isolation / air-gapped
- Compliance (HIPAA, FedRAMP, etc.)
- "on prem", "self-hosted", "self hosted Scalekit", etc.
**Key difference from cloud**: The `SCALEKIT_ENVIRONMENT_URL` and admin dashboard come from the user's deployment (`https://app.<your-domain>`), not `app.scalekit.com`.
## High-level decision flow
Ask one question at a time:
1. Evaluation (fast, uses bundled Postgres/Redis subcharts) or production (external services)?
2. Target environment (GKE, other cloud K8s, Minikube, air-gapped)?
3. Do they have a domain + TLS + GatewayClass ready?
4. Do they have a registry token from the Scalekit distribution portal?
Then load the matching reference:
- Evaluation → `references/quickstart.md`
- Production → `references/production-deployment.md`
- Configuration details → `references/configuration.md`
- Problems → `references/troubleshooting.md`
- Later maintenance → `references/upgrades.md`
## After the instance is running — integration
- Dashboard at `https://app.<your-domain>`
- `SCALEKIT_ENVIRONMENT_URL` from their deployment
- Credentials from the self-hosted dashboard
Then route to the normal skills with those values:
| Goal | Skill | Self-hosted note |
|------|-------|------------------|
| Login + sessions | `/saaskit:setup` (or framework variant) | Use the self-hosted URL + credentials |
| Production readiness | `/saaskit:production-readiness-saaskit` | Adapt network checks for internal cluster |
| Validate connection | `/saaskit:testing-auth-setup` | Point at self-hosted env |
| SDK / auth errors | `/saaskit:scalekit-code-doctor` | Same code, different env values |
**MCP note**: The plugin tooling MCP (`https://mcp.scalekit.com`) stays cloud. Your app uses the self-hosted `SCALEKIT_ENVIRONMENT_URL`.
## Quick requirements (high level)
- K8s 1.27+
- Postgres 15+ (3 DBs: scalekit, webhooks, openfga)
- Redis 6.2+
- SMTP
- Domain + wildcard DNS + HTTPS (Gateway API preferred)
- Registry token from distribution portal
For the full list and provider specifics, load the references.
**Source of truth**: Official self-hosted docs (`/self-hosted/*`).
---
**Did this help?**
Share the cluster/provider and exact step (or error) if they hit issues. We can refine the references.Validates a Scalekit auth integration by running the dryrun CLI against a live environment. Use when the user says "test my auth", "verify SSO setup", "check my login flow", "dryrun", or wants to confirm their Scalekit credentials and configuration are working.
# Testing Auth Setup
Runs the Scalekit dryrun CLI to validate that your auth integration is correctly configured against a live environment.
## Modes
| Mode | What it tests | When to use |
|------|--------------|-------------|
| `fsa` | Full-stack auth login flow | User is setting up or verifying login, callback, and session handling |
| `sso` | Enterprise SSO flow | User is setting up or verifying SAML/OIDC SSO with an identity provider |
## Prerequisites
Confirm these environment variables are available:
- `SCALEKIT_ENVIRONMENT_URL` — your Scalekit environment URL
- `SCALEKIT_CLIENT_ID` — your client ID from app.scalekit.com > Settings
## Running the test
### Full-stack auth (fsa)
```bash
npx @scalekit-sdk/dryrun --env_url=$SCALEKIT_ENVIRONMENT_URL --client_id=$SCALEKIT_CLIENT_ID --mode=fsa
```
### Enterprise SSO
Requires an `organization_id` — ask for it if not provided.
```bash
npx @scalekit-sdk/dryrun --env_url=$SCALEKIT_ENVIRONMENT_URL --client_id=$SCALEKIT_CLIENT_ID --mode=sso --organization_id=<organization_id>
```
## Choosing the mode
If the user doesn't specify a mode:
1. Check the project context — if there's SSO configuration (identity providers, SAML metadata), suggest `sso`.
2. Otherwise default to `fsa` as the most common starting point.
3. If ambiguous, ask which mode to use.
## After running
- Show the command output.
- Explain what passed and what failed in plain language.
- If the test fails, suggest specific next steps based on the error (missing redirect URI, invalid credentials, organization not found, etc.).
## When to switch skills
- Use `implementing-saaskit` for the initial auth setup.
- Use `implementing-modular-sso` for SSO configuration.
- Use `production-readiness-saaskit` for a full pre-launch review.Guides developers through Scalekit onboarding — installs the CLI, helps choose the right auth plugin (agentkit or saaskit), and walks through plugin setup for their AI coding tool. Use when a developer is new to Scalekit, needs to install the Scalekit plugin for Claude Code, Codex, Copilot CLI, Cursor, or other agents, wants to connect an AI agent to third-party services (Gmail, Slack, Notion, Google Calendar) via OAuth, or wants to add authentication (SSO, SCIM, sessions, RBAC) to a project but hasn't chosen an approach yet.
# Setup Scalekit
## Step 1 — Install the CLI (recommended)
The Scalekit CLI detects your tools and installs the authstack plugin (AgentKit + SaaSKit) for you.
```bash
npx @scalekit-inc/cli setup
```
For repeated use:
```bash
npm install -g @scalekit-inc/cli
scalekit setup
```
Target a specific tool:
```bash
scalekit setup claude
scalekit setup cursor
scalekit setup codex
scalekit setup copilot
```
Verify the plugin appears in your agent's plugin list after setup.
## Step 2 — Choose your plugin
| Plugin | Use case |
|--------|----------|
| `agentkit` | AI agent needs OAuth access to third-party services — connections, tool discovery, token storage / refresh |
| `saaskit` | Web app needs login, sessions, SSO, SCIM, MCP server auth, RBAC, or API keys |
## Step 3 — Native / direct install commands (CLI is preferred)
The commands below are the current native forms. Prefer the CLI in Step 1 for most users.
### Claude Code
```
/plugin marketplace add scalekit-inc/authstack
/plugin install agentkit@authstack # or saaskit@authstack
```
Verify: restart Claude Code, then run `/plugin list` — the plugin should appear as enabled.
### GitHub Copilot
```bash
copilot plugin marketplace add scalekit-inc/authstack
copilot plugin install agentkit@authstack # or saaskit@authstack
```
Verify: `copilot plugin list` should show the plugin.
### Codex and Cursor
The unified CLI (`scalekit setup codex` / `scalekit setup cursor`) handles download and placement for these tools. Direct installation is managed by the CLI.
### Other agents (OpenCode, Windsurf, Cline, Gemini CLI, 35+)
```bash
npx skills add scalekit-inc/authstack --list # see available skills
npx skills add scalekit-inc/authstack --skill integrating-agentkit
npx skills add scalekit-inc/authstack --skill implementing-saaskit
npx skills add scalekit-inc/authstack --all # or install everything
```
## Step 4 — Start building
Describe your goal and the installed skill will guide implementation:
- *"Add OAuth to my MCP server so Claude Desktop can connect"*
- *"Implement login and signup with JWT session management"*
- *"Connect my AI agent to Gmail and Google Calendar"*
- *"Add enterprise SSO to my existing app"*
## Documentation
| Resource | URL | When to use |
|----------|-----|-------------|
| LLM doc index | `https://docs.scalekit.com/llms.txt` | Maps each product to its doc set — start here |
| API reference | `https://docs.scalekit.com/apis` | Full REST API (OpenAPI-generated) |
| Docs sitemap | `https://docs.scalekit.com/sitemap-0.xml` | Find specific guides or pages |Discovers live tools for a Scalekit AgentKit connector and explains their input and output schemas. Use when a user asks what tools are available for Gmail, Slack, Salesforce, or another connector, wants to inspect `input_schema` or `output_schema`, or needs help narrowing the tool set for an agent.
# Discovering Connector Tools
Use live AgentKit metadata as the source of truth for tool names, required inputs, and output schemas.
Do not rely on static connector notes as a complete catalog. Those may lag the live platform.
## Discovery workflow
1. Identify the target connector or exact tool name.
2. Use the Scalekit SDK to fetch live tool metadata (see code below).
3. Summarize:
- tool name
- connector
- what the tool does
- required fields from `input_schema.required`
- optional fields from `input_schema.properties`
- important fields from `output_schema.properties`
4. Recommend the smallest useful tool set for the workflow.
## Live tool discovery (Python)
```python
from scalekit import ScalekitClient
import os
from dotenv import load_dotenv
load_dotenv()
sk_client = ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
)
# List all tools for a provider
tools = sk_client.actions.get_tools(providers=["GMAIL"], page_size=100)
for tool in tools.tools:
print(f"Tool: {tool.name}")
print(f" Description: {tool.description}")
print(f" Input schema: {tool.input_schema}")
print(f" Output schema: {tool.output_schema}")
# Get a specific tool by name
tool = sk_client.actions.get_tools(tool_name="gmail_fetch_mails")
```
## Live tool discovery (Node.js)
```typescript
import { ScalekitClient } from '@scalekit-sdk/node';
import 'dotenv/config';
const client = new ScalekitClient(
process.env.SCALEKIT_ENVIRONMENT_URL!,
process.env.SCALEKIT_CLIENT_ID!,
process.env.SCALEKIT_CLIENT_SECRET!
);
// List all tools for a provider
const tools = await client.actions.getTools({ providers: ['GMAIL'], pageSize: 100 });
for (const tool of tools.tools) {
console.log(`Tool: ${tool.name}`);
console.log(` Description: ${tool.description}`);
}
// Get a specific tool by name
const tool = await client.actions.getTools({ toolName: 'gmail_fetch_mails' });
```
## Terminology
- `connector`: Gmail, Slack, Salesforce, Notion, or a custom connector
- `connection`: the exact dashboard configuration name used for authorization
- `connected account`: the per-user authorized record
- `tool`: the executable action exposed by a connector
Use `connector` in explanations. Only use `provider` when the SDK or API filter field literally expects that name.
## Key rules
- `connection_name` is the exact dashboard value — may not equal the connector slug
- Always use live tool metadata, not static docs
- Restrict the tool set before handing to an LLM — fewer relevant tools improve selection accuracy
- **Before executing any tool**: verify the connected account status is `ACTIVE`. Tool execution fails silently or errors if the account is not yet authorized.
**If `get_tools` returns empty:** verify the connector is configured in the dashboard and the connection name matches exactly.
## Deep reference
- AgentKit overview: [docs.scalekit.com/agentkit/overview](https://docs.scalekit.com/agentkit/overview/)
- Tool discovery: [docs.scalekit.com/agentkit/tool-discovery](https://docs.scalekit.com/agentkit/tool-discovery/)
- Connectors catalog: [docs.scalekit.com/agentkit/connectors](https://docs.scalekit.com/agentkit/connectors/)
## When to switch skills
- Use `integrating-agentkit` for the full integration workflow (create account, authorize, execute).
- Use the Scalekit MCP server (`https://mcp.scalekit.com`) to validate a tool call interactively.
- Use `exposing-agentkit-via-mcp` to expose discovered tools over MCP.Guides developers through configuring a Scalekit AgentKit MCP endpoint with authenticated tool access. Use when exposing AgentKit tools over MCP, generating per-user MCP URLs, or connecting AI agents via LangChain or LangGraph MCP adapters.
# Exposing AgentKit via MCP
Scalekit lets you configure MCP endpoints that manage authentication, create personalized access URLs for users, and define which AgentKit tools are accessible. You can also bundle several toolkits (e.g., Gmail + Google Calendar) within a single endpoint.
[Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) is an open-source standard that enables AI systems to interface with external tools and data sources. Where the `integrating-agentkit` skill uses the SDK directly, this workflow configures AgentKit to expose tools over the MCP protocol so any compliant client — LangChain, Claude Desktop, MCP Inspector — can consume them.
> **Note:** AgentKit MCP servers only support Streamable HTTP transport.
## What you'll build
1. A Scalekit MCP server that fetches the user's latest email and creates a reminder calendar event
2. A LangGraph agent that connects to this server via `langchain-mcp-adapters` and invokes the tools
## Prerequisites
- [ ] **Scalekit credentials**: [app.scalekit.com](https://app.scalekit.com) → Settings → Copy `SCALEKIT_CLIENT_ID`, `SCALEKIT_CLIENT_SECRET`, `SCALEKIT_ENVIRONMENT_URL`
- [ ] **OpenAI API key**: `OPENAI_API_KEY`
> **Gmail is the only connector that does not require dashboard setup.** All other connectors (including Google Calendar) must be created in the Scalekit Dashboard before use:
>
> Go to **Scalekit Dashboard → AgentKit → Connections → + Create Connection → Select connector** → Set `Connection Name` → Save
> **Important**: The **Connection Name** you set in the dashboard is exactly what you use as the `connection_name` parameter in your code. They must match exactly.
For this example, create the Google Calendar connector:
- [ ] **Google Calendar connector**: Scalekit Dashboard → AgentKit → Connections → Create Connection → Google Calendar → `Connection Name = MY_CALENDAR` → Save
## Step 1 — Set up your environment
Install dependencies:
```bash
pip install scalekit-sdk-python langgraph>=0.6.5 langchain-mcp-adapters>=0.1.9 python-dotenv>=1.0.1 openai>=1.53.0 requests>=2.32.3
```
Add these imports to `main.py`:
```python
import os
import asyncio
from dotenv import load_dotenv
from scalekit import ScalekitClient
from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping
from scalekit.actions.types import GetMcpInstanceAuthStateResponse
from langgraph.prebuilt import create_react_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
```
Set the OpenAI key in your environment:
```bash
export OPENAI_API_KEY=xxxxxx
```
Initialize the Scalekit client:
```python
load_dotenv()
sk_client = ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
)
my_mcp = sk_client.actions.mcp
```
## Step 2 — Create an MCP config and server instance
Define the MCP config with `connection_tool_mappings` — each entry maps a connector to the tools it exposes:
```python
cfg_response = my_mcp.create_config(
name="reminder-manager",
description="Summarizes latest email and creates a reminder event",
connection_tool_mappings=[
# Gmail works directly — no dashboard setup required
McpConfigConnectionToolMapping(
connection_name="gmail",
tools=[
"gmail_fetch_mails",
],
),
# Google Calendar must be created in dashboard first
McpConfigConnectionToolMapping(
connection_name="MY_CALENDAR",
tools=[
"googlecalendar_create_event",
],
),
],
)
config_name = cfg_response.config.name
```
Create a server instance for a specific user (`john-doe`). Each user gets their own instance URL:
```python
inst_response = my_mcp.ensure_instance(
config_name=config_name,
user_identifier="john-doe",
)
mcp_url = inst_response.instance.url
print("Instance URL:", mcp_url)
```
## Step 3 — Authenticate the user
Retrieve auth state and print any OAuth links the user needs to visit:
```python
auth_state_response = my_mcp.get_instance_auth_state(
instance_id=inst_response.instance.id,
include_auth_links=True,
)
for conn in getattr(auth_state_response, "connections", []):
print(
"Connection:", conn.connection_name,
" Provider:", conn.provider,
" Auth Link:", conn.authentication_link,
" Status:", conn.connected_account_status,
)
```
> **Note:** Open every printed auth link in a browser and complete OAuth before proceeding to Step 4.
## Step 4 — Connect and invoke via MCP
Use `MultiServerMCPClient` with `streamable_http` transport, load the tools, and run the agent:
```python
async def main():
client = MultiServerMCPClient(
{
"reminder_demo": {
"transport": "streamable_http",
"url": mcp_url,
},
}
)
tools = await client.get_tools()
agent = create_react_agent("openai:gpt-4.1", tools)
response = await agent.ainvoke(
{"messages": "get 1 latest email and create a calendar reminder event in next 15 mins for a duration of 15 mins."}
)
print(response)
asyncio.run(main())
```
> **Note — MCP client compatibility:** You can test this MCP server with popular clients like MCP Inspector, Claude Desktop, and other spec-compliant implementations. Note that ChatGPT's beta connector feature may not work properly as it's still in beta and doesn't fully adhere to the MCP specification yet.
Full working example: [github.com/scalekit-inc/python-connect-demos/tree/main/mcp](https://github.com/scalekit-inc/python-connect-demos/tree/main/mcp)
## Deep reference
- AgentKit overview: [docs.scalekit.com/agentkit/overview](https://docs.scalekit.com/agentkit/overview/)
- Connections: [docs.scalekit.com/agentkit/connections](https://docs.scalekit.com/agentkit/connections/)
- Connected accounts: [docs.scalekit.com/agentkit/connected-accounts](https://docs.scalekit.com/agentkit/connected-accounts/)
- Tool discovery: [docs.scalekit.com/agentkit/tool-discovery](https://docs.scalekit.com/agentkit/tool-discovery/)
## When to switch skills
- Use `integrating-agentkit` for direct SDK integration without MCP.
- Use `discovering-connector-tools` when the user needs the current tool catalog or schema.
- Use the Scalekit MCP server (`https://mcp.scalekit.com`) to validate a tool call interactively.Integrates Scalekit AgentKit into a project so an agent can create connections, authorize users, discover tools, and execute authenticated tool calls on their behalf. Use when a user needs to set up a connection, create a connected account, generate an authorization link, or wire AgentKit tools into application code or an agent framework.
# AgentKit Integration
Scalekit handles the full OAuth lifecycle — authorization, token storage, and refresh — so agents can act on behalf of users in Gmail, Slack, Notion, Calendar, and other connectors.
**Required env vars**: `SCALEKIT_CLIENT_ID`, `SCALEKIT_CLIENT_SECRET`, `SCALEKIT_ENVIRONMENT_URL`
→ Get from [app.scalekit.com](https://app.scalekit.com): Developers → Settings → API Credentials
**Key concept — `connection_name`**: Every connector has a `connection_name` — the exact string set in the Scalekit Dashboard when creating the connection. It is used in all SDK calls (`get_or_create_connected_account`, `get_authorization_link`, `get_connected_account`). It may differ from the connector slug (e.g., the connector is "gmail" but the `connection_name` could be `"MY_GMAIL_PROD"`). Always use the exact dashboard value.
## Setup
Install the SDK and initialize the client:
> **Important**: Except for Gmail, all connectors must be configured in the Scalekit Dashboard first before creating authorization URLs.
>
> To set up a connector: **Scalekit Dashboard → AgentKit → Connections → + Create Connection → Select connector → Set Connection Name → Save**
<tabs>
**Python**
```bash
pip install scalekit-sdk-python
```
```python
from scalekit import ScalekitClient
import os
from dotenv import load_dotenv
load_dotenv()
sk_client = ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
)
actions = sk_client.actions
```
**Node.js**
```bash
npm install @scalekit-sdk/node
```
```typescript
import { ScalekitClient } from '@scalekit-sdk/node';
import 'dotenv/config';
const scalekitClient = new ScalekitClient(
process.env.SCALEKIT_ENVIRONMENT_URL!,
process.env.SCALEKIT_CLIENT_ID!,
process.env.SCALEKIT_CLIENT_SECRET!
);
const { connectedAccounts } = scalekitClient;
```
</tabs>
## Integration workflow
> **Gmail works without dashboard setup.** All other connectors must be configured first: **Dashboard → AgentKit → Connections → + Create Connection**. The **Connection Name** in the dashboard must match `connection_name` in code exactly.
Copy this checklist:
```
AgentKit Integration Progress:
- [ ] Step 1: SDK installed and client initialized
- [ ] Step 2: Connected account created for the user
- [ ] Step 3: User has authorized the connection (status = ACTIVE)
- [ ] Step 4: Access token fetched successfully
- [ ] Step 5: Downstream API call succeeds with fetched token
```
### Step 1 — Create a connected account
Replace `"user_123"` with the project's actual user ID. Replace `"gmail"` with the target connector.
**Python**
```python
response = actions.get_or_create_connected_account(
connection_name="gmail",
identifier="user_123"
)
connected_account = response.connected_account
```
**Node.js**
```typescript
const response = await connectedAccounts.getOrCreateConnectedAccount({
connector: 'gmail',
identifier: 'user_123',
});
const connectedAccount = response.connectedAccount;
```
### Step 2 — Authorize the user
If status is not `ACTIVE`, the user must complete OAuth. In a web app, redirect to `link`. In CLI/dev, print and wait.
**Python**
```python
if connected_account.status != "ACTIVE":
link_response = actions.get_authorization_link(
connection_name="gmail",
identifier="user_123"
)
print("Authorize here:", link_response.link)
input("Press Enter after authorizing...")
```
**Node.js**
```typescript
if (connectedAccount?.status !== 'ACTIVE') {
const linkResponse = await connectedAccounts.getMagicLinkForConnectedAccount({
connector: 'gmail',
identifier: 'user_123',
});
console.log('Authorize here:', linkResponse.link);
// Web app: redirect user to linkResponse.link
}
```
### Step 3 — Fetch OAuth tokens
ALWAYS call `get_connected_account` immediately before any API call — Scalekit auto-refreshes tokens and this guarantees the latest valid token.
**Python**
```python
response = actions.get_connected_account(
connection_name="gmail",
identifier="user_123"
)
tokens = response.connected_account.authorization_details["oauth_token"]
access_token = tokens["access_token"]
refresh_token = tokens["refresh_token"]
```
**Node.js**
```typescript
const accountResponse = await connectedAccounts.getConnectedAccountByIdentifier({
connector: 'gmail',
identifier: 'user_123',
});
const authDetails = accountResponse?.connectedAccount?.authorizationDetails;
const accessToken = authDetails?.details?.case === 'oauthToken'
? authDetails.details.value?.accessToken : undefined;
const refreshToken = authDetails?.details?.case === 'oauthToken'
? authDetails.details.value?.refreshToken : undefined;
```
### Step 4 — Call the third-party API
Use `access_token` from Step 3 as a Bearer token. Example: fetch 5 unread Gmail messages.
**Python**
```python
import requests
headers = {"Authorization": f"Bearer {access_token}"}
list_url = "https://gmail.googleapis.com/gmail/v1/users/me/messages"
messages = requests.get(
list_url, headers=headers, params={"q": "is:unread", "maxResults": 5}
).json().get("messages", [])
for msg in messages:
data = requests.get(
f"{list_url}/{msg['id']}", headers=headers,
params={"format": "metadata", "metadataHeaders": ["From", "Subject", "Date"]}
).json()
hdrs = data.get("payload", {}).get("headers", [])
print(next((h["value"] for h in hdrs if h["name"] == "Subject"), "No Subject"))
print(next((h["value"] for h in hdrs if h["name"] == "From"), "Unknown"))
print(data.get("snippet", ""))
print("-" * 50)
```
**Node.js**
```typescript
const listUrl = 'https://gmail.googleapis.com/gmail/v1/users/me/messages';
const params = new URLSearchParams({ q: 'is:unread', maxResults: '5' });
const { messages = [] } = await fetch(`${listUrl}?${params}`, {
headers: { Authorization: `Bearer ${accessToken}` },
}).then(r => r.json());
for (const msg of messages) {
const msgData = await fetch(
`${listUrl}/${msg.id}?format=metadata&metadataHeaders=From&metadataHeaders=Subject&metadataHeaders=Date`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
).then(r => r.json());
const h = msgData.payload?.headers ?? [];
console.log('Subject:', h.find(x => x.name === 'Subject')?.value ?? 'No Subject');
console.log('From:', h.find(x => x.name === 'From')?.value ?? 'Unknown');
console.log('Snippet:', msgData.snippet ?? '');
console.log('-'.repeat(50));
}
```
## Adapting to other connectors
Replace `"gmail"` with any supported connector name: `slack`, `notion`, `calendar`, etc.
The SDK workflow (Steps 1–3) is identical for all connectors. Only the downstream API call (Step 4) changes.
For connector-specific API details, see the [Scalekit Connectors catalog](https://docs.scalekit.com/agentkit/connectors/).
## Building agents
Use Scalekit tools with AI frameworks to build agents that can execute actions on behalf of users.
### LangChain agents
Create conversational agents with LangChain that can autonomously call Scalekit tools based on user intent.
**Python**
```python
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.prompts import ChatPromptTemplate
# Fetch tools from Scalekit in LangChain format
tools = actions.langchain.get_tools(
identifier="user_123",
providers=["GMAIL"],
page_size=100
)
# Define the agent prompt
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant with access to external tools."),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
# Create and run the agent
llm = ChatOpenAI(model="gpt-4o")
agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = executor.invoke({"input": "fetch my last 5 unread emails and summarize them"})
```
### Google ADK agents
Build agents using Google's Agent Development Kit with native Gemini integration.
**Python**
```python
from google.adk.agents import Agent
# Fetch tools from Scalekit in Google ADK format
gmail_tools = actions.google.get_tools(
providers=["GMAIL"],
identifier="user_123",
page_size=100
)
# Create the agent
agent = Agent(
name="gmail_assistant",
model="gemini-2.5-flash",
description="Gmail assistant that can read and manage emails",
instruction="You are a helpful Gmail assistant that can read, send, and organize emails.",
tools=gmail_tools
)
# Run the agent
response = agent.process_request("fetch my last 5 unread emails and summarize them")
```
For more examples and framework-specific patterns, see the [AgentKit code samples](https://docs.scalekit.com/agentkit/code-samples/).
## Deep reference
- AgentKit overview: [docs.scalekit.com/agentkit/overview](https://docs.scalekit.com/agentkit/overview/)
- Connections: [docs.scalekit.com/agentkit/connections](https://docs.scalekit.com/agentkit/connections/)
- Connected accounts: [docs.scalekit.com/agentkit/connected-accounts](https://docs.scalekit.com/agentkit/connected-accounts/)
- Tool discovery: [docs.scalekit.com/agentkit/tool-discovery](https://docs.scalekit.com/agentkit/tool-discovery/)
- Connectors catalog: [docs.scalekit.com/agentkit/connectors](https://docs.scalekit.com/agentkit/connectors/)
- BYOC (Bring Your Own Credentials): [docs.scalekit.com/agentkit/byoc](https://docs.scalekit.com/agentkit/launch-checklist/byoc/)
## When to switch skills
- Use `discovering-connector-tools` when the user needs the current tool catalog or schema.
- Use the Scalekit MCP server (`https://mcp.scalekit.com`) to validate a tool call interactively.
- Use `exposing-agentkit-via-mcp` when the user wants AgentKit tools exposed over MCP.Validates OAuth token flows, audits token storage security, verifies per-connector authorization, and checks monitoring configuration for Scalekit AgentKit implementations before production launch. Use when going live, doing a pre-launch review, or verifying AgentKit authorization and tool-calling setup is production-ready.
# Scalekit AgentKit Production Readiness
Work through each section in order — earlier sections are blockers for later ones.
---
## Quick checks (run first)
```bash
# Confirm production credentials are set (not dev/staging)
echo $SCALEKIT_ENVIRONMENT_URL # should be https://<subdomain>.scalekit.com (not .scalekit.dev)
echo $SCALEKIT_CLIENT_ID # should be set
echo $SCALEKIT_CLIENT_SECRET # should be set
# Verify token endpoint works
curl -s -o /dev/null -w "%{http_code}" -X POST "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
-d "client_id=$SCALEKIT_CLIENT_ID&client_secret=$SCALEKIT_CLIENT_SECRET&grant_type=client_credentials"
# Expected: 200
```
- [ ] HTTPS enforced on all auth endpoints
- [ ] API credentials in environment variables — `grep -r "skc_" src/` returns nothing
- [ ] Redirect URIs registered in dashboard match exactly what the app sends
---
## OAuth token flows
- [ ] Test authorization URL generation with correct scopes
- [ ] Validate `state` parameter in callbacks (CSRF protection)
- [ ] Test authorization code exchange for access + refresh tokens
- [ ] Verify access tokens are stored securely (not in localStorage or logs)
- [ ] Test automatic token refresh before expiry
- [ ] Verify token refresh handles concurrent requests correctly (no race conditions)
- [ ] Test behavior when refresh token expires — user prompted to re-authorize
- [ ] Verify revocation on logout clears stored tokens
**Per connected service:**
- [ ] Test OAuth flow end-to-end for each service (Gmail, Slack, Notion, etc.)
- [ ] Verify correct scopes requested — request minimum required
- [ ] Test API calls with valid access token succeed
- [ ] Test API calls with expired token trigger refresh correctly
- [ ] Test behavior on permission denied (user revoked access in the third-party app)
---
## Security
- [ ] Access tokens never logged or exposed in error messages
- [ ] Refresh tokens stored encrypted at rest
- [ ] Token storage scoped per user — no cross-user token access possible
- [ ] Webhook/callback endpoint validates signatures (if applicable)
---
## Monitoring and incident readiness
- [ ] Auth logs monitoring configured in **Dashboard > Auth Logs**
- [ ] Error tracking configured for OAuth failures and token refresh errors
- [ ] Alerts configured for repeated authorization failures
- [ ] Log retention policies configured
- [ ] Incident response runbook written (who to contact, how to revoke compromised tokens)
**Key metrics:** Token refresh success/failure rate, OAuth completion rate (initiated vs completed), per-service API error rates, token expiry distribution.
## Final smoke test
Run the full cycle in staging with production credentials:
1. Create a connected account for a test user → verify status returned
2. Generate auth link → complete OAuth → verify status is `ACTIVE`
3. Fetch access token → make a downstream API call → verify success
4. Wait for token expiry → re-fetch → verify auto-refresh works
5. Revoke access in the third-party app → verify graceful error handlingStarting point for any Scalekit AgentKit integration. Use when the user says "I want to add agent auth", "set up AgentKit", "where do I start", or is new to AgentKit and doesn't know which skill to use. Routes to the right skill based on what they're building.
# AgentKit — Where to Start
> **IMPORTANT:** This skill routes to the right skill — it does NOT implement the integration itself. Once you identify the right skill below, tell the user to invoke it and stop. Do not generate implementation code here.
---
## Step 1: Determine what to build
If answers aren't already clear from context, ask one question at a time:
1. **What are you building?**
- New agent that needs to call third-party tools on behalf of users (Gmail, Slack, Salesforce, etc.)
- Existing agent — adding connector access or fixing auth
- MCP server that exposes AgentKit tools
2. **What's your current state?**
- Starting from scratch
- Have a Scalekit account and environment already
- Have AgentKit set up, stuck on a specific step
---
## Step 2: Tell the user exactly which skill to invoke
Pick the best match and tell the user: "Run `/agentkit:<skill>` to get started."
| What you're building | Tell them to run |
|---|---|
| New agent calling third-party tools (Gmail, Slack, Salesforce…) on behalf of users | `/agentkit:integrating-agentkit` |
| Discover tools available for a connector, inspect schemas | `/agentkit:discovering-connector-tools` |
| Expose AgentKit tools over MCP for Claude Desktop, Cursor, VS Code | `/agentkit:exposing-agentkit-via-mcp` |
| Pre-launch checklist, going to production | `/agentkit:production-readiness-agentkit` |
| SDK errors, wrong imports, broken auth calls | `/saaskit:scalekit-code-doctor` |
After telling the user which skill to run, **stop**. The target skill handles implementation.
---
## Step 3: Environment setup (if new project)
Before starting any skill, verify credentials exist:
```bash
SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com
SCALEKIT_CLIENT_ID=<from dashboard>
SCALEKIT_CLIENT_SECRET=<from dashboard>
```
Get these from [app.scalekit.com](https://app.scalekit.com) → Developers → Settings → API Credentials.
The Scalekit MCP server (`https://mcp.scalekit.com`) is pre-configured in `.mcp.json`. Claude Code handles OAuth 2.1 auth automatically — no additional setup needed.
---
## Core AgentKit concepts (30-second orientation)
| Concept | What it is |
|---|---|
| **Connector** | A third-party app (Gmail, Slack, Salesforce, GitHub, etc.) |
| **Connection** | Your app's agreement with a connector (configured in dashboard) |
| **Connected account** | A specific user's authorization to use a connection |
| **Tool** | An action the agent can take (send email, create issue, etc.) |
Flow: User authorizes → connected account created → agent discovers tools → agent executes tool calls using that account.
**Dashboard setup note:** Gmail works without extra configuration. All other connectors (Slack, Salesforce, GitHub, Google Calendar, etc.) must be enabled and configured in the Scalekit Dashboard before users can connect them.
---
## When to switch skills
- **Already know what you need?** Skip this skill and invoke the target directly.
- **SDK errors?** Use `/saaskit:scalekit-code-doctor`.
- **Want to add B2B auth (login, SSO, SCIM) to your app?** Switch to the `saaskit` plugin: `/saaskit:setup`.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.
# Adding API Key Auth (Scalekit)
## Flow overview
```
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.
---
## 1. Initialize the client
```python
# 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"],
)
```
```javascript
// 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
// Go
scalekitClient := scalekit.NewScalekitClient(
os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
os.Getenv("SCALEKIT_CLIENT_ID"),
os.Getenv("SCALEKIT_CLIENT_SECRET"),
)
```
```java
// 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
# 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
```
```javascript
// Node.js
const response = await scalekit.token.createToken(organizationId, {
description: 'CI/CD pipeline token',
});
const opaqueToken = response.token;
const tokenId = response.tokenId;
```
```go
// Go
response, err := scalekitClient.Token().CreateToken(
ctx, organizationId, scalekit.CreateTokenOptions{
Description: "CI/CD pipeline token",
},
)
opaqueToken := response.Token
tokenId := response.TokenId
```
```java
// 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
# 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",
)
```
```javascript
// Node.js
const response = await scalekit.token.createToken(organizationId, {
userId: 'usr_12345',
customClaims: { team: 'engineering', environment: 'production' },
description: 'Deployment service token',
});
```
```go
// 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
// 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
# 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
```
```javascript
// 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
// 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
// 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
# 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",
)
```
```javascript
// 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
# Python — by token string or token_id
scalekit_client.tokens.invalidate_token(token=opaque_token)
# or
scalekit_client.tokens.invalidate_token(token=token_id)
```
```javascript
// Node.js
await scalekit.token.invalidateToken(opaqueToken); // or tokenId
```
```go
// Go
_ = scalekitClient.Token().InvalidateToken(ctx, opaqueToken) // or tokenId
```
```java
// Java
scalekitClient.tokens().invalidate(opaqueToken); // or tokenId
```
---
## 6. Middleware pattern (protect endpoints)
```python
# 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)
```
```javascript
// 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
// 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)
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
# 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:
```bash
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
# 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
```
```javascript
// 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
# 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
```
```javascript
// 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.