Blog

JWT의 typ 값은 왜 대소문자를 가리면 안 될까

Spring Security 버전을 올렸더니 잘 통과하던 JWT가 거부된다는 issue가 올라왔습니다. token은 그대로였고 header의 typ 값을 검사하는 코드만 달라졌습니다. at+jwt를 기대하는 곳에 AT+JWT가 오면 실패했습니다.

이 글에서는 typ이 무엇인지, 왜 대소문자를 가리면 안 되는지, 그리고 이 차이가 코드 어디에서 생겼는지 적습니다.

typ은 token의 종류를 적는 값입니다

JWT는 점으로 나뉜 세 부분으로 되어 있고 첫 부분이 header입니다. header에는 서명 방식과 함께 typ이라는 값이 들어갈 수 있습니다.

JSON
{
  "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비교 방식
NimbusJOSEObjectTypeequals를 재정의해서 대소문자를 무시
Spring SecurityStringequals 그대로, 대소문자를 구분

새 코드는 허용하는 값들을 String collection에 담고 contains로 확인했습니다.

JwtTypeValidator.java고치기 전
// contains는 String의 equals로 비교한다
if (this.validTypes.contains(typ)) {
    return OAuth2TokenValidatorResult.success();
}

contains는 안에서 equals를 씁니다. String의 equals는 대소문자를 구분합니다. 코드만 보면 이상한 곳이 없습니다.

대소문자를 무시한다는 규칙은 검사 코드가 아니라 값 type의 equals 안에 들어 있었습니다.

library를 걷어 내고 같은 일을 하는 코드를 새로 쓰면서 type이 맡던 그 규칙도 함께 빠졌습니다.

고친 내용

JwtTypeValidator.java의 validate()에서 허용하는 값을 하나씩 꺼내 equalsIgnoreCase로 비교하도록 바꿨습니다.

JwtTypeValidator.java70~84행
-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에 어떤 규칙이 들어 있었는지도 함께 봐야 한다는 것을 배웠습니다.