JWT의 typ 값은 왜 대소문자를 가리면 안 될까
Spring Security 버전을 올렸더니 잘 통과하던 JWT가 거부된다는 issue가 올라왔습니다. token은 그대로였고 header의 typ 값을 검사하는 코드만 달라졌습니다. at+jwt를 기대하는 곳에 AT+JWT가 오면 실패했습니다.
이 글에서는 typ이 무엇인지, 왜 대소문자를 가리면 안 되는지, 그리고 이 차이가 코드 어디에서 생겼는지 적습니다.
typ은 token의 종류를 적는 값입니다
JWT는 점으로 나뉜 세 부분으로 되어 있고 첫 부분이 header입니다. header에는 서명 방식과 함께 typ이라는 값이 들어갈 수 있습니다.
{
"alg": "RS256",
"typ": "at+jwt"
}typ은 이 token이 어떤 종류인지 알려 줍니다. 예를 들어 at+jwt는 OAuth 2.0의 access token이라는 뜻입니다. 받는 쪽은 이 값을 보고 access token 자리에 다른 종류의 token이 들어오는 것을 막을 수 있습니다.
typ에 적는 값은 media type 이름입니다. text/html, application/json 같은 것들과 같은 종류입니다. media type 이름은 대소문자를 구분하지 않습니다. 그래서 at+jwt와 AT+JWT는 같은 값으로 봐야 합니다.
예전 코드는 구분하지 않았고, 새 코드는 구분했습니다
Spring Security는 원래 Nimbus library의 JOSEObjectTypeVerifier로 typ을 검사했습니다. 6.5에서 이를 대신하는 자체 구현 JwtTypeValidator가 들어왔습니다.
두 구현은 하는 일이 거의 같습니다. 그런데 값을 담는 type이 달랐습니다.
| 값을 담는 type | 비교 방식 | |
|---|---|---|
| Nimbus | JOSEObjectType | equals를 재정의해서 대소문자를 무시 |
| Spring Security | String | equals 그대로, 대소문자를 구분 |
새 코드는 허용하는 값들을 String collection에 담고 contains로 확인했습니다.
// contains는 String의 equals로 비교한다
if (this.validTypes.contains(typ)) {
return OAuth2TokenValidatorResult.success();
}contains는 안에서 equals를 씁니다. String의 equals는 대소문자를 구분합니다. 코드만 보면 이상한 곳이 없습니다.
대소문자를 무시한다는 규칙은 검사 코드가 아니라 값 type의 equals 안에 들어 있었습니다.
library를 걷어 내고 같은 일을 하는 코드를 새로 쓰면서 type이 맡던 그 규칙도 함께 빠졌습니다.
고친 내용
JwtTypeValidator.java의 validate()에서 허용하는 값을 하나씩 꺼내 equalsIgnoreCase로 비교하도록 바꿨습니다.
-if (this.validTypes.contains(typ)) {
- return OAuth2TokenValidatorResult.success();
+// 허용하는 값을 하나씩 꺼내서
+for (String validType : this.validTypes) {
+ // 대소문자를 무시하고 비교한다
+ if (validType.equalsIgnoreCase(typ)) {
+ return OAuth2TokenValidatorResult.success();
+ }
}그리고 validator에는 at+jwt를, token header에는 AT+JWT를 넣었을 때 통과하는지 확인하는 테스트를 더했습니다.
이 수정은 Spring Security 6.5.7부터 적용됩니다.
마치며
고친 코드는 네 줄입니다. 그런데 비교 규칙이 JOSEObjectType의 equals 안에 있었기 때문에 이 네 줄이 필요했습니다. 그 규칙은 눈에 보이는 검사 코드만 봐서는 알 수 없었습니다.
library를 자체 구현으로 바꿀 때는 method가 하는 일만 옮겨서는 부족합니다. 그 library가 쓰던 type의 equals, hashCode, compareTo에 어떤 규칙이 들어 있었는지도 함께 봐야 한다는 것을 배웠습니다.