401과 403의 차이: 인증이 없는 요청과 거부된 요청
HTTP 401 Unauthorized와 403 Forbidden의 차이를 RFC 9110과 OAuth 2.0 Bearer 토큰 명세로 정리했습니다. WWW-Authenticate 헤더, 재요청 규칙, 404로 존재를 숨기는 경우도 다룹니다.
API를 만들다 보면 로그인하지 않은 요청과, 로그인은 했지만 그 작업이 허용되지 않은 요청에 각각 어떤 상태 코드를 돌려줄지 정해야 합니다. HTTP 명세를 따르면 보통 앞쪽은 401, 뒤쪽은 403입니다. 그런데 401의 이름이 Unauthorized라서 둘이 자주 뒤바뀝니다.
이 글은 HTTP의 의미를 정의한 RFC 9110과 OAuth 2.0 Bearer 토큰 사용법을 정한 RFC 6750을 기준으로 두 코드를 구분합니다.
401: 유효한 인증 정보가 없다
RFC 9110은 401을 “대상 리소스에 대한 유효한 인증 자격 증명이 없어서 요청이 적용되지 않았다”는 뜻으로 정의합니다. 이름은 Unauthorized지만 정의가 다루는 것은 권한보다 인증입니다. 요청한 쪽이 누구인지 유효하게 증명했는지가 기준입니다.
401에는 반드시 지켜야 할 조건이 하나 있습니다. 401 응답을 만드는 서버는 대상 리소스에 적용할 수 있는 챌린지(challenge)를 하나 이상 담은 WWW-Authenticate 헤더를 보내야 합니다(MUST). 챌린지는 클라이언트가 어떤 인증 방식으로 다시 시도할 수 있는지 알려 주는 값입니다. RFC 6750에는 다음 예가 나옵니다.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"
이 헤더 없이 401만 보내면 명세 위반입니다.
요청에 이미 자격 증명이 들어 있었다면 401은 그 자격 증명이 거부됐다는 뜻입니다. 클라이언트는 새 Authorization 헤더를 붙여 다시 요청할 수 있습니다(MAY). 사용자 에이전트가 이미 한 번 이상 인증을 시도했는데 같은 챌린지를 담은 401이 또 오면, 응답 본문을 사용자에게 보여 주는 것이 좋다고(SHOULD) 명세는 적고 있습니다. 본문에 진단 정보가 들어 있을 수 있기 때문입니다.
403: 이해했지만 이행하지 않는다
403은 서버가 요청을 이해했지만 이행을 거부한다는 뜻입니다. 거부 이유를 알리고 싶다면 서버는 응답 본문에 적을 수 있습니다.
403에서는 WWW-Authenticate를 보낼 의무가 없습니다. 다만 RFC 9110 11.6.1절은 다른 자격 증명을 쓰면 응답이 달라질 수 있음을 알리기 위해 401이 아닌 응답에도 이 헤더를 보낼 수 있다고(MAY) 허용합니다.
요청에 자격 증명이 들어 있었다면, 서버는 그것만으로는 접근을 허용하기에 부족하다고 판단한 것입니다. 클라이언트는 같은 자격 증명으로 자동 재요청하지 않는 것이 좋고(SHOULD NOT), 새롭거나 다른 자격 증명으로는 다시 시도할 수 있습니다(MAY).
403이 언제나 “로그인한 사용자의 권한 부족”만 뜻하지는 않습니다. RFC 9110은 자격 증명과 무관한 이유로도 요청이 거부될 수 있다고 적고 있습니다.
| 401 Unauthorized | 403 Forbidden | |
|---|---|---|
| RFC 9110의 정의 | 유효한 인증 자격 증명이 없다 | 요청을 이해했지만 이행을 거부한다 |
WWW-Authenticate |
반드시 보낸다(MUST) | 의무 없음 |
| 다시 요청할 때 | 새 Authorization으로 재요청 가능(MAY) |
같은 자격 증명으로 자동 재요청하지 않는다(SHOULD NOT). 다른 자격 증명은 가능(MAY) |
실무에서는 “인증을 다시 하면 결과가 달라질 수 있는가”를 두 코드를 가르는 기준으로 자주 씁니다. 다만 이것은 RFC가 정한 판정 절차가 아닙니다. RFC 9110은 각 코드의 뜻을 정의하고, 자격 증명이 없거나 유효하지 않거나 불완전한 요청에는 오리진 서버가 401을 보내는 것이 좋다고(SHOULD) 적습니다.
OAuth Bearer 토큰에서는
RFC 6750은 Bearer 토큰으로 보호된 리소스에서 오류 코드와 상태 코드를 다음처럼 짝짓습니다.
invalid_request: 필요한 매개변수가 없거나 요청 형식이 잘못됨 → 400(SHOULD)invalid_token: 토큰이 만료·폐기됐거나 형식이 잘못됐거나 다른 이유로 유효하지 않음 → 401(SHOULD)insufficient_scope: 토큰이 가진 것보다 높은 권한이 필요함 → 403(SHOULD)
invalid_token이면 클라이언트는 새 액세스 토큰을 받아 다시 요청할 수 있습니다. insufficient_scope 응답에는 필요한 scope를 함께 알려 줄 수 있습니다(MAY). 정리하면 만료된 토큰은 401, 유효하지만 범위가 부족한 토큰은 403입니다.
요청에 인증 정보가 전혀 없을 때는 error 같은 오류 정보를 넣지 않는 것이 좋다고(SHOULD NOT) RFC 6750은 규정합니다. 클라이언트가 인증이 필요한 줄 몰랐거나 지원하지 않는 방식으로 시도한 경우가 여기에 들어갑니다. 앞에서 본 realm만 있는 챌린지가 그 예입니다.
RFC 6750은 2012년 문서이고, RFC Editor는 RFC 8996과 RFC 9700이 이 문서를 갱신했다고 표시합니다. RFC 9700은 OAuth 2.0 보안 모범 사례를 다루지만, 여기서 인용한 401·403·insufficient_scope 대응은 바꾸지 않습니다.
존재 자체를 숨기고 싶을 때: 404
RFC 9110은 403 절에서, 금지된 리소스가 현재 존재한다는 사실을 숨기고 싶은 오리진 서버는 403 대신 404를 보내도 된다고(MAY) 적습니다.
404의 정의도 두 경우를 모두 포함합니다. 현재 표현을 찾지 못한 경우와, 서버가 표현이 있다는 사실을 밝힐 생각이 없는 경우입니다. 403은 그 주소에 무언가가 있다는 사실을 드러낼 수 있으므로 404라는 선택지가 있는 것입니다.
404는 메서드 정의나 명시적인 캐시 제어가 달리 정하지 않으면 휴리스틱으로 캐시될 수 있습니다. 권한이 바뀌면 바로 보여야 하는 리소스에 404를 쓴다면 캐시 동작을 휴리스틱에 맡기지 말고 원하는 동작에 맞는 정책을 명시적으로 골라야 합니다. 응답 저장을 허용할지, 공유 캐시가 응답을 재사용해도 되는지에 따라 필요한 정책이 달라지므로 아무 Cache-Control 값이나 붙인다고 해결되지는 않습니다. 지시어별 차이는 Cache-Control의 no-cache와 no-store 차이에서 다뤘습니다.
브라우저가 로그인 창을 띄울 수 있는 경우
WHATWG Fetch 표준의 HTTP-network-or-cache fetch 절차에는 401을 받았을 때 사용자에게 사용자 이름과 비밀번호를 묻는 단계가 있습니다. 이 단계의 바깥 조건은 네 가지입니다.
- 응답 상태가 401이다.
- 응답 테인팅(response tainting)이
"cors"가 아니다. - Fetch 내부 값
includeCredentials가 true다. - 요청에 사용자에게 물을 때 쓸 수 있는 traversable(창)이 있다.
includeCredentials는 쿠키나 Authorization을 실제로 보냈는지를 나타내는 값이 아니라 Fetch가 내부에서 계산하는 불리언입니다. 요청의 credentials mode가 "include"이거나, "same-origin"이면서 응답 테인팅이 "basic"이면 true가 됩니다. Cross-Origin-Embedder-Policy 검사로 다시 false가 될 수도 있고, 실제로 보낸 자격 증명이 없어도 true일 수 있습니다.
바깥 조건을 만족해도 곧바로 묻지는 않습니다. 안쪽 조건이 하나 더 있어서, 요청의 use-URL-credentials 플래그가 설정되지 않았거나 요청이 이미 인증을 위한 재요청(authentication fetch)일 때만 사용자에게 묻습니다. use-URL-credentials는 URL에 들어 있는 사용자 이름과 비밀번호를 우선하게 하는 플래그이고, 기본값은 설정되지 않은 상태입니다.
이 절차에서 바로 끌어낼 수 있는 결론은 제한적입니다. fetch()로 다른 출처의 API를 CORS 요청으로 호출해 응답 테인팅이 "cors"가 되면, 401을 받아도 이 단계에 들어가지 않습니다. 반대로 조건을 모두 만족했다고 모든 브라우저가 로그인 창을 띄우는 것은 아닙니다. 표준의 이 부분에는 여러 개이거나 빠진 WWW-Authenticate 헤더, 파싱 문제 등에 “Needs testing” 메모가 붙어 있으므로 실제 화면 동작은 브라우저마다 확인해야 합니다.
프록시가 인증을 요구할 때는 401이 아니라 407 Proxy Authentication Required와 Proxy-Authenticate 헤더를 씁니다.
상태 코드를 고르는 보통의 순서
아래 목록은 RFC가 정한 판정 절차가 아니라, 앞의 정의와 권고를 따를 때 흔히 쓰는 기본값입니다.
- 인증 정보가 없거나 유효하지 않다 → 401과
WWW-Authenticate - 누구인지는 확인됐지만 이 작업은 허용되지 않는다 → 403
- 리소스가 있다는 사실도 알리고 싶지 않다 → 404도 허용됨
403은 자격 증명과 무관한 이유로도 쓸 수 있습니다. 같은 RFC 9110의 3xx 코드는 301, 302, 307, 308 리다이렉트의 차이에서 정리했습니다.
참고한 명세와 문서
- RFC 9110 — HTTP Semantics: 11.3·11.4·11.6.1절(인증 헤더), 15.5.2절(401), 15.5.4절(403), 15.5.5절(404), 15.5.8절(407)
- RFC 6750 — OAuth 2.0 Bearer Token Usage: 3절과 3.1절(오류 코드와 상태 코드)
- RFC 9700 — OAuth 2.0 Security Best Current Practice
- RFC 6750 문서 정보(RFC Editor): 갱신 관계
- WHATWG Fetch Standard — HTTP-network-or-cache fetch (Living Standard)
링크와 내용은 2026년 10월 3일에 확인했습니다. Fetch는 계속 갱신되는 표준입니다.