From 24b738a6f2f85169f8a77ceabd7a0a73407d53cc Mon Sep 17 00:00:00 2001 From: Saif Ali Shaik Date: Tue, 16 Jun 2026 22:12:29 +0530 Subject: [PATCH] v2.0.0 --- .agents/plugins/marketplace.json | 32 + .claude-plugin/marketplace.json | 25 + .cursor-plugin/marketplace.json | 28 + .github/plugin/marketplace.json | 25 + .gitignore | 1 + README.md | 2 + plugins/agentkit/.claude-plugin/plugin.json | 13 + plugins/agentkit/.codex-plugin/plugin.json | 24 + plugins/agentkit/.cursor-plugin/plugin.json | 16 + plugins/agentkit/.github/plugin/plugin.json | 22 + plugins/agentkit/.mcp.json | 8 + plugins/agentkit/mcp.json | 8 + .../discovering-connector-tools/SKILL.md | 102 +++ .../skills/exposing-agentkit-via-mcp/SKILL.md | 171 ++++++ .../skills/integrating-agentkit/SKILL.md | 289 +++++++++ .../production-readiness-agentkit/SKILL.md | 78 +++ plugins/agentkit/skills/setup/SKILL.md | 79 +++ plugins/saaskit/.claude-plugin/plugin.json | 13 + plugins/saaskit/.codex-plugin/plugin.json | 24 + plugins/saaskit/.cursor-plugin/plugin.json | 16 + plugins/saaskit/.github/plugin/plugin.json | 31 + plugins/saaskit/.mcp.json | 8 + plugins/saaskit/mcp.json | 8 + .../saaskit/skills/adding-api-auth/SKILL.md | 511 +++++++++++++++ .../saaskit/skills/adding-mcp-oauth/SKILL.md | 319 ++++++++++ .../adding-mcp-oauth/express-reference.md | 144 +++++ .../adding-mcp-oauth/fastapi-reference.md | 161 +++++ .../adding-mcp-oauth/fastmcp-reference.md | 136 ++++ .../implementing-access-control/SKILL.md | 135 ++++ .../skills/implementing-modular-sso/SKILL.md | 581 ++++++++++++++++++ .../implementing-saaskit-nextjs/SKILL.md | 237 +++++++ .../implementing-saaskit-python/SKILL.md | 111 ++++ .../django-reference.md | 250 ++++++++ .../flask-reference.md | 224 +++++++ .../skills/implementing-saaskit/SKILL.md | 120 ++++ .../implementing-saaskit/go-reference.md | 386 ++++++++++++ .../implementing-saaskit/laravel-reference.md | 208 +++++++ .../springboot-reference.md | 232 +++++++ .../implementing-scim-provisioning/SKILL.md | 251 ++++++++ .../skills/managing-saaskit-sessions/SKILL.md | 349 +++++++++++ .../migrating-to-saaskit/AUDIT-CHECKLIST.md | 33 + .../migrating-to-saaskit/IMPORT-SAMPLES.md | 108 ++++ .../skills/migrating-to-saaskit/SKILL.md | 157 +++++ .../production-readiness-saaskit/SKILL.md | 131 ++++ .../skills/scalekit-code-doctor/SKILL.md | 125 ++++ .../references/COMMON-MISTAKES.md | 479 +++++++++++++++ .../references/EXAMPLE-REPOS.md | 61 ++ .../references/REFERENCE.md | 504 +++++++++++++++ plugins/saaskit/skills/setup/SKILL.md | 86 +++ .../skills/testing-auth-setup/SKILL.md | 58 ++ skills/setup-scalekit/SKILL.md | 83 +++ 51 files changed, 7203 insertions(+) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/marketplace.json create mode 100644 .cursor-plugin/marketplace.json create mode 100644 .github/plugin/marketplace.json create mode 100644 .gitignore create mode 100644 plugins/agentkit/.claude-plugin/plugin.json create mode 100644 plugins/agentkit/.codex-plugin/plugin.json create mode 100644 plugins/agentkit/.cursor-plugin/plugin.json create mode 100644 plugins/agentkit/.github/plugin/plugin.json create mode 100644 plugins/agentkit/.mcp.json create mode 100644 plugins/agentkit/mcp.json create mode 100644 plugins/agentkit/skills/discovering-connector-tools/SKILL.md create mode 100644 plugins/agentkit/skills/exposing-agentkit-via-mcp/SKILL.md create mode 100644 plugins/agentkit/skills/integrating-agentkit/SKILL.md create mode 100644 plugins/agentkit/skills/production-readiness-agentkit/SKILL.md create mode 100644 plugins/agentkit/skills/setup/SKILL.md create mode 100644 plugins/saaskit/.claude-plugin/plugin.json create mode 100644 plugins/saaskit/.codex-plugin/plugin.json create mode 100644 plugins/saaskit/.cursor-plugin/plugin.json create mode 100644 plugins/saaskit/.github/plugin/plugin.json create mode 100644 plugins/saaskit/.mcp.json create mode 100644 plugins/saaskit/mcp.json create mode 100644 plugins/saaskit/skills/adding-api-auth/SKILL.md create mode 100644 plugins/saaskit/skills/adding-mcp-oauth/SKILL.md create mode 100644 plugins/saaskit/skills/adding-mcp-oauth/express-reference.md create mode 100644 plugins/saaskit/skills/adding-mcp-oauth/fastapi-reference.md create mode 100644 plugins/saaskit/skills/adding-mcp-oauth/fastmcp-reference.md create mode 100644 plugins/saaskit/skills/implementing-access-control/SKILL.md create mode 100644 plugins/saaskit/skills/implementing-modular-sso/SKILL.md create mode 100644 plugins/saaskit/skills/implementing-saaskit-nextjs/SKILL.md create mode 100644 plugins/saaskit/skills/implementing-saaskit-python/SKILL.md create mode 100644 plugins/saaskit/skills/implementing-saaskit-python/django-reference.md create mode 100644 plugins/saaskit/skills/implementing-saaskit-python/flask-reference.md create mode 100644 plugins/saaskit/skills/implementing-saaskit/SKILL.md create mode 100644 plugins/saaskit/skills/implementing-saaskit/go-reference.md create mode 100644 plugins/saaskit/skills/implementing-saaskit/laravel-reference.md create mode 100644 plugins/saaskit/skills/implementing-saaskit/springboot-reference.md create mode 100644 plugins/saaskit/skills/implementing-scim-provisioning/SKILL.md create mode 100644 plugins/saaskit/skills/managing-saaskit-sessions/SKILL.md create mode 100644 plugins/saaskit/skills/migrating-to-saaskit/AUDIT-CHECKLIST.md create mode 100644 plugins/saaskit/skills/migrating-to-saaskit/IMPORT-SAMPLES.md create mode 100644 plugins/saaskit/skills/migrating-to-saaskit/SKILL.md create mode 100644 plugins/saaskit/skills/production-readiness-saaskit/SKILL.md create mode 100644 plugins/saaskit/skills/scalekit-code-doctor/SKILL.md create mode 100644 plugins/saaskit/skills/scalekit-code-doctor/references/COMMON-MISTAKES.md create mode 100644 plugins/saaskit/skills/scalekit-code-doctor/references/EXAMPLE-REPOS.md create mode 100644 plugins/saaskit/skills/scalekit-code-doctor/references/REFERENCE.md create mode 100644 plugins/saaskit/skills/setup/SKILL.md create mode 100644 plugins/saaskit/skills/testing-auth-setup/SKILL.md create mode 100644 skills/setup-scalekit/SKILL.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..db77806 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,32 @@ +{ + "name": "scalekit-authstack", + "interface": { + "displayName": "Scalekit AuthStack" + }, + "plugins": [ + { + "name": "agentkit", + "source": { + "source": "local", + "path": "./plugins/agentkit" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Authentication" + }, + { + "name": "saaskit", + "source": { + "source": "local", + "path": "./plugins/saaskit" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Authentication" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..f6adf51 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", + "name": "scalekit-authstack", + "description": "Auth infrastructure for developers — connect AI agents to 100+ OAuth services, or add enterprise SSO, SCIM, and MCP auth to your B2B app. Skip the protocol layer.", + "owner": { + "name": "Scalekit Inc", + "email": "support@scalekit.com" + }, + "plugins": [ + { + "name": "agentkit", + "description": "Give your AI agent authenticated access to Gmail, Slack, Salesforce, and 100+ services. OAuth flows, token vault, and tool discovery handled — write agent logic, not auth plumbing.", + "source": "./plugins/agentkit", + "category": "AI Agent Auth", + "homepage": "https://docs.scalekit.com/agentkit/overview/" + }, + { + "name": "saaskit", + "description": "Enterprise auth for B2B apps without writing protocols. Login, sessions, SSO (Okta, Azure AD, Google), SCIM, MCP OAuth, RBAC, and API keys — any framework, any stack.", + "source": "./plugins/saaskit", + "category": "B2B App Auth", + "homepage": "https://docs.scalekit.com/authenticate/fsa/quickstart/" + } + ] +} diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json new file mode 100644 index 0000000..bddf65a --- /dev/null +++ b/.cursor-plugin/marketplace.json @@ -0,0 +1,28 @@ +{ + "name": "scalekit-authstack", + "owner": { + "name": "Scalekit Inc.", + "email": "support@scalekit.com" + }, + "metadata": { + "description": "Auth infrastructure for developers — connect AI agents to 100+ OAuth services, or add enterprise SSO, SCIM, and MCP auth to your B2B app. Skip the protocol layer.", + "version": "1.0.0", + "pluginRoot": "plugins" + }, + "plugins": [ + { + "name": "agentkit", + "source": "agentkit", + "description": "Connect your AI agent to 3,000+ services without writing OAuth plumbing — users authorize once, agents act on their behalf.", + "category": "AI Agent Auth", + "homepage": "https://docs.scalekit.com/agentkit/overview/" + }, + { + "name": "saaskit", + "source": "saaskit", + "description": "Enterprise auth for B2B apps without writing protocols — login, sessions, SSO, SCIM, MCP OAuth, and RBAC in one stack.", + "category": "B2B App Auth", + "homepage": "https://docs.scalekit.com" + } + ] +} diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json new file mode 100644 index 0000000..b95d2b0 --- /dev/null +++ b/.github/plugin/marketplace.json @@ -0,0 +1,25 @@ +{ + "name": "scalekit-authstack", + "owner": { + "name": "Scalekit Inc.", + "email": "support@scalekit.com" + }, + "metadata": { + "description": "Auth infrastructure for developers — connect AI agents to 100+ OAuth services, or add enterprise SSO, SCIM, and MCP auth to your B2B app. Skip the protocol layer.", + "version": "1.0.0" + }, + "plugins": [ + { + "name": "agentkit", + "description": "Connect your AI agent to 3,000+ services without writing OAuth plumbing — users authorize once, agents act on their behalf.", + "version": "2.0.0", + "source": "./plugins/agentkit" + }, + { + "name": "saaskit", + "description": "Enterprise auth for B2B apps without writing protocols — login, sessions, SSO, SCIM, MCP OAuth, and RBAC in one stack.", + "version": "2.0.0", + "source": "./plugins/saaskit" + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c816185 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.claude \ No newline at end of file diff --git a/README.md b/README.md index 3a0228a..2f94bbe 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # authstack + +Claude Code plugin scaffolded by doraval. \ No newline at end of file diff --git a/plugins/agentkit/.claude-plugin/plugin.json b/plugins/agentkit/.claude-plugin/plugin.json new file mode 100644 index 0000000..ff55e80 --- /dev/null +++ b/plugins/agentkit/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "agentkit", + "description": "Give your AI agent authenticated access to Gmail, Slack, Salesforce, and 100+ services. OAuth flows, token vault, and tool discovery handled — write agent logic, not auth plumbing.", + "version": "2.0.0", + "author": { + "name": "Scalekit Inc.", + "email": "hi@scalekit.com" + }, + "homepage": "https://docs.scalekit.com/agentkit/overview/", + "repository": "https://github.com/scalekit-inc/authstack", + "license": "MIT", + "keywords": ["scalekit", "agentkit", "agent-auth", "connectors", "tools", "connected-accounts"] +} diff --git a/plugins/agentkit/.codex-plugin/plugin.json b/plugins/agentkit/.codex-plugin/plugin.json new file mode 100644 index 0000000..52f2aa9 --- /dev/null +++ b/plugins/agentkit/.codex-plugin/plugin.json @@ -0,0 +1,24 @@ +{ + "name": "agentkit", + "version": "2.0.0", + "description": "Connect your AI agent to 3,000+ services without writing OAuth plumbing — users authorize once, agents act on their behalf.", + "author": { + "name": "Scalekit Inc.", + "email": "hi@scalekit.com", + "url": "https://scalekit.com" + }, + "homepage": "https://docs.scalekit.com/agentkit/overview/", + "repository": "https://github.com/scalekit-inc/authstack", + "license": "MIT", + "keywords": ["scalekit", "agentkit", "agent-auth", "oauth", "connectors", "tool-calling", "connected-accounts", "mcp"], + "skills": "./skills/", + "interface": { + "displayName": "AgentKit by Scalekit", + "shortDescription": "Connect your AI agent to 3,000+ services without writing OAuth plumbing — users authorize once, agents act on their behalf.", + "longDescription": "LLMs can't natively talk to Gmail, Slack, Salesforce, or any OAuth service. Every connector means custom OAuth flows, token storage, refresh logic, and scope enforcement — weeks of plumbing before your agent does anything useful.\n\nAgentKit handles the entire delegation layer: users authorize once, tokens never touch your code, agents call tools under user permissions with full audit trails. 3,000+ connectors. Works with any LLM or framework. Scoped to the user, not a service account.", + "developerName": "Scalekit Inc.", + "category": "AI Agent Auth", + "capabilities": ["Read", "Write"], + "websiteURL": "https://docs.scalekit.com/agentkit/overview/" + } +} diff --git a/plugins/agentkit/.cursor-plugin/plugin.json b/plugins/agentkit/.cursor-plugin/plugin.json new file mode 100644 index 0000000..9c547f3 --- /dev/null +++ b/plugins/agentkit/.cursor-plugin/plugin.json @@ -0,0 +1,16 @@ +{ + "name": "agentkit", + "displayName": "AgentKit by Scalekit", + "description": "Connect your AI agent to 3,000+ services without writing OAuth plumbing — users authorize once, agents act on their behalf.", + "version": "2.0.0", + "author": { + "name": "Scalekit Inc.", + "email": "hi@scalekit.com" + }, + "homepage": "https://docs.scalekit.com/agentkit/overview/", + "repository": "https://github.com/scalekit-inc/authstack", + "license": "MIT", + "keywords": ["scalekit", "agentkit", "agent-auth", "oauth", "connectors", "tool-calling", "connected-accounts", "mcp"], + "skills": "./skills", + "mcpServers": "./mcp.json" +} diff --git a/plugins/agentkit/.github/plugin/plugin.json b/plugins/agentkit/.github/plugin/plugin.json new file mode 100644 index 0000000..96d1bfe --- /dev/null +++ b/plugins/agentkit/.github/plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "agentkit", + "description": "Connect your AI agent to 3,000+ services without writing OAuth plumbing — users authorize once, agents act on their behalf.", + "version": "2.0.0", + "author": { + "name": "Scalekit Inc.", + "email": "hi@scalekit.com", + "url": "https://scalekit.com" + }, + "homepage": "https://docs.scalekit.com/agentkit/overview/", + "repository": "https://github.com/scalekit-inc/authstack", + "license": "MIT", + "keywords": ["scalekit", "agentkit", "agent-auth", "oauth", "connectors", "tool-calling", "connected-accounts", "mcp"], + "skills": [ + "./skills/setup", + "./skills/integrating-agentkit", + "./skills/discovering-connector-tools", + "./skills/exposing-agentkit-via-mcp", + "./skills/production-readiness-agentkit" + ], + "mcpServers": ".mcp.json" +} diff --git a/plugins/agentkit/.mcp.json b/plugins/agentkit/.mcp.json new file mode 100644 index 0000000..783b9a1 --- /dev/null +++ b/plugins/agentkit/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "scalekit": { + "type": "http", + "url": "https://mcp.scalekit.com" + } + } +} diff --git a/plugins/agentkit/mcp.json b/plugins/agentkit/mcp.json new file mode 100644 index 0000000..783b9a1 --- /dev/null +++ b/plugins/agentkit/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "scalekit": { + "type": "http", + "url": "https://mcp.scalekit.com" + } + } +} diff --git a/plugins/agentkit/skills/discovering-connector-tools/SKILL.md b/plugins/agentkit/skills/discovering-connector-tools/SKILL.md new file mode 100644 index 0000000..c741fa7 --- /dev/null +++ b/plugins/agentkit/skills/discovering-connector-tools/SKILL.md @@ -0,0 +1,102 @@ +--- +name: discovering-connector-tools +description: 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. \ No newline at end of file diff --git a/plugins/agentkit/skills/exposing-agentkit-via-mcp/SKILL.md b/plugins/agentkit/skills/exposing-agentkit-via-mcp/SKILL.md new file mode 100644 index 0000000..0f04789 --- /dev/null +++ b/plugins/agentkit/skills/exposing-agentkit-via-mcp/SKILL.md @@ -0,0 +1,171 @@ +--- +name: exposing-agentkit-via-mcp +description: 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. diff --git a/plugins/agentkit/skills/integrating-agentkit/SKILL.md b/plugins/agentkit/skills/integrating-agentkit/SKILL.md new file mode 100644 index 0000000..17ac9c1 --- /dev/null +++ b/plugins/agentkit/skills/integrating-agentkit/SKILL.md @@ -0,0 +1,289 @@ +--- +name: integrating-agentkit +description: 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** + + + +**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; +``` + + + +## 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. diff --git a/plugins/agentkit/skills/production-readiness-agentkit/SKILL.md b/plugins/agentkit/skills/production-readiness-agentkit/SKILL.md new file mode 100644 index 0000000..d28d40d --- /dev/null +++ b/plugins/agentkit/skills/production-readiness-agentkit/SKILL.md @@ -0,0 +1,78 @@ +--- +name: production-readiness-agentkit +description: 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://.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 handling diff --git a/plugins/agentkit/skills/setup/SKILL.md b/plugins/agentkit/skills/setup/SKILL.md new file mode 100644 index 0000000..19029a3 --- /dev/null +++ b/plugins/agentkit/skills/setup/SKILL.md @@ -0,0 +1,79 @@ +--- +name: setup +description: Starting 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:` 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= +SCALEKIT_CLIENT_SECRET= +``` + +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`. diff --git a/plugins/saaskit/.claude-plugin/plugin.json b/plugins/saaskit/.claude-plugin/plugin.json new file mode 100644 index 0000000..e0d7d05 --- /dev/null +++ b/plugins/saaskit/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "saaskit", + "description": "Enterprise auth for B2B apps without writing protocols. Login, sessions, SSO (Okta, Azure AD, Google), SCIM, MCP OAuth, RBAC, and API keys — any framework, any stack.", + "version": "2.0.0", + "author": { + "name": "Scalekit Inc.", + "email": "hi@scalekit.com" + }, + "homepage": "https://docs.scalekit.com", + "repository": "https://github.com/scalekit-inc/authstack", + "license": "MIT", + "keywords": ["scalekit", "saaskit", "authentication", "sso", "scim", "mcp-auth", "sessions", "rbac"] +} diff --git a/plugins/saaskit/.codex-plugin/plugin.json b/plugins/saaskit/.codex-plugin/plugin.json new file mode 100644 index 0000000..9ddd66d --- /dev/null +++ b/plugins/saaskit/.codex-plugin/plugin.json @@ -0,0 +1,24 @@ +{ + "name": "saaskit", + "version": "2.0.0", + "description": "Enterprise auth for B2B apps without writing protocols — login, sessions, SSO, SCIM, MCP OAuth, and RBAC in one stack.", + "author": { + "name": "Scalekit Inc.", + "email": "hi@scalekit.com", + "url": "https://scalekit.com" + }, + "homepage": "https://docs.scalekit.com", + "repository": "https://github.com/scalekit-inc/authstack", + "license": "MIT", + "keywords": ["scalekit", "saaskit", "authentication", "sso", "scim", "mcp-auth", "sessions", "rbac", "api-keys"], + "skills": "./skills/", + "interface": { + "displayName": "SaaSKit by Scalekit", + "shortDescription": "Enterprise auth for B2B apps without writing protocols — login, sessions, SSO, SCIM, MCP OAuth, and RBAC in one stack.", + "longDescription": "Every enterprise feature request — SSO, SCIM, MFA, RBAC, MCP server auth — is a different protocol to implement correctly. One wrong call in a token validation flow or session lifecycle and you're debugging auth at 2am instead of shipping.\n\nSaaSKit handles the full auth stack: hosted login pages, session management, enterprise SSO (Okta, Azure AD, Google), SCIM provisioning, MCP OAuth, and API key management. Add auth in ~100 lines. Any framework, any stack. Focus on your core product.", + "developerName": "Scalekit Inc.", + "category": "B2B App Auth", + "capabilities": ["Read", "Write"], + "websiteURL": "https://docs.scalekit.com" + } +} diff --git a/plugins/saaskit/.cursor-plugin/plugin.json b/plugins/saaskit/.cursor-plugin/plugin.json new file mode 100644 index 0000000..2a46cd8 --- /dev/null +++ b/plugins/saaskit/.cursor-plugin/plugin.json @@ -0,0 +1,16 @@ +{ + "name": "saaskit", + "displayName": "SaaSKit by Scalekit", + "description": "Enterprise auth for B2B apps without writing protocols — login, sessions, SSO, SCIM, MCP OAuth, and RBAC in one stack.", + "version": "2.0.0", + "author": { + "name": "Scalekit Inc.", + "email": "hi@scalekit.com" + }, + "homepage": "https://docs.scalekit.com", + "repository": "https://github.com/scalekit-inc/authstack", + "license": "MIT", + "keywords": ["scalekit", "saaskit", "authentication", "sso", "scim", "mcp-auth", "sessions", "rbac", "api-keys"], + "skills": "./skills", + "mcpServers": "./mcp.json" +} diff --git a/plugins/saaskit/.github/plugin/plugin.json b/plugins/saaskit/.github/plugin/plugin.json new file mode 100644 index 0000000..3fffc62 --- /dev/null +++ b/plugins/saaskit/.github/plugin/plugin.json @@ -0,0 +1,31 @@ +{ + "name": "saaskit", + "description": "Enterprise auth for B2B apps without writing protocols — login, sessions, SSO, SCIM, MCP OAuth, and RBAC in one stack.", + "version": "2.0.0", + "author": { + "name": "Scalekit Inc.", + "email": "hi@scalekit.com", + "url": "https://scalekit.com" + }, + "homepage": "https://docs.scalekit.com", + "repository": "https://github.com/scalekit-inc/authstack", + "license": "MIT", + "keywords": ["scalekit", "saaskit", "authentication", "sso", "scim", "mcp-auth", "sessions", "rbac", "api-keys"], + "skills": [ + "./skills/setup", + "./skills/implementing-saaskit", + "./skills/implementing-saaskit-nextjs", + "./skills/implementing-saaskit-python", + "./skills/implementing-modular-sso", + "./skills/implementing-scim-provisioning", + "./skills/adding-mcp-oauth", + "./skills/implementing-access-control", + "./skills/managing-saaskit-sessions", + "./skills/migrating-to-saaskit", + "./skills/adding-api-auth", + "./skills/testing-auth-setup", + "./skills/production-readiness-saaskit", + "./skills/scalekit-code-doctor" + ], + "mcpServers": ".mcp.json" +} diff --git a/plugins/saaskit/.mcp.json b/plugins/saaskit/.mcp.json new file mode 100644 index 0000000..783b9a1 --- /dev/null +++ b/plugins/saaskit/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "scalekit": { + "type": "http", + "url": "https://mcp.scalekit.com" + } + } +} diff --git a/plugins/saaskit/mcp.json b/plugins/saaskit/mcp.json new file mode 100644 index 0000000..783b9a1 --- /dev/null +++ b/plugins/saaskit/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "scalekit": { + "type": "http", + "url": "https://mcp.scalekit.com" + } + } +} diff --git a/plugins/saaskit/skills/adding-api-auth/SKILL.md b/plugins/saaskit/skills/adding-api-auth/SKILL.md new file mode 100644 index 0000000..6b2a240 --- /dev/null +++ b/plugins/saaskit/skills/adding-api-auth/SKILL.md @@ -0,0 +1,511 @@ +--- +name: adding-api-auth +description: 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 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 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="", + 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=" \ + -d "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. diff --git a/plugins/saaskit/skills/adding-mcp-oauth/SKILL.md b/plugins/saaskit/skills/adding-mcp-oauth/SKILL.md new file mode 100644 index 0000000..8236bcf --- /dev/null +++ b/plugins/saaskit/skills/adding-mcp-oauth/SKILL.md @@ -0,0 +1,319 @@ +--- +name: adding-mcp-oauth +description: 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:///resources/" + ], + "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:///resources/" + ], + "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 +``` +Expected: `HTTP/1.1 401 Unauthorized` + +**Check 2 – Confirm WWW-Authenticate header:** +The response must include: +``` +WWW-Authenticate: Bearer realm="OAuth", resource_metadata="https:///.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:///.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) + + diff --git a/plugins/saaskit/skills/adding-mcp-oauth/express-reference.md b/plugins/saaskit/skills/adding-mcp-oauth/express-reference.md new file mode 100644 index 0000000..80a48a3 --- /dev/null +++ b/plugins/saaskit/skills/adding-mcp-oauth/express-reference.md @@ -0,0 +1,144 @@ +# Express.js MCP OAuth Authentication with Scalekit + +## Overview + +Pattern for building production-ready MCP servers using Express.js, TypeScript, and OAuth 2.1 Bearer token authentication via Scalekit. Provides fine-grained control over HTTP request handling, middleware chains, and server behavior. + +## When to Use This Pattern + +- **Node.js ecosystem**: Leverage existing npm packages and TypeScript tooling +- **Custom middleware chains**: Rate limiting, request logging, complex authorization +- **Existing Express applications**: Add MCP capabilities to established codebases +- **Fine-grained HTTP control**: Routing, CORS policies, health checks, multiple endpoints + +## Core Architecture + +### Token Validation Flow + +``` +MCP Client → Express Server (401 + WWW-Authenticate) +MCP Client → Scalekit (Exchange code for token) +Scalekit → MCP Client (Bearer token) +MCP Client → Express Server (POST /mcp + Bearer token) +Express Middleware → Scalekit SDK (Validate token) +McpServer → Tool Handler → Response +``` + +### Key Components + +1. **Express Middleware**: Validates Bearer tokens before routing to MCP handlers +2. **Scalekit Node SDK**: Validates JWT signatures, expiration, issuer, and audience +3. **McpServer**: Official MCP SDK server handling JSON-RPC and tool registration +4. **StreamableHTTPServerTransport**: Bridges Express HTTP to MCP protocol +5. **Zod Schema Validation**: Type-safe input validation for tool parameters +6. **OAuth Resource Metadata Endpoint**: `/.well-known/oauth-protected-resource` for client discovery + +## Implementation Patterns + +### Environment Configuration + +Required variables: `SCALEKIT_ENVIRONMENT_URL`, `SCALEKIT_CLIENT_ID`, `SCALEKIT_CLIENT_SECRET`, `EXPECTED_AUDIENCE`, `PROTECTED_RESOURCE_METADATA`, `PORT`. + +### Scalekit Client Initialization + +```typescript +import { ScalekitClient } from '@scalekit-sdk/node'; + +const scalekit = new ScalekitClient(SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, SCALEKIT_CLIENT_SECRET); +``` + +Initialize once at module level for connection pooling. + +### MCP Server Setup + +```typescript +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; + +const server = new McpServer({ name: 'Greeting MCP', version: '1.0.0' }); + +server.tool( + 'greet_user', + 'Greets the user with a personalized message.', + { name: z.string().min(1, 'Name is required') }, + async ({ name }: { name: string }) => ({ + content: [{ type: 'text', text: `Hi ${name}, welcome to Scalekit!` }] + }) +); +``` + +### Express Middleware Authentication + +```typescript +app.use(async (req: Request, res: Response, next: NextFunction) => { + if (req.path === '/.well-known/oauth-protected-resource' || req.path === '/health') { + next(); + return; + } + + const header = req.headers.authorization; + const token = header?.startsWith('Bearer ') + ? header.slice('Bearer '.length).trim() + : undefined; + + if (!token) { + res.status(401).set('WWW-Authenticate', WWW_HEADER_VALUE).json({ error: 'Missing Bearer token' }); + return; + } + + try { + await scalekit.validateToken(token, { audience: [EXPECTED_AUDIENCE] }); + next(); + } catch (error) { + res.status(401).set('WWW-Authenticate', WWW_HEADER_VALUE).json({ error: 'Token validation failed' }); + } +}); +``` + +**Key**: Always `return` after sending a response to prevent "headers already sent" errors. + +### MCP Transport Layer + +```typescript +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; + +app.post('/', async (req: Request, res: Response) => { + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); +}); +``` + +Setting `sessionIdGenerator: undefined` ensures stateless operation for serverless deployments. + +## Common Pitfalls + +**Mismatched Audience**: `EXPECTED_AUDIENCE` must match the Server URL in Scalekit exactly — including trailing slash. + +**Headers Already Sent**: Always `return` after `res.json()` or `res.send()` in middleware. + +**Middleware Order**: Correct order: CORS → body parsing → authentication → routes. + +**TypeScript Module Resolution**: Always include `.js` extension when importing from MCP SDK. + +## Dependencies + +```json +{ + "dependencies": { + "@modelcontextprotocol/sdk": "^1.13.0", + "@scalekit-sdk/node": "^2.0.1", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^5.1.0", + "zod": "^3.25.57" + } +} +``` + +## Reference + +- Full example: [scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node) +- [MCP SDK Documentation](https://github.com/modelcontextprotocol/typescript-sdk) +- [Scalekit Node SDK](https://github.com/scalekit-inc/scalekit-sdk-node) +- [Scalekit MCP Auth Demos](https://github.com/scalekit-inc/mcp-auth-demos/tree/main) diff --git a/plugins/saaskit/skills/adding-mcp-oauth/fastapi-reference.md b/plugins/saaskit/skills/adding-mcp-oauth/fastapi-reference.md new file mode 100644 index 0000000..3df366b --- /dev/null +++ b/plugins/saaskit/skills/adding-mcp-oauth/fastapi-reference.md @@ -0,0 +1,161 @@ +# FastAPI + FastMCP OAuth Authentication with Scalekit + +## Overview + +Pattern for building production-ready MCP servers using FastAPI and FastMCP with OAuth 2.1 Bearer token authentication via Scalekit. Provides fine-grained control over authentication middleware, token validation, and server behavior compared to FastMCP's built-in OAuth provider. + +## When to Use This Pattern + +- **Custom middleware requirements**: Rate limiting, request logging, complex authorization +- **Existing FastAPI applications**: Integrate MCP tools into established codebases +- **Advanced authorization**: Scope-based access control, multi-tenancy, custom claims +- **Full HTTP control**: CORS policies, health checks, multiple endpoints alongside MCP tools + +**Don't use this pattern** if FastMCP's built-in OAuth provider meets your needs — the additional FastAPI layer adds complexity. + +## Core Architecture + +### Token Validation Flow + +``` +MCP Client → FastAPI Server (401 + WWW-Authenticate) +MCP Client → Scalekit (Exchange code for token) +Scalekit → MCP Client (Bearer token) +MCP Client → FastAPI Server (Request + Bearer token) +FastAPI Middleware → Scalekit SDK (Validate token) +FastAPI → MCP Tool Handler → Response +``` + +## Implementation Patterns + +### Middleware Authentication Pattern + +```python +@app.middleware("http") +async def auth_middleware(request: Request, call_next): + if request.url.path in {"/health", "/.well-known/oauth-protected-resource"}: + return await call_next(request) + + auth_header = request.headers.get("authorization") + if not auth_header or not auth_header.startswith("Bearer "): + return Response( + '{"error": "Missing Bearer token"}', + status_code=401, + headers={"WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{RESOURCE_METADATA_URL}"'}, + media_type="application/json" + ) + + token = auth_header.split("Bearer ", 1)[1].strip() + + options = TokenValidationOptions( + issuer=SCALEKIT_ENVIRONMENT_URL, + audience=[EXPECTED_AUDIENCE] + ) + + try: + is_valid = scalekit_client.validate_access_token(token, options=options) + if not is_valid: + raise ValueError("Invalid token") + except Exception: + return Response( + '{"error": "Token validation failed"}', + status_code=401, + headers=WWW_HEADER, + media_type="application/json" + ) + + return await call_next(request) +``` + +### FastMCP Tool Registration + +```python +@mcp.tool( + name="greet_user", + description="Greets the user with a personalized message." +) +async def greet_user(name: str, ctx: Context | None = None) -> dict: + return { + "content": [{"type": "text", "text": f"Hi {name}, welcome to Scalekit!"}] + } +``` + +### Application Mounting + +```python +mcp_app = mcp.http_app(path="/") +app = FastAPI(lifespan=mcp_app.lifespan) + +# Add middleware (CORS, auth, etc.) +app.add_middleware(CORSMiddleware, ...) + +# Add custom endpoints +@app.get("/health") +async def health_check(): + return {"status": "healthy"} + +# Mount MCP at root — MUST be last +app.mount("/", mcp_app) +``` + +**Layering order**: Create FastMCP HTTP app → Create FastAPI app with shared lifespan → Add middleware → Register custom endpoints → Mount FastMCP last. + +## Common Pitfalls + +**Mismatched Audience**: `EXPECTED_AUDIENCE` must match the Server URL in Scalekit exactly. + +**Middleware Order**: Add middleware before mounting MCP app; mount MCP last. + +**Missing Resource Metadata**: Verify `PROTECTED_RESOURCE_METADATA` JSON is copied correctly from Scalekit dashboard. + +**Development vs Production URLs**: Use environment-specific values for `EXPECTED_AUDIENCE`. + +## Dependencies + +```txt +mcp>=1.0.0 +fastapi>=0.104.0 +fastmcp>=0.8.0 +uvicorn>=0.24.0 +pydantic>=2.5.0 +python-dotenv>=1.0.0 +httpx>=0.25.0 +python-jose[cryptography]>=3.3.0 +scalekit-sdk-python>=2.4.0 +``` + +## Extension Patterns + +### Scope-Based Authorization + +```python +# In middleware — attach scopes to request state +decoded = jwt.decode(token, options={"verify_signature": False}) +request.state.scopes = decoded.get("scope", "").split() + +# In tool — check scopes +@mcp.tool() +async def admin_tool(ctx: Context) -> dict: + if "admin" not in ctx.request_context.state.scopes: + raise PermissionError("Requires admin scope") +``` + +### Multi-Tenancy + +```python +# In middleware +request.state.org_id = decoded.get("org_id") + +# In tool +@mcp.tool() +async def get_org_data(ctx: Context) -> dict: + org_id = ctx.request_context.state.org_id + data = await fetch_data_for_org(org_id) + return {"content": [{"type": "text", "text": json.dumps(data)}]} +``` + +## Reference + +- Full example: [scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python) +- [FastMCP Documentation](https://github.com/jlowin/fastmcp) +- [Scalekit MCP Auth Demos](https://github.com/scalekit-inc/mcp-auth-demos/tree/main) diff --git a/plugins/saaskit/skills/adding-mcp-oauth/fastmcp-reference.md b/plugins/saaskit/skills/adding-mcp-oauth/fastmcp-reference.md new file mode 100644 index 0000000..354075d --- /dev/null +++ b/plugins/saaskit/skills/adding-mcp-oauth/fastmcp-reference.md @@ -0,0 +1,136 @@ +# FastMCP OAuth with Scalekit Provider + +Secure your FastMCP server with OAuth 2.1 in just 5 lines of code using Scalekit's built-in provider. This approach handles token validation, scope enforcement, and authentication flows automatically. + +## FastMCP advantage + +**Standard MCP OAuth**: ~30 lines of middleware code, manual token validation +**FastMCP with Scalekit provider**: ~5 lines of configuration, automatic token handling + +## Setup workflow + +``` +FastMCP OAuth Setup: +- [ ] Step 1: Register MCP server in Scalekit +- [ ] Step 2: Install FastMCP and dependencies +- [ ] Step 3: Configure Scalekit provider +- [ ] Step 4: Add scope validation to tools +- [ ] Step 5: Test with MCP Inspector +``` + +## Step 1: Register MCP server + +In Scalekit dashboard: + +1. Navigate to **Dashboard > MCP Servers > Add MCP Server** +2. Enter server name (e.g., `FastMCP Todo Server`) +3. Set **Server URL** to `http://localhost:3002/` (include trailing slash) +4. Define scopes for your tools (e.g., `todo:read`, `todo:write`) +5. Click **Save** and note the `resource_id` + +**Critical**: Use base URL with trailing slash. FastMCP appends `/mcp` automatically. + +## Step 2: Install dependencies + +```bash +mkdir fastmcp-server && cd fastmcp-server +python3 -m venv venv && source venv/bin/activate +pip install "fastmcp>=2.13.0.2" python-dotenv +``` + +## Step 3: Configure Scalekit provider + +```python +import os +from dotenv import load_dotenv +from fastmcp import FastMCP +from fastmcp.server.auth.providers.scalekit import ScalekitProvider + +load_dotenv() + +mcp = FastMCP( + "Your Server Name", + stateless_http=True, + auth=ScalekitProvider( + environment_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), + client_id=os.getenv("SCALEKIT_CLIENT_ID"), + resource_id=os.getenv("SCALEKIT_RESOURCE_ID"), + mcp_url=os.getenv("MCP_URL"), + ), +) + +if __name__ == "__main__": + mcp.run(transport="http", port=int(os.getenv("PORT", "3002"))) +``` + +The Scalekit provider handles token validation, OAuth flow, WWW-Authenticate headers, and the discovery endpoint automatically. + +## Step 4: Add scope validation to tools + +```python +from fastmcp.server.dependencies import AccessToken, get_access_token + +def _require_scope(scope: str) -> str | None: + token: AccessToken = get_access_token() + if scope not in token.scopes: + return f"Insufficient permissions: `{scope}` scope required." + return None + +@mcp.tool +def create_todo(title: str, description: str = None) -> dict: + """Create a new todo item. Requires todo:write scope.""" + error = _require_scope("todo:write") + if error: + return {"error": error} + todo_id = str(uuid.uuid4()) + return {"id": todo_id, "title": title, "description": description} + +@mcp.tool +def list_todos() -> dict: + """List all todos. Requires todo:read scope.""" + error = _require_scope("todo:read") + if error: + return {"error": error} + return {"todos": [...]} +``` + +## Step 5: Test with MCP Inspector + +```bash +python server.py +# In another terminal: +npx @modelcontextprotocol/inspector@latest +``` + +In Inspector: enter URL `http://localhost:3002/mcp`, leave auth fields empty (uses dynamic client registration), click Connect. + +## Environment variable reference + +| Variable | Description | Example | +|----------|-------------|---------| +| `SCALEKIT_ENVIRONMENT_URL` | Your Scalekit environment URL | `https://yourenv.scalekit.com` | +| `SCALEKIT_CLIENT_ID` | Client ID from Scalekit dashboard | `skc_...` | +| `SCALEKIT_RESOURCE_ID` | MCP server resource ID | `res_...` | +| `MCP_URL` | Base URL with trailing slash | `http://localhost:3002/` | +| `PORT` | HTTP server port | `3002` | + +## Scope design patterns + +- **Read-only**: `*:read` (e.g., `todo:read`, `data:read`) +- **Write operations**: `*:write` (e.g., `todo:write`) +- **Admin operations**: `*:admin` (e.g., `system:admin`) +- **Multiple scopes per tool**: Return error if ANY required scope is missing + +## Common issues + +**Token validation fails**: Verify `SCALEKIT_RESOURCE_ID` matches dashboard. Check `MCP_URL` has trailing slash. + +**Scope errors persist**: Verify scopes are defined in Scalekit dashboard. Check scope strings match exactly (case-sensitive). + +**MCP Inspector connection fails**: Leave auth fields empty (uses DCR). Check browser console for OAuth errors. + +## Reference + +- Full example: [scalekit-inc/mcp-auth-demos/tree/main/todo-fastmcp](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/todo-fastmcp) +- FastMCP docs: [fastmcp.dev](https://fastmcp.dev) +- Scalekit docs: [docs.scalekit.com/authenticate/mcp/fastmcp-quickstart](https://docs.scalekit.com/authenticate/mcp/fastmcp-quickstart) diff --git a/plugins/saaskit/skills/implementing-access-control/SKILL.md b/plugins/saaskit/skills/implementing-access-control/SKILL.md new file mode 100644 index 0000000..6f1c456 --- /dev/null +++ b/plugins/saaskit/skills/implementing-access-control/SKILL.md @@ -0,0 +1,135 @@ +--- +name: implementing-access-control +description: 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=" http://localhost:3000/api/admin/users +# Expected: 200 + +# Test with a token missing the required role +curl -H "Cookie: accessToken=" 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 only diff --git a/plugins/saaskit/skills/implementing-modular-sso/SKILL.md b/plugins/saaskit/skills/implementing-modular-sso/SKILL.md new file mode 100644 index 0000000..cff2f8c --- /dev/null +++ b/plugins/saaskit/skills/implementing-modular-sso/SKILL.md @@ -0,0 +1,581 @@ +--- +name: implementing-modular-sso +description: Implements 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 + + com.scalekit + scalekit-sdk-java + +``` + +### Environment Configuration + +Add these credentials to `.env` file (fetch from Dashboard > Developers > Settings > API credentials): + +```env +SCALEKIT_ENVIRONMENT_URL= +SCALEKIT_CLIENT_ID= +SCALEKIT_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):** +``` +/oauth/authorize? + response_type=code& + client_id=& + redirect_uri=& + scope=openid profile email& + organization_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 + + +``` +## 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. \ No newline at end of file diff --git a/plugins/saaskit/skills/implementing-saaskit-nextjs/SKILL.md b/plugins/saaskit/skills/implementing-saaskit-nextjs/SKILL.md new file mode 100644 index 0000000..5a3b241 --- /dev/null +++ b/plugins/saaskit/skills/implementing-saaskit-nextjs/SKILL.md @@ -0,0 +1,237 @@ +--- +name: implementing-saaskit-nextjs +description: 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. diff --git a/plugins/saaskit/skills/implementing-saaskit-python/SKILL.md b/plugins/saaskit/skills/implementing-saaskit-python/SKILL.md new file mode 100644 index 0000000..08c4f4d --- /dev/null +++ b/plugins/saaskit/skills/implementing-saaskit-python/SKILL.md @@ -0,0 +1,111 @@ +--- +name: implementing-saaskit-python +description: 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. diff --git a/plugins/saaskit/skills/implementing-saaskit-python/django-reference.md b/plugins/saaskit/skills/implementing-saaskit-python/django-reference.md new file mode 100644 index 0000000..91b8e35 --- /dev/null +++ b/plugins/saaskit/skills/implementing-saaskit-python/django-reference.md @@ -0,0 +1,250 @@ +# Scalekit Auth — Django + +## Dependencies + +```bash +pip install scalekit-sdk-python python-dotenv django +``` + +## Environment variables + +```env +SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev +SCALEKIT_CLIENT_ID=your_client_id +SCALEKIT_CLIENT_SECRET=your_client_secret +SCALEKIT_REDIRECT_URI=http://localhost:8000/auth/callback +``` + +Load with `python-dotenv` or Django's built-in settings from env. + +## Client initialization — initialize once + +In `yourapp/auth_client.py`: + +```python +import os +from scalekit import ScalekitClient + +_sc = None + +def get_scalekit_client() -> ScalekitClient: + global _sc + if _sc is None: + _sc = ScalekitClient( + env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), + client_id=os.getenv("SCALEKIT_CLIENT_ID"), + client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), + ) + return _sc +``` + +## Auth flow at a glance + +``` +GET /auth/login + → get_authorization_url() → 302 to Scalekit + +GET /auth/callback?code=... + → authenticate_with_code() → set session → redirect to /dashboard + +Middleware: ScalekitAuthMiddleware + → validate_access_token() → refresh if expired → pass or redirect /auth/login + +GET /auth/logout + → get_logout_url() → clear session → 302 to Scalekit end-session +``` + +## Views + +```python +# yourapp/views.py +import os, secrets +from django.shortcuts import redirect +from django.http import HttpRequest, HttpResponse +from scalekit.common.scalekit import AuthorizationUrlOptions, LogoutUrlOptions +from .auth_client import get_scalekit_client + +REDIRECT_URI = os.getenv("SCALEKIT_REDIRECT_URI", "http://localhost:8000/auth/callback") + +def login(request: HttpRequest): + state = secrets.token_urlsafe(32) + request.session["oauth_state"] = state + sc = get_scalekit_client() + options = AuthorizationUrlOptions() + options.state = state + auth_url = sc.get_authorization_url(REDIRECT_URI, options) + return redirect(auth_url) + +def callback(request: HttpRequest): + stored_state = request.session.pop("oauth_state", None) + if not stored_state or stored_state != request.GET.get("state"): + return HttpResponse("CSRF mismatch", status=403) + if error := request.GET.get("error"): + return HttpResponse(f"Auth error: {error}", status=400) + + sc = get_scalekit_client() + result = sc.authenticate_with_code(request.GET["code"], REDIRECT_URI) + + request.session["access_token"] = result.access_token + request.session["refresh_token"] = result.refresh_token + request.session["id_token"] = result.id_token + request.session.cycle_key() # session fixation protection + + return redirect("/dashboard") + +def logout(request: HttpRequest): + id_token = request.session.get("id_token", "") + sc = get_scalekit_client() + logout_url = sc.get_logout_url(LogoutUrlOptions(post_logout_redirect_uri="http://localhost:8000")) + request.session.flush() + return redirect(logout_url) +``` + +## Middleware + +```python +# yourapp/middleware.py +from django.shortcuts import redirect +from .auth_client import get_scalekit_client + +SKIP_PATHS = {"/auth/login", "/auth/callback", "/auth/logout"} + +class ScalekitAuthMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.path in SKIP_PATHS: + return self.get_response(request) + + access_token = request.session.get("access_token") + refresh_token = request.session.get("refresh_token") + + if not access_token: + return redirect(f"/auth/login?next={request.path}") + + sc = get_scalekit_client() + try: + sc.validate_access_token(access_token) + except Exception: + # Token expired — attempt silent refresh + if not refresh_token: + request.session.flush() + return redirect("/auth/login") + try: + refreshed = sc.refresh_access_token(refresh_token) + request.session["access_token"] = refreshed.access_token + request.session["refresh_token"] = refreshed.refresh_token + except Exception: + request.session.flush() + return redirect("/auth/login") + + return self.get_response(request) +``` + +Register in `settings.py`: + +```python +MIDDLEWARE = [ + # ... Django default middleware ... + "yourapp.middleware.ScalekitAuthMiddleware", +] +``` + +## URL configuration + +```python +# yourapp/urls.py +from django.urls import path +from . import views + +urlpatterns = [ + path("auth/login", views.login, name="auth_login"), + path("auth/callback", views.callback, name="auth_callback"), + path("auth/logout", views.logout, name="auth_logout"), + path("dashboard/", views.dashboard, name="dashboard"), +] +``` + +## Session storage + +Use database-backed sessions for production: + +```python +# settings.py +SESSION_ENGINE = "django.contrib.sessions.backends.db" +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SECURE = True # HTTPS only +SESSION_COOKIE_SAMESITE = "Lax" +SESSION_COOKIE_AGE = 86400 # 24 hours +``` + +## Implementation checklist + +``` +- [ ] Step 1: pip install scalekit-sdk-python python-dotenv django +- [ ] Step 2: Set SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, SCALEKIT_CLIENT_SECRET in .env +- [ ] Step 3: Create auth_client.py with singleton ScalekitClient +- [ ] Step 4: Implement login, callback, logout views +- [ ] Step 5: Add ScalekitAuthMiddleware to MIDDLEWARE in settings.py +- [ ] Step 6: Register /auth/login, /auth/callback, /auth/logout routes +- [ ] Step 7: Configure SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SECURE=True +- [ ] Step 8: Register callback URI in Scalekit dashboard +- [ ] Step 9: Call session.cycle_key() after login (session fixation protection) +- [ ] Step 10: Test: login → /dashboard → token refresh → logout +``` + +## Troubleshooting + +**`authenticate_with_code` raises exception**: The `redirect_uri` must exactly match the URI in the Scalekit dashboard — including scheme, host, and path. + +**Session not persisting across requests**: Ensure `django.contrib.sessions` is in `INSTALLED_APPS` and `python manage.py migrate` has been run. + +**CSRF errors on callback**: The `/auth/callback` GET route is not subject to Django's CSRF middleware (CSRF applies to POST). Add it to `CSRF_EXEMPT_URLS` if you hit issues. + +**Redirect loop in middleware**: Verify `SKIP_PATHS` includes `/auth/login`, `/auth/callback`, and `/auth/logout`. Also ensure static file paths are excluded. + +## Tactics + +### Deep link preservation + +```python +# In login view +next_url = request.GET.get("next", "/dashboard") +if not next_url.startswith("/"): + next_url = "/dashboard" +request.session["next"] = next_url + +# In callback view +next_url = request.session.pop("next", "/dashboard") +return redirect(next_url) +``` + +### Cache-Control: no-store on protected views + +```python +from django.views.decorators.cache import never_cache + +@never_cache +def dashboard(request): + ... +``` + +### IDP-initiated SSO + +```python +def idp_login(request): + sc = get_scalekit_client() + claims = sc.get_idp_initiated_login_claims(request.GET.get("idp_initiated_login", "")) + opts = AuthorizationUrlOptions() + opts.organization_id = claims.organization_id or None + opts.connection_id = claims.connection_id or None + opts.login_hint = claims.login_hint or None + auth_url = sc.get_authorization_url(REDIRECT_URI, options=opts) + return redirect(auth_url) +``` + +## Reference + +- Scalekit Python SDK: [docs.scalekit.com/apis](https://docs.scalekit.com/apis) +- Django sessions: [docs.djangoproject.com/topics/http/sessions](https://docs.djangoproject.com/en/stable/topics/http/sessions/) diff --git a/plugins/saaskit/skills/implementing-saaskit-python/flask-reference.md b/plugins/saaskit/skills/implementing-saaskit-python/flask-reference.md new file mode 100644 index 0000000..0a533d1 --- /dev/null +++ b/plugins/saaskit/skills/implementing-saaskit-python/flask-reference.md @@ -0,0 +1,224 @@ +# Scalekit Auth — Flask + +## Dependencies + +```bash +pip install scalekit-sdk-python python-dotenv flask flask-session +``` + +## Environment variables + +```env +SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev +SCALEKIT_CLIENT_ID=your_client_id +SCALEKIT_CLIENT_SECRET=your_client_secret +SCALEKIT_REDIRECT_URI=http://localhost:5000/auth/callback +SECRET_KEY=your-flask-secret-key +``` + +## App setup + +```python +# app.py +import os, secrets +from dotenv import load_dotenv +from flask import Flask, redirect, request, session, url_for, jsonify +from flask_session import Session +from scalekit import ScalekitClient +from scalekit.common.scalekit import AuthorizationUrlOptions, LogoutUrlOptions + +load_dotenv() + +app = Flask(__name__) +app.secret_key = os.getenv("SECRET_KEY") +app.config["SESSION_TYPE"] = "filesystem" # or "redis", "sqlalchemy" +app.config["SESSION_COOKIE_HTTPONLY"] = True +app.config["SESSION_COOKIE_SECURE"] = True +app.config["SESSION_COOKIE_SAMESITE"] = "Lax" +Session(app) + +REDIRECT_URI = os.getenv("SCALEKIT_REDIRECT_URI", "http://localhost:5000/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"), +) +``` + +## Auth flow at a glance + +``` +GET /auth/login + → get_authorization_url() → 302 to Scalekit + +GET /auth/callback?code=... + → authenticate_with_code() → set session → redirect to /dashboard + +@require_auth decorator + → validate_access_token() → refresh if expired → pass or redirect /auth/login + +GET /auth/logout + → get_logout_url() → clear session → 302 to Scalekit end-session +``` + +## Routes + +```python +@app.get("/auth/login") +def login(): + state = secrets.token_urlsafe(32) + session["oauth_state"] = state + options = AuthorizationUrlOptions() + options.state = state + auth_url = sc.get_authorization_url(REDIRECT_URI, options) + return redirect(auth_url) + +@app.get("/auth/callback") +def callback(): + stored = session.pop("oauth_state", None) + if not stored or stored != request.args.get("state"): + return "CSRF mismatch", 403 + if error := request.args.get("error"): + return f"Auth error: {error}", 400 + + result = sc.authenticate_with_code(request.args["code"], REDIRECT_URI) + session["access_token"] = result.access_token + session["refresh_token"] = result.refresh_token + session["id_token"] = result.id_token + session.modified = True + + return redirect(session.pop("next", "/dashboard")) + +@app.get("/auth/logout") +def logout(): + id_token = session.get("id_token", "") + logout_url = sc.get_logout_url(LogoutUrlOptions(post_logout_redirect_uri="http://localhost:5000")) + session.clear() + return redirect(logout_url) +``` + +## Auth decorator + +```python +from functools import wraps +from flask import g + +def require_auth(f): + @wraps(f) + def decorated(*args, **kwargs): + access_token = session.get("access_token") + refresh_token = session.get("refresh_token") + + if not access_token: + session["next"] = request.path + return redirect(url_for("login")) + + try: + sc.validate_access_token(access_token) + except Exception: + if not refresh_token: + session.clear() + return redirect(url_for("login")) + try: + refreshed = sc.refresh_access_token(refresh_token) + session["access_token"] = refreshed.access_token + session["refresh_token"] = refreshed.refresh_token + except Exception: + session.clear() + return redirect(url_for("login")) + + return f(*args, **kwargs) + return decorated +``` + +## Protected routes + +```python +@app.get("/dashboard") +@require_auth +def dashboard(): + return jsonify({"status": "authenticated"}) +``` + +## Implementation checklist + +``` +- [ ] Step 1: pip install scalekit-sdk-python python-dotenv flask flask-session +- [ ] Step 2: Set SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, SCALEKIT_CLIENT_SECRET, SECRET_KEY in .env +- [ ] Step 3: Initialize ScalekitClient at module level (single instance per process) +- [ ] Step 4: Configure Flask-Session with filesystem or Redis backend +- [ ] Step 5: Implement /auth/login, /auth/callback, /auth/logout routes +- [ ] Step 6: Create require_auth decorator; apply to protected routes +- [ ] Step 7: Set SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_SECURE=True, SESSION_COOKIE_SAMESITE=Lax +- [ ] Step 8: Register callback URI in Scalekit dashboard +- [ ] Step 9: Test: login → /dashboard → token refresh → logout +``` + +## Troubleshooting + +**`authenticate_with_code` raises exception**: The `redirect_uri` must exactly match the URI in the Scalekit dashboard — including scheme, host, path, and no trailing slash. + +**Session data lost between requests**: Ensure `SECRET_KEY` is set and consistent. With `SESSION_TYPE="filesystem"`, verify the session directory is writable. + +**`SameSite=Lax` breaking SSO callback**: Some IdPs POST the callback. If you use POST-based SSO, set `SESSION_COOKIE_SAMESITE = "None"` with `SESSION_COOKIE_SECURE = True`. + +**Token refresh race condition**: Multiple concurrent requests with the same refresh token can exhaust it. Use a per-user lock or treat `invalid_grant` as session expiry. + +## Tactics + +### Blueprint structure for larger apps + +```python +# auth/routes.py +from flask import Blueprint +auth_bp = Blueprint("auth", __name__, url_prefix="/auth") + +@auth_bp.get("/login") +def login(): ... + +# app.py +from auth.routes import auth_bp +app.register_blueprint(auth_bp) +``` + +### IDP-initiated SSO + +```python +@app.get("/auth/idp-login") +def idp_login(): + claims = sc.get_idp_initiated_login_claims(request.args.get("idp_initiated_login", "")) + opts = AuthorizationUrlOptions() + opts.organization_id = claims.organization_id or None + opts.connection_id = claims.connection_id or None + opts.login_hint = claims.login_hint or None + return redirect(sc.get_authorization_url(REDIRECT_URI, options=opts)) +``` + +### Cache-Control: no-store on protected responses + +```python +from flask import make_response + +@app.get("/dashboard") +@require_auth +def dashboard(): + resp = make_response(jsonify({"status": "authenticated"})) + resp.headers["Cache-Control"] = "no-store" + return resp +``` + +### Redis session backend (production) + +```python +app.config["SESSION_TYPE"] = "redis" +app.config["SESSION_REDIS"] = redis.from_url(os.getenv("REDIS_URL")) +app.config["SESSION_USE_SIGNER"] = True +app.config["SESSION_KEY_PREFIX"] = "sk_session:" +app.config["PERMANENT_SESSION_LIFETIME"] = 86400 +``` + +## Reference + +- Scalekit Python SDK: [docs.scalekit.com/apis](https://docs.scalekit.com/apis) +- Flask-Session: [flask-session.readthedocs.io](https://flask-session.readthedocs.io/) diff --git a/plugins/saaskit/skills/implementing-saaskit/SKILL.md b/plugins/saaskit/skills/implementing-saaskit/SKILL.md new file mode 100644 index 0000000..1e97ee2 --- /dev/null +++ b/plugins/saaskit/skills/implementing-saaskit/SKILL.md @@ -0,0 +1,120 @@ +--- +name: implementing-saaskit +description: 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= +SCALEKIT_CLIENT_ID= +SCALEKIT_CLIENT_SECRET= +SCALEKIT_REDIRECT_URI= # 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. diff --git a/plugins/saaskit/skills/implementing-saaskit/go-reference.md b/plugins/saaskit/skills/implementing-saaskit/go-reference.md new file mode 100644 index 0000000..24d8681 --- /dev/null +++ b/plugins/saaskit/skills/implementing-saaskit/go-reference.md @@ -0,0 +1,386 @@ +# Scalekit Auth in Go (Gin) + +Scalekit is an OIDC/OAuth2 provider. Unlike frameworks that auto-wire OAuth2, Go requires you to +manually implement four handlers: **authorize → callback → session → logout**. Use `scalekit-sdk-go/v2`. + +## Dependencies + +```bash +go get github.com/scalekit-inc/scalekit-sdk-go/v2 +go get github.com/gin-gonic/gin +go get github.com/gin-contrib/cors +go get github.com/golang-jwt/jwt/v5 +``` + +## Environment variables + +```bash +SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev +SCALEKIT_CLIENT_ID=your_client_id +SCALEKIT_CLIENT_SECRET=your_client_secret +PORT=8080 +``` + +Never commit secrets. Load with `godotenv` or equivalent. + +## Global client — initialize once + +Use `sync.Once` so the client is created exactly once across all requests: + +```go +var ( + globalClient scalekit.Scalekit + clientOnce sync.Once + clientErr error +) + +func GetScaleKitClient() (scalekit.Scalekit, error) { + clientOnce.Do(func() { + envURL := os.Getenv("SCALEKIT_ENVIRONMENT_URL") + id := os.Getenv("SCALEKIT_CLIENT_ID") + secret := os.Getenv("SCALEKIT_CLIENT_SECRET") + globalClient = scalekit.NewScalekitClient(envURL, id, secret) + }) + return globalClient, clientErr +} +``` + +Call `GetScaleKitClient()` once at startup to fail fast on bad credentials. + +## Auth flow at a glance + +``` +GET /api/authorize + → GetAuthorizationUrl() → 302 to Scalekit + +GET /api/scalekit/callback?code=... + → AuthenticateWithCode() → redirect to /dashboard or /onboarding + +GET /api/session (every page load) + → ValidateAccessToken() → refresh if expired → return user JSON + +GET /api/logout + → GetLogoutUrl() → clear cookies → 302 to Scalekit end-session +``` + +## Handler: Authorize + +Builds the authorization URL and redirects the browser: + +```go +func AuthorizeHandler(c *gin.Context) { + sc, _ := GetScaleKitClient() + + stateBytes, _ := json.Marshal(map[string]any{ + "next": c.Query("next"), + "csrf": randomString(12), + }) + state := base64.StdEncoding.EncodeToString(stateBytes) + + opts := scalekit.AuthorizationUrlOptions{ + State: state, + Scopes: []string{"openid", "profile", "email", "offline_access"}, + } + if v := c.Query("organization_id"); v != "" { opts.OrganizationId = v } + if v := c.Query("connection_id"); v != "" { opts.ConnectionId = v } + if v := c.Query("login_hint"); v != "" { opts.LoginHint = v } + + authURL, err := sc.GetAuthorizationUrl(callbackURL(c), opts) + if err != nil { + c.JSON(500, gin.H{"error": "Failed to build authorization URL"}) + return + } + c.Redirect(http.StatusFound, authURL.String()) +} + +func callbackURL(c *gin.Context) string { + proto := "https" + if strings.Contains(c.Request.Host, "localhost") { proto = "http" } + return proto + "://" + c.Request.Host + "/api/scalekit/callback" +} +``` + +## Handler: Callback + +Exchange the authorization code for tokens; set httpOnly cookies: + +```go +func CallbackHandler(c *gin.Context) { + if e := c.Query("error"); e != "" { + c.JSON(400, gin.H{"error": c.Query("error_description")}) + return + } + + sc, _ := GetScaleKitClient() + resp, err := sc.AuthenticateWithCode( + c.Request.Context(), + c.Query("code"), + callbackURL(c), + scalekit.AuthenticationOptions{}, + ) + if err != nil { + c.JSON(500, gin.H{"error": "Token exchange failed"}) + return + } + + c.SetCookie("auth_access_token", resp.AccessToken, 86400, "/", "", false, true) + c.SetCookie("auth_refresh_token", resp.RefreshToken, 2592000, "/", "", false, true) + c.SetCookie("id_token", resp.IdToken, 86400, "/", "", false, false) + + claims, _ := decodeJWTPayload(resp.AccessToken) + redirect := "/onboarding" + if _, hasOrg := claims["xoid"]; hasOrg { + redirect = "/dashboard" + } + c.Redirect(http.StatusFound, getUIBaseURL(c)+redirect) +} +``` + +`resp` fields: `AccessToken`, `RefreshToken`, `IdToken`, `User` (email, name, etc.). + +## Handler: Session + +Validate on every authenticated page load; silently refresh expired tokens: + +```go +func SessionHandler(c *gin.Context) { + accessToken, _ := c.Cookie("auth_access_token") + refreshToken, _ := c.Cookie("auth_refresh_token") + + sc, _ := GetScaleKitClient() + + valid, err := sc.ValidateAccessToken(c.Request.Context(), accessToken) + if err != nil || !valid { + refreshed, err := sc.RefreshAccessToken(c.Request.Context(), refreshToken) + if err != nil { + LogoutHandler(c) + return + } + c.SetCookie("auth_access_token", refreshed.AccessToken, 86400, "/", "", false, true) + c.SetCookie("auth_refresh_token", refreshed.RefreshToken, 2592000, "/", "", false, true) + accessToken = refreshed.AccessToken + } + + claims, _ := decodeJWTPayload(accessToken) + userID, _ := getStringClaim(claims, "sub") + + userResp, _ := sc.User().GetUser(context.Background(), userID) + c.JSON(200, gin.H{ + "authenticated": true, + "user": gin.H{ + "id": userResp.User.Id, + "email": userResp.User.Email, + "first_name": userResp.User.UserProfile.FirstName, + "last_name": userResp.User.UserProfile.LastName, + }, + }) +} +``` + +## Handler: Logout + +Clear all cookies and redirect to Scalekit's end-session endpoint: + +```go +func LogoutHandler(c *gin.Context) { + idToken, _ := c.Cookie("id_token") + sc, _ := GetScaleKitClient() + + logoutURL, _ := sc.GetLogoutUrl(scalekit.LogoutUrlOptions{ + IdTokenHint: idToken, + PostLogoutRedirectUri: getUIBaseURL(c), + }) + + c.SetCookie("auth_access_token", "", -1, "/", "", false, true) + c.SetCookie("auth_refresh_token", "", -1, "/", "", false, true) + c.SetCookie("id_token", "", -1, "/", "", false, false) + c.Redirect(http.StatusFound, logoutURL.String()) +} +``` + +## Handler: IDP-initiated login (enterprise SSO) + +When an IdP starts the login (e.g. Okta tile click), Scalekit sends a signed JWT: + +```go +func IdpInitiatedLoginHandler(c *gin.Context) { + sc, _ := GetScaleKitClient() + claims, err := sc.GetIdpInitiatedLoginClaims( + c.Request.Context(), + c.Query("idp_initiated_login"), + ) + if err != nil { + c.JSON(400, gin.H{"error": "invalid idp_initiated_login token"}) + return + } + opts := scalekit.AuthorizationUrlOptions{ + Scopes: []string{"openid", "profile", "email", "offline_access"}, + } + if claims.OrganizationID != "" { opts.OrganizationId = claims.OrganizationID } + if claims.ConnectionID != "" { opts.ConnectionId = claims.ConnectionID } + if claims.LoginHint != "" { opts.LoginHint = claims.LoginHint } + + authURL, _ := sc.GetAuthorizationUrl(callbackURL(c), opts) + c.Redirect(http.StatusFound, authURL.String()) +} +``` + +## JWT utility helpers + +```go +func decodeJWTPayload(token string) (map[string]interface{}, error) { + parts := strings.Split(token, ".") + if len(parts) != 3 { + return nil, fmt.Errorf("invalid JWT format") + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return nil, err + } + var claims map[string]interface{} + return claims, json.Unmarshal(payload, &claims) +} + +func getStringClaim(claims map[string]interface{}, key string) (string, error) { + v, ok := claims[key].(string) + if !ok || v == "" { + return "", fmt.Errorf("claim %q missing or empty", key) + } + return v, nil +} +``` + +## Scalekit JWT claims reference + +| Claim | Meaning | Notes | +|---|---|---| +| `sub` | Scalekit user ID | Always present | +| `xoid` | External org ID (e.g. `wspace_abc`) | Absent → user has no org yet → send to `/onboarding` | +| `xuid` | Your app's user DB ID | Absent → create user locally, then call `UpdateUser` to write it back | +| `permissions` | User permissions in org | Check before authorizing sensitive actions | +| `roles` | User roles in org | Derive `is_admin` from role names | + +## Route registration + +```go +api := r.Group("/api") +api.GET("/authorize", AuthorizeHandler) +api.GET("/login/initiate", IdpInitiatedLoginHandler) +api.GET("/scalekit/callback", CallbackHandler) +api.GET("/session", SessionHandler) +api.GET("/logout", LogoutHandler) +``` + +## CORS — required for cookie-based auth + +```go +r.Use(cors.New(cors.Config{ + AllowOrigins: []string{"https://yourdomain.com"}, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, +})) +``` + +## Implementation checklist + +``` +- [ ] Step 1: go get scalekit-sdk-go/v2, gin, cors, jwt/v5 +- [ ] Step 2: Set SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, SCALEKIT_CLIENT_SECRET in .env +- [ ] Step 3: Create handlers/client.go — sync.Once singleton +- [ ] Step 4: Create handlers/utils.go — decodeJWTPayload, getStringClaim, callbackURL, getUIBaseURL +- [ ] Step 5: Implement AuthorizeHandler → GetAuthorizationUrl → redirect +- [ ] Step 6: Implement CallbackHandler → AuthenticateWithCode → set cookies → redirect +- [ ] Step 7: Implement SessionHandler → ValidateAccessToken → RefreshAccessToken if expired +- [ ] Step 8: Implement LogoutHandler → GetLogoutUrl → clear cookies → redirect +- [ ] Step 9: Register all four routes under /api +- [ ] Step 10: Configure CORS with AllowCredentials: true +- [ ] Step 11: Register callback URI in Scalekit dashboard +- [ ] Step 12: Test: login → /dashboard → GET /api/session → logout +``` + +## Troubleshooting + +**`invalid_grant` on callback**: The `redirectURL` in `AuthenticateWithCode` must exactly match the URI registered in the Scalekit dashboard — including scheme and path. + +**Session handler stuck in logout loop**: `ValidateAccessToken` returns `false` on both expiry *and* network errors. Log `err` before deciding to refresh vs. logout. + +**`xoid` missing**: The user has no organization. This is expected for new signups — route to `/onboarding`. + +**CORS / cookie not sent**: Ensure `AllowCredentials: true` is set in CORS config. + +## Reference + +- Full working example: [scalekit-inc/coffee-desk-demo](https://github.com/scalekit-inc/coffee-desk-demo) +- Scalekit Go SDK: [scalekit-inc/scalekit-sdk-go](https://github.com/scalekit-inc/scalekit-sdk-go) +- Scalekit docs: https://docs.scalekit.com + +## Tactics + +### SameSite=Lax — set explicitly on each cookie + +Gin's `c.SetCookie` does not expose a `SameSite` parameter. Use `http.SetCookie` directly: + +```go +http.SetCookie(c.Writer, &http.Cookie{ + Name: "auth_access_token", + Value: resp.AccessToken, + Path: "/", + MaxAge: 86400, + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + Secure: !strings.Contains(c.Request.Host, "localhost"), +}) +``` + +### Secure flag in production + +Never hardcode `secure: false`. Detect localhost at runtime: + +```go +func isSecure(c *gin.Context) bool { + return !strings.Contains(c.Request.Host, "localhost") +} +``` + +### CSRF via state parameter + +The base64-encoded state in `AuthorizeHandler` already carries a CSRF token. Validate it in `CallbackHandler` before exchanging the code. + +### Deep link preservation via state + +The state JSON includes `"next"`. After a successful callback, extract it and redirect: + +```go +next := stateData["next"] +if next == "" || !strings.HasPrefix(next, "/") { + next = "/dashboard" +} +c.Redirect(http.StatusFound, getUIBaseURL(c)+next) +``` + +### Cache-Control: no-store on protected endpoints + +```go +func SessionHandler(c *gin.Context) { + c.Header("Cache-Control", "no-store") + // ... +} +``` + +### Token refresh race condition + +Multiple browser tabs hitting `/api/session` simultaneously can each attempt a refresh with the same refresh token. Use a per-user mutex or treat `invalid_grant` on refresh as session expiry. + +### 401 vs redirect for JSON clients + +Return `401` for `Accept: application/json` requests instead of redirecting: + +```go +if strings.Contains(c.GetHeader("Accept"), "application/json") { + c.JSON(401, gin.H{"error": "unauthenticated"}) +} else { + c.Redirect(http.StatusFound, "/login") +} +``` diff --git a/plugins/saaskit/skills/implementing-saaskit/laravel-reference.md b/plugins/saaskit/skills/implementing-saaskit/laravel-reference.md new file mode 100644 index 0000000..3a4f0dc --- /dev/null +++ b/plugins/saaskit/skills/implementing-saaskit/laravel-reference.md @@ -0,0 +1,208 @@ +# Scalekit Auth — Laravel + +Reference repo: [scalekit-inc/scalekit-laravel-auth-example](https://github.com/scalekit-inc/scalekit-laravel-auth-example) + +## Project structure + +``` +app/ +├── Services/ +│ └── ScalekitClient.php # Raw HTTP OAuth client (no PHP SDK) +├── Http/ +│ ├── Controllers/ +│ │ └── AuthController.php +│ └── Middleware/ +│ ├── ScalekitAuth.php # Session auth gate +│ ├── ScalekitPermission.php # Per-route permission check +│ └── ScalekitTokenRefresh.php # Auto token refresh on every request + +config/ +└── scalekit.php # Reads from env via config('scalekit.*') + +routes/ +└── web.php # Named routes + middleware groups +``` + +## 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:8000/auth/callback +``` + +> No official Scalekit PHP SDK exists. This app uses **Laravel's `Http` facade** with raw HTTP calls. + +## Key methods (ScalekitClient service) + +| Method | HTTP call | Auth | +|---|---|---| +| `getAuthorizationUrl($state)` | Builds `{env_url}/oauth/authorize?response_type=code&...` | None | +| `exchangeCodeForTokens($code)` | `POST {env_url}/oauth/token` with `grant_type=authorization_code` | Basic Auth | +| `refreshAccessToken($refreshToken)` | `POST {env_url}/oauth/token` with `grant_type=refresh_token` | Basic Auth | +| `validateTokenAndGetClaims($token)` | Manual base64 JWT decode — no signature verification | — | +| `hasPermission($token, $permission)` | Decodes JWT, checks permission claim chain | — | +| `logout($accessToken)` | Builds `{env_url}/oidc/logout?post_logout_redirect_uri=...` | None | + +## Session storage schema + +```php +session([ + 'scalekit_user' => [ + 'sub', 'email', 'name', 'given_name', 'family_name', + 'preferred_username', + 'claims' // merged array of ALL claims + ], + 'scalekit_tokens' => [ + 'access_token', 'refresh_token', 'id_token', + 'expires_at', 'expires_in', + ], + 'scalekit_roles' => [], + 'scalekit_permissions' => [], +]); +``` + +## Auth flow + +### Login (`GET /login`) + +```php +$state = Str::random(32); +session(['oauth_state' => $state]); +$authUrl = $this->scalekitClient->getAuthorizationUrl($state); +return view('auth.login', ['auth_url' => $authUrl]); +``` + +### Callback (`GET /auth/callback`) + +1. Validate `state` vs `session('oauth_state')` → 400 on mismatch +2. Exchange code for tokens +3. Decode ID token + access token claims, merge +4. Write session keys +5. Redirect to dashboard + +### Logout (`GET|POST /logout`) + +```php +$logoutUrl = $this->scalekitClient->logout($accessToken); +session()->flush(); +return redirect($logoutUrl); +``` + +## Middleware + +### Registration in `bootstrap/app.php` (Laravel 11) + +```php +->withMiddleware(function (Middleware $middleware) { + $middleware->alias([ + 'scalekit.auth' => \App\Http\Middleware\ScalekitAuth::class, + 'scalekit.permission' => \App\Http\Middleware\ScalekitPermission::class, + ]); + $middleware->append(\App\Http\Middleware\ScalekitTokenRefresh::class); +}) +``` + +### `ScalekitAuth` — session gate + +Redirects to `auth.login` with `->with('next', $request->path())` if `scalekit_user` session key is missing. + +### `ScalekitPermission` — parameterised permission check + +Validates access token claims via `ScalekitClient::hasPermission()`. On failure: 403 view. + +### `ScalekitTokenRefresh` — auto refresh on every request + +Skipped paths: `login`, `auth/callback`, `logout`. Buffer: 5 minutes. On `invalid_grant`: flush session. + +## Routes + +```php +// Public +Route::get('/', [AuthController::class, 'home'])->name('auth.home'); +Route::get('/login', [AuthController::class, 'login'])->name('auth.login'); +Route::get('/auth/callback', [AuthController::class, 'callback'])->name('auth.callback'); + +// Protected group +Route::middleware(['scalekit.auth'])->group(function () { + Route::get('/dashboard', [AuthController::class, 'dashboard'])->name('auth.dashboard'); + Route::match(['get', 'post'], '/logout', [AuthController::class, 'logout'])->name('auth.logout'); + Route::get('/sessions', [AuthController::class, 'sessions'])->name('auth.sessions'); + Route::post('/sessions/refresh-token', [AuthController::class, 'refreshToken'])->name('auth.refresh_token'); + + Route::get('/organization/settings', [AuthController::class, 'organizationSettings']) + ->middleware('scalekit.permission:organization:settings') + ->name('auth.organization_settings'); +}); +``` + +## Install + +```bash +composer require firebase/php-jwt # Only if using JWT signature verification +php artisan key:generate +php artisan migrate +php artisan serve +``` + +Copy `.env.example` to `.env` and fill in the four `SCALEKIT_*` values. + +## Tactics + +### SameSite=Lax — required for OAuth callbacks + +In `config/session.php`: + +```php +'same_site' => 'lax', +'secure' => env('SESSION_SECURE_COOKIE', false), +'http_only' => true, +``` + +### CSRF exclusion for the OAuth callback + +GET callbacks are not subject to CSRF. For webhook POST endpoints, exclude them in `bootstrap/app.php`. + +### Deep link preservation + +```php +// In login +$next = $request->query('next', route('auth.dashboard')); +if (!str_starts_with($next, '/')) { $next = route('auth.dashboard'); } +session(['next' => $next]); + +// In callback +$next = session()->pull('next', route('auth.dashboard')); +return redirect($next); +``` + +### Cache-Control: no-store on protected responses + +```php +return response() + ->view('auth.dashboard', ['user' => session('scalekit_user', [])]) + ->header('Cache-Control', 'no-store'); +``` + +### CORS for JavaScript clients + +In `config/cors.php`: + +```php +'paths' => ['api/*', 'auth/*', 'sessions/*'], +'allowed_origins' => ['http://localhost:3000'], +'supports_credentials' => true, +``` + +### Session fixation after login + +```php +session()->regenerate(); +return redirect($next); +``` + +## Reference + +- Full working example: [scalekit-inc/scalekit-laravel-auth-example](https://github.com/scalekit-inc/scalekit-laravel-auth-example) +- Scalekit docs: https://docs.scalekit.com diff --git a/plugins/saaskit/skills/implementing-saaskit/springboot-reference.md b/plugins/saaskit/skills/implementing-saaskit/springboot-reference.md new file mode 100644 index 0000000..15e8fa9 --- /dev/null +++ b/plugins/saaskit/skills/implementing-saaskit/springboot-reference.md @@ -0,0 +1,232 @@ +# Scalekit Auth in Spring Boot + +Scalekit acts as an OIDC provider. Spring Security's `oauth2-client` starter handles the full +authorization code flow — no custom filters needed. + +## Required dependencies + +Add to `pom.xml` (Spring Boot 3.2+, Java 17+): + +```xml + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-security + + + com.scalekit + scalekit-sdk-java + 2.0.4 + +``` + +## Configuration + +`src/main/resources/application.yml`: + +```yaml +scalekit: + env-url: ${SCALEKIT_ENVIRONMENT_URL} + client-id: ${SCALEKIT_CLIENT_ID} + client-secret: ${SCALEKIT_CLIENT_SECRET} + redirect-uri: ${SCALEKIT_REDIRECT_URI:http://localhost:8080/login/oauth2/code/scalekit} + +spring: + security: + oauth2: + client: + registration: + scalekit: + client-id: ${scalekit.client-id} + client-secret: ${scalekit.client-secret} + authorization-grant-type: authorization_code + redirect-uri: ${scalekit.redirect-uri} + scope: openid,profile,email,offline_access + client-name: Scalekit + provider: + scalekit: + issuer-uri: ${scalekit.env-url} + authorization-uri: ${scalekit.env-url}/oauth/authorize + token-uri: ${scalekit.env-url}/oauth/token + user-info-uri: ${scalekit.env-url}/userinfo + jwk-set-uri: ${scalekit.env-url}/keys + user-name-attribute: sub +``` + +## Scalekit SDK bean + +```java +@Configuration +public class ScalekitConfig { + + @Value("${scalekit.env-url}") + private String envUrl; + + @Value("${scalekit.client-id}") + private String clientId; + + @Value("${scalekit.client-secret}") + private String clientSecret; + + @Bean + public ScalekitClient scalekitClient() { + return new ScalekitClient(envUrl, clientId, clientSecret); + } +} +``` + +## Security filter chain + +```java +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, + ClientRegistrationRepository clientRegistrationRepository) throws Exception { + http + .authorizeHttpRequests(authz -> authz + .requestMatchers("/", "/login", "/error", "/css/**", "/js/**").permitAll() + .anyRequest().authenticated() + ) + .oauth2Login(oauth2 -> oauth2 + .loginPage("/login") + .defaultSuccessUrl("/dashboard", true) + ) + .logout(logout -> logout + .logoutSuccessHandler(oidcLogoutSuccessHandler(clientRegistrationRepository)) + .invalidateHttpSession(true) + .clearAuthentication(true) + ); + return http.build(); + } + + private LogoutSuccessHandler oidcLogoutSuccessHandler( + ClientRegistrationRepository clientRegistrationRepository) { + OidcClientInitiatedLogoutSuccessHandler handler = + new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository); + handler.setPostLogoutRedirectUri("{baseUrl}"); + return handler; + } +} +``` + +## Accessing user identity in controllers + +```java +@GetMapping("/dashboard") +public String dashboard(@AuthenticationPrincipal OidcUser oidcUser, Model model) { + model.addAttribute("name", oidcUser.getFullName()); + model.addAttribute("email", oidcUser.getEmail()); + model.addAttribute("subject", oidcUser.getSubject()); + model.addAttribute("claims", oidcUser.getClaims()); + return "dashboard"; +} +``` + +## Application routes + +| Route | Auth? | Notes | +|---|---|---| +| `/` | No | Home page | +| `/login` | No | Custom login page | +| `/dashboard` | Yes | Protected; redirects to login | +| `/oauth2/authorization/scalekit` | No | Starts OIDC flow | +| `/auth/callback` | No | Handled by Spring Security automatically | +| `/logout` | Yes | Triggers OIDC end-session | + +## Scalekit Dashboard setup checklist + +``` +- [ ] Get Environment URL (e.g., https://your-env.scalekit.dev) +- [ ] Get Client ID and Client Secret from Settings > API Credentials +- [ ] Add allowed redirect URI: http://localhost:8080/login/oauth2/code/scalekit +- [ ] Optionally add post-logout redirect: http://localhost:8080 +``` + +## Workflow + +``` +- [ ] Step 1: Add Maven dependencies +- [ ] Step 2: Add application.yml OAuth2 provider/registration config +- [ ] Step 3: Create ScalekitConfig bean +- [ ] Step 4: Create SecurityConfig filter chain +- [ ] Step 5: Inject @AuthenticationPrincipal OidcUser in protected controllers +- [ ] Step 6: Configure redirect URIs in Scalekit dashboard +- [ ] Step 7: Run app and verify login → dashboard → logout flow +``` + +## Troubleshooting + +**JWKS timeout / JWT verification errors**: Spring Security fetches JWKS on every token validation. Increase the decoder timeout. + +**Redirect URI mismatch**: The `redirect-uri` in `application.yml` must exactly match what is registered in the Scalekit dashboard. + +**Enable debug logging**: + +```yaml +logging: + level: + org.springframework.security.oauth2: TRACE +``` + +## Reference + +- Full working example: [scalekit-inc/scalekit-springboot-auth-example](https://github.com/scalekit-inc/scalekit-springboot-auth-example) +- Scalekit docs: https://docs.scalekit.com + +## Tactics + +### SameSite=Lax on the session cookie + +```yaml +server: + servlet: + session: + cookie: + same-site: lax + http-only: true + secure: true +``` + +### Deep link preservation + +Remove `true` from `defaultSuccessUrl("/dashboard", true)` to respect saved-request redirect. + +### CORS for browser clients + +```java +@Bean +public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(List.of("http://localhost:3000")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; +} +``` + +### AJAX: 401 instead of redirect + +```java +.exceptionHandling(ex -> ex + .authenticationEntryPoint((request, response, authException) -> { + String accept = request.getHeader("Accept"); + if (accept != null && accept.contains("application/json")) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication required"); + } else { + response.sendRedirect("/login"); + } + })) +``` + +### OIDC logout vs local logout + +`OidcClientInitiatedLogoutSuccessHandler` calls the Scalekit end-session endpoint. Always use the OIDC handler — a plain `logoutSuccessUrl()` only clears the local session. diff --git a/plugins/saaskit/skills/implementing-scim-provisioning/SKILL.md b/plugins/saaskit/skills/implementing-scim-provisioning/SKILL.md new file mode 100644 index 0000000..8b6b158 --- /dev/null +++ b/plugins/saaskit/skills/implementing-scim-provisioning/SKILL.md @@ -0,0 +1,251 @@ +--- +name: implementing-scim-provisioning +description: 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://.scalekit.com' +SCALEKIT_CLIENT_ID='' +SCALEKIT_CLIENT_SECRET='' +SCALEKIT_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 +// +``` + +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/) diff --git a/plugins/saaskit/skills/managing-saaskit-sessions/SKILL.md b/plugins/saaskit/skills/managing-saaskit-sessions/SKILL.md new file mode 100644 index 0000000..e3c5548 --- /dev/null +++ b/plugins/saaskit/skills/managing-saaskit-sessions/SKILL.md @@ -0,0 +1,349 @@ +--- +name: managing-saaskit-sessions +description: 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 `. +- 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. \ No newline at end of file diff --git a/plugins/saaskit/skills/migrating-to-saaskit/AUDIT-CHECKLIST.md b/plugins/saaskit/skills/migrating-to-saaskit/AUDIT-CHECKLIST.md new file mode 100644 index 0000000..5b54ef9 --- /dev/null +++ b/plugins/saaskit/skills/migrating-to-saaskit/AUDIT-CHECKLIST.md @@ -0,0 +1,33 @@ +# Auth Code Audit Checklist + +## Flows to locate and document +- [ ] Sign-up endpoint and validation logic +- [ ] Login endpoint (password, OAuth, SSO paths) +- [ ] Session middleware (where tokens are validated per request) +- [ ] Refresh token handling +- [ ] RBAC enforcement points (middleware, decorators, guards) +- [ ] Email verification trigger and callback +- [ ] Logout + session invalidation + +## Data to export +- [ ] Users table (email, name, email_verified, created_at) +- [ ] Organizations / tenants table +- [ ] Role definitions and user-role join table +- [ ] OAuth / SSO provider configs (client IDs, domains) + +## Format for export +Produce a JSON array per entity type: + +```json +[ + { + "email": "user@example.com", + "external_id": "usr_001", + "first_name": "Jane", + "last_name": "Doe", + "email_verified": true, + "org_external_id": "org_123", + "roles": ["admin"] + } +] +``` diff --git a/plugins/saaskit/skills/migrating-to-saaskit/IMPORT-SAMPLES.md b/plugins/saaskit/skills/migrating-to-saaskit/IMPORT-SAMPLES.md new file mode 100644 index 0000000..403fb54 --- /dev/null +++ b/plugins/saaskit/skills/migrating-to-saaskit/IMPORT-SAMPLES.md @@ -0,0 +1,108 @@ +# Import Code Samples + +## Python — Create organization +```python +from scalekit.v1.organizations.organizations_pb2 import CreateOrganization + +result = scalekit_client.organization.create_organization( + CreateOrganization( + display_name="Megasoft Inc", + external_id="org_123", + metadata={"plan": "enterprise"} + ) +) +``` + +## Python — Create user in organization +```python +from scalekit.v1.users.users_pb2 import CreateUser +from scalekit.v1.commons.commons_pb2 import UserProfile + +user_msg = CreateUser( + email="user@example.com", + external_id="usr_987", + metadata={"department": "engineering"}, + user_profile=UserProfile(first_name="John", last_name="Doe") +) +create_resp, _ = scalekit_client.user.create_user_and_membership("org_123", user_msg) +``` + +## Go — Create organization +```go +result, err := scalekit.Organization.CreateOrganization(ctx, "Megasoft Inc", + scalekit.CreateOrganizationOptions{ExternalID: "org_123"}) +``` + +## Go — Create user in organization +```go +createUser := &usersv1.CreateUser{ + Email: "user@example.com", + ExternalId: "usr_987", + UserProfile: &usersv1.CreateUserProfile{ + FirstName: "John", + LastName: "Doe", + }, +} +resp, err := scalekitClient.User().CreateUserAndMembership(ctx, "org_123", createUser) +``` + +## Java — Create organization +```java +CreateOrganization createOrg = CreateOrganization.newBuilder() + .setDisplayName("Megasoft Inc") + .setExternalId("org_123") + .build(); +scalekitClient.organizations().createOrganization(createOrg); +``` + +## Java — Create user +```java +CreateUser createUser = CreateUser.newBuilder() + .setEmail("user@example.com") + .setExternalId("usr_987") + .setUserProfile(CreateUserProfile.newBuilder().setFirstName("John").setLastName("Doe").build()) + .build(); +scalekitClient.users().createUserAndMembership("org_123", createUser); +``` + +## Node.js — Create organization +```javascript +const result = await scalekit.organization.createOrganization( + "Megasoft Inc", + { externalId: "org_123", metadata: { plan: "enterprise" } } +); +``` + +## Node.js — Create user in organization +```javascript +const { user } = await scalekit.user.createUserAndMembership("org_scalekit_id", { + email: "user@example.com", + externalId: "usr_987", + userProfile: { firstName: "John", lastName: "Doe" }, +}); +``` + +## cURL — Create organization +```sh +curl -L -X POST '/api/v1/organizations' \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{ + "display_name": "Megasoft Inc", + "external_id": "org_123", + "metadata": { "plan": "enterprise" } + }' +``` + +## cURL — Create user in organization +```sh +curl -L -X POST '/api/v1/organizations//users' \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{ + "email": "user@example.com", + "external_id": "usr_987", + "send_invitation_email": false, + "user_profile": { "first_name": "John", "last_name": "Doe" } + }' +``` diff --git a/plugins/saaskit/skills/migrating-to-saaskit/SKILL.md b/plugins/saaskit/skills/migrating-to-saaskit/SKILL.md new file mode 100644 index 0000000..445003b --- /dev/null +++ b/plugins/saaskit/skills/migrating-to-saaskit/SKILL.md @@ -0,0 +1,157 @@ +--- +name: migrating-to-saaskit +description: 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//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. diff --git a/plugins/saaskit/skills/production-readiness-saaskit/SKILL.md b/plugins/saaskit/skills/production-readiness-saaskit/SKILL.md new file mode 100644 index 0000000..2b0eebd --- /dev/null +++ b/plugins/saaskit/skills/production-readiness-saaskit/SKILL.md @@ -0,0 +1,131 @@ +--- +name: production-readiness-saaskit +description: 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://.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: '' })); +" +# 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: `.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 succeeds diff --git a/plugins/saaskit/skills/scalekit-code-doctor/SKILL.md b/plugins/saaskit/skills/scalekit-code-doctor/SKILL.md new file mode 100644 index 0000000..8a42e5a --- /dev/null +++ b/plugins/saaskit/skills/scalekit-code-doctor/SKILL.md @@ -0,0 +1,125 @@ +--- +name: scalekit-code-doctor +description: Use 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://.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`. \ No newline at end of file diff --git a/plugins/saaskit/skills/scalekit-code-doctor/references/COMMON-MISTAKES.md b/plugins/saaskit/skills/scalekit-code-doctor/references/COMMON-MISTAKES.md new file mode 100644 index 0000000..45ea547 --- /dev/null +++ b/plugins/saaskit/skills/scalekit-code-doctor/references/COMMON-MISTAKES.md @@ -0,0 +1,479 @@ +# Common Mistakes in Scalekit Code + +This file catalogs known anti-patterns, hallucinated methods, and security issues found in Scalekit integrations. Each entry shows the wrong pattern and the correct fix. Use this as a lookup during both generation and review. 10 categories. + +--- + +## 1. Wrong Import Paths + +### Node.js + +**Wrong:** +```typescript +import ScalekitClient from '@scalekit-sdk/node'; // default import — use named import +import { ScalekitClient } from 'scalekit'; // wrong package name +import { ScalekitClient } from 'scalekit-sdk-node'; // wrong package name +``` + +**Correct:** +```typescript +import { ScalekitClient } from '@scalekit-sdk/node'; +``` + +Only `ScalekitClient` is the canonical named export from `@scalekit-sdk/node`. Older docs sometimes show `Scalekit` as an alias — treat that as deprecated/wrong; the runtime export is `ScalekitClient`. Always use `ScalekitClient`. + +### Python + +**Wrong:** +```python +from scalekit_sdk import ScalekitClient # wrong module name +from scalekit.client import ScalekitClient # internal path, not public API +import scalekit # missing class import +pip install scalekit # wrong pip package name +``` + +**Correct:** +```python +from scalekit import ScalekitClient +# pip install scalekit-sdk-python +``` + +### Go + +**Wrong:** +```go +import "github.com/scalekit-inc/scalekit-sdk-go" // missing version +import "github.com/scalekit/scalekit-sdk-go/v2" // wrong org name +``` + +**Correct:** +```go +import scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2" +``` + +### Java + +**Wrong:** +```java +import com.scalekit.sdk.ScalekitClient; // wrong package path +import io.scalekit.ScalekitClient; // wrong package +``` + +**Correct:** +```java +import com.scalekit.ScalekitClient; +``` + +--- + +## 2. Wrong Sub-Client Names (Python vs Node) + +Python and Node use different pluralization for some sub-clients. Using the wrong one causes `AttributeError` in Python or `TypeError` in Node. + +| Sub-client | Node.js (singular) | Python (plural) | +|------------|--------------------|--------------------| +| Users | `client.user.getUser(...)` | `client.users.get_user(...)` | +| Roles | `client.role.listRoles(...)` | `client.roles.list_roles(...)` | +| Permissions | `client.permission.listPermissions(...)` | `client.permissions.list_permissions(...)` | +| Sessions | `client.session.getSession(...)` | `client.sessions.get_session(...)` | + +Sub-clients that are the SAME in both: `organization`, `connection`, `domain`, `directory`. + +**Wrong (Python):** +```python +client.user.get_user(user_id) # AttributeError: 'ScalekitClient' has no attribute 'user' +client.role.list_roles() # AttributeError +client.session.revoke_session(sid) # AttributeError +``` + +**Correct (Python):** +```python +client.users.get_user(user_id) +client.roles.list_roles() +client.sessions.revoke_session(sid) +``` + +--- + +## 3. Wrong Method Names + +### Node.js + +| Wrong | Correct | Notes | +|-------|---------|-------| +| `scalekit.authenticate(code)` | `scalekit.authenticateWithCode(code, redirectUri)` | Missing `WithCode` suffix and `redirectUri` param | +| `scalekit.getAuthUrl(...)` | `scalekit.getAuthorizationUrl(redirectUri, options?)` | Wrong method name | +| `scalekit.login(...)` | `scalekit.getAuthorizationUrl(redirectUri, options?)` | No `login` method | +| `scalekit.logout(...)` | `scalekit.getLogoutUrl(options?)` | Returns URL, doesn't perform logout | +| `scalekit.verifyToken(token)` | `scalekit.validateAccessToken(token)` or `scalekit.validateToken(token)` | Wrong name | +| `scalekit.createOrganization(...)` | `scalekit.organization.createOrganization(...)` | Must use sub-client | +| `scalekit.getOrganization(...)` | `scalekit.organization.getOrganization(...)` | Must use sub-client | + +### Python + +| Wrong | Correct | Notes | +|-------|---------|-------| +| `client.authenticateWithCode(...)` | `client.authenticate_with_code(...)` | Python uses snake_case | +| `client.getAuthorizationUrl(...)` | `client.get_authorization_url(...)` | Python uses snake_case | +| `client.getLogoutUrl(...)` | `client.get_logout_url(...)` | Python uses snake_case | +| `client.validateToken(...)` | `client.validate_access_token(...)` | Different method name in Python | +| `client.verify_webhook(...)` | `client.verify_webhook_payload(...)` | Missing `_payload` suffix | + +### Go + +| Wrong | Correct | Notes | +|-------|---------|-------| +| `client.AuthenticateWithCode(code, uri)` | `client.AuthenticateWithCode(ctx, code, uri, options)` | Missing `ctx` parameter | +| `client.GetAuthorizationUrl(uri)` | `client.GetAuthorizationUrl(uri, options)` | Missing `options` param (required in Go) | +| `client.Organization.Create(...)` | `client.Organization().CreateOrganization(ctx, request)` | Use accessor method `Organization()`, not field | + +### Java + +| Wrong | Correct | Notes | +|-------|---------|-------| +| `client.organization.create(...)` | `client.organizations().create(...)` | Use `organizations()` accessor method, plural | +| `client.getOrganization(id)` | `client.organizations().getById(id)` | Use sub-client accessor | +| `client.connections.list(...)` | `client.connections().listConnectionsByOrganization(orgId)` | Use accessor method | + +--- + +## 4. Missing Required Parameters + +### `authenticateWithCode` — missing `redirectUri` + +**Wrong:** +```typescript +const result = await scalekit.authenticateWithCode(code); +``` + +**Correct:** +```typescript +const result = await scalekit.authenticateWithCode(code, redirectUri); +``` + +The `redirectUri` must exactly match the one used in `getAuthorizationUrl` AND what's registered in the Scalekit dashboard. + +### `getAuthorizationUrl` — missing `state` for CSRF + +**Wrong:** +```typescript +const authUrl = scalekit.getAuthorizationUrl(redirectUri); +``` + +**Correct:** +```typescript +import crypto from 'crypto'; +const state = crypto.randomBytes(32).toString('base64url'); +// Store state in session/cookie for validation in callback +const authUrl = scalekit.getAuthorizationUrl(redirectUri, { state }); +``` + +While `state` is technically optional, omitting it is a **CSRF vulnerability**. Always generate and validate it. + +### Go — missing `context.Context` + +**Wrong:** +```go +resp, err := client.AuthenticateWithCode(code, redirectUri, opts) +``` + +**Correct:** +```go +resp, err := client.AuthenticateWithCode(ctx, code, redirectUri, opts) +``` + +All Go network methods require `context.Context` as the first parameter. + +--- + +## 5. Auth Flow Gaps + +### Missing callback handler + +If you see a login route that generates an auth URL but no corresponding callback route, the flow is incomplete. The callback MUST: +1. Validate the `state` parameter against the stored value +2. Call `authenticateWithCode(code, redirectUri)` +3. Store the session (tokens + user info) +4. Redirect to the application + +### Missing `state` validation in callback + +**Wrong:** +```typescript +app.get('/auth/callback', async (req, res) => { + const { code } = req.query; + const result = await scalekit.authenticateWithCode(code, redirectUri); + // ... store session +}); +``` + +**Correct:** +```typescript +app.get('/auth/callback', async (req, res) => { + const { code, state } = req.query; + + const storedState = req.session.oauthState; // or from cookie + if (!state || state !== storedState) { + return res.status(403).send('CSRF validation failed'); + } + + const result = await scalekit.authenticateWithCode(code, redirectUri); + // ... store session +}); +``` + +### Incomplete logout — only clearing local session + +**Wrong:** +```typescript +app.post('/logout', (req, res) => { + req.session.destroy(); + res.redirect('/'); +}); +``` + +**Correct:** +```typescript +app.post('/logout', (req, res) => { + const logoutUrl = scalekit.getLogoutUrl({ + idTokenHint: req.session.idToken, + postLogoutRedirectUri: 'https://yourapp.com', + }); + req.session.destroy(); + res.redirect(logoutUrl); // Ends IdP session too +}); +``` + +Without calling `getLogoutUrl()`, the user's IdP session persists and they get silently re-authenticated on next login. + +### Missing IdP-initiated login handling + +If the callback route doesn't check for `idp_initiated_login` query parameter, IdP-initiated SSO won't work: + +```typescript +app.get('/auth/callback', async (req, res) => { + const { idp_initiated_login, code, state } = req.query; + + if (idp_initiated_login) { + const claims = await scalekit.getIdpInitiatedLoginClaims(idp_initiated_login); + const authUrl = scalekit.getAuthorizationUrl(redirectUri, { + connectionId: claims.connection_id, + organizationId: claims.organization_id, + loginHint: claims.login_hint, + ...(claims.relay_state && { state: claims.relay_state }), + }); + return res.redirect(authUrl); + } + + // Normal SP-initiated flow continues... +}); +``` + +--- + +## 6. Security Anti-Patterns + +### `sameSite: 'strict'` on session cookies + +**Wrong:** +```typescript +res.cookie('session', data, { sameSite: 'strict', httpOnly: true, secure: true }); +``` + +**Correct:** +```typescript +res.cookie('session', data, { sameSite: 'lax', httpOnly: true, secure: true }); +``` + +OAuth callbacks are cross-site redirects from Scalekit back to your app. `strict` drops the cookie on that redirect, causing CSRF state mismatch errors on every login. + +### Missing `httpOnly` flag + +**Wrong:** +```typescript +res.cookie('session', data, { secure: true }); +``` + +**Correct:** +```typescript +res.cookie('session', data, { httpOnly: true, secure: true, sameSite: 'lax' }); +``` + +Without `httpOnly`, JavaScript can read the session cookie — XSS becomes session hijacking. + +### Open redirect via unvalidated `next` parameter + +**Wrong:** +```typescript +const next = req.query.next; +res.redirect(next); // Attacker can set next=https://evil.com +``` + +**Correct:** +```typescript +const next = req.query.next; +// Only allow relative paths +if (!next || !next.startsWith('/') || next.startsWith('//')) { + return res.redirect('/dashboard'); +} +res.redirect(next); +``` + +### Hardcoded client secret + +**Wrong:** +```typescript +const scalekit = new ScalekitClient( + 'https://myapp.scalekit.com', + 'skc_12345', + 'sks_secret_abc123' // NEVER hardcode secrets +); +``` + +**Correct:** +```typescript +const scalekit = new ScalekitClient( + process.env.SCALEKIT_ENVIRONMENT_URL!, + process.env.SCALEKIT_CLIENT_ID!, + process.env.SCALEKIT_CLIENT_SECRET! +); +``` + +### Webhook handler without signature verification + +**Wrong:** +```typescript +app.post('/webhooks', express.json(), (req, res) => { + const event = req.body; // Trusting unverified payload + handleEvent(event); + res.sendStatus(200); +}); +``` + +**Correct:** +```typescript +app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => { + const isValid = scalekit.verifyWebhookPayload( + process.env.SCALEKIT_WEBHOOK_SECRET!, + req.headers, + req.body.toString() + ); + + if (!isValid) { + return res.sendStatus(401); + } + + const event = JSON.parse(req.body.toString()); + handleEvent(event); + res.sendStatus(200); +}); +``` + +Note: The webhook body must be raw (not JSON-parsed) for signature verification to work. + +### Client-side navigation for OAuth redirect + +**Wrong:** +```typescript +// React / Next.js +router.push(authUrl); // Client-side route change +``` + +**Correct:** +```typescript +window.location.href = authUrl; // Full page navigation required for OAuth +``` + +OAuth redirects are full HTTP redirects to an external domain (Scalekit/IdP). Client-side routing doesn't work. + +--- + +## 7. Environment Variable Mistakes + +| Wrong | Correct | Issue | +|-------|---------|-------| +| `SCALEKIT_URL` | `SCALEKIT_ENVIRONMENT_URL` | Missing `ENV_` | +| `SCALEKIT_SECRET` | `SCALEKIT_CLIENT_SECRET` | Missing `CLIENT_` | +| `SCALEKIT_ID` | `SCALEKIT_CLIENT_ID` | Missing `CLIENT_` | +| `SCALEKIT_CALLBACK_URL` | `SCALEKIT_REDIRECT_URI` | Wrong name entirely | +| `http://myapp.scalekit.com` | `https://myapp.scalekit.com` | Must be HTTPS | +| `https://myapp.scalekit.com/` | `https://myapp.scalekit.com` | No trailing slash | + +--- + +## 8. Client Instantiation Mistakes + +### Creating a new client per request + +**Wrong:** +```typescript +app.get('/api/data', async (req, res) => { + const scalekit = new ScalekitClient(envUrl, clientId, clientSecret); // per-request! + // ... +}); +``` + +**Correct:** +```typescript +// Module-level singleton +const scalekit = new ScalekitClient( + process.env.SCALEKIT_ENVIRONMENT_URL!, + process.env.SCALEKIT_CLIENT_ID!, + process.env.SCALEKIT_CLIENT_SECRET! +); + +app.get('/api/data', async (req, res) => { + // Use the singleton + const result = await scalekit.validateAccessToken(token); +}); +``` + +The client manages its own token lifecycle and connection pooling. Creating it per request wastes resources and can hit rate limits. + +--- + +## 9. Token Refresh Race Conditions + +When multiple browser tabs trigger token refresh simultaneously, the second request often fails because the first one already consumed the refresh token. + +**Mitigation pattern:** +```typescript +// Before refreshing, set a short-lived flag +const REFRESH_LOCK_KEY = 'refresh_in_progress'; + +async function refreshToken(session) { + if (session[REFRESH_LOCK_KEY]) return; // Another tab is refreshing + + session[REFRESH_LOCK_KEY] = true; + try { + const result = await scalekit.refreshAccessToken(session.refreshToken); + session.accessToken = result.accessToken; + session.refreshToken = result.refreshToken; + } finally { + delete session[REFRESH_LOCK_KEY]; + } +} +``` + +--- + +## 10. Missing Scopes + +### Refresh tokens require `offline_access` scope + +**Wrong:** +```typescript +const authUrl = scalekit.getAuthorizationUrl(redirectUri, { + scopes: ['openid', 'profile', 'email'], +}); +// Later: scalekit.refreshAccessToken(refreshToken) → fails because no refresh token was issued +``` + +**Correct:** +```typescript +const authUrl = scalekit.getAuthorizationUrl(redirectUri, { + scopes: ['openid', 'profile', 'email', 'offline_access'], +}); +``` + +Without `offline_access`, the authorization server won't issue a refresh token. \ No newline at end of file diff --git a/plugins/saaskit/skills/scalekit-code-doctor/references/EXAMPLE-REPOS.md b/plugins/saaskit/skills/scalekit-code-doctor/references/EXAMPLE-REPOS.md new file mode 100644 index 0000000..a66718b --- /dev/null +++ b/plugins/saaskit/skills/scalekit-code-doctor/references/EXAMPLE-REPOS.md @@ -0,0 +1,61 @@ +# Example Repos + +Working examples by framework. Fetch the matching repo for real, tested patterns when generating or reviewing code. + +## SaaSKit — Auth examples + +| Framework | Repo | What it shows | +|-----------|------|---------------| +| Next.js (App Router) | [scalekit-nextjs-auth-example](https://github.com/scalekit-inc/scalekit-nextjs-auth-example) | SSO, sessions, protected routes | +| Next.js (Pages) | [nextjs-example-apps](https://github.com/scalekit-inc/nextjs-example-apps) | React SSO flows | +| Next.js + Auth.js | [scalekit-authjs-example](https://github.com/scalekit-developers/scalekit-authjs-example) | Enterprise SSO with next-auth v5 | +| Express.js | [scalekit-express-auth-example](https://github.com/scalekit-inc/scalekit-express-auth-example) | Node SDK, sessions | +| Express.js | [scalekit-express-example](https://github.com/scalekit-developers/scalekit-express-example) | SSO, middleware | +| FastAPI | [scalekit-fastapi-auth-example](https://github.com/scalekit-inc/scalekit-fastapi-auth-example) | Python SDK, OAuth 2.0 | +| FastAPI | [scalekit-fastapi-example](https://github.com/scalekit-developers/scalekit-fastapi-example) | Async auth, Pydantic | +| Django | [scalekit-django-auth-example](https://github.com/scalekit-inc/scalekit-django-auth-example) | Django auth integration | +| Flask | [scalekit-flask-auth-example](https://github.com/scalekit-inc/scalekit-flask-auth-example) | Flask sessions | +| Spring Boot | [scalekit-springboot-auth-example](https://github.com/scalekit-inc/scalekit-springboot-auth-example) | Spring Security, OIDC | +| Go (Gin) | [scalekit-go-example](https://github.com/scalekit-developers/scalekit-go-example) | Go SDK, SSO | +| Laravel | [scalekit-laravel-auth-example](https://github.com/scalekit-inc/scalekit-laravel-auth-example) | REST API, Laravel | +| Astro | [astro-scalekit-auth-example](https://github.com/scalekit-developers/astro-scalekit-auth-example) | Auth, SSO, social login | +| .NET | [dotnet-example-apps](https://github.com/scalekit-inc/dotnet-example-apps) | ASP.NET Core, SAML/OIDC | +| Expo (mobile) | [expo-scalekit-sample](https://github.com/scalekit-inc/expo-scalekit-sample) | OAuth 2.0 + PKCE | + +## SaaSKit — Integration examples + +| Integration | Repo | +|-------------|------| +| AWS Cognito | [scalekit-cognito-sso](https://github.com/scalekit-inc/scalekit-cognito-sso) | +| Firebase | [scalekit-firebase-sso](https://github.com/scalekit-inc/scalekit-firebase-sso) | +| Supabase | [scalekit-supabase-example](https://github.com/scalekit-inc/scalekit-supabase-example) | +| Multi-app SSO | [multiapp-demo](https://github.com/scalekit-inc/multiapp-demo) | +| OIDC/SAML/SCIM | [oidc-saml-scim-examples](https://github.com/scalekit-developers/oidc-saml-scim-examples) | +| Full demo app | [coffee-desk-demo](https://github.com/scalekit-inc/coffee-desk-demo) | + +## AgentKit — Agent and MCP examples + +| Framework | Repo | +|-----------|------| +| LangChain | [sample-langchain-agent](https://github.com/scalekit-inc/sample-langchain-agent) | +| Google ADK | [google-adk-agent-example](https://github.com/scalekit-inc/google-adk-agent-example) | +| Vercel AI SDK | [vercel-ai-agent-toolkit](https://github.com/scalekit-developers/vercel-ai-agent-toolkit) | +| MCP Auth | [mcp-auth-demos](https://github.com/scalekit-inc/mcp-auth-demos) | +| FastMCP | [fastmcp-scalekit-example](https://github.com/scalekit-inc/fastmcp-scalekit-example) | +| Agent auth | [agent-auth-examples](https://github.com/scalekit-developers/agent-auth-examples) | + +## Developer tools + +| Tool | Repo | +|------|------| +| Dryrun CLI | [scalekit-dryrun](https://github.com/scalekit-inc/scalekit-dryrun) | +| MCP server | [scalekit-mcp-server](https://github.com/scalekit-inc/scalekit-mcp-server) | +| API collections | [api-collections](https://github.com/scalekit-inc/api-collections) | + +## Frontend SDKs + +| SDK | Repo | +|-----|------| +| React | [scalekit-react-sdk](https://github.com/scalekit-inc/scalekit-react-sdk) | +| Vue | [scalekit-vue-sdk](https://github.com/scalekit-inc/scalekit-vue-sdk) | +| Expo | [scalekit-expo-sdk](https://github.com/scalekit-inc/scalekit-expo-sdk) | \ No newline at end of file diff --git a/plugins/saaskit/skills/scalekit-code-doctor/references/REFERENCE.md b/plugins/saaskit/skills/scalekit-code-doctor/references/REFERENCE.md new file mode 100644 index 0000000..f4c5bb8 --- /dev/null +++ b/plugins/saaskit/skills/scalekit-code-doctor/references/REFERENCE.md @@ -0,0 +1,504 @@ +# Scalekit API Reference — Compact Lookup + +This file contains every correct SDK method signature and REST endpoint. Use it as ground truth when generating or reviewing Scalekit code. If a method isn't listed here, do NOT assume it exists — verify against the live SDK source or `https://docs.scalekit.com/apis`. + +--- + +## Client Initialization + +### Node.js (`@scalekit-sdk/node`) + +```typescript +import { ScalekitClient } from '@scalekit-sdk/node'; + +const scalekit = new ScalekitClient( + process.env.SCALEKIT_ENVIRONMENT_URL!, // string — environment URL + process.env.SCALEKIT_CLIENT_ID!, // string — client ID + process.env.SCALEKIT_CLIENT_SECRET! // string — client secret +); +``` + +### Python (`scalekit-sdk-python`) + +```python +from scalekit import ScalekitClient + +scalekit_client = ScalekitClient( + os.environ.get('SCALEKIT_ENVIRONMENT_URL'), # str — environment URL + os.environ.get('SCALEKIT_CLIENT_ID'), # str — client ID + os.environ.get('SCALEKIT_CLIENT_SECRET') # str — client secret +) +``` + +### Go (`github.com/scalekit-inc/scalekit-sdk-go/v2`) + +```go +import scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2" + +client := scalekit.NewScalekitClient( + os.Getenv("SCALEKIT_ENVIRONMENT_URL"), // string — environment URL + os.Getenv("SCALEKIT_CLIENT_ID"), // string — client ID + os.Getenv("SCALEKIT_CLIENT_SECRET"), // string — client secret +) +``` + +### Java (`com.scalekit:scalekit-sdk-java`) + +```java +import com.scalekit.ScalekitClient; + +ScalekitClient client = new ScalekitClient( + System.getenv("SCALEKIT_ENVIRONMENT_URL"), // String — environment URL + System.getenv("SCALEKIT_CLIENT_ID"), // String — client ID + System.getenv("SCALEKIT_CLIENT_SECRET") // String — client secret +); +``` + +--- + +## Environment Variables + +| Variable | Purpose | Format | +|----------|---------|--------| +| `SCALEKIT_ENVIRONMENT_URL` | Environment URL | `https://.scalekit.com` (prod) or `https://.scalekit.dev` (dev) | +| `SCALEKIT_CLIENT_ID` | Client ID | String from dashboard | +| `SCALEKIT_CLIENT_SECRET` | Client secret | String from dashboard | +| `SCALEKIT_REDIRECT_URI` | OAuth callback URL | Must exactly match dashboard config | +| `SCALEKIT_WEBHOOK_SECRET` | Webhook signing secret | Format: `whsec_...` | + +Note: The REST API docs use `SCALEKIT_ENVIRONMENT_URL` in some examples. Both `SCALEKIT_ENVIRONMENT_URL` and `SCALEKIT_ENVIRONMENT_URL` are acceptable — just be consistent within a project. + +--- + +## Auth Methods (called directly on the client) + +### Node.js + +| Method | Signature | Returns | +|--------|-----------|---------| +| `getAuthorizationUrl` | `(redirectUri: string, options?: AuthorizationUrlOptions) → string` | Authorization URL string | +| `authenticateWithCode` | `(code: string, redirectUri: string, options?: AuthenticationOptions) → Promise` | Tokens + user info | +| `getIdpInitiatedLoginClaims` | `(idpInitiatedLoginToken: string, options?: TokenValidationOptions) → Promise` | IDP login claims | +| `validateAccessToken` | `(token: string, options?: TokenValidationOptions) → Promise` | Boolean | +| `validateToken` | `(token: string, options?: TokenValidationOptions) → Promise` | Decoded JWT payload | +| `verifyScopes` | `(token: string, requiredScopes: string[]) → boolean` | Boolean | +| `getLogoutUrl` | `(options?: LogoutUrlOptions) → string` | Logout URL string | +| `refreshAccessToken` | `(refreshToken: string) → Promise` | New tokens | +| `verifyWebhookPayload` | `(secret: string, headers: Record, payload: string) → boolean` | Boolean | +| `verifyInterceptorPayload` | `(secret: string, headers: Record, payload: string) → boolean` | Boolean | +| `generateClientToken` | `(clientId: string, clientSecret: string) → Promise` | M2M access token | +| `getClientAccessToken` | `() → Promise` | M2M access token (uses stored credentials) | + +**AuthorizationUrlOptions**: `scopes?: string[]`, `state?: string`, `nonce?: string`, `loginHint?: string`, `domainHint?: string`, `connectionId?: string`, `organizationId?: string`, `provider?: string`, `codeChallenge?: string`, `codeChallengeMethod?: string`, `prompt?: string` + +**LogoutUrlOptions**: `idTokenHint?: string`, `postLogoutRedirectUri?: string`, `state?: string` + +**AuthenticationOptions**: `codeVerifier?: string` + +### Python + +| Method | Signature | Returns | +|--------|-----------|---------| +| `get_authorization_url` | `(redirect_uri: str, options?: AuthorizationUrlOptions) → str` | Authorization URL string | +| `authenticate_with_code` | `(code: str, redirect_uri: str, options?: CodeAuthenticationOptions) → dict` | Tokens + user info | +| `get_idp_initiated_login_claims` | `(idp_initiated_login_token: str, options?: TokenValidationOptions) → IdpInitiatedLoginClaims` | IDP login claims | +| `validate_access_token` | `(token: str, options?: TokenValidationOptions) → bool` | Boolean | +| `validate_access_token_and_get_claims` | `(token: str, options?: TokenValidationOptions) → dict` | Decoded claims dict (raises on invalid/expired) — **canonical method when you need claims** | +| `get_logout_url` | `(options?: LogoutUrlOptions) → str` | Logout URL string | +| `refresh_access_token` | `(refresh_token: str) → dict` | New tokens | +| `verify_webhook_payload` | `(secret: str, headers: Dict[str, str], payload: str\|bytes) → bool` | Boolean | + +**AuthorizationUrlOptions** (Python): `scopes`, `state`, `nonce`, `login_hint`, `domain_hint`, `connection_id`, `organization_id`, `provider`, `prompt` — all `Optional[str]` (scopes is `Optional[list[str]]`) + +**LogoutUrlOptions** (Python): `id_token_hint`, `post_logout_redirect_uri`, `state` — all `Optional[str]` + +### Go + +| Method | Signature | Returns | +|--------|-----------|---------| +| `GetAuthorizationUrl` | `(redirectUri string, options AuthorizationUrlOptions) → (*url.URL, error)` | URL + error | +| `AuthenticateWithCode` | `(ctx context.Context, code string, redirectUri string, options AuthenticationOptions) → (*AuthenticationResponse, error)` | Response + error | +| `GetIdpInitiatedLoginClaims` | `(ctx context.Context, idpInitiatedLoginToken string) → (*IdpInitiatedLoginClaims, error)` | Claims + error | +| `GetAccessTokenClaims` | `(ctx context.Context, accessToken string) → (*AccessTokenClaims, error)` | Claims + error | +| `ValidateAccessToken` | `(ctx context.Context, accessToken string) → (bool, error)` | Boolean + error | +| `RefreshAccessToken` | `(ctx context.Context, refreshToken string) → (*TokenResponse, error)` | Tokens + error | +| `GetLogoutUrl` | `(options LogoutUrlOptions) → string` | Logout URL string | +| `VerifyWebhookPayload` | `(secret string, headers map[string][]string, payload []byte) → bool` | Boolean | + +**Go AuthorizationUrlOptions fields**: `Scopes []string`, `State string`, `Nonce string`, `LoginHint string`, `DomainHint string`, `ConnectionId string`, `OrganizationId string`, `Provider string`, `CodeChallenge string`, `CodeChallengeMethod string`, `Prompt string` + +Note: Go methods take `context.Context` as the first parameter for network calls. `GetAuthorizationUrl` and `GetLogoutUrl` do NOT take context (they're local-only operations). + +### Java + +| Method | Signature | Returns | +|--------|-----------|---------| +| `getAuthorizationUrl` | `(redirectUri: String, options: AuthorizationUrlOptions) → String` | Authorization URL string | +| `authenticateWithCode` | `(code: String, redirectUri: String, options: AuthenticationOptions) → AuthenticationResponse` | Tokens + user info | +| `getIdpInitiatedLoginClaims` | `(idpInitiatedLoginToken: String) → IdpInitiatedLoginClaims` | Claims | +| `validateToken` | `(token: String) → Claims` | JWT Claims | +| `getLogoutUrl` | `(options: LogoutUrlOptions) → String` | Logout URL string | + +--- + +## Sub-client Methods + +### Node.js sub-clients (accessed via `client..`) + +**client.organization** +| Method | Signature | +|--------|-----------| +| `createOrganization` | `(name: string, options?) → Promise` | +| `getOrganization` | `(id: string) → Promise` | +| `getOrganizationByExternalId` | `(externalId: string) → Promise` | +| `listOrganizations` | `(options?) → Promise` | +| `updateOrganization` | `(id: string, organization) → Promise` | +| `deleteOrganization` | `(id: string) → Promise` | +| `generatePortalLink` | `(organizationId: string, features?) → Promise` | +| `updateOrganizationSettings` | `(id: string, settings) → Promise` | + +**client.connection** +| Method | Signature | +|--------|-----------| +| `getConnection` | `(id: string) → Promise` | +| `listConnections` | `(options?) → Promise` | +| `listConnectionsByDomain` | `(domain: string, options?) → Promise` | +| `enableConnection` | `(connectionId: string) → Promise` | +| `disableConnection` | `(connectionId: string) → Promise` | + +**client.domain** +| Method | Signature | +|--------|-----------| +| `createDomain` | `(domain: string) → Promise` | +| `getDomain` | `(domain: string) → Promise` | +| `listDomains` | `(options?) → Promise` | +| `deleteDomain` | `(domain: string) → Promise` | + +**client.user** +| Method | Signature | +|--------|-----------| +| `createUser` | `(organizationId: string, user) → Promise` | +| `createUserAndMembership` | `(organizationId: string, request) → Promise` | +| `getUser` | `(id: string) → Promise` | +| `listUsers` | `(options?) → Promise` | +| `listOrganizationUsers` | `(organizationId: string, options?) → Promise` | +| `updateUser` | `(id: string, user) → Promise` | +| `deleteUser` | `(id: string) → Promise` | +| `searchUsers` | `(options) → Promise` | +| `searchOrganizationUsers` | `(organizationId: string, options) → Promise` | + +**client.directory** +| Method | Signature | +|--------|-----------| +| `listDirectories` | `(organizationId: string) → Promise` | +| `getDirectory` | `(organizationId: string, directoryId: string) → Promise` | +| `listDirectoryUsers` | `(organizationId: string, directoryId: string, options?) → Promise` | +| `listDirectoryGroups` | `(organizationId: string, directoryId: string, options?) → Promise` | +| `enableDirectory` | `(organizationId: string, directoryId: string) → Promise` | +| `disableDirectory` | `(organizationId: string, directoryId: string) → Promise` | + +**client.role** +| Method | Signature | +|--------|-----------| +| `createRole` | `(role) → Promise` | +| `getRole` | `(roleId: string) → Promise` | +| `listRoles` | `(options?) → Promise` | +| `updateRole` | `(roleId: string, role) → Promise` | +| `deleteRole` | `(roleId: string) → Promise` | + +**client.permission** +| Method | Signature | +|--------|-----------| +| `createPermission` | `(permission) → Promise` | +| `listPermissions` | `(options?) → Promise` | +| `updatePermission` | `(permissionId: string, permission) → Promise` | +| `deletePermission` | `(permissionId: string) → Promise` | + +**client.session** +| Method | Signature | +|--------|-----------| +| `getSession` | `(sessionId: string) → Promise` | +| `getUserSessions` | `(userId: string, options?) → Promise` | +| `revokeSession` | `(sessionId: string) → Promise` | +| `revokeAllUserSessions` | `(userId: string) → Promise` | + +**client.connectedAccounts** +| Method | Signature | +|--------|-----------| +| `listConnectedAccounts` | `(options?) → Promise` | +| `getConnectedAccountAuth` | `(options) → Promise` | +| `createConnectedAccount` | `(request) → Promise` | +| `updateConnectedAccount` | `(request) → Promise` | +| `deleteConnectedAccount` | `(request) → Promise` | + +**client.tools** +| Method | Signature | +|--------|-----------| +| `executeTool` | `(request) → Promise` | + +### Python sub-clients (accessed via `client..`) + +Python uses `snake_case` method names. **Important**: Some Python sub-client names are **plural** while Node uses singular. This is a common source of bugs. + +| Node.js | Python | Difference | +|---------|--------|------------| +| `client.user` | `client.users` | Plural in Python | +| `client.role` | `client.roles` | Plural in Python | +| `client.permission` | `client.permissions` | Plural in Python | +| `client.session` | `client.sessions` | Plural in Python | +| `client.organization` | `client.organization` | Same | +| `client.connection` | `client.connection` | Same | +| `client.domain` | `client.domain` | Same | +| `client.directory` | `client.directory` | Same | +| `client.connectedAccounts` | `client.connected_accounts` | snake_case in Python | + +Methods: +- `client.organization.create_organization(organization)` +- `client.organization.get_organization(organization_id)` +- `client.organization.list_organizations(page_size, page_token?)` +- `client.organization.update_organization(organization_id, organization)` +- `client.organization.delete_organization(organization_id)` +- `client.organization.generate_portal_link(organization_id, features?)` +- `client.connection.list_connections(organization_id, include?)` +- `client.connection.get_connection(organization_id, connection_id)` +- `client.connection.enable_connection(organization_id, connection_id)` +- `client.connection.disable_connection(organization_id, connection_id)` +- `client.domain.create_domain(organization_id, domain_name)` +- `client.domain.list_domains(organization_id)` +- `client.domain.delete_domain(organization_id, domain_id)` +- `client.directory.list_directories(organization_id)` +- `client.directory.get_directory(organization_id, directory_id)` +- `client.directory.list_directory_users(organization_id, directory_id, options?)` +- `client.directory.list_directory_groups(organization_id, directory_id, options?)` +- `client.users.create_user(organization_id, user)` +- `client.users.get_user(user_id)` +- `client.users.list_users(options?)` +- `client.users.update_user(user_id, user)` +- `client.users.delete_user(user_id)` +- `client.roles.create_role(role)` +- `client.roles.list_roles(options?)` +- `client.roles.update_role(role_id, role)` +- `client.roles.delete_role(role_id)` +- `client.permissions.create_permission(permission)` +- `client.permissions.list_permissions(options?)` +- `client.sessions.get_session(session_id)` +- `client.sessions.get_user_sessions(user_id, options?)` +- `client.sessions.revoke_session(session_id)` +- `client.sessions.revoke_all_user_sessions(user_id)` + +Additional Python-only methods on client: +- `client.validate_access_token_and_get_claims(token, options?) → dict` — validates and returns decoded claims +- `client.verify_scopes(token, required_scopes) → bool` — checks scopes, raises on missing +- `client.generate_client_token(client_id, client_secret, scopes?) → str` — M2M token generation +- `client.get_client_access_token() → str` — M2M token using stored credentials +- `client.verify_interceptor_payload(secret, headers, payload) → bool` — interceptor signature verification + +Note: Python connection/domain/directory methods often require `organization_id` as the first parameter, unlike Node which uses option objects. + +### Go sub-clients + +Go uses `PascalCase` and typed request/response objects: +- `client.Organization().CreateOrganization(ctx, request)` +- `client.Organization().GetOrganization(ctx, organizationId)` +- `client.Organization().ListOrganizations(ctx, pageSize, pageToken)` +- `client.Organization().UpdateOrganization(ctx, organizationId, request)` +- `client.Organization().DeleteOrganization(ctx, organizationId)` +- `client.Organization().GeneratePortalLink(ctx, organizationId, features)` +- `client.Connection().GetConnection(ctx, organizationId, connectionId)` +- `client.Connection().ListConnections(ctx, organizationId)` +- `client.Connection().EnableConnection(ctx, organizationId, connectionId)` +- `client.Connection().DisableConnection(ctx, organizationId, connectionId)` +- `client.Domain().CreateDomain(ctx, organizationId, domainName)` +- `client.Domain().ListDomains(ctx, organizationId)` +- `client.Domain().DeleteDomain(ctx, organizationId, domainId)` +- `client.Directory().ListDirectories(ctx, organizationId)` +- `client.Directory().GetDirectory(ctx, organizationId, directoryId)` +- `client.Directory().ListDirectoryUsers(ctx, organizationId, directoryId, options)` +- `client.Directory().ListDirectoryGroups(ctx, organizationId, directoryId, options)` +- `client.User().CreateUser(ctx, organizationId, request)` +- `client.User().GetUser(ctx, userId)` +- `client.User().ListUsers(ctx, options)` +- `client.User().UpdateUser(ctx, userId, request)` +- `client.User().DeleteUser(ctx, userId)` +- `client.Role().ListRoles(ctx)` +- `client.Role().CreateRole(ctx, request)` +- `client.Session().GetSession(ctx, sessionId)` +- `client.Session().RevokeSession(ctx, sessionId)` +- `client.Session().RevokeAllUserSessions(ctx, userId)` + +### Java sub-clients + +Java uses accessor methods that return typed clients: +- `client.organizations().create(request) → CreateOrganizationResponse` +- `client.organizations().getById(organizationId) → Organization` +- `client.organizations().getByExternalId(externalId) → Organization` +- `client.organizations().list(pageSize, pageToken) → ListOrganizationsResponse` +- `client.organizations().update(organizationId, request) → Organization` +- `client.organizations().delete(organizationId)` +- `client.organizations().generatePortalLink(organizationId, features) → Link` +- `client.connections().listConnectionsByOrganization(organizationId) → ListConnectionsResponse` +- `client.connections().getConnection(organizationId, connectionId) → GetConnectionResponse` +- `client.connections().enableConnection(organizationId, connectionId)` +- `client.connections().disableConnection(organizationId, connectionId)` +- `client.domains().listDomainsByOrganizationId(organizationId) → ListDomainsResponse` +- `client.domains().createDomain(organizationId, domainName) → CreateDomainResponse` +- `client.domains().deleteDomain(organizationId, domainId)` +- `client.directories().listDirectories(organizationId) → ListDirectoriesResponse` +- `client.directories().getDirectory(organizationId, directoryId) → GetDirectoryResponse` +- `client.directories().listDirectoryUsers(organizationId, directoryId) → ListDirectoryUsersResponse` +- `client.directories().listDirectoryGroups(organizationId, directoryId) → ListDirectoryGroupsResponse` +- `client.users().getUser(userId) → GetUserResponse` +- `client.users().listUsers(options) → ListUsersResponse` +- `client.users().createUser(organizationId, request) → CreateUserResponse` +- `client.users().createUserAndMembership(organizationId, request) → CreateUserAndMembershipResponse` +- `client.users().updateUser(userId, request) → UpdateUserResponse` +- `client.users().deleteUser(userId)` +- `client.roles().listRoles() → ListRolesResponse` +- `client.roles().createRole(request) → CreateRoleResponse` +- `client.roles().updateRole(roleId, request) → UpdateRoleResponse` +- `client.roles().deleteRole(roleId)` +- `client.permissions().listPermissions() → ListPermissionsResponse` +- `client.permissions().createPermission(request) → CreatePermissionResponse` +- `client.sessions().getSession(sessionId) → SessionDetails` +- `client.sessions().getUserSessions(userId, filter) → UserSessionDetails` +- `client.sessions().revokeSession(sessionId)` +- `client.sessions().revokeAllUserSessions(userId)` + +Note: Java does NOT yet support Connected Accounts, Tools, or Actions in the public API. + +--- + +## REST API Endpoints + +Base URL: `https://.scalekit.com` (production) or `https://.scalekit.dev` (development) + +Authentication: Bearer token from `POST /oauth/token` with `client_credentials` grant. + +Endpoints are grouped by product suite: **SaaSKit** (authentication, SSO, SCIM, RBAC, sessions) and **AgentKit** (connections, tool calling, MCP auth). + +### Token endpoint +``` +POST /oauth/token +Content-Type: application/x-www-form-urlencoded + +client_id={client_id}&client_secret={client_secret}&grant_type=client_credentials +``` + +### AgentKit — Connected Accounts +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/connected_accounts` | List connected accounts | +| POST | `/api/v1/connected_accounts` | Create a connected account | +| PUT | `/api/v1/connected_accounts` | Update connected account credentials | +| POST | `/api/v1/connected_accounts:delete` | Delete a connected account | +| GET | `/api/v1/connected_accounts/auth` | Get connected account auth details | +| GET | `/api/v1/connected_accounts:search` | Search connected accounts | +| POST | `/api/v1/connected_accounts/magic_link` | Generate authentication magic link | +| POST | `/api/v1/connected_accounts/user/verify` | Verify connected account user | + +### SaaSKit — Connections (SSO) +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/connections` | List connections | + +### SaaSKit — Organizations +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/organizations` | List organizations | +| POST | `/api/v1/organizations` | Create an organization | +| GET | `/api/v1/organizations/{id}` | Get organization details | +| PATCH | `/api/v1/organizations/{id}` | Update organization | +| DELETE | `/api/v1/organizations/{id}` | Delete an organization | +| PUT | `/api/v1/organizations/{id}/portal_links` | Generate admin portal link | +| PATCH | `/api/v1/organizations/{id}/settings` | Toggle organization settings | + +### SaaSKit — Roles +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/organizations/{org_id}/roles` | List organization roles | +| POST | `/api/v1/organizations/{org_id}/roles` | Create organization role | +| GET | `/api/v1/organizations/{org_id}/roles/{role_name}` | Get role details | +| PUT | `/api/v1/organizations/{org_id}/roles/{role_name}` | Update role | +| DELETE | `/api/v1/organizations/{org_id}/roles/{role_name}` | Delete role | +| PATCH | `/api/v1/organizations/{org_id}/roles:set_defaults` | Set default roles | + +### SaaSKit — Users & Memberships +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/users` | List users | +| POST | `/api/v1/users` | Create a user | +| GET | `/api/v1/users/{id}` | Get user details | +| PATCH | `/api/v1/users/{id}` | Update user | +| DELETE | `/api/v1/users/{id}` | Delete user | +| GET | `/api/v1/users:search` | Search users | +| GET | `/api/v1/organizations/{org_id}/users` | List organization users | +| GET | `/api/v1/organizations/{org_id}/users:search` | Search organization users | +| POST | `/api/v1/memberships/organizations/{organization_id}/users/{id}` | Add user to organization | +| DELETE | `/api/v1/memberships/organizations/{organization_id}/users/{id}` | Remove user from organization | +| PATCH | `/api/v1/memberships/organizations/{organization_id}/users/{id}` | Update membership | +| PATCH | `/api/v1/invites/organizations/{organization_id}/users/{id}/resend` | Resend invitation | + +### SaaSKit — Sessions +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/users/{user_id}/sessions` | Get user sessions | +| POST | `/api/v1/users/{user_id}/sessions:revoke_all` | Revoke all user sessions | +| POST | `/api/v1/sessions/{session_id}:revoke` | Revoke a session | + +### AgentKit — Tools +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/v1/execute_tool` | Execute a tool using a connected account | + +### SaaSKit — Organization API Clients (M2M) +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/v1/organizations/{organization_id}/clients` | List org API clients | +| POST | `/api/v1/organizations/{organization_id}/clients` | Create org API client | +| GET | `/api/v1/organizations/{organization_id}/clients/{client_id}` | Get org API client | +| DELETE | `/api/v1/organizations/{organization_id}/clients/{client_id}` | Delete org API client | +| PATCH | `/api/v1/organizations/{organization_id}/clients/{client_id}` | Update org API client | + +--- + +## Error Handling + +### Node.js exception hierarchy +``` +ScalekitException (base) +├── ScalekitValidateTokenFailureException +├── ScalekitServerException (HTTP 400-599) +│ ├── properties: httpStatus, errorCode, message, errDetails +│ └── Specific subclasses for 400, 401, 403, 404, 409, 422, 429, 500, 502, 503, 504 +└── WebhookVerificationError +``` + +Import: `import { ScalekitServerException } from '@scalekit-sdk/node'` + +### Python exceptions +``` +ScalekitException (base) +``` + +### Go errors +All methods return `(result, error)`. Check `err != nil` for all network calls. + +### Java exceptions +All methods may throw checked exceptions. Wrap in try-catch. + +--- + +## Common Token Claims + +Access tokens from Scalekit contain these standard claims: +- `sub` — User ID +- `email` — User email +- `name` — Display name +- `org_id` — Organization ID +- `roles` — Array of role names +- `permissions` — Array of permission strings (also available at `https://scalekit.com/permissions` or `scalekit:permissions` claim paths) + +Permission claims should be checked in this priority order: +1. `permissions` claim +2. `https://scalekit.com/permissions` claim +3. `scalekit:permissions` claim \ No newline at end of file diff --git a/plugins/saaskit/skills/setup/SKILL.md b/plugins/saaskit/skills/setup/SKILL.md new file mode 100644 index 0000000..34b269a --- /dev/null +++ b/plugins/saaskit/skills/setup/SKILL.md @@ -0,0 +1,86 @@ +--- +name: setup +description: Starting point for any Scalekit SaaSKit integration. Use when the user says "I want to add auth", "set up Scalekit", "where do I start", or is new to SaaSKit and doesn't know which skill to use. Routes to the right skill based on framework and what they're building. +--- + +# SaaSKit — Where to Start + +> **IMPORTANT:** This skill routes to the right skill — it does NOT implement auth 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. **New or existing codebase?** + - New project + - Adding auth to an existing app + +2. **Framework?** + - Next.js (App Router or Pages Router) + - Python (Django / FastAPI / Flask) + - Go + - Other / not sure + +3. **What are you adding?** + - Login, sessions, and user management (most common starting point) + - Enterprise SSO (Okta, Azure AD, Google Workspace, etc.) + - SCIM / user provisioning (sync users from a directory) + - Secure an MCP server with OAuth 2.1 + - API keys for developers + - Not sure / full auth stack + +--- + +## Step 2: Tell the user exactly which skill to invoke + +Pick the best match and tell the user: "Run `/saaskit:` to get started." + +| Framework | What you're adding | Tell them to run | +|---|---|---| +| Next.js | Login + sessions | `/saaskit:implementing-saaskit-nextjs` | +| Python | Login + sessions | `/saaskit:implementing-saaskit-python` | +| Go / other | Login + sessions | `/saaskit:implementing-saaskit` | +| Any | Enterprise SSO | `/saaskit:implementing-modular-sso` | +| Any | SCIM provisioning | `/saaskit:implementing-scim-provisioning` | +| Any | MCP server auth | `/saaskit:adding-mcp-oauth` | +| Any | API keys | `/saaskit:adding-api-auth` | +| Any | RBAC / permissions | `/saaskit:implementing-access-control` | +| Any | Migrating from Auth0 / Firebase / custom auth | `/saaskit:migrating-to-saaskit` | + +If the user wants **login + SSO + SCIM** (full B2B auth stack), tell them to start with `/saaskit:implementing-saaskit` (or the framework variant), then chain to `/saaskit:implementing-modular-sso` once login is working. + +When routing, include one or two relevant orientation sentences from below so the user has context before reading the skill. Then **stop** — the target skill handles implementation. + +### Orientation notes by topic + +**Enterprise SSO:** SSO in Scalekit is scoped to an organization — every auth request needs `organizationId` or a domain hint to reach the right IdP. Two modes: Modular SSO (you manage users/sessions) vs Full-Stack SaaSKit (Scalekit manages users). Most B2B SaaS apps with existing user management use Modular SSO. + +**SCIM provisioning:** Scalekit bridges the customer's identity provider (Okta, Azure AD) and your app via webhooks. Requires two parts: a webhook endpoint in your app, AND SCIM configuration on the customer's IdP side. + +**MCP OAuth:** Requires Streamable HTTP transport — stdio does not support OAuth. The MCP server must expose a `/.well-known/oauth-protected-resource` discovery endpoint. + +--- + +## 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= +SCALEKIT_CLIENT_SECRET= +``` + +Get these from [app.scalekit.com](https://app.scalekit.com) → Developers → Settings → API Credentials. + +Use `/saaskit:testing-auth-setup` to validate credentials and connection end-to-end before writing any auth code. + +--- + +## When to switch skills + +- **Already know what you need?** Skip this skill and invoke the target directly. +- **SDK errors or wrong imports?** Use `/saaskit:scalekit-code-doctor`. +- **Production checklist?** Use `/saaskit:production-readiness-saaskit`. diff --git a/plugins/saaskit/skills/testing-auth-setup/SKILL.md b/plugins/saaskit/skills/testing-auth-setup/SKILL.md new file mode 100644 index 0000000..19a2b91 --- /dev/null +++ b/plugins/saaskit/skills/testing-auth-setup/SKILL.md @@ -0,0 +1,58 @@ +--- +name: testing-auth-setup +description: 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= +``` + +## 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. \ No newline at end of file diff --git a/skills/setup-scalekit/SKILL.md b/skills/setup-scalekit/SKILL.md new file mode 100644 index 0000000..2cbf3ef --- /dev/null +++ b/skills/setup-scalekit/SKILL.md @@ -0,0 +1,83 @@ +--- +name: setup-scalekit +description: 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 + +```bash +npm i -g @scalekit-inc/cli +``` + +Verify: `scalekit --version` should print a version number. + +## 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 — Install for your tool + +### Claude Code + +``` +/plugin marketplace add scalekit-inc/claude-code-authstack +/plugin install agentkit@claude-code-authstack # or saaskit +``` + +Verify: restart Claude Code, then run `/plugin list` — the plugin should appear as enabled. + +### GitHub Copilot CLI + +```bash +copilot plugin marketplace add scalekit-inc/github-copilot-authstack +copilot plugin install agentkit@github-copilot-authstack # or saaskit +``` + +Verify: `copilot plugin list` should show the plugin. + +### Codex + +```bash +curl -fsSL https://raw.githubusercontent.com/scalekit-inc/codex-authstack/main/install.sh | bash +``` + +Post-install: restart Codex → Plugin Directory → select **Scalekit Auth Stack** → enable your plugin. + +### Cursor + +```bash +curl -fsSL https://raw.githubusercontent.com/scalekit-inc/cursor-authstack/main/install.sh | bash +``` + +Post-install: restart Cursor → **Settings → Cursor Settings → Plugins** → enable your plugin. + +### Other agents (OpenCode, Windsurf, Cline, Gemini CLI, 35+) + +```bash +npx skills add scalekit-inc/skills --list # see available skills +npx skills add scalekit-inc/skills --skill integrating-agentkit +npx skills add scalekit-inc/skills --skill implementing-saaskit +npx skills add scalekit-inc/skills --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 |