API 토큰 설계의 다양한 방식과 각각의 장단점을 살펴보는 긴 안내서입니다.

이미지 제공: Annie Ruygt
우리는 Fly.io입니다. 이 글은 Fly.io에 관한 글은 아니지만, 어쨌든 우리 얘기를 조금은 들어야 합니다. 제 블로그고, 제 규칙이니까요. 우리 사용자들은 Docker 컨테이너를 보내오고, 우리는 그것을 Firecracker microvm으로 변환한 뒤 전 세계의 자체 하드웨어에서 호스팅합니다. 작동하는 Dockerfile만 있다면 시작해서 실행하기까지 10분도 채 걸리지 않습니다.
사실 이 글은 Fly.io에 대한 글은 아닙니다. 다만 배경 설명을 위해 앞부분에서 우리 이야기를 조금 하겠습니다.
지난 몇 주 동안 제 삶의 주제는 API 보안이었습니다. 저는 Fly.io를 위한 새로운 권한 시스템을 작업 중이고, 선택지를 두고 이것저것 많이 조사했습니다. 이 주제로 팟캐스트도 녹음했습니다. 결론을 미루지 않고 바로 말씀드리면, 우리는 Macaroon 기반 방식의 도입을 진행하고 있고, 이에 대해서는 뒤에서 더 읽게 되실 겁니다.
이 글은 깁니다. 여러분은 특정 종류의 토큰 하나에만 관심이 있을 수도 있습니다. 그래서 쉽게 찾아갈 수 있도록 목차를 준비했습니다.
Fly.io는 애플리케이션 호스팅 플랫폼입니다. Fly.io에서 실행되는 애플리케이션이 상호작용하는 컨트롤 플레인과, 사용자들이 상호작용하는 API가 있다고 생각하시면 됩니다. 대부분은 우리의 CLI인 flyctl을 통해 사용합니다. 여기서 우리가 이야기하는 것은 바로 그 flyctl 부분입니다.
현재 Fly.io의 API 접근은 전부 아니면 전무입니다. 모두가 루트 자격 증명을 갖고 있습니다. 우리가 원하는 것은 세분화된 권한입니다. 우리가 해결하고 싶은 큰 문제는 두 가지입니다.
이것이 사람들이 보통 IAM이라고 부르는 API의 역할입니다. IAM 문제를 해결하는 방법은 아주 많고, 하나같이 파고들기 재미있는 주제들입니다.
제가 여기서 관심 있는 것은 최종 사용자를 위한 API 보안, 즉 “리테일” 보안입니다.
제가 여기서 다루지 않는, 밀접하게 관련된 API 보안 문제도 있습니다. 바로 서비스 간 인증입니다. 현대의 애플리케이션은 작은 서비스들의 집합으로 이루어져 있습니다. 이상적으로는 그들 사이에 보안 계층이 있어야 합니다. 하지만 리테일 API IAM을 Kerberos나 mTLS로 구현하는 경우는 없습니다. 이런 접근법을 더 읽고 싶다면 예전에 다른 곳에 길게 쓴 글이 있습니다.
또 다른 관련 문제는 연합 인증과 single sign-on입니다. Google, Apple, Okta는 요청을 그들 플랫폼의 신원에 매핑하는 토큰을 제공합니다. 그런 토큰 형식은 여기와 관련이 있지만, 제가 원하는 것이 연합 신원 자체는 아니라는 점은 분명히 하고 싶습니다.
대부분의 API 보안 방식은 API 요청에 함께 전달되는 토큰으로 귀결됩니다. 토큰은 어떤 식으로든 접근 규칙과 연결됩니다. API는 요청을 받아 토큰을 추출하고, 접근 규칙을 찾아, 어떻게 처리할지 결정합니다.
읽는 동안 다음 질문들을 머릿속에 계속 두고 보시면 좋겠습니다.
지금은 2021년이므로, API에서 HTTP basic authentication으로 사용자 이름과 비밀번호를 전달하는 것이 나쁜 생각이라는 점을 제가 굳이 설명할 필요는 없을 겁니다. 어떤 형태이든 토큰은 충분히 길고 무작위처럼 보여야 합니다.
보안 관점에서 보면 꽤 이기기 어려운 토큰 생성기는 다음과 같습니다.
>>> binascii.hexlify(os.urandom(16))
b'46d684a052c29cdce14c7e03e19da0f9'
난수 토큰 테이블을 유지하고, 그것을 사용자 테이블과 연결하고, 다시 그 사용자들을 허용된 동작과 연결하면 됩니다. 이걸 어떻게 하는지는 굳이 제가 설명할 필요가 없을 겁니다. CRUD 앱이 원래 이렇게 동작하니까요.
하지만 제가 여러분께 말해줄 필요가 있을 만한 것은, 이것이 IAM 문제를 장기적으로도 처리하는 좋은 방법이라는 점입니다. 난수 토큰은 암호학적으로 무섭지 않습니다. 쉽게 폐기할 수 있고 만료도 시킬 수 있습니다. 함께 붙는 권한 로직도 깔끔하고 표현력이 좋습니다. 그냥 여러분의 API 코드일 뿐입니다.
솔직히 말해 단순 난수 토큰의 가장 큰 단점은 너무 재미없다는 것입니다. 만약 이것으로 충분하다면, 그리고 대부분의 애플리케이션은 충분합니다, 아마 그렇게 해야 합니다. 보안상의 이유로 그렇게 한다고 스스로에게 허락하세요. 보안은 이제부터 제가 이야기할 온갖 화려한 토큰들이 더 문제를 일으키는 영역입니다.
데이터베이스를 반드시 쳐야 하는 요청의 비율을 최소화하려 한다고 가정해 봅시다. 주류 웹 애플리케이션 프레임워크는 이미 이를 돕는 기능을 갖고 있는 경우가 많습니다.
예를 들어 Rails에는 MessageVerifier와 MessageEncryptor가 있습니다. 속성 묶음을 넘기면, HMAC-SHA2와 encrypt-then-MAC AES-CBC를 이용해 변조 방지 문자열을 되돌려줍니다. 선택적으로 암호화도 가능합니다. 이 문자열을 쿠키에 넣습니다. 서버는 루트 비밀 하나만 기억하면 되고, 데이터베이스 대신 쿠키에서 사용자 데이터를 꺼낼 수 있습니다. Rails 세션이 이렇게 동작합니다.
Python 프레임워크에도 비슷한 기능이 있지만, 훌륭한 Python pyca/cryptography 라이브러리도 있습니다. 여기에는 토큰에 맞게 최적화된 같은 기능을 제공하는 Fernet가 포함되어 있습니다.
일반적인 사용자 세션을 API 토큰으로 그대로 쓰기는 대체로 어렵습니다. API 토큰의 핵심 특징은 로그아웃되지 않는다는 점이니까요. 하지만 같은 기능을 이용해 API 토큰을 만들 수는 있습니다. 여러 서비스 사이에서 루트 비밀을 공유하면 됩니다. 아마 그게 괜찮을 수도 있습니다. 그러면 마이크로서비스는 중앙 서비스에 의존하지 않아도 됩니다.
플랫폼 토큰은 비교적 단순하고, 상태 비저장일 수 있습니다. 문제는 무엇일까요? 사실상 토큰을 데이터베이스 캐시로 쓰는 셈이고, 캐시 일관성은 사람을 짜증 나게 합니다.
시작부터 가장 단순한 형태의 토큰 폐기를 잃게 됩니다. 애초의 전제 자체가 데이터베이스에 토큰을 대조하지 않는다는 것이므로, 이제 폐기 여부를 판단할 다른 방법을 придумать 해야 합니다. 갱신을 위한 표준 프로토콜도 없으니, 짧은 만료 시간을 가진 토큰도 잘 맞지 않습니다.
여기서 제가 여러 번 봤고, 꽤 마음에 드는 패턴 하나는 사용자를 “버전화”하는 것입니다. 사용자 테이블에 토큰 버전을 넣고, 토큰에는 현재 버전을 담게 합니다. 폐기하려면 데이터베이스의 버전을 올리면 됩니다. 그러면 발급되어 있던 토큰은 무효가 됩니다. 물론 그렇게 하려면 상태를 유지해야 하지만, 그 상태는 매우 싸게 유지할 수 있습니다. 사용자 버전의 Redis 캐시를 두고, 없으면 데이터베이스로 내려가면 충분합니다.
앞서 “플랫폼 토큰” 절에 첨부된 모든 증거물과 부록은 본 절에 그대로 포함되며, 그 일부를 이룹니다.
설계상 OAuth는 연합 프로토콜입니다. 정석적으로 말하면 OAuth는 제3자가 여러분 계정으로 트윗을 올리게 해 줍니다. 하지만 그것은 우리가 해결하려는 문제가 아닙니다.
그럼에도 OAuth 2.0은 인기 있고, 토큰과 관련해 여러분이 마주칠 거의 모든 지루한 문제를 이미 겪었으며, 정도는 제각각이지만 대응책도 내놓았습니다. 그래서 여러분 자신의 API IAM 상황에서 그 작업의 혜택을 가져다 쓰는 것이 가능합니다. 실제로 많은 사람들이 그렇게 합니다.
예를 들어 OAuth 2.0에는 짧은 만료 토큰을 위한 내장 해법이 있습니다. OAuth 2.0에는 “Refresh Token”이 있고, 이것으로 “Access Token”을 교환합니다. Access Token은 실제 API 작업에 사용하는 토큰이고, 빠르게 만료됩니다. OAuth 2.0 라이브러리들은 Refresh Token 사용법을 알고 있습니다. 그리고 이것들은 사용 빈도가 더 낮고 데이터베이스를 괴롭히지 않기 때문에 폐기도 쉽습니다.
OAuth 2.0 Access Token은 불투명한 문자열이므로, 플랫폼 토큰으로 하던 것과 같은 일을 그대로 할 수 있습니다. 아니면 그냥 그 안에 플랫폼 토큰을 넣어도 됩니다.
OAuth 2.0의 “암호학”이라고 할 만한 부분은, 있는 그대로 보자면 단순합니다. 독립형 single-page application에서는 좀 까다로워지지만, 그건 다른 모든 것도 마찬가지입니다. 예전에는 단순한 클라이언트-서버 앱에 OAuth를 무비판적으로 끼워 넣는 사람들을 비꼬곤 했습니다. 지금은 그렇지 않습니다.
잠깐 역사 이야기를 해봅시다. OAuth가 등장했고, 앱은 사용자를 대신해 트윗을 올릴 수 있게 되었고, 신은 자신이 만든 것을 보시고 좋다고 하셨습니다. 그러다 누군가가, 사용자 대신 트윗을 올릴 수 있다면 그 능력을 신원의 증거로 사용해 “Twitter로 로그인”할 수도 있겠다고 깨달았습니다. 트윗 자체는 불필요해졌고, 사람들은 대충 사용자 프로필을 읽을 수 있는 OAuth 토큰을 신원 증명으로 쓰기 시작했습니다.
이건 문제입니다. 사용자 프로필을 읽을 수 있는 능력은 좋은 신원 증명이 아니기 때문입니다. 여러분은 어떤 앱이 데이팅 앱에서 “Twitter로 로그인”할 수 있는지와는 전혀 무관한 이유로 그 앱에 그 권한을 줄 수 있습니다. 그래서 사람들은 온갖 취약점을 찾아냈습니다.
그리하여 OpenID Connect, 줄여서 OIDC가 등장합니다. OIDC는 OAuth 2.0과 JWT라는 암호학적 토큰 표준의 악마적 결혼입니다. OIDC의 목적은 분명합니다. JWT로 인코딩된 “Identity Token”을 제공해 누가 로그인하는지 알려줍니다.
여기서 우리가 OIDC 자체에 큰 관심이 있는 것은 아닙니다. 하지만 OIDC를 세상에 나오게 한 그 엘드리치한 의식은 수많은 JWT를 세상에 풀어놓았고, 이제 그것은 우리가 생각해야 할 문제가 되었습니다.
순수한 기능 관점에서 보면, JWT는 OAuth 2.0 안에 플랫폼 토큰을 집어넣은 것보다 크게 더 많은 일을 하지 않습니다. 하지만 JWT는 표준화되어 있고, “Fernet로 암호화한 JSON을 OAuth Access Token에 넣는 방식”은 그렇지 않습니다. 그래서 JWT 주변에는 엄청난 개발자 경험 생태계가 자라났습니다. 그래서 불행하게도 JWT는 사용성이 정말 좋습니다.
그게 왜 불행하냐고요? JWT는 나쁩니다.
이 글은 JWT가 왜 나쁜지에 대한 글은 아닙니다. 물론 여러분이 이 글을 읽고 제 의견에 동의하게 되기를 바라기는 합니다. 그래서 짧게만 말하겠습니다.
첫째, JWT는 위원회식 설계로 만들어진 암호학적 주방 싱크대입니다. JWT는 HMAC-SHA2 같은 MAC으로 보호할 수도 있습니다. 또는 RSA 디지털 서명을 쓸 수도 있습니다. 또는 static-ephemeral P-curve elliptic curve Diffie Hellman으로 암호화할 수도 있습니다. 이건 발에 겨누는 총 정도가 아니라, 아예 발을 향해 Rock Island Arsenal 전체를 배치해 놓은 수준입니다. 암호학 취약점을 좋아하는 사람이라면 거의 사랑할 수밖에 없습니다. TLS 밖에서 invalid curve point attack 같은 걸 어디서 또 보겠습니까?
다음으로, JWT의 JSON 의미론은 신중하게 설계되어 있지 않습니다. JWT는 키에 목적이나 심지어 도메인 파라미터조차 바인딩하지 않으며, JWT 라이브러리들은 RSA와 HMAC-SHA2가 그저 같은 문제를 푸는 서로 바꿔 쓸 수 있는 해결책이라고 가정하고 작성되어 있습니다. 그래서 사람들이 RSA로 서명된 JWT를 가져와 JWT 헤더를 RS256에서 HS256으로 바꾸고 — 이 이름들 이야기는 시작도 하지 맙시다 — 라이브러리는 아무 생각 없이 공개 서명 키를 비밀 MAC 키처럼 취급하는 버그가 생깁니다. 그리고 alg=none도 있죠.
JWT는 너무 인기가 많아서, 상태 비저장 인증 토큰이라는 개념 자체와 동의어가 되어버렸습니다. 하지만 상태 비저장 토큰은 JWT 없이도 충분히 간단히 만들 수 있고, 실제로 JWT 이전에도 널리 사용되고 있었습니다.
JWT를 비판하는 것은 어떤 면에서는 달을 향해 울부짖는 일처럼 느껴집니다. OIDC에서 이것이 선택 사항이 아니고, Google과 Apple은 single sign-on을 OIDC로 구현하기 때문입니다. 우리의 엉뚱한 팟캐스트 친구 Jonathan Rudenberg가 이에 대해 좋은 관찰을 했습니다. 여러분의 애플리케이션이 Apple 같은 곳과 직접 연결성을 유지하고 있다면, Apple 서버와의 TLS 연결을 신뢰하는 것만으로 OIDC JWT를 어느 정도 안전하게 사용할 수 있습니다. 토큰 자체의 암호학적 결함들에 그렇게까지 신경 쓸 필요조차 없다는 뜻입니다.
악마조차 감당 못하는 의식도 있습니다. OIDC의 경쟁자는 SAML이고, 이것은 XML DSIG에 기반합니다. XML 문서를 서명된 토큰으로 바꾸는 방식입니다. XML 문서를 서명된 토큰으로 바꾸면 안 됩니다. XML에 서명하면 안 됩니다. XML DSIG는 인터넷에서 널리 쓰이는 암호 형식 가운데 최악입니다. JWT의 모든 결함을 가져오세요. 검증 방법을 알아내기 위해서조차 신뢰할 수 없는 데이터를 광범위하게 파싱해야 한다는 점까지 포함해서요. 거기에 하나의 문서 안에 수십 개의 서로 다른 서명된 하위 트리가 존재할 수 있는 DOM 모델을 섞고, 여기에 서명 전에 문서를 변환하는 플러그형 canonicalization 계층까지 얹으세요. 문제를 너무 복잡하게 만들어서 사실상 모든 SAML 라이브러리가 감싸는 C 언어 구현이 하나뿐인 수준이 되게 하세요. 당연히 여러분은 이것으로 API 인증을 하지는 않겠지만, 눈치채셨겠지만 저는 여기서 좀 쌓인 걸 풀고 있습니다.
PASETO는 힙스터 JWT입니다. 좋은 의미로 하는 말입니다. 개발자 경험은 본질적으로 JWT와 거의 같지만, 토큰을 현대적 암호학에 고정하려고 합니다.
JWT가 암호학적 주방 싱크대라면, PASETO는 좀 더 작은 욕실 세면대입니다. 여기서 제가 비판적으로 말하는 이유는, PASETO가 몇 가지 타당한 이유로 토큰 괴짜들 사이에서 이미 좋은 평가를 받고 있고 제 도움이 필요하지 않기 때문입니다.
현재 네 가지 버전이 있고, 각각은 대칭형 “local”과 비대칭형 “public” 두 종류의 토큰을 정의합니다. Version 1은 “NIST-compliant” AES-CTR, HMAC-SHA2, RSA를 씁니다. Version 2에는 XChaPoly와 Ed25519가 있습니다. Version 3은 RSA를 P-384 ECDSA로 바꿉니다. Version 4는 XChaPoly를 XChaCha와 Blake2 KMAC으로 바꿉니다. JSON 대신 CBOR를 쓰려면 v4를 v4c로 바꿀 수 있습니다. 정말 많습니다.
제가 PASETO에 대해 갖는 문제의식은, 이것이 본질적으로 JWT와 같은 것이라는 점입니다. 몇 가지 알고리즘을 추가하고 몇 가지를 금지하면, 거의 JWT로부터 만들 수 있을 정도입니다.
PASETO는 이제 널리 받아들여진 관행인, 매개변수를 실시간으로 협상하는 대신 프로토콜 전체를 버전화하는 방식을 지지합니다. 이것은 강력한 장점이 되어야 합니다. 하지만 PASETO에는 8가지 버전이 있고, 그중 4가지는 “current”입니다. 제가 보기에는 PASETO가 놓친 프로토콜 버저닝의 아이디어 중 하나는 여러 버전을 동시에 계속 날리고 다니면 안 된다는 점입니다. Version 3과 4는 부분적으로 Thai Duong이 발견한 취약점 하나 때문에 나왔습니다. 아주 심각한 건 아니었지만요. 그런데 PASETO 라이브러리들은 여러 버전을 지원하고, 어떤 경우에는 동적으로 지원하기도 합니다. 옛 버전은 없애세요!
IRTF CFG는 IETF의 암호학 검토 위원회입니다. 제가 평생 이해하지 못할 이유로, PASETO 저자들은 이를 CFRG 검토에 제출했습니다. 이러지 마세요. 그 스레드에서 Neil Madden은 PASETO가 JWT의 RSA/HMAC 문제를 그대로 물려받았다는 점을 지적했습니다. 그래서 이제 모든 PASETO 버전에는 사람들이 키를 강한 타입으로 다루도록 주의시키는 “Algorithm Lucidity” 경고가 들어가 있습니다.
저는 이것이 꼭 PASETO의 잘못이라고 보지는 않습니다. 오히려 근본 아이디어 자체가 불가능한 삼위일체라고 생각합니다. 암호학적 유연성, 암호학적 오용 저항성, 그리고 자바스크립트풍 개발자 경험을 동시에 얻는 것은 어렵습니다.
그리고 한 가지 더. “NIST-compliant” PASETO 버전은 굳이 만들 필요가 없었던 실수였습니다.
저는 발급 시각이나 대상 같은 토큰 메타데이터와 무작위 사용자 속성 묶음을 한데 담아 인증하는 JSON 토큰이 못마땅합니다. 암호학 엔지니어들은 제가 이걸 두고 장광설을 늘어놓으면 고개를 갸웃하는데, 아마 그들은 최소한의 데이터만 담는 OIDC JWT를 떠올리고 있어서일 겁니다. 개발자들이 만들어내는 이상한 JWT들, 사용자 데이터와 메타데이터가 뒤섞이는 그런 토큰들을 생각하지 않는 거죠. 이것 역시 제게는 굳이 그럴 필요가 없는 실수처럼 보입니다. 메타데이터 상당수가 선택 사항이라는 점도 마찬가지입니다. 왜죠? 중요한데요!
그래도 JWT보다는 PASETO를 쓰는 편이 훨씬 낫습니다. PASETO에 대한 제 입장은 이렇습니다. 쓴다면 대칭형 토큰이 필요한지 비대칭형 토큰이 필요한지에 대해 정말 명확한 판단을 하세요. 둘은 서로 다른 용도의 서로 다른 물건입니다. 그리고 버전은 하나만 지원하세요.
PASETO가 하려는 일을, 단점 없이 거의 그대로 얻는 방법이 있습니다. 그냥 여러분만의 강한 타입 프로토콜 형식을 정의하면 됩니다. David Adrian은 이것을 “Protobuf Token”이라고 부릅니다.
할 일은 이와 같은 Protocol Buffer 스키마를 정의하는 것뿐입니다.
message SignedToken {
bytes signature = 0;
bytes token = 1;
}
message Token {
string userId = 0;
uint64 not_before = 1;
uint64 not_after = 2;
// and other stuff
}
토큰의 모든 의미론을 Token 메시지에 밀어 넣고, 첫 번째 Protobuf 인코딩을 통해 문자열로 직렬화합니다. 여기에 Ed25519로 서명합니다. 서명 블록에는 “Protobuf-Token-v1” 같은 버전 문자열을 이어 붙이세요. 그런 다음 토큰 바이트 문자열을 SignedToken의 token 필드에 넣고, 서명을 채웁니다. 한 번 더 직렬화하면 끝입니다.
이 두 번의 인코딩은 두 가지를 제공합니다. 첫째, 토큰을 디코드하고 검증하는 방법이 오직 하나뿐입니다. 둘째, 토큰 안의 모든 것이 서명되므로 어떤 메타데이터가 서명 대상인지 모호하지 않습니다. 토큰은 작고 다루기 쉽고, 임의의 선택적 클레임을 담도록 확장할 수도 있습니다. Protocol Buffers는 이런 데 강합니다.
토큰이 아예 필요 없을 수도 있습니다. 대신 키만 두고, 그것으로 요청을 인증하면 됩니다. AWS API가 바로 이런 방식으로 동작합니다.
우리는 보통 API에 일반적인 HTTP 요청을 보내고, 추가 헤더에 “bearer token”을 담아 전달합니다. bearer token은 bearer bond와 비슷해서, 한 번 손에 들어오면 끝장입니다. 인증된 요청에는 이 문제가 없습니다.
이렇게 하려면 HTTP 요청의 canonicalization 방식이 필요합니다. 동일한 HTTP 요청도 여러 표현을 가질 수 있으니, MAC 태그 계산에 사용할 딱 하나의 표현을 정해야 합니다. 쉬워 보이지만 이것은 AWS 방식 초기 구현에서 취약점의 원인이었습니다. 그냥 AWS Version 4를 쓰세요.
정규화된 요청에 대해 HMAC을 계산하고, AWS라면 그냥 여러분의 AWS_SECRET_ACCESS_KEY를 쓰면 됩니다, 결과 태그를 매개변수로 붙입니다.
신의 가호가 있기를 바라지만, 여기서 X509를 사용해 사람들이 요청에 서명할 수 있도록 인증서와 키를 발급하는 방법도 있습니다. Facebook이 내부적으로 그런 일을 했던 것으로 보입니다.
인증된 요청에는 좋은 점이 있습니다. bearer token이 없고, 따라서 곰도 없습니다. 가장 큰 문제는 물류적인 것입니다. 요청 인증 코드를 만드는 일이 번거롭기 때문에, 앱이 아주 커지지 않는 이상 결국 여러분의 공식 SDK만이 모든 요청 서명 작업을 해 주는 유일한 대화 수단이 되기 쉽습니다.
자, 여기 멋진 요령이 있습니다. Messages, Photos, Presence, Ivermectin Advocacy 같은 서비스 여러 개가 있습니다. 그리고 이들 서비스와 사용자 모두가 통신할 수 있는 중앙 Authentication 서비스가 있습니다.
Authentication은 루트 키를 보유합니다. Messages가 온라인이 되면, 예를 들어 신원을 증명하는 mTLS 연결을 Authentication에 맺습니다. 그러면 HMAC(k=root, v=“Messages”)인 서비스 키를 발급받습니다.
이제 사용자 “Alice”가 도착합니다. Authentication은 그녀에게 키를 발급합니다. 그 키는 HMAC(k=HMAC(k=root, v=“Messages”), v=“Alice”)입니다.

우리가 여기서 무엇을 했는지 보이시나요? Messages는 Alice의 키를 갖고 있지 않습니다. 하지만 그녀의 키는 단지 Messages 키 아래에서 사용자 이름에 대해 계산한 HMAC이므로, 서비스는 그것을 재구성해서 메시지를 검증할 수 있습니다.
CATS와 비슷한 구성을 사용해 요청에 서명할 수도 있고, Protobuf Token에 서명할 수도 있습니다. 이 경우 Ed25519 대신 HMAC이나 AEAD를 쓰면 됩니다. 공개 키 암호학이 제공하는 결합도 완화 이점의 일부를 얻는 셈입니다. Messages는 자기 자신을 등록하고 주기적으로 키를 교체하기 위해 Authentication과 가끔 접촉하기만 하면 됩니다. 그러면 Alice가 키를 얻을 수 있는 유일한 방법은 Authentication이 이를 승인했을 때뿐이라고 믿고, 모든 사용자로부터 오는 요청을 인증하기에 충분합니다.
전 세계 사용자 가까이에서 애플리케이션을 실행하는 방법을 설명하는 데 5,000단어는 필요 없습니다. 시드니에서 암스테르담까지, 작동하는 Dockerfile만 있으면 됩니다.

조용하고 비도 안 오는 곳으로 산책을 나가 Macaroon 이야기를 해봅시다.
여러분의 서비스에 대한 황금 티켓이 있다고 상상해 봅시다. 어떤 동작이든 허용하는 인증된 토큰입니다. 이것을 bearer token으로 돌려 쓰기에는 너무 위험합니다.
이제 그 황금 토큰에 caveat를 추가한다고 상상해 봅시다. 쓰기는 안 되고 읽기만 허용된다. 단일 문서에 대해서만 허용된다. 특정 IP에서 온 요청에 대해서만, 혹은 특정 사용자 ID로 독립적으로 인증된 세션에서만 허용된다. 이렇게 약화된 토큰은 훨씬 덜 위험합니다. 사실 충분히 단단히 잠가 놓으면 민감 정보가 아니게 만들 수도 있습니다.
우리는 CAT가 사용자 키를 파생할 때 쓰는 것과 같은 요령을 활용합니다. 황금 티켓으로 시작해서 그것을 루트 키 아래에서 HMAC합니다. 이제 읽기 전용으로 만들고 싶다면, 토큰에 또 하나의 메시지 계층을 추가하고, 직전 계층의 MAC 태그를 키로 사용해 새 계층에 MAC을 계산합니다. 새 토큰의 보유자는 황금 티켓의 원래 MAC 태그를 알아낼 수 없습니다. 토큰에는 새로 연결된 MAC 태그만 담기기 때문입니다. 하지만 서비스는 루트 키를 갖고 있으므로 모든 중간 값을 다시 유도할 수 있습니다.

Macaroon은 이 아이디어를 중심으로 만들어진 토큰 형식입니다. 크게 세 가지를 해냅니다.
Attenuation: 사용자는 발급 서비스와 대화하지 않고도 토큰을 제한할 수 있습니다. 모든 caveat는 true로 평가되어야 합니다. 새 caveat로 이전 caveat를 되돌릴 수는 없습니다. 서비스는 기본 caveat 유형만 알면 되고, 사용자가 원할 수 있는 온갖 엉뚱한 조합마다 특별 처리 코드를 둘 필요가 없습니다.
Confinement: 적절한 caveat 유형이 있다면, 예를 들어 특정 mTLS 클라이언트 인증서 아래의 세션에서만 유효하다든가, 하루 중 특정 시간에만 유효하다든가 하도록 설정할 수 있습니다. 그러면 유용하면서도 안전하게 전달할 수 있는 Macaroon을 만들 수 있습니다.
Delegation: Macaroon에는 “third-party caveat”가 있어서 로직을 다른 시스템에 위임할 수 있습니다. third-party caveat는 암호화되어 있어서, 사용자는 그것을 해결하기 위해 통신할 수 있는 제3자 서비스의 URL만 볼 수 있습니다. 제3자 시스템은 “discharge Macaroon”을 발급하고, 이것을 원래 Macaroon과 함께 제출해 caveat를 해소합니다.
이 아이디어들은 서로 시너지를 냅니다. 인증을 IAM 서비스에 위임하고, 그다음 추가적인 서비스별 접근 규칙을 first-party caveat로 덧붙일 수 있습니다. 폐기 서비스가 사용자의 토큰을 검증하면, 시스템 나머지 부분은 폐기가 어떻게 구현되는지 알 필요가 없습니다. 감사 로깅도 마찬가지이고, 악용 방지도 마찬가지입니다.
세상에는 가끔 너무 아름다운 것이 많아서, 심장이 주저앉을 것처럼 감당이 안 될 때가 있습니다.
하지만 Macaroon이 인기가 없는 데에는 이유가 있습니다.
첫째, Macaroon용 라이브러리 생태계가 있긴 하지만 그다지 좋지 않습니다. 어떤 라이브러리도 개발자가 원하는 모든 caveat, 혹은 대부분의 caveat를 지원할 수 없습니다. 그래서 “표준” Macaroon은 caveat 형식으로 타입 없는 문자열 DSL을 사용하고, 이를 해석하는 책임을 사용하는 서비스에 떠넘깁니다.
그리고 투박하기도 합니다. 앞서의 대부분 형식은 OAuth 2.0 안에 끼워 넣는 모습을 상상할 수 있습니다. 하지만 third-party caveat는 그걸 깨뜨립니다. 여러분의 Macaroon API는 까다로울 겁니다. 사용자는 실제 요청 하나를 만들기 위해 여러 질의 결과를 생성하고 저장해야 할 수도 있습니다.
Macaroon은 대칭 암호학에 의존합니다. 이것은 장점이자 단점입니다. 시스템을 급격히 단순화해 주지만, 서비스들 사이의 관계를 공유 키로 표현해야 한다는 뜻이기도 합니다. 물론 HS256 JWT도 마찬가지지만, Macaroon 논문에서 꽤 크게 벗어나지 않는 한, 공개 키가 주는 이점을 얻을 수는 없습니다. CAT-caroons 같은 것을 придумать하지 않는다면요.
실제로는 caveat를 올바르게 추론하기가 까다로울 수 있습니다. caveat 집합을 순회하다가 하나라도 false가 나오면 즉시 실패하는 루프를 짜는 것은 쉽습니다. 하지만 실수로 권한을 줄이는 대신 늘려버리는 caveat 의미론을 도입할 수 있습니다. 데이터베이스에 사용자 ID를 물어 “이걸 해도 되나?”를 답하려는 코드가 있고, 비슷한 일을 하는 caveat 구성을 작성할 수도 있는데, 일관된 Macaroon 설계에서는 그런 것은 결코 원치 않는 방향입니다.
이 문제들에 대해 저는 더 할 말이 많습니다! 하지만 지금은, 제가 수년간 Macaroon을 옹호해 왔고, 그러다 직접 구현해 보았고, 이제는 아마 더는 그렇게 북을 두드리지는 않을 것 같다는 정도만 말해도 충분합니다. 그래도 잘 맞는 곳에서는 정말 잘 맞을 것이라고 생각합니다. 제 결론은 이렇습니다. attenuation, confinement, delegation 세 가지가 모두 여러분의 설계와 공명한다면, Macaroon은 아마 잘 작동할 겁니다. 셋 중 하나라도 빠진다면, 다른 것을 고려해 보세요.
마지막으로, Geoffroy Couprie의 Biscuit가 있습니다. Biscuit은 이런 식의 지나치게 긴 블로그 글을 써 보겠다고 앉아서 모든 조사를 다 한 다음, 차라리 다른 모든 토큰의 단점을 해결하는 암호학적 토큰을 직접 쓰기로 결정했을 때 나올 법한 결과물입니다.
Biscuit은 Macaroon의 영향을 강하게 받았습니다. Couprie는 JWT의 영향도 받았다고 주장하지만, 저는 잘 모르겠습니다. Macaroon처럼 사용자는 Biscuit을 약화시킬 수 있습니다. 하지만 Macaroon과 달리 다음과 같습니다.
Biscuit은 HMAC 대신 공개 키 서명에 의존하므로, third-party caveat의 필요성을 어느 정도 줄여 줍니다.
단순한 불리언 caveat 대신, Biscuit은 토큰이 어떤 연산을 허용하는지 평가하기 위해 Datalog 프로그램을 내장합니다.
Biscuit은 믿을 수 없을 만큼 야심찹니다.
우선, Macaroon의 단순한 암호학을 공개 키 서명으로 바꾸는 일은 쉽지 않습니다. Macaroon에 caveat를 추가하는 암호학적 과정은 사소할 정도로 간단합니다. 이전 caveat의 MAC 태그를 다음 caveat의 HMAC 키로 넘겨주기만 하면 됩니다. 하지만 서명에는 그에 상응하는 간단한 연산이 없습니다.
Biscuit에 제안된 암호학은 처음에는 pairing curve의 달나라 수학으로 시작했습니다. Keller Fuchs가 curve VRF로 그것을 저궤도로 끌어내렸습니다. 그다음에는 aggregated Gamma-Signatures를 들고 블록체인 나라로 우회했습니다. 하지만 결국 Biscuit의 핵심 암호학은 지구로 돌아와 꽤 단순한 Ed25519 서명 체이닝으로 정착했습니다.
Biscuit 토큰의 caveat 구조는 유연합니다. 아마 지나치게 유연할 정도입니다. 하지만 형식적으로는 엄밀합니다. 흥미로운 조합이죠. 이것은 Protocol Buffers로 컴파일하고 직렬화한 일련의 서명된 프로그램을 평가하는 방식으로 동작합니다. 서비스는 요청으로부터 “당신은 cats2.webp를 요청하고 있다” 또는 “당신이 요청하는 작업은 WRITE다” 같은 사실 패턴을 도출합니다. 토큰 자체에는 새로운 사실 패턴을 도출하는 규칙과, 그 패턴을 술어에 대조하는 checker가 포함됩니다.
솔직히 말해, 처음 Biscuit을 읽었을 때 저는 꽤 미쳤다고 생각했습니다. 제안이 “pairing curves”에서 저를 놓치지 않았다면, Datalog를 설명하기 시작했을 때는 놓쳤을 겁니다. 하지만 그 후 제가 직접 Macaroon을 구현해 보았고, 이제는 어느 정도 이해가 갑니다. 다른 어떤 토큰도 주지 못하는 Biscuit의 장점 하나는, 토큰이 정확히 어떤 작업을 허용하는지에 대한 명확성입니다. 텍스트로 렌더링하면 Biscuit caveat는 정책 문서처럼 읽힙니다.
사실 그게 제가 가진 유일한 큰 우려입니다. Biscuit의 진짜 장점을 살리려면, 사실상 여러분의 모든 인가 로직을 토큰 안으로 옮겨야 하는 건 아닐까 싶습니다. 이전까지 “가장 표현력이 풍부한 토큰”이라는 타이틀을 갖고 있던 Macaroon조차도, 애초에 어떤 caveat를 표현할 수 있는지에 대해 호스트 서비스가 강력한 선택을 하고 있었습니다. Biscuit은 인가 정책에 대한 서비스의 기여를 거의 구성 원자 수준까지 줄여버리고, 보안 정책 전부를 Prolog로부터 도출합니다. 이것이 강력할 수 있다는 점은 이해하지만, 사용하려면 거의 전면적으로 그 철학을 받아들여야 할 것 같다는 점도 보입니다.
여기 점수표가 있습니다.

믿기 어렵겠지만, 비밀번호와 SAML을 제외하면 저는 이 모든 방식에서 마음에 드는 점이 하나씩은 있다고 생각합니다.
저는 여전히, 지루하지만 믿을 수 있는 난수 토큰이 과소평가되어 있다고 믿습니다. 그리고 사람들은 실제로는 달성할 수도 없고 필요도 없을 상태 비저장성을 좇느라 지나치게 복잡해진다고 생각합니다. Facebook 같은 규모가 아닌 대부분의 시스템에서 토큰 데이터베이스는 그렇게 확장하기 어려운 물건이 아니니까요.
몇 달 전의 저였다면, Macaroon도 다른 의미에서 과소평가되었다고 말했을 겁니다. Big Star의 “#1 Record”가 그런 식으로 과소평가된 것처럼요. 지금은 그냥 첫 Sex Pistols 공연 같은 종류의 과소평가라고 생각합니다. 그것을 읽은 사람마다 자기만의 토큰 형식을 만들었습니다. 우리는 Macaroon으로 계속 나아가고 있고, 그 점에 저는 아주 신이 납니다. 하지만 전형적인 CRUD 애플리케이션에 그것을 추천하라고 하면 망설여질 겁니다.
하지만, JWT는 쓰지 마세요.