Auth System
Internal auth system details — supported auth types, OAuth flow internals, credential encryption, and injection logic.
Overview
Every plugin can have one or more auth methods that define how end-users authenticate with the external service. When a tool is executed, the runtime proxy decrypts the user's stored credentials and injects them into the HTTP request automatically.
Supported Auth Types
| Type | Config Fields | Injection Behavior |
|---|---|---|
oauth2 | authorize_url, token_url, client_id, client_secret, scopes, extra_params | Authorization: Bearer {access_token} — auto-refreshes expired tokens |
bearer_token | header_name (default: Authorization), header_prefix (default: Bearer) | {header_name}: {prefix} {token} |
api_key | header_name (default: X-API-Key) | {header_name}: {api_key} |
service_account | — | Authorization: Bearer {access_token} |
custom | injection_rules[] — array of { target, name, value } with {{field}} placeholders | Template-based header injection |
no_auth | — | No credentials injected |
Auth Method Fields
| Field | Type | Description |
|---|---|---|
type | string | One of the supported types above |
label | string | Human-readable name (e.g. "Google OAuth", "API Key") |
isDefault | boolean | Whether this is the default method for the plugin |
config | JSON | Type-specific configuration (see table above) |
A plugin can have multiple auth methods (e.g. both OAuth2 and API key), but each type must be unique per plugin.
How Auth Injection Works
At tool execution time (executeTool in lib/proxy/executor.ts):
- The connected account for the developer's user + plugin is found
- Stored credentials are decrypted from AES-256-GCM ciphertext
- Based on the auth method type, credentials are injected into request headers
- For OAuth2: if the token is expired and a refresh token exists, the proxy auto-refreshes before making the call
- For custom auth:
injection_rulesare processed, replacing{{field}}placeholders with credential values
OAuth 2.0 Flow
NIN implements the full OAuth 2.0 Authorization Code flow. The platform acts as a proxy between your end-users and OAuth providers.
OAuth Config Example
{
"authorize_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"scopes": ["https://www.googleapis.com/auth/gmail.send"],
"extra_params": {
"access_type": "offline",
"prompt": "consent"
}
}Flow Sequence
1. SDK calls POST /api/v1/connections/initiate
→ Server builds authorization URL with signed state token
→ Returns { connectUrl: "https://provider.com/oauth/authorize?..." }
2. User is redirected to the provider and authorizes
3. Provider redirects to GET /api/v1/oauth/callback?code=...&state=...
→ Server verifies state token signature (HMAC-SHA256)
→ Finds pending connection record
→ Exchanges code for tokens at token_url
→ Encrypts tokens (AES-256-GCM)
→ Upserts connected_accounts record
→ Cleans up pending_connections
→ Redirects user to developer's redirectUrl with ?status=successState Token
The state parameter is an HMAC-signed payload:
{
"developerId": "uuid",
"userId": "user_123",
"pluginId": "uuid",
"authMethodId": "uuid",
"redirectUrl": "https://your-app.com/callback",
"nonce": "random-hex",
"isMcp": false
}For MCP OAuth flows, the state includes additional fields:
{
"developerId": "uuid",
"userId": "user_123",
"pluginId": "uuid",
"authMethodId": null,
"redirectUrl": "https://your-app.com/callback",
"nonce": "random-hex",
"isMcp": true,
"codeVerifier": "pkce-verifier-string"
}Encoded as base64url and signed with HMAC-SHA256 using ENCRYPTION_KEY.
Auto-Refresh
During tool execution, if the access token has expired:
- The proxy calls the provider's
token_urlwithgrant_type=refresh_token - On success, the new tokens are encrypted and stored
- The request proceeds with the fresh access token
- If refresh fails, the request continues with the old token (best-effort)
OAuth Callback URL
All plugins share a single callback URL:
{NEXT_PUBLIC_APP_URL}/api/v1/oauth/callbackRegister this in each OAuth provider's settings as an authorized redirect URI.
MCP OAuth (PKCE)
For MCP-enabled plugins, NIN implements OAuth 2.0 with PKCE (Proof Key for Code Exchange) following the MCP Authorization Specification.
Discovery
When an admin configures an MCP server URL, NIN auto-discovers the OAuth setup:
- Fetches
/.well-known/oauth-protected-resourcefrom the MCP server (RFC 9470) - Fetches
/.well-known/oauth-authorization-serverfrom the auth server (RFC 8414) - Falls back to path-aware issuer discovery or
401 WWW-Authenticatechallenge discovery when needed - Registers as an OAuth client via the registration endpoint (RFC 7591) when supported
- Stores the
clientIdand OAuth metadata inmcpConfig
PKCE Flow
1. SDK calls POST /api/v1/connections/initiate with connectionType: "mcp"
→ Server generates code_verifier + code_challenge (S256)
→ Builds authorization URL with code_challenge param
→ Stores code_verifier in signed state token
→ Returns { connectUrl: "https://auth.service.com/authorize?...&code_challenge=..." }
2. User authorizes at the provider
3. Provider redirects to GET /api/v1/oauth/callback?code=...&state=...
→ Server detects isMcp flag in state
→ Extracts code_verifier from state
→ Exchanges code with code_verifier at token endpoint
→ Encrypts MCP tokens (AES-256-GCM)
→ Stores in mcpAccessToken / mcpRefreshToken / mcpTokenExpiresAt
→ Redirects user backMCP Token Refresh
MCP tokens are auto-refreshed during tool execution. The refresh uses the stored mcpRefreshToken plus the same effective client credentials that created the grant. This prevents refresh failures when credentials can come from the user, developer, or admin layer.
Dual Credentials
For plugins with toolSource: "both", a single connectedAccounts record can hold:
- REST fields (
credentials,auth_method_id) — for manual tool execution - MCP fields (
mcp_access_token,mcp_refresh_token,mcp_token_expires_at) — for MCP tool execution
These are independent and refreshed separately.
Credential Resolution
For MCP OAuth, client credentials resolve in this order:
- End-user credentials, when the developer allows them
- Developer-level plugin override
- Admin-level plugin default
The effective credentials are persisted on the connection so later refresh requests continue to use the same client identity.
Direct Credentials
For API-key-based or bearer-token-based services, users submit credentials directly. They are encrypted and stored identically to OAuth tokens.
Auth Method Configs
API Key:
{ "header_name": "X-API-Key" }Bearer Token:
{ "header_name": "Authorization", "header_prefix": "Bearer" }Custom:
{
"injection_rules": [
{ "target": "header", "name": "X-Custom-Auth", "value": "Token {{api_key}}" }
]
}Hosted Connect Pages
When initiateConnection is called for a non-OAuth auth method, the server returns a URL to a hosted connect page (/connect/[plugin]). The page shows a form where the user enters credentials, which are encrypted and stored via POST /api/connect/submit.
Encryption
All credentials are encrypted at rest using AES-256-GCM.
Key
32-byte value stored as a 64-character hex string in ENCRYPTION_KEY:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Ciphertext Format
{iv_hex}:{ciphertext_hex}:{auth_tag_hex}| Component | Size | Description |
|---|---|---|
| IV | 12 bytes | Random initialization vector |
| Ciphertext | Variable | The encrypted payload |
| Auth Tag | 16 bytes | GCM authentication tag for tamper detection |
When Decryption Happens
Credentials are only decrypted at the moment of tool execution. They are never exposed through dashboard API responses, SDK responses, execution logs, or any other endpoint.
Dashboard Auth Helpers
The platform provides several server-side helpers in lib/auth/ for protecting API routes:
| Helper | Description |
|---|---|
authenticateDeveloper(req) | Verifies the Firebase ID token from the Authorization header. Returns the developer record or null. |
requireAdmin(req) | Same as authenticateDeveloper but throws 403 if the user's role is not "admin". |
authenticateDeveloperFlexible(req) | Supports both Firebase tokens and API keys. If the header starts with Bearer nk_, validates as an API key; otherwise falls back to Firebase. |
requirePluginAccess(req, pluginId) | Verifies the user is either an admin or the creator of the specified plugin. Returns the developer record or throws 403. |
When to use which
- Admin-only routes (categories CRUD, review actions): use
requireAdmin - Any-user routes (plugin creation, category listing, MCP discovery): use
authenticateDeveloper - Plugin-scoped routes (update/delete plugin, manage tools/auth methods): use
requirePluginAccess - Dual-transport routes (API key or dashboard): use
authenticateDeveloperFlexible