theAuth를 사용한 프로덕션 환경 MCP 서버 보안 설정하기
요약
본 가이드는 AI 에이전트와 인간을 위한 오픈 소스 인증 솔루션인 theAuth를 활용하여 MCP 서버를 프로덕션 환경에 안전하게 설정하는 방법을 다룹니다. 토큰 만료, 스코프 지정, 인자 검사, 속도 제한 등 실제 운영 환경에서 필수적인 보안 장치들을 구축하고 체크리스트를 제공합니다.
핵심 포인트
- theAuth는 AI 에이전트와 인간을 위한 오픈 소스 인증 솔루션입니다.
- 프로덕션 MCP 서버에는 토큰 만료, 스코프, 속도 제한 등 추가 보안 장치가 필요합니다.
- Hono 기반의 엔드포인트 구축과 함께 6가지 보호 장치를 적용할 수 있습니다.
theAuth는 AI 에이전트와 인간을 위한 오픈 소스 인증(auth) 솔루션입니다. GitHub에서 리포지토리 스타하기 · 문서 읽기 · 빠른 시작하기 · theauth.dev
대부분의 MCP 튜토리얼은 데모가 작동하는 순간에 끝납니다. 도구 서버는 tools/list를 응답하고, 클라이언트는 녹색 점을 표시하며, 모두가 만족합니다. 그러다가 누군가가 그 서버를 공개 URL 뒤에 배치합니다.
저는 '녹색 점' 이후의 과정을 설명하고자 합니다. 오아스(OAuth) 이론은 이미 좋은 글들이 많으니 제외하겠습니다. 이 가이드는 프로덕션 레이어에서 필요한 부분들을 다룹니다: 빠르게 만료되는 토큰, 개별 도구에 연결된 스코프(scopes), 인자 검사(argument checks), 속도 제한(rate limits), 그리고 쿼리할 수 있는 감사 추적(audit trail)입니다. 저는 AI 에이전트와 인간을 위한 오픈 소스 인증 라이브러리인 theAuth를 사용할 것입니다. 왜냐하면 제가 그 소스 코드와 문서를 충분히 자세히 읽었기 때문에, 어디까지가 한계인지 말씀드릴 수 있기 때문입니다. 이 중 네 가지 지점에서 멈추는 것이 중요하므로, 각각 표시하겠습니다.
이 글을 끝까지 읽으면, 여섯 가지 보호 장치가 적용된 Hono 기반의 MCP 엔드포인트와 함께 풀 리퀘스트(pull request) 템플릿에 붙여넣을 수 있는 체크리스트를 갖게 될 것입니다.
이 가이드는 theAuth 가이드 중 8개 중 7번째입니다. 독립적으로 존재하므로, 지금 바로 시작할 수 있습니다. 만약 필요하다면, 가이드 5에서 에이전트 신원(agent identities)에 대한 배경 지식을 얻을 수 있습니다.
| Guide | Title | Read it when |
|---|---|---|
| 1 | 기존 Next.js 앱에 로그인 추가하기 | 아직 인증 기능이 없는 앱을 가지고 있을 때 |
| ... | ||
| Building for people? Start at guide 1. Building for AI agents? Start at guide 5. Every guide links to the docs page for each concept it touches. |
TL;DR
| Layer | 기능 | theAuth 구성 요소 | 오늘 배포 여부 |
|---|---|---|---|
| Identity | PKCE S256을 사용한 OAuth 2.1, 단기 토큰 | createMcpModule | 예 |
| ... | |||
| 작동하는 세션을 확보하려면 데이터베이스와 로그인 페이지가 이미 존재하는지 확인하세요. |
전제 조건 (Prerequisites)
Node.js(ESM 및 top-level await 지원), Postgres 또는 SQLite 데이터베이스, 그리고 로그인된 사용자가 누구인지 알려줄 수 있는 로그인 페이지가 필요합니다. 또한 토큰 서명을 위한 32자 비밀 키도 필요합니다.
저는 이미 HTTP를 통해 JSON-RPC로 통신하는 MCP 서버를 가지고 있다고 가정하겠습니다. 이 서버의 전송 핸들러는 handleMcp라고 부르겠습니다. 사용하시는 MCP SDK가 제공하는 것으로 교체하세요.
패키지를 설치하세요:
pnpm add @glinr/theauth @glinr/theauth-hono hono @hono/node-server
만약 theAuth를 사용해 본 적이 없다면, 빠른 시작(quickstart)에서 약 6단계에 걸쳐 데이터베이스 설정을 다룹니다. 이 가이드는 그 이후부터 시작합니다.
두 가지 종류의 토큰과 그것이 중요한 이유
어떤 코드를 작성하기 전에, 한 가지 구분이 하루의 혼란을 막아줍니다.
theAuth는 두 가지 다른 Bearer 자격 증명(credential)을 발행합니다. 첫 번째는 MCP 모듈이 생성하는 OAuth 액세스 토큰입니다. 이 HS256 JWT에는 사용자 ID, 클라이언트 ID, 대상(audience), 그리고 스코프가 포함됩니다. 데스크톱 어시스턴트 같은 MCP 클라이언트는 사용자가 접근을 승인한 후에 이것을 받습니다.
두 번째는 kv_로 접두사가 붙은 에이전트 토큰입니다. 이는 theauth.agent.create를 사용하여 생성하며, theAuth는 오직 그 해시(hash)만 저장하고 이를 권한 목록에 매핑합니다. 스크립트와 서비스 계정이 이것을 사용합니다. 에이전트 식별 페이지에서 그 수명 주기(lifecycle)를 다룹니다.
서로 다른 코드가 각각의 토큰을 확인합니다. mcp.requireScopes는 OAuth 토큰을 처리합니다. 권한 엔진과 게이트웨이는 에이전트 토큰을 처리합니다. 제가 게이트웨이 소스를 확인해 보니, theauth.agent.validateToken을 호출하므로 OAuth JWT를 허용하지 않습니다. 프록시 섹션에 도달했을 때 이 점을 명심하세요.
계획은 다음과 같습니다. OAuth 토큰은 어떤 사람이 어떤 클라이언트를 승인했는지 증명합니다. 서비스 에이전트는 사용자의 도구에 대한 권한 규칙을 보유합니다. 둘 다 동일한 감사 로그(audit log)를 공급합니다.
1단계: 스토리지 콜백 구축
MCP 모듈 자체 데이터베이스가 없습니다. 대신 클라이언트, 코드, 토큰에 대한 콜백을 제공해야 합니다. MCP OAuth 페이지에서 모든 내용을 확인할 수 있습니다.
다음은 코드가 어디서든 실행되도록 인메모리 맵(in-memory maps)으로 구현된 형태입니다. 배포하기 전에 이 맵들을 테이블 읽기(table reads)로 대체해야 합니다.
// lib/mcp-store.ts
import type { McpAccessToken, McpAuthorizationCode, McpClient } from '@glinr/theauth/mcp';
...
이 파일의 두 줄이 중요합니다. consumeAuthorizationCode는 읽기 시 코드를 삭제해야 합니다. 그렇지 않으면 도난당한 코드가 두 번 작동할 수 있습니다. 그리고 resolveUserId는 사용자의 로그인 시스템으로 연결되는 다리 역할을 합니다. 이것을 잘못 설정하면 전체 플로우가 엉뚱한 사람에게 권한을 부여하게 됩니다.
2단계: 프로덕션을 위한 모듈 구성
이제 theAuth 인스턴스와 MCP 모듈을 생성합니다. 기본값들은 데모에 친화적입니다. 저는 이 중 네 가지를 변경했습니다.
// lib/theauth.ts
import { createTheAuth } from '@glinr/theauth';
import { createMcpModule } from '@glinr/theauth/mcp';
...
왜 이 네 가지일까요?
액세스 토큰 TTL(Time To Live)이 가장 중요합니다. 소스 코드의 검증 경로를 보면, 서명(signature), 발급자(issuer), 만료 시간(expiry), 스코프(scopes)를 확인합니다. 스토리지에서 토큰을 조회하지 않습니다. 폐기된 액세스 토큰은 exp까지 계속 작동합니다. TTL이 곧 무효화 기간이 되므로, 보안 검토자에게 15분의 시간을 방어할 수 있습니다. 자신만의 값을 선택하되, 의도적으로 선택하세요.
코드의 TTL은 낮춰야 합니다. 건강한 플로우에서는 인증 코드가 몇 초 동안만 존재해야 하기 때문입니다. 2분은 관대한 시간입니다.
allowedResources는 오디언스(audience)를 고정합니다. 목록이 설정되면, 해당 리소스 외의 것을 요청하는 클라이언트는 거부됩니다. 이것이 없으면 어떤 클라이언트든 무엇에 바인딩된 토큰을 요청할 수 있습니다.
consentPage는 등록이 기본적으로 열려 있기 때문에 중요합니다. MCP 사양은 동적 클라이언트 등록(RFC 7591)을 사용하므로, 모든 클라이언트가 /mcp/register에서 자신을 등록할 수 있습니다. consentPage를 구성하면 권한 부여 엔드포인트는 코드를 발급하는 대신 해당 페이지로 리디렉션됩니다. 그러면 귀하의 페이지가 사용자에게 클라이언트 이름과 스코프를 보여주고, 사용자가 동의하면 mcp.approveConsent(...)를 호출합니다. 동의 페이지가 없으면, 로그인한 사용자가 악성 링크에 접속할 경우 문서 내용을 바탕으로 볼 때 프롬프트 없이 코드가 발급됩니다.
hooks page에서는 onViolation에 대해 설명합니다. 여기서 경고하는 내용은 반복할 가치가 있습니다: 해당 훅은 fire and forget 방식이므로, throw는 처리되지 않은 거부(unhandled rejection)가 됩니다. 위의 try/catch는 장식이 아닙니다.
Step 3: 알려진 경로를 포함하여 라우트 마운트하기
Hono 어댑터는 모든 것을 접두사 아래에 마운트합니다. 모듈을 전달하면 등록, 권한 부여 및 토큰 경로가 등록됩니다. Hono 어댑터 페이지에 전체 목록이 있습니다.
// src/index.ts
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
...
마지막 두 경로는 거의 모든 사람을 당황하게 합니다. 어댑터는 메타데이터 문서를 마운트 지점과 상대적으로 등록하므로, 이들은 /api/theauth/.well-known/...에 위치합니다. 원본 루트에서 /.well-known/oauth-protected-resource를 탐색하는 클라이언트는 404 오류를 받고 포기합니다. 루트 경로를 직접 추가하세요.
순서도 중요합니다. Hono에서는 미들웨어가 app.route 호출보다 먼저 와야 합니다.
터미널에서 확인해 보세요:
curl -s "$MCP_RESOURCE_URL/.well-known/oauth-protected-resource"
응답에서 authorization_servers에 귀하의 발급자(issuer)가, scopes_supported에 스코프가 표시되어야 합니다. 응답이 비어 있거나 404인 경우, 더 진행하기 전에 이를 수정하세요.
Step 4: 도구별로 스코프 요구하기
대부분의 가이드에서는 전체 /mcp 라우트에 하나의 requireScopes(['mcp:read'])를 적용합니다. 이는 read_file과 run_job을 동일하게 취급한다는 의미입니다. 이 둘은 같지 않습니다.
streamable HTTP를 사용하는 MCP 서버는 보통 하나의 엔드포인트를 가지며, 도구 이름은 JSON-RPC 본문 안에 들어 있습니다. 본문을 읽어 도구를 찾고, 이를 스코프에 매핑합니다.
// lib/tool-scopes.ts
export const TOOL_SCOPES: Record<string, string> = {
list_files: 'mcp:read',
...
알 수 없는 도구는 null을 반환하고, 라우트는 이를 거부합니다. 이는 기본적으로 거부(deny by default)를 의미합니다. 다음 달에 추가된 도구라도 스코프가 부여되기 전까지는 아무런 권한이 없으며, 이것이 원하는 동작 방식입니다.
이제 라우트에 대해 살펴보겠습니다. 이 라우트는 순서대로 다섯 가지 작업을 수행합니다: 파싱(parse), 스코프 확인(check scopes), 오디언스 확인(check the audience), 사용자별 제한 확인(check the per-user limit), 그리고 포워딩(forward).
// src/mcp-route.ts
import type { Context } from 'hono';
import { createRateLimiter } from '@glinr/theauth/auth';
...
src/index.ts에서 app.post('/mcp', mcpRoute)로 등록합니다.
이 라우트에는 자세히 살펴볼 만한 세 가지 세부 사항이 있습니다.
mcp.requireScopes는 즉시 사용 가능한 응답을 반환합니다. 토큰이 누락되었거나 잘못된 경우, 리소스 메타데이터를 가리키는 WWW-Authenticate 헤더와 함께 401 에러가 발생합니다. 스코프가 없는 유효한 토큰의 경우, insufficient_scope 도전 과제(challenge) 및 업그레이드 URL과 함께 403 에러가 발생합니다. 잘 작동하는 클라이언트는 이를 사용하여 사용자에게 더 많은 접근 권한을 요청할 수 있습니다. 이 응답들 중 어느 것도 직접 만들 필요는 없습니다.
두 번째 세부 사항은 오디언스 비교입니다. 문서에는 단도직입적으로 설명되어 있습니다: validateToken은 오디언스 클레임이 존재하는지 확인만 할 뿐, 이를 리소스 URL과 비교하지 않습니다. 동일한 시크릿으로 서명되었지만 다른 리소스를 위해 발급된 토큰이라도 통과할 수 있습니다. 제가 추가한 두 줄의 검사가 이 간극을 메웁니다. 2단계의 allowedResources 설정은 클라이언트가 요청할 수 있는 것을 제한하고, 여기서의 비교는 우리가 받아들이는 것을 제한합니다.
세 번째는 session.tokenId입니다. 이 세션은 JWT의 jti 클레임에서 가져옵니다. 토큰이 만료되기 전에 차단해야 하는 ID 목록(deny list)을 보유하면 됩니다. 호출당 하나의 셋 조회(set lookup) 비용이 발생하며, 이는 짧은 TTL(Time To Live)로 인해 발생하는 공백을 메워줍니다.
단계 5: 권한 엔진을 사용하여 도구 정책 추가하기
스코프(Scopes)는 "이 클라이언트가 아예 도구를 호출할 수 있는가?"에 답합니다. 스코프는 "이 경로에 쓸 수 있는가?" 또는 "이번 시간에 이 호출이 500번째인가?"라는 질문에는 답할 수 없습니다. 권한 엔진(permission engine)이 이를 처리합니다.
이 엔진은 에이전트(agents)를 기반으로 작동하므로, MCP 서버를 대표하는 서비스 에이전트 하나를 생성해야 합니다. 이는 요청당 실행되는 것이 아니라 배포 시 한 번만 실행하십시오.
// scripts/create-service-agent.ts
import { theauth } from '../src/lib/theauth.js';
...
그런 다음 헬퍼는 이 경로(route)를 호출합니다:
// lib/tool-policy.ts
import path from 'node:path';
import { theauth } from './theauth.js';
...
이제 주의할 점들이 있습니다. 왜냐하면 하나하나가 문서 재확인 비용을 발생시켰기 때문입니다.
allowedArgPatterns는 arguments의 모든 문자열 값을 모든 패턴과 비교합니다. 만약 path와 함께 content를 전달한다면, content 역시 ^/srv/data/와 일치해야 하며, 이 경우 호출은 실패합니다. 이것이 헬퍼가 오직 path만 포워딩하는 이유입니다.
정규 표현식(regex)이 경로 감옥(path jail)은 아닙니다. /srv/data/../../etc/passwd는 ^/srv/data/와 일치합니다. path.posix.normalize 호출이 먼저 점(dot)들을 축소시키기 때문에, 패턴이 보는 문자열은 /etc/passwd가 되고 이는 실패하게 됩니다. 따라서 정규화 후 매칭해야 합니다. 만약 도구가 파일 경로를 사용한다면, 또한 시(sy)로 해결하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기