NIN Connect Docs
Platform (Self-Host)

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

TypeConfig FieldsInjection Behavior
oauth2authorize_url, token_url, client_id, client_secret, scopes, extra_paramsAuthorization: Bearer {access_token} — auto-refreshes expired tokens
bearer_tokenheader_name (default: Authorization), header_prefix (default: Bearer){header_name}: {prefix} {token}
api_keyheader_name (default: X-API-Key){header_name}: {api_key}
service_accountAuthorization: Bearer {access_token}
custominjection_rules[] — array of { target, name, value } with {{field}} placeholdersTemplate-based header injection
no_authNo credentials injected

Auth Method Fields

FieldTypeDescription
typestringOne of the supported types above
labelstringHuman-readable name (e.g. "Google OAuth", "API Key")
isDefaultbooleanWhether this is the default method for the plugin
configJSONType-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):

  1. The connected account for the developer's user + plugin is found
  2. Stored credentials are decrypted from AES-256-GCM ciphertext
  3. Based on the auth method type, credentials are injected into request headers
  4. For OAuth2: if the token is expired and a refresh token exists, the proxy auto-refreshes before making the call
  5. For custom auth: injection_rules are 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=success

State 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:

  1. The proxy calls the provider's token_url with grant_type=refresh_token
  2. On success, the new tokens are encrypted and stored
  3. The request proceeds with the fresh access token
  4. 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/callback

Register 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:

  1. Fetches /.well-known/oauth-protected-resource from the MCP server (RFC 9470)
  2. Fetches /.well-known/oauth-authorization-server from the auth server (RFC 8414)
  3. Falls back to path-aware issuer discovery or 401 WWW-Authenticate challenge discovery when needed
  4. Registers as an OAuth client via the registration endpoint (RFC 7591) when supported
  5. Stores the clientId and OAuth metadata in mcpConfig

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 back

MCP 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:

  1. End-user credentials, when the developer allows them
  2. Developer-level plugin override
  3. 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}
ComponentSizeDescription
IV12 bytesRandom initialization vector
CiphertextVariableThe encrypted payload
Auth Tag16 bytesGCM 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:

HelperDescription
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

On this page