OAuth2 인증 및 DCR 가이드
Custom Remote MCP 서버에 OAuth 2.0 인증을 적용하는 방법을 설명합니다. Dynamic Client Registration (DCR)을 지원하면 사용자가 URL만 입력하여 자동으로 인증을 완료할 수 있습니다.
개요
| 질문 | 답변 |
|---|---|
| Custom MCP에 OAuth2를 지원하나요? | ✅ 완전히 지원합니다. |
| 각 사용자가 개별 인증할 수 있나요? | ✅ 예, 토큰은 사용자별로 관리됩니다. |
| DCR이 필수인가요? | ❌ 아니요, 선택사항입니다. 없으면 수동으로 Client ID/Secret을 입력합니다. |
| DCR이 있으면 무엇이 좋은가요? | 자동 Client 등록으로 사용자 경험이 크게 개선됩니다. |
OAuth2 인증 방식
AIP는 MCP 서버의 URL을 입력하면 자동으로 OAuth 메타데이터를 감지하여 다음 3가지 방식 중 하나로 동작합니다.
OAuthWithDCR (자동 Client 등록)
- 조건: MCP 서버가 DCR 스펙을 지원
- 사용자 조작: URL만 입력
- AIP 처리: 자동으로 Client 등록 수행
- 결과: OAuth 팝업이 자동으로 표시됨
OAuth (수동 Client ID/Secret 입력)
- 조건: MCP 서버가 DCR을 지원하지 않음
- 사용자 조작: Client ID / Secret을 직접 입력
- AIP 처리: 입력된 자격증명으로 OAuth 처리
- 결과: OAuth 팝업 표시
NoAuth (인증 없음)
- 조건: OAuth 메타데이터 Discovery에서 사용할 수 있는 메타데이터를 얻지 못함
- 사용자 조작: 없음
- AIP 처리: 이번 설정에서는 OAuth 흐름을 시작하지 않음
- 결과: OAuth 팝업 없음
MCP 서버 URL을 입력하는 즉시, AIP가 자동으로 OAuth Discovery를 수행하여 위 방식 중 하나를 결정합니다.
DCR이란
Dynamic Client Registration (DCR) 은 RFC 7591 표준 프로토콜로, OAuth2 클라이언트(AIP)가 Authorization Server에 자동으로 등록하여 Client ID와 Secret을 발급받는 방식입니다.
DCR이 없는 경우 (기존 방식)
- 사용자가 해당 서비스에서 OAuth App을 수동으로 생성합니다.
- Client ID / Secret을 발급받습니다.
- AIP에 수동으로 입력합니다.
- OAuth 팝업이 진행됩니다.
DCR이 있는 경우
- 사용자가 AIP에 MCP Server URL을 입력합니다.
- AIP가 자동으로 서버에 Client를 등록합니다.
- Client ID / Secret이 자동으로 발급됩니다.
- OAuth 팝업이 진행됩니다 (사용자 입력 불필요).
DCR 지원 여부 판단 기준
AIP는 MCP 서버의 Authorization Server Metadata에 registration_endpoint 필드가 있는지 확인합니다:
// Example response from /.well-known/oauth-authorization-server
{
"issuer": "https://mcp.example.com",
"authorization_endpoint": "https://mcp.example.com/oauth/authorize",
"token_endpoint": "https://mcp.example.com/oauth/token",
"registration_endpoint": "https://mcp.example.com/oauth/register" // DCR supported if present
}AIP의 OAuth 자동 감지 흐름
MCP 서버 URL을 입력하면 AIP 백엔드에서 다음 순서로 자동 감지합니다:
- MCP 서버에 POST 요청 전송
- 401 응답 +
WWW-Authenticate헤더 → Protected Resource Metadata URL 추출 - 401이 아닌 경우 →
/.well-known/oauth-protected-resource확인
- 401 응답 +
- Protected Resource Metadata에서
authorization_servers추출 - Authorization Server Metadata (ASM) 조회 (우선순위 순서)
- RFC 8414:
/.well-known/oauth-authorization-server/{path} - OIDC (path insertion):
/.well-known/openid-configuration/{path} - OIDC (path appending):
/{path}/.well-known/openid-configuration
- RFC 8414:
- 인증 방식 결정
- ASM 조회 실패 → NoAuth (메타데이터 fallback)
- ASM에
registration_endpoint있음 → OAuthWithDCR - ASM에
registration_endpoint없음 → OAuth (수동 입력)
Discovery 과정에서 OAuth 메타데이터를 확인하지 못하면 메타데이터 판정이 NoAuth로 fallback되며 OAuth 팝업이 표시되지 않습니다. 반면 DCR 실행 단계의 실패는 별도로 처리되어, AIP가 DCR 미지원 오류를 반환하고 UI는 수동 Client ID / Secret 입력으로 전환됩니다.
설정 가이드
DCR 지원 서버 연동 (예: Linear)
URL 입력
Integrations → All Integrations → 사용자 정의 MCP 연동 설정을 클릭하고 MCP Server URL을 입력합니다.
자동 인증
AIP가 자동으로 DCR을 수행하고 OAuth 팝업이 표시됩니다. 서비스 계정으로 로그인하면 연동이 완료됩니다.
토큰 관리
각 사용자별로 독립적으로 인증되며, 토큰은 사용자별로 별도 관리됩니다.
DCR 미지원 서버 연동 (예: Salesforce)
AIP에서 MCP 추가
- Integrations → All Integrations → 사용자 정의 MCP 연동 설정에서 MCP Server URL을 입력합니다.
- Install을 클릭하면 Client ID / Secret 입력 다이얼로그가 표시됩니다. 다이얼로그에는 OAuth Callback URL(복사 버튼 제공)과 필요한 Scope 목록이 함께 표시됩니다.
OAuth App 생성
MCP 서버의 관리 콘솔에서 OAuth App을 생성합니다:
- OAuth Settings를 활성화합니다.
- Callback URL을 입력합니다. AIP의 Client ID / Secret 입력 다이얼로그에서 OAuth Callback URL을 복사하여 등록합니다.
- 필요한 Scope를 선택합니다.
- Client ID와 Client Secret을 메모합니다.
OAuth 로그인
- AIP의 Client ID / Secret 입력 다이얼로그로 돌아가 앞 단계에서 메모한 Client ID / Secret을 입력합니다.
- OAuth 팝업에서 서비스 계정으로 로그인합니다. 로그인이 완료되면 AIP가 서비스로부터 인증 토큰을 수신하여 연동이 완료됩니다. 내부 처리 과정에 대한 자세한 내용은 OAuth2 내부 처리 흐름을 참조하세요.
Salesforce Custom Domain 환경에서는 AIP가 자동으로 재로그인을 요청하여 cross-org OAuth 오류를 방지합니다. 문제가 지속되는 경우, 별도의 브라우저 프로필 사용을 시도해 보세요.
내부 네트워크 서버 연동 (Edge Tunnel 필요)
MCP 서버가 내부 네트워크 또는 localhost에서 실행 중인 경우:
- AIP Desktop CLI로 Edge Tunnel을 시작합니다.
- Custom MCP 설정 시 “Use Edge Tunnel” 옵션을 체크합니다.
- 이후 OAuth Discovery 및 인증 흐름은 동일하게 진행됩니다.
자세한 내용은 Edge Tunnel 문서를 참조하세요.
DCR 미지원 시의 어려움
DCR을 지원하지 않는 MCP 서버를 연동할 때 발생할 수 있는 문제점입니다:
- 수동 클라이언트 준비 필요 — 연동을 설정하는 주체가 OAuth App을 생성 또는 등록하고 Client ID / Secret을 제공해야 합니다.
- Scope 설정 오류 — AIP가 필요한 Scope를 안내하지만, 사용자가 OAuth App에 수동으로 추가해야 합니다. 누락 시 도구 호출이 실패합니다.
- Client Secret 보안 — Secret을 AIP에 직접 입력하는 구조로, 관리 부담이 증가합니다.
- 운영 복잡성 — Client ID/Secret의 만료 또는 재발급 시 재입력이 필요합니다.
- 높은 도입 장벽 — 기술에 익숙하지 않은 사용자에게 OAuth App 생성은 어려운 작업입니다.
DCR 구현 가이드 (MCP 서버 개발자용)
MCP 서버에서 DCR을 지원하려면 메타데이터에 노출되는 2개의 엔드포인트와, 메타데이터가 가리키는 registration_endpoint 구현이 모두 필요합니다.
1. Protected Resource Metadata
GET /.well-known/oauth-protected-resource
Response:
{
"resource": "https://mcp.example.com",
"authorization_servers": ["https://auth.example.com"]
}2. Authorization Server Metadata
registration_endpoint가 포함되어야 AIP가 DCR로 인식합니다.
GET /.well-known/oauth-authorization-server
Response:
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"registration_endpoint": "https://auth.example.com/register",
"scopes_supported": ["read", "write"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_basic"]
}registration_endpoint가 없으면 AIP는 자동으로 OAuth(수동 입력) 방식으로 fallback합니다.
code_challenge_methods_supported에 S256을 포함하면 AIP가 자동으로 PKCE를 적용하여 보안이 강화됩니다. 포함을 권장합니다.
3. Client Registration Endpoint
registration_endpoint는 AIP가 자동 Client 등록 시 직접 호출하는 POST 엔드포인트입니다. RFC 7591 형식의 클라이언트 메타데이터를 받아 client_id, client_secret 등의 등록 결과를 반환해야 합니다.
POST /oauth/register
Content-Type: application/json
Request:
{
"client_name": "Custom MCP Client of AI Platform",
"redirect_uris": ["https://{aip-domain}/integration/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"scope": "read write",
"token_endpoint_auth_method": "client_secret_basic"
}
Response (201 Created):
{
"client_id": "aip_123456789",
"client_secret": "replace-with-secure-secret",
"client_id_issued_at": 1712236800,
"client_secret_expires_at": 0,
"redirect_uris": ["https://{aip-domain}/integration/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "client_secret_basic"
}구현 예제 (Node.js)
const crypto = require('crypto');
// Protected Resource Metadata
app.get('/.well-known/oauth-protected-resource', (req, res) => {
res.json({
resource: 'https://mcp.example.com',
authorization_servers: ['https://auth.example.com']
});
});
// Authorization Server Metadata
app.get('/.well-known/oauth-authorization-server', (req, res) => {
res.json({
issuer: 'https://auth.example.com',
authorization_endpoint: 'https://auth.example.com/authorize',
token_endpoint: 'https://auth.example.com/token',
registration_endpoint: 'https://auth.example.com/register',
scopes_supported: ['read', 'write', 'admin'],
response_types_supported: ['code'],
code_challenge_methods_supported: ['S256'],
grant_types_supported: ['authorization_code', 'refresh_token'],
token_endpoint_auth_methods_supported: ['client_secret_basic']
});
});
// Client Registration Endpoint
app.post('/oauth/register', (req, res) => {
res.status(201).json({
client_id: `aip_${crypto.randomBytes(16).toString('hex')}`,
client_secret: crypto.randomBytes(32).toString('hex'),
client_id_issued_at: Math.floor(Date.now() / 1000),
client_secret_expires_at: 0,
redirect_uris: req.body.redirect_uris ?? [],
grant_types: req.body.grant_types ?? ['authorization_code', 'refresh_token'],
token_endpoint_auth_method: req.body.token_endpoint_auth_method ?? 'client_secret_basic'
});
});구현 예제 (Python)
from flask import Flask, jsonify, request
import time
import secrets
app = Flask(__name__)
@app.route('/.well-known/oauth-protected-resource')
def oauth_resource_metadata():
return jsonify({
'resource': 'https://mcp.example.com',
'authorization_servers': ['https://auth.example.com']
})
@app.route('/.well-known/oauth-authorization-server')
def oauth_authorization_server():
return jsonify({
'issuer': 'https://auth.example.com',
'authorization_endpoint': 'https://auth.example.com/authorize',
'token_endpoint': 'https://auth.example.com/token',
'registration_endpoint': 'https://auth.example.com/register',
'scopes_supported': ['read', 'write', 'admin'],
'response_types_supported': ['code'],
'code_challenge_methods_supported': ['S256'],
'grant_types_supported': ['authorization_code', 'refresh_token'],
'token_endpoint_auth_methods_supported': ['client_secret_basic']
})
@app.route('/oauth/register', methods=['POST'])
def oauth_register():
payload = request.get_json(silent=True) or {}
return jsonify({
'client_id': f'aip_{secrets.token_hex(16)}',
'client_secret': secrets.token_hex(32),
'client_id_issued_at': int(time.time()),
'client_secret_expires_at': 0,
'redirect_uris': payload.get('redirect_uris', []),
'grant_types': payload.get('grant_types', ['authorization_code', 'refresh_token']),
'token_endpoint_auth_method': payload.get('token_endpoint_auth_method', 'client_secret_basic')
}), 201예제는 문서 설명용 최소 구현입니다. 실제 운영 환경에서는 등록된 클라이언트를 영속 저장하고, 허용된 redirect URI 검증과 client secret 보호 정책을 함께 구현해야 합니다.
문제 해결
OAuth 팝업이 표시되지 않는 경우
- OAuth 메타데이터 Discovery가 정상 동작하는지 확인합니다.
PRM 응답에
curl https://mcp.example.com/.well-known/oauth-protected-resourceauthorization_servers가 있으면, 그 값이 가리키는 Authorization Server Metadata도 함께 접근 가능한지 확인합니다. - AIP에서 MCP 서버 URL로 HTTP 접근이 가능한지 확인합니다 (방화벽 등).
- 내부 네트워크 서버의 경우 “Use Edge Tunnel” 옵션을 체크했는지 확인합니다.
- MCP Server URL이 정확한지 확인합니다 (예:
https://host/sse). - MCP 서버가 표준 OAuth2 Authorization Code Flow를 지원하는지 확인합니다.
인증 실패 에러
- Client ID / Secret이 정확한지 확인합니다.
- OAuth App의 리다이렉트 URI 화이트리스트에 AIP의 OAuth 콜백 URL을 추가합니다.
- 서버 로그에서 OAuth 엔드포인트 에러 메시지를 확인합니다.
사용자별 인증이 동작하지 않는 경우
- 다른 사용자 계정으로 다시 로그인합니다.
- MCP 서버에서 사용자별로 다른 토큰이 발급되는지 확인합니다.