Microsoft.AspNetCore.Authentication.JwtBearer 사용 중 토큰이 잘못됐을 때 올바른 401 응답을 반환하는 방법

Microsoft.AspNetCore.Authentication.JwtBearer 사용 중 토큰이 잘못됐을 때 올바른 401 응답을 반환하는 방법

JWT 토큰 인증

JSON Web Token은 이름 그대로 웹에서 사용되는 JSON 토큰이다. 에서 JSON 형식의 토큰을 사용해 인증을 처리하는 데 주로 사용된다.
ASP.NET에서 JWT를 사용하는 데 주로 쓰이는 패키지는 Microsoft.AspNetCore.Authentication.JwtBearer 패키지인데, 일반적인 상황에서는 그냥저냥 기본 설정만 건드린 채 사용해도 문제가 없으나 내 경우에는 CORS 호출이 기본인 상황에서 사용하다가 예상하지 못한 문제를 맞이했다.1

프론트엔드에서 fetch 사용 시 401 Unauthorized 문제

Next.JS로 구축된 프론트엔드에서는 문제가 없다. 크롬 확장 프로그램팝업 페이지나 커스텀 페이지에서도 문제가 없다. 문제는 콘텐츠 스크립트 에서 발생했다.

콘텐츠 스크립트는 React를 사용하여 구현되어 있는 상태이고, 직접 만든 Provider 및 hook을 사용해 JWT 토큰을 일정 시간마다 받아오고, 리프레시할 때 401 또는 403 에러가 발생하면 그에 맞는 처리를 하도록 코드를 짰다.

문제는 정상적인 상황에서, 즉 팝업 또는 사용자 정의 페이지에서 인증을 완료하고 이를 API 서버에서 검증하려 할 땐 아무 문제 없이 정상 작동하던 로직이 토큰의 기한이 만료되어 401 상태 코드를 받아와야 하는 상황에서는 작동하지 않고 예외를 던진다는 것이다.

분명 ASP.NET 서버에서 다음과 같이 CORS를 허용하였는데

app.UseCors(builder =>  
{  
builder.AllowAnyOrigin()  
.AllowAnyHeader()  
.AllowAnyMethod();  
});

막상 에러를 까보면 cors 에러도 함께 발생하고 있었다. 즉, 챌린지가 성공한 상태에서는 CORS 정책이 적용된 상태로 문제없이 반환 되었으나, 챌린지가 실패하면 정의된 CORS가 무시되는 이상한 상황이었던 것.

심지어 콘텐츠 스크립트 측에서 try...catch 문으로 예외 처리를 하려 해도 브라우저 콘솔 로그 상에서는 분명 401 에러가 표시되지만 catch 문에서는 fetch failed만 출력되는 머리아픈 상황이 발생하고 있었다.

해결 과정

나만 이런 문제를 겪고 있진 않을 것이라 판단하여 30분 가량 구글링을 해보았으나, 관련 사례를 찾지 못하였다. 이 말은 즉 내 사례가 흔한 사례가 아니라는 것이고, 이를 다른 관점에서 바라보면 지금 내가 짠 로직이 일반적이지 않다고도 볼 수 있겠다고 생각했다.

그럼 문제가 뭘까 하고 고민해보니, 일단은 401 상태 코드가 반환될 때 본문이 없는 게 가장 큰 문제가 아닌가 생각되었다. 따라서 이를 어떻게 처리할 수 있나 찾아보니 JwtBearerOptions.EventsJwtBearerEvents 인스턴스를 할당해주면 된다고 하더라.

appBuilder.Services.AddJwtBearer(options =>
              {
                  options.Events = new()
                  {
                      OnAuthenticationFailed = async context =>
                      {
                          context.Response.StatusCode = 401;

                          await context.Response.WriteAsync("Invalid token").ConfigureAwait(false);
                      },
                  };
              })

하지만 문제는 전혀 해결되지 않았고, 이전과 똑같이 CORS 에러가 먼저 출력된 뒤 401 에러 로그 출력, 그리고 마지막으로 try...catch에서 걸리는 failed to fetch 에러 로그가 출력되었다.

다시 생각해보니 CORS 헤더가 문제인 것 같았다. 그래서 코드를 다음과 같이 변경해봤다.

options.Events = new()
{
    OnAuthenticationFailed = async context =>
    {
        context.Response.Headers.Add("Access-Control-Allow-Origin", "*");

        context.Response.Headers.Add("Access-Control-Allow-Headers",
                                     "Origin, X-Requested-With, Content-Type, Accept, Authorization");

        context.Response.StatusCode = 401;
    },
};

app.UseCors를 사용했지만 Microsoft.AspNetCore.Authentication.JwtBearer에서 인증 실패시 반환하는 헤더는 별도로 동작하는 것처럼 보였기 때문이다.

그리고 이 예상은 맞아들었다. 위처럼 OnAuthenticationFailed 핸들러에서 응답 헤더에 CORS 설정을 해주니 정상 작동한다.


  1. 다른 상황을 테스트하지 않았으므로 꼭 CORS 환경 하에서만 이 포스트의 문제가 발생하리라는 법은 없다. ↩︎

댓글

이 블로그의 인기 게시물

C# 남아도는 메모리에도 불구하고 OutOfMemoryException이 발생한다면?

MySQL 데이터 타입과 Java 데이터 타입 비교/매칭

테일즈위버 OST 전곡 모음