Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ Search a single file from a package for lines matching a JavaScript regular expr
How the server resolves a token depends on the transport:

- **stdio mode** reads one token at startup from the environment and uses it for every request. Set `SOCKET_API_TOKEN`. The server also accepts these aliases, in priority order: `SOCKET_API_TOKEN` → `SOCKET_API_KEY` → `SOCKET_CLI_API_TOKEN` → `SOCKET_CLI_API_KEY` → `SOCKET_SECURITY_API_TOKEN` → `SOCKET_SECURITY_API_KEY`. `SOCKET_API_TOKEN` is canonical; `SOCKET_API_KEY` is the alias most local setups already export. Because the process belongs to one user, this token is yours and scopes every tool to your account.
- **HTTP mode** scopes the organization tools to the caller, never to the server's own token. Send your Socket API token as an `Authorization: Bearer <token>` header on each request, or use an OAuth access token when the server runs OAuth. The server uses that per-request token for the Socket API calls it makes on your behalf. A shared deployment never answers `organizations`, `alerts`, `threat_feed`, or `package_files` with the operator's data: when a request carries no token, those tools return the auth-required error. `depscore` alone may fall back to the server's startup token, since package scores are the same for every caller.
- **HTTP mode** scopes the organization tools to the caller, never to the server's own token. Send your credential as an `Authorization: Bearer <token>` header on each request: a raw Socket API token (recognized by its `sktsec_` prefix) is used directly and works whether or not the server runs OAuth, while any other token is treated as an OAuth access token and validated through introspection when the server runs OAuth. The server uses that per-request token for the Socket API calls it makes on your behalf. A shared deployment never answers `organizations`, `alerts`, `threat_feed`, or `package_files` with the operator's data: when a request carries no token, those tools return the auth-required error. `depscore` alone may fall back to the server's startup token, since package scores are the same for every caller.

Generate a token from the [Socket dashboard](https://socket.dev/) under API tokens, then export it before launching the server:

Expand Down
33 changes: 31 additions & 2 deletions lib/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,31 @@ export function applyClientApiKey(req: AuthenticatedRequest): void {
req.auth = { token, clientId: 'socket-api-key', scopes: [] }
}

// Socket API tokens carry this prefix (e.g. `sktsec_t_...`); OAuth access
// tokens do not. We use it to recognize a raw Socket key sent over the
// standard `Authorization: Bearer` header so it bypasses OAuth introspection.
const SOCKET_API_KEY_PREFIX = 'sktsec_'

// Recognize a Socket API key on the `Authorization: Bearer` header by its
// `sktsec_` prefix and apply it to `req.auth`, returning true when matched.
// Lets a caller authenticate with a raw Socket key even on an OAuth server:
// the key skips introspection and acts on the caller's behalf, while a
// non-prefixed token (an OAuth access token) falls through to OAuth. An
// invalid key fails at the downstream Socket API call.
export function applySocketApiKey(req: AuthenticatedRequest): boolean {
const authHeader = getRequestHeaderValue(req.headers.authorization).trim()
const [type, token] = authHeader.split(/\s+/u)
if (
(type || '').toLowerCase() !== 'bearer' ||
!token ||
!token.startsWith(SOCKET_API_KEY_PREFIX)
) {
return false
}
req.auth = { token, clientId: 'socket-api-key', scopes: [] }
return true
}

// Destroy a session — close transport (best-effort) and detach the MCP
// server. Safe to call multiple times.
export function destroySession(
Expand Down Expand Up @@ -391,7 +416,11 @@ export async function routeRequest(

patchAcceptHeader(req)

if (isOauthEnabled()) {
// A `sktsec_`-prefixed Bearer token authenticates as a Socket API key in
// both modes; only a non-prefixed token on an OAuth server goes through
// introspection.
const hasApiKey = applySocketApiKey(req as AuthenticatedRequest)
if (!hasApiKey && isOauthEnabled()) {
const authResult = await authenticateRequest(
req as AuthenticatedRequest,
res,
Expand All @@ -400,7 +429,7 @@ export async function routeRequest(
if (!authResult.ok) {
return
}
} else {
} else if (!hasApiKey) {
applyClientApiKey(req as AuthenticatedRequest)
}

Expand Down
20 changes: 20 additions & 0 deletions test/unit/http-server.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { describe, expect, test } from 'vitest'

import {
applyClientApiKey,
applySocketApiKey,
destroySession,
handleDelete,
handleGet,
Expand Down Expand Up @@ -47,6 +48,25 @@ test('applyClientApiKey ignores a non-bearer scheme', () => {
expect(req.auth).toBeUndefined()
})

test('applySocketApiKey matches a sktsec_-prefixed Bearer token', () => {
const req = reqWith('Bearer sktsec_t_example')
expect(applySocketApiKey(req)).toBe(true)
expect(req.auth?.token).toBe('sktsec_t_example')
expect(req.auth?.clientId).toBe('socket-api-key')
})

test('applySocketApiKey ignores a non-prefixed Bearer token (OAuth)', () => {
const req = reqWith('Bearer oauth-access-token')
expect(applySocketApiKey(req)).toBe(false)
expect(req.auth).toBeUndefined()
})

test('applySocketApiKey returns false when no Authorization header', () => {
const req = reqWith()
expect(applySocketApiKey(req)).toBe(false)
expect(req.auth).toBeUndefined()
})

// A Readable stream doubles as a stand-in for IncomingMessage here:
// readPostBody only async-iterates the request and calls `.destroy()`,
// both of which Readable provides.
Expand Down