Akan.js
Docs
문서컨벤션레퍼런스Cheatsheet
Akan.js
문서컨벤션레퍼런스Cheatsheet
Akan.js

MIT 라이선스 하에 배포되었습니다.

Akan.js 공식 컨설팅 서비스AkansoftCopyright © 2026 Akan.js 모든 권리 보유.시스템 관리자bassman
사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
일반▾
인증과 권한에이전트를 위한 OAuth스키마 설계텍스트 검색엣지 컴퓨팅파일 관리Single Sign-OnDataList & Enum
인터페이스▾
CRUDEndpointMCP 서버에이전트 채팅Form
관측성▾
로깅의존성 주입에러 처리메트릭
성능▾
캐싱이미지 최적화지연 로딩쿼리변경큐실시간
모바일▾
설정Push NotificationsDeep LinksUI & Keyboard데스크톱 배포
개발▾
문서화스키마 문서스크립트콘솔도커쿠버네티스PWA테스트
사람함께에이전트▾
사람 — 직접 정하고 책임지는 비즈니스 규칙과 흐름. 직접 읽어보세요.
함께 — 개념은 알아두고, 세부 규칙은 에이전트가 따릅니다.
에이전트 — 에이전트가 따르는 규칙과 레퍼런스. 필요할 때 찾아보세요.
일반▾
인증과 권한에이전트를 위한 OAuth스키마 설계텍스트 검색엣지 컴퓨팅파일 관리Single Sign-OnDataList & Enum
인터페이스▾
CRUDEndpointMCP 서버에이전트 채팅Form
관측성▾
로깅의존성 주입에러 처리메트릭
성능▾
캐싱이미지 최적화지연 로딩쿼리변경큐실시간
모바일▾
설정Push NotificationsDeep LinksUI & Keyboard데스크톱 배포
개발▾
문서화스키마 문서스크립트콘솔도커쿠버네티스PWA테스트
이전인증과 권한다음스키마 설계

에이전트에게는 쿠키가 없습니다

Claude Code에 내 앱의 /mcp를 연결했더니 호출마다 401이 돌아옵니다. 브라우저는 세션 쿠키로 로그인돼 있지만, CLI 에이전트에게는 브라우저도 그 쿠키도 없습니다. 게다가 /mcp는 cookie 헤더를 아예 지웁니다.
그래서 에이전트에게는 토큰이 필요한데, 토큰을 내어 주는 로그인 폼은 없습니다. libs/shared를 쓰는 앱은 토큰을 발행하는 OAuth 2.1 서버를 /mcp와 같은 프로세스에서 이미 제공합니다. 배포는 하나, 역할은 둘입니다:
리소스 서버
토큰을 받아 쓰는 쪽입니다. 토큰 없는 호출에는 어디서 로그인할지 알려 주는 401로 답합니다.
/mcp
인가 서버
토큰을 발행하는 쪽입니다. 같은 프로세스에서 도는 내 앱 자신이며, 스스로를 issuer로 내세웁니다.
/oauth/*
이 페이지에서 쓰는 말
용어설명
MCP 클라이언트
/mcp를 호출하는 에이전트 프로그램입니다. Claude Code, claude.ai, Cursor가 그렇습니다.
issuer (iss)
인가 서버의 공개 origin입니다. 클라이언트는 이 값을 바이트 단위로 비교합니다.
aud
토큰이 어느 리소스용인지 적는 클레임입니다. 여기서는 /mcp의 URL입니다.
grant
한 클라이언트가 한 계정으로 동작할 권한입니다. refresh 계보와 거기서 발행된 모든 토큰을 묶은 단위입니다.
PKCE
코드를 교환하는 쪽이 코드를 요청한 쪽과 같다는 것을 증명하는 일회용 비밀입니다. S256만 씁니다.
동의 페이지
로그인한 사용자가 클라이언트를 승인하거나 거절하는 화면입니다.
직접 할 일
직접 할 일은 많지 않습니다. 어떤 클라이언트가 흐름을 시작할 수 있는지, 동의 페이지가 어디 있는지, 그리고 시크릿 하나입니다. 순서는 다음과 같습니다:
  1. libs/shared를 쓰는 앱에서 시작합니다. OAuth 라우트와, 흐름을 시작하는 /mcp의 401이 함께 따라옵니다.
  2. 동의 페이지를 동기화합니다. 이 페이지는 libs/shared/page에 있으므로, akan.config.ts의 syncPageLibs로 가져온 뒤 akan sync를 실행합니다.
  3. local 밖의 모든 배포에 JWT_SECRET을 설정합니다.
  4. 기본값이 맞지 않을 때만 oauth 키를 추가합니다. 어떤 클라이언트가 흐름을 시작할 수 있는지, 동의 페이지가 어디 있는지 같은 것입니다.
  5. MCP 클라이언트에 https://<host>/mcp를 연결합니다. 클라이언트가 스스로 등록하고, 브라우저를 열어 로그인과 동의를 받은 뒤 토큰을 받습니다.
2단계는 앱 설정의 한 줄입니다:
apps/koyo/akan.config.ts
나중에 grant를 끊는 방법은 아래 폐기는 grant 단위로에서 다룹니다.
핸드셰이크 전체
5단계에서 클라이언트를 연결하면, 아래 단계는 모두 MCP 클라이언트와 앱이 알아서 진행합니다. 직접 작성할 것은 없습니다.
처음부터 끝까지, 핸드셰이크 한 번
MCP 클라이언트
브라우저
내 앱
토큰 없이 POST /mcp
401 WWW-Authenticate resource_metadata
GET /.well-known/oauth-protected-resource/mcp
authorization_servers가 이 앱 자신을 가리킵니다
GET /.well-known/oauth-authorization-server
POST /oauth/register
client_id
code_challenge와 함께 /oauth/authorize를 엽니다
GET /oauth/authorize
로그인 페이지로 302, 그다음 동의 페이지로
POST approveOAuthConsent
code와 iss를 담아 redirect_uri로 302
루프백 redirect로 code 전달
code_verifier와 함께 POST /oauth/token
액세스 토큰과 리프레시 토큰
Bearer 토큰으로 POST /mcp
tools/list
MCP 클라이언트
브라우저
내 앱
MCP 클라이언트 → 내 앱토큰 없이 POST /mcp
내 앱 → MCP 클라이언트401 WWW-Authenticate resource_metadata
MCP 클라이언트 → 내 앱GET /.well-known/oauth-protected-resource/mcp
내 앱 → MCP 클라이언트authorization_servers가 이 앱 자신을 가리킵니다
MCP 클라이언트 → 내 앱GET /.well-known/oauth-authorization-server
MCP 클라이언트 → 내 앱POST /oauth/register
내 앱 → MCP 클라이언트client_id

앱이 제공하는 라우트

아래 라우트는 RFC 8414와 모든 MCP 클라이언트가 찾는 origin 루트에 놓입니다. libs/shared와 /mcp에 함께 들어 있어서 직접 작성할 것은 없습니다.
라우트설명
/.well-known/oauth-protected-resource/mcp
/mcp가 직접 제공하는 RFC 9728 메타데이터이며, 접미사 없는 경로로도 답합니다. 이 앱을 인가 서버로 알립니다.
/.well-known/oauth-authorization-server
RFC 8414 메타데이터입니다. S256 전용 PKCE, metadata document client id, iss 파라미터를 알립니다.
/oauth/authorize
클라이언트, redirect URI, PKCE challenge를 검증한 뒤 브라우저를 동의 페이지로 보냅니다.
/oauth/consent
libs/shared/page/oauth/consent의 동의 페이지이며, 앱이 동기화해야 제공됩니다.
/oauth/token
인가 코드나 리프레시 토큰을 새 토큰 쌍으로 교환합니다.
/oauth/register
RFC 7591 동적 등록이며 기본으로 켜져 있습니다. Claude Code와 claude.ai가 여기서 등록합니다.
/oauth/revoke
RFC 7009입니다. 클라이언트가 토큰을 반납하면 그 grant 전체가 닫힙니다.
선언 방식
프로토콜 라우트는 libs/shared/lib/_oauth의 평범한 signal endpoint입니다. 모두 같은 옵션 네 개를 펼쳐 씁니다:
libs/shared/lib/_oauth/oauth.signal.ts
옵션설명
guards: [Public]
클라이언트는 아직 자격 증명이 없고, 바로 그것을 받으러 온 것입니다.
prefix: false
경로에서 service prefix를 뺍니다.
globalPrefix: false
전역 API prefix도 빼서 라우트가 origin 루트에 놓입니다.
mcp: false
MCP 목록에서 뺍니다. 이 라우트는 목록에 오르는 통로이지 목록 위의 툴이 아닙니다.
기본으로 들어 있는 안전장치
  • 동의 페이지는 redirect host를 보여 줍니다. 진짜 클라이언트와 사칭을 가르는 유일한 단서이고, 루프백 redirect라면 "내 컴퓨터에서 실행되는 앱"이라는 경고가 붙습니다.
  • 인가 요청은 짧게 삽니다. 10분간 유효하며, 처음 연 로그인 계정에 묶입니다.
  • 인가 코드는 한 번만 씁니다. 60초간 유효하며, 교환이 실패해도 첫 사용에서 소진됩니다.
  • 폐기 응답은 아무것도 알려 주지 않습니다. /oauth/revoke는 토큰이 살아 있었든 아니든 200을 돌려주므로, 토큰이 유효한지 떠보는 수단이 되지 않습니다.

토큰에 담기는 것

액세스 토큰은 앱 자신의 access JWT입니다. 브라우저 세션과 같은 클레임에 OAuth용 클레임 넷이 더해집니다. 그래서 AccountMiddleware와 guard는 평소대로 판정하고, service나 signal은 이 호출이 OAuth로 왔다는 사실을 몰라도 됩니다.
클레임설명
selfme
브라우저 세션과 같은 신원 정보입니다. AccountMiddleware가 전처럼 읽습니다.
iss
발행자, 곧 내 앱의 공개 origin입니다.
aud
MCP 엔드포인트의 URL(oauth.resource)입니다. /mcp는 이 값이 없는 토큰을 거부합니다.
client_id
grant를 발급받은 클라이언트입니다.
sub
user:<id> 또는 admin:<id>이며, 토큰이 대신하는 계정입니다.
sid
grant의 계보 id입니다. grant를 폐기하면 이 값이 거부 목록에 오릅니다.
  • scope는 없습니다. 호출자가 무엇을 할 수 있는지는 이미 guard가 정하므로, scope는 그보다 약한 사본일 뿐입니다. 메타데이터도 토큰도 scope를 싣지 않고, 동의 페이지도 사용자에게 그렇게 알립니다. AKAN_MCP_SCOPES는 다른 issuer가 발급한 토큰용이며, 이 서버를 쓰는 앱에 설정하면 이 서버가 발급한 토큰이 모두 거절됩니다.
  • 살아 있는 계정에서 발행합니다. 스냅샷이 아니므로 role을 바꾸면 다음 토큰에 반영됩니다.
/mcp가 확인하는 것
토큰이 없으면 /mcp는 401로 답하며 메타데이터 위치를 알려 줍니다. 토큰이 있으면 클라이언트가 Authorization 헤더에 실어 보냅니다:
Terminal
  • 헤더만 읽습니다. /mcp는 account middleware가 돌기 전에 cookie 헤더를 지웁니다. 그러지 않으면 같은 사이트의 페이지가 방문자의 세션으로 tools/call을 부를 수 있는데, 이 라우트는 CrossSiteGuard를 거치지 않기 때문입니다.
  • aud가 없으면 들어오지 못합니다. issuer를 지정하면 같은 issuer가 다른 리소스용 토큰도 발행하므로, audience가 없는 토큰은 RFC 8707이 막으려는 confused deputy 상황입니다. 다른 리소스용으로 발급된 토큰도 거부합니다.
  • 수명은 한 시간이고, 리프레시 토큰은 회전합니다. 액세스 토큰은 accessTokenSeconds 동안(기본 한 시간) 유효합니다. 리프레시 토큰은 쓸 때마다 회전하며 30일간 유지됩니다.

클라이언트를 알아보는 방법

무엇을 허가하기 전에 서버는 client_id를 알아봐야 합니다. 아래 세 출처를 순서대로 확인하고, 어디에도 없는 클라이언트에게는 redirect 대신 오류 페이지로 답합니다. 검증되지 않은 목적지로는 아무것도 보내지 않습니다.
출처(확인 순서)설명
static
oauth.clients에 직접 적은 클라이언트입니다. clientSecret을 주면 confidential 클라이언트가 됩니다.
dynamic
/oauth/register에 스스로 등록한 클라이언트입니다(RFC 7591). Claude Code와 claude.ai가 이렇게 옵니다.
metadataDocument
client_id 자체가 등록 정보를 담은 HTTPS URL인 클라이언트입니다.
  • 시크릿은 해시로 보관합니다. static 클라이언트의 clientSecret은 설정에는 평문으로 적지만, 서버가 보관하기 전에 해시합니다. 생략하면 public 클라이언트입니다.
  • 등록은 열려 있지만 제한이 있습니다. 누구나 등록할 수 있지만 주소당 시간당 20회까지이고, 등록 정보는 90일간 유지됩니다.
  • 서버를 내부로 겨눌 수 없습니다. metadata document는 가져오기 전에 호스트를 먼저 확인하고, 사설 대역을 가리키면 거부합니다.
redirect URI 규칙
코드가 어디로 떨어질지는 redirect URI가 정하므로 검사가 엄격합니다. dynamic 클라이언트와 metadata document 클라이언트는 아래 URI만 등록할 수 있습니다:
redirect URI설명
https://…
항상 허용하며, 정확히 같아야 합니다.
http://127.0.0.1http://[::1]http://localhost
루프백이라 항상 허용하며, 포트만 달라도 됩니다.
cursor://…<scheme>://…
allowedRedirectSchemes에 적힌 scheme만 허용하며, 정확히 같아야 합니다.
http://<other host>
허용하지 않습니다.
  • localhost도 루프백으로 칩니다. RFC 8252는 권하지 않지만 Claude Code가 이 주소로 redirect합니다.
  • 포트가 달라도 되는 이유. 네이티브 클라이언트는 그때 비어 있는 포트를 잡고, Claude Code는 세션마다 새 포트를 고릅니다.
  • 기본 scheme이 cursor인 이유. Cursor 데스크톱 클라이언트는 cursor://…/oauth/callback으로 등록하므로, 이를 거부하는 서버는 Cursor에서 아예 쓸 수 없습니다.
  • fragment는 안 됩니다. #fragment가 붙은 redirect URI는 등록할 수 없습니다.
  • static 클라이언트는 scheme 검사를 거치지 않습니다. redirectUris를 내 설정에 직접 적으므로, 정확히 같은지만 봅니다. 루프백의 포트 예외는 똑같이 적용됩니다.
  • PKCE는 S256만 받습니다. challenge가 멀쩡해도 code_challenge_method를 빠뜨린 요청은 거부합니다.

앱별 설정

모든 설정은 앱 server env의 oauth 키 하나에 다른 모듈 옵션과 나란히 둡니다. 아예 적지 않아도 기본값만으로 앱 자신의 도메인에서 동작하는 서버가 됩니다.
자체 URL scheme을 쓰는 데스크톱 클라이언트를 따로 내놓는 앱이라면 이렇게 추가합니다:
apps/koyo/env/env.server.main.ts
enabledboolean기본값 true
끄면 인가 서버와 /mcp의 자격 증명 확인이 함께 사라져 /mcp가 익명으로 열립니다.
issuerstring기본값 앱의 호스트
클라이언트가 바이트 단위로 비교하는 공개 origin입니다. 터널이나 호스트를 바꾸는 edge 뒤에서 지정합니다.
resourcestring기본값 <issuer>/mcp
MCP 엔드포인트의 정규 URL이자 모든 토큰의 aud입니다. MCP를 /mcp에서 옮겼다면 지정합니다.
consentPathstring기본값 /oauth/consent
동의 페이지의 경로이며 basePath를 포함합니다. 예: /office/oauth/consent.
signinPathstring기본값 /signin
로그인하지 않은 브라우저가 먼저 가는 경로이며, ?redirect=로 동의 페이지에 돌아옵니다. basePath를 포함합니다.
clientsOAuthStaticClient[]기본값 []
직접 선언하는 클라이언트 목록입니다. 필드는 아래에 있습니다.
dynamicRegistrationboolean기본값 true
RFC 7591 자가 등록입니다. 끄면 404로 답하고 static과 metadata document 클라이언트만 남습니다.
allowedRedirectSchemesstring[]기본값 ["cursor"]
HTTPS와 루프백 외에 허용할 private-use redirect scheme입니다.
accessTokenSecondsnumber기본값 3600
액세스 토큰의 수명이자, 폐기된 grant의 id가 거부 목록에 남는 기간입니다.
clientIdMetadata.enabledboolean기본값 true
HTTPS client_id를 metadata document로 읽습니다. 끄면 메타데이터에서도 알리지 않습니다.
clientIdMetadata.refusePrivateAddressesboolean기본값 true
문서를 가져오기 전에 호스트를 확인합니다. 끄면 호스트 이름만 믿습니다.
  • 기본 issuer는 local에서 http://localhost:<port>, 그 밖에서는 https://<host>입니다. host는 HOST_NAME, hostname, <app>-<environment>.<serveDomain> 순서로 정해집니다.
  • 경로에는 basePath가 붙습니다. /office 아래에서 도는 앱은 /office/oauth/consent, /office/signin으로 적습니다.
  • 위의 koyo://는 allowedRedirectSchemes에 적지 않아도 됩니다. static 클라이언트의 redirectUris는 scheme 검사를 거치지 않습니다. 이 옵션은 dynamic 클라이언트와 metadata document 클라이언트가 등록할 수 있는 범위만 넓힙니다.
직접 선언하는 클라이언트
clients의 각 항목은 OAuthStaticClient입니다:

폐기는 grant 단위로

토큰 하나만 폐기하는 일은 없습니다. 단위는 grant, 즉 refresh 계보와 거기서 발행된 모든 액세스 토큰입니다. 닫을 수 있는 쪽은 둘입니다. 토큰을 쥔 클라이언트와, 그 토큰이 대신하는 계정입니다.
클라이언트 쪽에서
클라이언트는 RFC 7009로 둘 중 어느 토큰이든 반납하고, 그 토큰이 가리키는 grant가 닫힙니다:
Terminal
계정 쪽에서
연결된 앱 페이지에는 평범한 fetch 두 개면 됩니다:
호출설명
fetch.listOAuthConnections()
로그인한 계정에 대해 grant를 가진 애플리케이션을 grant마다 한 줄씩 나열합니다.
fetch.revokeOAuthConnection(sessionId)
애플리케이션 하나의 grant를 닫습니다. id가 이 계정의 살아 있는 grant가 아니면 false입니다.
  • 각 줄에는 클라이언트 이름, 접속한 user agent, 시작과 만료 시각, 그리고 지금 호출한 연결인지를 뜻하는 isCurrent가 담깁니다. 브라우저 세션은 목록에 없습니다.
  • 다른 것은 건드리지 않습니다. grant 하나를 폐기해도 그 계정의 다른 grant와 브라우저 세션은 그대로입니다.
  • guard는 Every이고, MCP에는 올리지 않습니다. 둘 다 mcp: false입니다. 툴 호출 안에서 다른 연결을 모두 나열하고 끊을 수 있는 에이전트야말로, 연결된 앱 페이지가 사람 손에 남겨 두려는 바로 그 레버이기 때문입니다.

사람은 되고 모델은 안 되는 일

에이전트가 진짜 토큰을 들게 되면, guard에게 한 번도 묻지 않았던 구분이 필요한 일이 생깁니다. 주문 환불은 사장이 직접 누르면 괜찮지만, 모델이 그러기로 하면 곤란합니다.
서버는 다음 세 가지로 둘을 구분하며, 단서는 이것이 전부입니다:
  • /mcp로 들어온 호출은 문 앞에서 표시됩니다. context.origin이 "mcp"입니다.
  • 이 서버가 발행한 토큰은 어떤 전송 방식으로 오든 client_id와, MCP 리소스를 가리키는 aud를 싣고 있습니다.
  • 브라우저 세션은 둘 다 싣지 않으므로 사람으로 읽힙니다.
이 단서를 읽는 레버는 둘이고, 서로 다른 질문에 답합니다:
apps/koyo/lib/icecreamOrder/icecreamOrder.signal.ts
레버
거부

이 페이지

에이전트에게는 쿠키가 없습니다
앱이 제공하는 라우트
토큰에 담기는 것
클라이언트를 알아보는 방법
앱별 설정
폐기는 grant 단위로
사람은 되고 모델은 안 되는 일
clientIdstring필수
이 클라이언트가 내미는 client_id입니다.
redirectUrisstring[]필수
쓸 수 있는 모든 redirect URI입니다. 요청은 이 중 하나와 정확히 같아야 하며, 루프백은 포트만 달라도 됩니다.
clientNamestring
동의 페이지가 사용자에게 보여 주는 이름입니다.
clientSecretstring
주면 confidential 클라이언트가 되고, 생략하면 public 클라이언트입니다.
tokenEndpointAuthMethod"none" | "client_secret_post" | "client_secret_basic"기본값 client_secret_post
confidential 클라이언트가 시크릿을 보내는 방식입니다. 시크릿이 없으면 항상 none입니다.
grant를 폐기하면 일어나는 일
  • 액세스 토큰은 다음 호출에서 거절됩니다. 액세스 토큰은 서버에 상태를 남기지 않아 지울 수 없으므로, 대신 모든 토큰이 싣고 있는 grant의 계보 id(sid)를 accessTokenSeconds 동안 거부 목록에 올립니다.
  • 회전 직후의 재사용은 허용합니다. 회전 후 30초 안에 다시 쓰인 리프레시 토큰에는 회전으로 답합니다. 같은 토큰을 두 번 들고 있는 클라이언트는 도둑이 아니기 때문입니다.
  • 늦게 다시 쓰면 계보를 폐기합니다. 그보다 늦게 재사용되면 그 grant의 계보만 폐기하고 다른 것은 건드리지 않습니다.
  • 다른 클라이언트에게 발급된 리프레시 토큰은 그대로 거부합니다.
목록 노출
분기
에이전트 호출에 각 레버가 하는 일
guards: [Every, Person]
✓
호출을 거부하고, endpoint를 MCP 카탈로그에서 뺍니다.
.with(AgentCall)
✓
✓
같은 판정을 boolean으로 handler에 넘겨 분기하게 합니다.
✓예아니요
  • Person은 숨기는 것이 아니라 없앱니다. static agents = false를 선언하므로, MCP 카탈로그는 이 guard가 지키는 endpoint를 호출자별로 숨기지 않고 통째로 거부합니다.
  • AgentCall은 endpoint가 아니라 부수 효과를 좁힙니다. endpoint는 그대로 호출할 수 있고 목록에도 남습니다. 달라지는 것은 그 호출이 일으키는 일로, 고객 메일도 push도 되돌릴 수 없는 부수 효과도 없습니다.
판정은 isAgentCall(context)로 읽으세요. 두 레버 모두 @libs/shared/srvkit의 이 함수를 씁니다. context.origin === "mcp"이거나, 토큰이 client_id나 aud를 싣고 있는지를 봅니다. 이 클레임을 cast로 직접 들여다보지 마세요. 클레임이 없다는 사실은 전송 방식마다 뜻이 다르고, 직접 만든 검사는 guard의 답과 어긋나게 됩니다.
함께 볼 페이지
권한 부여→
각 guard가 에이전트에게 무엇을 공개하는지, 거절 문구가 어떻게 나가는지.
MCP 서버→
카탈로그, resource URI, rate limit.
MCP 클라이언트 → 브라우저code_challenge와 함께 /oauth/authorize를 엽니다
브라우저 → 내 앱GET /oauth/authorize
내 앱 → 브라우저로그인 페이지로 302, 그다음 동의 페이지로
브라우저 → 내 앱POST approveOAuthConsent
내 앱 → 브라우저code와 iss를 담아 redirect_uri로 302
브라우저 → MCP 클라이언트루프백 redirect로 code 전달
MCP 클라이언트 → 내 앱code_verifier와 함께 POST /oauth/token
내 앱 → MCP 클라이언트액세스 토큰과 리프레시 토큰
MCP 클라이언트 → 내 앱Bearer 토큰으로 POST /mcp
내 앱 → MCP 클라이언트tools/list
local 밖에서는 JWT_SECRET이 필수입니다. 모든 배포에 이 값이나 security.jwtSecret을 설정하세요. 둘 다 없으면 시크릿이 앱 이름, 환경, 레포 이름에서 파생됩니다. URL만 봐도 알 수 있는 문자열 셋이므로, 이 페이지의 모든 토큰을 admin 세션까지 위조할 수 있게 됩니다.
refusePrivateAddresses는 egress 정책이 있는 곳에서만 끄세요. 네트워크가 사설 대역을 이미 막아 두지 않았다면, client_id URL로 서버를 자기 네트워크에 겨눌 수 있습니다.