인증 제공자 연결하기
인증 제공자 연결하기
직전 튜토리얼에서는 리소스 권한을 추가해 사용자에게 비공개 대화 기능을 줬어요. 하지만 여전히 하드코딩된 토큰으로 인증하고 있어서 안전하지 않죠. 이제 OAuth2를 사용해 그 토큰들을 실제 사용자 계정으로 바꿔볼게요.
출처: 문서
본문
직전 튜토리얼에서 리소스 권한을 추가해 사용자에게 비공개 대화를 제공했습니다. 하지만 여전히 하드코딩된 토큰으로 인증하고 있어 안전하지 않습니다. 이제 OAuth2를 사용해 그 토큰들을 실제 사용자 계정으로 교체할 것입니다.
같은 Auth 객체와 리소스 수준 접근 제어를 유지하면서, 인증을 Supabase를 Identity Provider로 사용하도록 업그레이드합니다. 이 튜토리얼은 Supabase를 사용하지만, 개념은 모든 OAuth2 제공자에 적용됩니다. 다음을 배우게 됩니다:
- 테스트 토큰을 실제 JWT 토큰으로 교체하기
- 안전한 사용자 인증을 위해 OAuth2 제공자와 통합하기
- 기존 인증 로직을 유지하면서 사용자 세션과 메타데이터 처리하기
배경
OAuth2에는 세 가지 주요 역할이 있습니다:
- 권한 부여 서버(Authorization server): 사용자 인증을 처리하고 토큰을 발급하는 Identity Provider(예: Supabase, Auth0, Google)
- 애플리케이션 백엔드(Application backend): 여러분의 LangGraph 애플리케이션. 토큰을 검증하고 보호된 리소스(대화 데이터)를 제공합니다.
- 클라이언트 애플리케이션(Client application): 사용자가 여러분의 서비스와 상호작용하는 웹 또는 모바일 앱
표준 OAuth2 흐름은 대략 다음과 같이 작동합니다:
sequenceDiagram
participant User
participant Client
participant AuthServer
participant Agent Server
User->>Client: Initiate login
User->>AuthServer: Enter credentials
AuthServer->>Client: Send tokens
Client->>Agent Server: Request with token
Agent Server->>AuthServer: Validate token
AuthServer->>Agent Server: Token valid
Agent Server->>Client: Serve request (e.g., run agent or graph)
사전 준비 사항
이 튜토리얼을 시작하기 전에 다음이 있는지 확인하세요:
- 두 번째 튜토리얼의 봇이 오류 없이 실행 중.
- 인증 서버로 사용할 Supabase 프로젝트.
1. 의존성 설치
필요한 의존성을 설치합니다. custom-auth 디렉터리에서 시작하고 langgraph-cli가 설치되어 있는지 확인하세요:
cd custom-auth
pip install -U "langgraph-cli[inmem]"
cd custom-auth
uv add "langgraph-cli[inmem]"
2. 인증 제공자 설정
다음으로 인증 서버의 URL과 인증용 비밀 키(service role key)를 가져옵니다. Supabase를 사용하고 있으므로 Supabase 대시보드에서 이 작업을 할 수 있습니다:
- 왼쪽 사이드바에서 "Project Settings"를 클릭한 다음 "API"를 클릭합니다.
- 프로젝트 URL을 복사해
.env파일에 추가합니다.
echo "SUPABASE_URL=your-project-url" >> .env
- service role 키(비밀 키)를 복사해
.env파일에 추가합니다:
echo "SUPABASE_SERVICE_KEY=your-service-role-key" >> .env
- "anon public" 키를 복사해 기록해 둡니다. 이 키는 나중에 클라이언트 코드를 설정할 때 사용됩니다.
SUPABASE_URL=your-project-url
SUPABASE_SERVICE_KEY=your-service-role-key
3. 토큰 검증 구현
이전 튜토리얼에서는 Auth 객체를 사용해 하드코딩된 토큰을 검증하고 리소스 소유권을 추가했습니다.
이제 Supabase의 실제 JWT 토큰을 검증하도록 인증을 업그레이드합니다. 주요 변경 사항은 모두 @auth.authenticate 데코레이트 함수에 있습니다:
- 하드코딩된 토큰 목록을 확인하는 대신, Supabase에 HTTP 요청을 보내 토큰을 검증합니다.
- 검증된 토큰에서 실제 사용자 정보(ID, 이메일)를 추출합니다.
- 기존 리소스 인증 로직은 그대로 유지됩니다.
src/security/auth.py를 업데이트해 이를 구현합니다:
import os
import httpx
from langgraph_sdk import Auth
auth = Auth()
# This is loaded from the `.env` file you created above
SUPABASE_URL = os.environ["SUPABASE_URL"]
SUPABASE_SERVICE_KEY = os.environ["SUPABASE_SERVICE_KEY"]
@auth.authenticate
async def get_current_user(authorization: str *** None):
"""Validate JWT tokens and extract user information."""
assert authorization
scheme, token = authorization.split()
assert scheme.lower() == "bearer"
try:
# Verify token with auth provider
async with httpx.AsyncClient() as client:
response = await client.get(
f"{SUPABASE_URL}/auth/v1/user",
headers={
"Authorization": authorization,
"apiKey": SUPABASE_SERVICE_KEY,
},
)
assert response.status_code == 200
user = response.json()
return {
"identity": user["id"], # Unique user identifier
"email": user["email"],
"is_authenticated": True,
}
except Exception as e:
raise Auth.exceptions.HTTPException(status_code=401, detail=str(e))
# ... the rest is the same as before
# Keep our resource authorization from the previous tutorial
@auth.on
async def add_owner(ctx, value):
"""Make resources private to their creator using resource metadata."""
filters = {"owner": ctx.user.identity}
metadata = value.setdefault("metadata", {})
metadata.update(filters)
return filters
가장 중요한 변경은 이제 실제 인증 서버로 토큰을 검증한다는 점입니다. 인증 핸들러에는 Supabase 프로젝트의 비밀 키가 있으며, 이를 사용해 사용자의 토큰을 검증하고 사용자 정보를 추출할 수 있습니다.
4. 인증 흐름 테스트
새로운 인증 흐름을 테스트해 봅시다. 다음 코드를 파일이나 노트북에서 실행할 수 있습니다. 다음을 제공해야 합니다:
import os
import httpx
from getpass import getpass
from langgraph_sdk import get_client
# Get email from command line
email = getpass("Enter your email: ")
base_email = email.split("@")
password = "secure-password" # CHANGEME
email1 = f"{base_email[0]}+1@{base_email[1]}"
email2 = f"{base_email[0]}+2@{base_email[1]}"
SUPABASE_URL = os.environ.get("SUPABASE_URL")
if not SUPABASE_URL:
SUPABASE_URL = getpass("Enter your Supabase project URL: ")
# This is your PUBLIC anon key (which is safe to use client-side)
# Do NOT mistake this for the secret service role key
SUPABASE_ANON_KEY = os.environ.get("SUPABASE_ANON_KEY")
if not SUPABASE_ANON_KEY:
SUPABASE_ANON_KEY = getpass("Enter your public Supabase anon key: ")
async def sign_up(email: str, password: str):
"""Create a new user account."""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{SUPABASE_URL}/auth/v1/signup",
json={"email": email, "password": password},
headers={"apiKey": SUPABASE_ANON_KEY},
)
assert response.status_code == 200
return response.json()
# Create two test users
print(f"Creating test users: {email1} and {email2}")
await sign_up(email1, password)
await sign_up(email2, password)
⚠️ 계속하기 전에: 이메일을 확인하고 두 확인 링크를 모두 클릭하세요. 사용자의 이메일을 확인하기 전까지 Supabase는 /login 요청을 거부합니다.
이제 사용자가 자신의 데이터만 볼 수 있는지 테스트합니다. 진행하기 전에 서버가 실행 중인지 확인하세요(langgraph dev 실행). 다음 스니펫은 이전에 인증 제공자 구성 중 Supabase 대시보드에서 복사한 "anon public" 키가 필요합니다.
async def login(email: str, password: str):
"""Get an access token for an existing user."""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{SUPABASE_URL}/auth/v1/token?grant_type=password",
json={
"email": email,
"password": password
},
headers={
"apikey": SUPABASE_ANON_KEY,
"Content-Type": "application/json"
},
)
assert response.status_code == 200
return response.json()["access_token"]
# Log in as user 1
user1_token = await login(email1, password)
user1_client = get_client(
url="http://localhost:2024", headers={"Authorization": f"Bearer {user1_token}"}
)
# Create a thread as user 1
thread = await user1_client.threads.create()
print(f"✅ User 1 created thread: {thread['thread_id']}")
# Try to access without a token
unauthenticated_client = get_client(url="http://localhost:2024")
try:
await unauthenticated_client.threads.create()
print("❌ Unauthenticated access should fail!")
except Exception as e:
print("✅ Unauthenticated access blocked:", e)
# Try to access user 1's thread as user 2
user2_token = await login(email2, password)
user2_client = get_client(
url="http://localhost:2024", headers={"Authorization": f"Bearer {user2_token}"}
)
try:
await user2_client.threads.get(thread["thread_id"])
print("❌ User 2 shouldn't see User 1's thread!")
except Exception as e:
print("✅ User 2 blocked from User 1's thread:", e)
출력은 다음과 같아야 합니다:
✅ User 1 created thread: d6af3754-95df-4176-aa10-dbd8dca40f1a
✅ Unauthenticated access blocked: Client error '403 Forbidden' for url 'http://localhost:2024/threads'
✅ User 2 blocked from User 1's thread: Client error '404 Not Found' for url 'http://localhost:2024/threads/d6af3754-95df-4176-aa10-dbd8dca40f1a'
인증과 권한 부여가 함께 작동합니다:
- 사용자는 봇에 접근하려면 로그인해야 합니다.
- 각 사용자는 자신의 스레드만 볼 수 있습니다.
모든 사용자는 Supabase 인증 제공자가 관리하므로, 추가 사용자 관리 로직을 구현할 필요가 없습니다.
다음 단계
LangGraph 애플리케이션에 프로덕션 준비된 인증 시스템을 성공적으로 구축했습니다! 달성한 것을 정리해 봅시다:
- 인증 제공자(Supabase) 설정
- 이메일/비밀번호 인증으로 실제 사용자 계정 추가
- Agent Server에 JWT 토큰 검증 통합
- 사용자가 자신의 데이터만 접근할 수 있도록 적절한 권한 부여 구현
- 다음 인증 과제를 처리할 준비가 된 기반 구축
이제 프로덕션 인증이 있으니 다음을 고려해 보세요:
- 선호하는 프레임워크로 웹 UI 구축하기 (Custom Auth 템플릿에 예시가 있습니다)
- 인증에 관한 개념 가이드에서 인증과 권한 부여의 다른 측면 알아보기
- 레퍼런스 문서를 읽은 후 핸들러와 설정을 더 맞춤화하기
더 알아보기
- 인증 개념 전반은 Authentication 문서를 참고하세요.
- 리소스 소유권 설정은 Resource auth 문서를 확인해 보세요.