audience mismatch가 발생하면 저는 먼저 token을 다시 발급받기보다 이 token이 누구를 위해 발급됐는지부터 확인합니다. JWT의 aud claim은 resource server가 자기 자신을 이 token의 intended audience로 인식할 수 있는지 판단하는 핵심 값입니다.
JWT Decoder에서 payload를 열어 aud를 확인하고, API 설정의 expected audience와 정확히 비교합니다.
aud claim이 의미하는 것
RFC 7519에서 aud는 JWT를 받아들이도록 의도된 대상(principal)을 나타냅니다. 값은 하나의 문자열일 수도 있고 배열일 수도 있습니다.
{ "aud": "https://api.example.com" }
또는:
{
"aud": ["https://api.example.com", "secondary-service"]
}
저는 verifier가 예상하는 값이 배열 안에 실제로 존재하는지 확인합니다. 단순 문자열 비교만 구현하면 multi-audience token을 잘못 거부할 수 있습니다.
제가 가장 먼저 확인하는 네 가지
- 현재 요청에 access token을 보내고 있는가?
- token의
aud에 현재 API가 포함되어 있는가? - API의 expected audience 설정이 현재 환경과 맞는가?
- issuer가 이 API용 token을 실제로 발급하도록 설정되어 있는가?
이 네 가지 중 하나가 어긋나는 경우가 대부분입니다.
ID token과 access token을 혼동하지 않습니다
브라우저 login 이후 두 token이 모두 JWT처럼 보일 수 있습니다. ID token의 aud가 OAuth/OIDC client ID인 경우가 많기 때문에 이를 API에 보내면 resource server의 expected audience와 맞지 않을 수 있습니다.
예:
{
"aud": "my-web-client-id"
}
API가 기대하는 값이:
https://api.example.com
이면 signature가 정상이어도 audience validation은 실패해야 합니다.
저는 JWT라는 형식만 보고 token 용도를 결정하지 않습니다. provider 문서와 발급 flow를 확인합니다.
환경 mismatch를 찾습니다
local/staging/production에서 audience가 다르게 설정된 경우가 흔합니다.
local → api://local
staging → api://staging
production → api://production
staging token을 production API에 보내면 token이 cryptographically valid해도 audience mismatch가 정상적인 결과입니다.
이때 저는 frontend 환경 변수와 backend verifier 설정을 둘 다 비교합니다. 한쪽만 고치면 같은 문제가 다시 생깁니다.
문자열 normalize를 함부로 하지 않습니다
다음 두 값은 사람이 보기에는 비슷하지만 verifier 관점에서는 다를 수 있습니다.
https://api.example.com
https://api.example.com/
issuer와 audience를 임의로 lower-case 처리하거나 slash를 제거해 맞추기보다, identity provider가 발급하는 canonical value와 API 설정을 정확히 맞춥니다.
aud 배열은 어떻게 검증하는가
제가 기대하는 방식은 대략 다음과 같습니다.
function audienceMatches(aud, expected) {
if (typeof aud === "string") return aud === expected;
if (Array.isArray(aud)) return aud.includes(expected);
return false;
}
실제 production에서는 직접 구현하기보다 검증 라이브러리의 audience 옵션을 사용합니다. 중요한 것은 “token에 어떤 aud든 하나 있으면 통과”가 아니라 내 expected audience가 포함되어야 한다는 점입니다.
azp도 확인할 때가 있습니다
OIDC ID Token처럼 여러 audience가 존재하는 특정 context에서는 azp(authorized party)가 의미를 가질 수 있습니다. 저는 azp를 보편적인 aud 대체물처럼 사용하지 않고, 해당 프로토콜과 provider의 validation 규칙에 따라 확인합니다.
즉 aud mismatch가 났다고 무조건 azp를 대신 비교하지 않습니다.
Signature와 audience는 서로 다른 검사입니다
제가 가장 중요하게 분리하는 부분입니다.
signature verified
≠
audience accepted
신뢰하는 issuer가 실제로 서명한 token이어도 다른 API를 위해 발급됐을 수 있습니다. 그래서 signature 검증을 통과한 뒤에도 audience validation이 필요합니다.
JWKS Validator에서 expected audience를 지정하면 signature와 claim policy를 함께 확인할 수 있습니다.
audience mismatch가 401로 보일 때
많은 API가 audience mismatch를 401 Unauthorized로 표현합니다. 이때 JWT 401 Unauthorized 가이드의 전체 순서를 적용합니다.
제가 확인하는 순서:
Authorization header
→ exp/nbf
→ signature
→ issuer
→ audience
→ scope/role
이렇게 해야 audience 하나에만 집착해서 실제 signature 또는 expiration 문제를 놓치지 않습니다.
제가 하지 않는 해결책
Audience validation을 끈다
가장 빠르게 “해결”되는 것처럼 보이지만 다른 resource용 token이 내 API에서 받아들여질 수 있습니다.
aud가 없으면 그냥 통과한다
API가 audience를 요구하는 설계라면 missing audience도 실패로 처리합니다.
모든 환경 audience를 한 verifier에 무조건 허용한다
production endpoint에서 staging/local audience까지 허용하면 환경 경계가 약해집니다.
token payload의 임의 claim을 audience 대신 사용한다
provider와 API 계약에서 정의하지 않은 claim으로 trust policy를 우회하지 않습니다.
재현 테스트를 만들 때
저는 최소 네 개 fixture를 준비합니다.
1. correct aud → pass
2. wrong aud → fail
3. aud array containing expected → pass
4. missing aud → policy에 따라 fail
그리고 signature와 issuer는 동일하게 유지합니다. 그래야 audience 정책만 독립적으로 테스트할 수 있습니다.
제 최종 체크리스트
- access token인지 확인합니다.
aud의 실제 타입이 string인지 array인지 봅니다.- API expected audience를 확인합니다.
- 두 값을 exact match 규칙으로 비교합니다.
- staging/production 설정이 뒤섞이지 않았는지 봅니다.
- signature와 issuer validation도 별도로 통과했는지 확인합니다.
- OIDC context에서 필요한 경우에만
azp규칙을 적용합니다. - audience validation을 끄는 것으로 문제를 숨기지 않습니다.
제가 audience mismatch를 볼 때 핵심 질문은 하나입니다. “이 token은 정말 지금 요청을 받는 이 API를 위해 발급됐는가?” 그 답이 명확해야 signature가 정상인 token도 안전하게 받아들일 수 있습니다.