WWW-Authenticate
고친 사람 github-actions[bot]
WWW-Authenticate 는 서버가 요청을 거절하면서 신원을 어떻게 밝히면 되는지 알려 주는 응답 헤더입니다. 클라이언트는 이 헤더를 읽고 무엇을 어떤 꼴로 실어 다시 보낼지 정합니다. 누구인지 확인하지 못했다는 401 응답에는 이 헤더가 반드시 붙습니다.
쉽고 빠른 이해
WWW-Authenticate 는 「이런 방식으로 증명해 오세요」라는 안내문입니다. 토큰 없이 서버를 부르면 서버가 WWW-Authenticate: Bearer realm="api" 를 실어 거절합니다.
Bearer 는 토큰을 내미는 방식의 이름입니다. realm="api" 는 보호 구역의 이름입니다. 합치면 「api 구역에 들어오려면 토큰을 붙여 다시 오라」는 뜻입니다.
이 안내가 없으면 클라이언트는 거절당한 까닭만 알고 풀 방법은 모릅니다. 서버마다 받는 증명 방식이 다르기 때문입니다.
- 서버가 거절 응답에 받아 주는 증명 방식을 적어 보냅니다
- 클라이언트가 그중 자기가 다룰 줄 아는 방식을 하나 고릅니다
- 그 방식대로 신원 정보를 실어 다시 요청합니다
대가는 읽기가 까다롭다는 것입니다. 한 줄에 방식 여럿과 방식마다 딸린 설정값 여럿이 쉼표로 섞여 들어옵니다.
상세
이 절은 WWW-Authenticate 헤더 한 줄이 무엇으로 이루어지는지부터 봅니다. 그다음 방식이 여럿 실린 줄을 읽는 법, 토큰 방식이 싣는 오류 정보, 이 헤더가 실리는 응답을 차례로 짚습니다.
HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜)는 클라이언트가 서버에 요청을 보내고 응답을 받는 규약입니다. 요청과 응답 앞머리에는 「이름: 값」 꼴의 줄이 붙습니다. 이 줄을 헤더라고 부릅니다. 헤더는 본문과 따로 부가 정보를 나릅니다.
WWW-Authenticate 는 서버가 응답에 싣는 헤더입니다. 이름 앞의 WWW 는 World Wide Web(월드 와이드 웹)의 줄임말이고, Authenticate 는 「신원을 확인하다」라는 뜻입니다. 요청을 보낸 쪽이 누구인지 확인하는 일을 인증이라고 부릅니다.
이 헤더가 있는 까닭
서버가 신원을 확인하지 못하면 401 이라는 상태 코드로 요청을 거절합니다. 상태 코드는 요청이 어떻게 끝났는지 알리는 세 자릿수 값입니다. 401 은 「누구인지 모르겠다」는 뜻입니다.
그런데 401 만으로는 클라이언트가 할 일을 정할 수 없습니다. 아이디와 비밀번호를 보내야 하는지, 로그인 뒤 받은 토큰을 보내야 하는지 알 수 없기 때문입니다. WWW-Authenticate 가 그 빈칸을 채웁니다.
이렇게 서버가 「이 방식으로 증명해 보라」고 요구하는 것을 챌린지라고 합니다. WWW-Authenticate 는 이 챌린지를 싣는 헤더입니다.
클라이언트는 요구대로 신원 정보를 실어 요청을 다시 보냅니다. 챌린지에 답하는 이 요청을 응답이라고 합니다. 서버가 보내는 HTTP 응답과는 다른 뜻입니다. 요구와 답의 이 한 쌍을 챌린지-응답이라고 합니다.
sequenceDiagram
participant 클라이언트
participant 서버
클라이언트->>서버: 신원 정보 없이 요청
서버-->>클라이언트: 401 과 WWW-Authenticate
클라이언트->>서버: Authorization 을 실어 다시 요청
서버-->>클라이언트: 200 과 결과
둘째 화살표가 이 헤더가 오가는 때입니다. 셋째 화살표의 Authorization 은 클라이언트가 신원 정보를 싣는 요청 헤더입니다. 두 헤더는 짝을 이룹니다. 하나가 요구하고 하나가 답합니다.
챌린지 한 줄의 생김새
챌린지는 방식의 이름 하나와 그 방식이 쓰는 매개변수 몇 개로 이루어집니다. 매개변수는 방식마다 따로 두는 설정값입니다. 가장 짧은 꼴은 이렇습니다.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="admin"
맨 앞의 Basic 이 방식의 이름입니다. 신원을 증명하는 방식을 인증 스킴이라고 부릅니다. 스킴마다 무엇을 어떤 꼴로 실을지가 따로 정해져 있습니다.
스킴 이름 뒤에는 한 칸을 띄우고 매개변수를 적습니다. 매개변수는 이름=값 꼴이고 여럿이면 쉼표로 잇습니다. 위 예의 realm="admin" 이 매개변수 하나입니다.
스킴 이름과 매개변수 이름은 대소문자를 가리지 않습니다. basic 이라고 적어도 Basic 과 같은 스킴입니다. 값은 따옴표로 감싸 보내기도 합니다. 따옴표 없이 보내기도 합니다.
백엔드에서 자주 만나는 스킴은 셋입니다. 스킴마다 클라이언트가 Authorization 에 싣는 것이 다릅니다. 표에 나오는 낱말 셋을 먼저 풉니다.
Base64 는 바이트를 글자 64개로 옮겨 적는 방식입니다. 누구나 되돌릴 수 있어서 감추는 효과는 없습니다.
액세스 토큰은 「이 사용자는 이미 확인됐다」는 것을 담은 문자열입니다. 요청마다 비밀번호를 보내지 않으려고 씁니다.
해시는 입력을 되돌릴 수 없는 짧은 값으로 줄이는 계산입니다. 비밀번호를 해시로 바꿔 보내면 중간에서 읽혀도 비밀번호가 드러나지 않습니다.
| 스킴 | 클라이언트가 싣는 것 |
|---|---|
| [[Basic 인증 | Basic]] |
| [[Bearer 토큰 | Bearer]] |
| [[Digest 인증 | Digest]] |
realm 과 보호 영역
realm 은 거의 모든 스킴이 쓰는 매개변수입니다. 서버가 신원 정보를 요구하는 영역에 붙인 이름입니다.
한 서버 안에서도 관리자 화면과 일반 화면이 서로 다른 신원 정보를 요구할 수 있습니다. realm 은 그 둘을 가립니다. 같은 서버의 같은 realm 을 보호 영역이라고 부릅니다.
보호 영역 하나에서 통한 신원 정보는 그 영역의 다른 주소에서도 통합니다. 그래서 브라우저는 한 번 입력받은 아이디와 비밀번호를 같은 보호 영역의 다음 요청에 다시 붙여 보냅니다.
realm 값은 큰따옴표로 감싸 보내야 합니다. 받는 쪽은 따옴표 없는 값도 읽을 줄 알아야 합니다. 보내는 쪽은 예전부터 이어진 관례를 따라 따옴표 꼴만 씁니다.
한 줄에 실린 챌린지 여럿
서버는 스킴을 하나만 받으라는 법이 없습니다. 토큰도 받고 아이디와 비밀번호도 받는 서버라면 챌린지 둘을 한꺼번에 내밉니다.
WWW-Authenticate: Bearer realm="api", Basic realm="api", charset="UTF-8"
이 줄에는 챌린지가 둘 들었습니다. 매개변수가 어느 챌린지에 딸리는지는 아래 그림과 같습니다. charset 은 아이디와 비밀번호를 어떤 문자 인코딩으로 적어 보낼지 알리는 Basic 의 매개변수입니다.
flowchart TD
H["WWW-Authenticate 한 줄"]
subgraph C1["챌린지 1"]
S1["스킴 · Bearer"]
P1["realm"]
end
subgraph C2["챌린지 2"]
S2["스킴 · Basic"]
P2["realm"]
P3["charset"]
end
H --> C1
H --> C2
S1 --> P1
S2 --> P2
S2 --> P3
위 예시 줄에서 쉼표는 두 가지 일을 합니다. 첫 쉼표는 Bearer 챌린지와 Basic 챌린지를 가릅니다. 둘째 쉼표는 Basic 챌린지 안의 realm 과 charset 을 가릅니다.
어느 쪽인지는 쉼표 뒤를 보고 가립니다. 쉼표 뒤에 이름=값 이 오면 앞 챌린지의 매개변수입니다. = 없는 낱말이 오면 새 챌린지의 스킴 이름입니다.
따옴표 안의 쉼표는 어느 쪽도 아닙니다. realm="a, b" 의 쉼표는 값의 일부입니다. 그래서 이 헤더를 쉼표로 잘라 읽는 코드는 쉽게 틀립니다. 직접 짜기보다 HTTP 라이브러리가 주는 파서를 씁니다.
챌린지 여럿은 헤더 줄 여럿으로 나눠 보내도 됩니다. 한 줄에 쉼표로 잇는 것과 뜻이 같습니다.
WWW-Authenticate: Bearer realm="api"
WWW-Authenticate: Basic realm="api"
받은 클라이언트는 그중 자기가 다룰 줄 아는 스킴을 하나 고릅니다. 여럿을 다룰 줄 알면 가장 안전한 쪽을 고릅니다.
Bearer 가 싣는 오류 정보
Bearer 스킴은 OAuth 2.0 과 함께 쓰려고 만든 스킴입니다. OAuth 2.0 은 사용자가 비밀번호를 건네지 않고도 다른 애플리케이션에 권한을 맡기는 규약입니다. 권한을 맡은 쪽은 발급받은 액세스 토큰을 Bearer 스킴으로 실어 보냅니다.
이 스킴은 realm 말고도 거절한 까닭을 알리는 매개변수를 둡니다. 아래 표의 넷입니다.
| 매개변수 | 싣는 것 |
|---|---|
error |
거절한 까닭을 가리키는 정해진 낱말 |
error_description |
개발자가 읽을 설명 문장 |
error_uri |
설명을 담은 웹 페이지 주소 |
scope |
이 요청에 필요한 권한 범위 |
scope 는 토큰이 허락받은 일의 범위입니다. 「글 읽기」만 허락받은 토큰으로 글을 쓰려 하면 범위가 모자랍니다. 이 범위를 스코프라고 부릅니다.
아래는 수명이 다한 토큰을 거절하는 응답입니다. 만료된 토큰이라 새 토큰을 받아 오라는 뜻입니다.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
error="invalid_token",
error_description="The access token expired"
보기 좋게 세 줄로 나눠 적었지만 실제로 보내는 것은 헤더 한 줄입니다. 쉼표 뒤가 모두 이름=값 이라서 챌린지는 Bearer 하나뿐입니다.
error 에 들어가는 낱말은 셋으로 정해져 있습니다. 낱말마다 함께 보내는 상태 코드도 정해져 있습니다.
error 값 |
뜻 | 상태 코드 |
|---|---|---|
invalid_request |
요청 꼴이 틀렸다. 필요한 값이 빠졌거나 같은 값이 두 번 왔다 | [[HTTP 400 |
invalid_token |
토큰이 만료됐거나 취소됐거나 망가졌다 | 401 |
insufficient_scope |
토큰은 멀쩡한데 권한 범위가 모자란다 | [[HTTP 403 |
클라이언트가 할 일은 이 낱말로 갈립니다. invalid_token 이면 새 토큰을 받아 다시 보냅니다. insufficient_scope 면 토큰을 바꿔도 같은 범위로는 안 풀리므로 더 넓은 범위를 받아야 합니다.
요청에 토큰이 아예 없었다면 서버는 error 를 싣지 않기를 권합니다. 이때는 Bearer realm="example" 처럼 스킴과 realm 만 보냅니다. 토큰이 없는 요청은 오류가 아니라 인증을 처음 요구받는 단계로 봅니다.
이 헤더가 실리는 응답
401 응답에는 WWW-Authenticate 를 반드시 실어야 합니다. 챌린지가 최소 하나는 들어 있어야 합니다. 이 헤더가 없는 401 을 받은 클라이언트는 어떤 스킴으로 다시 보낼지 모릅니다.
401 이 아닌 응답에도 실을 수 있습니다. 앞 표의 insufficient_scope 가 403 에 실리는 것이 그런 경우입니다. 성공한 응답에 실어 「신원을 밝히면 다른 결과를 줄 수 있다」고 알리기도 합니다.
클라이언트와 서버 사이에서 요청을 대신 받아 넘기는 중간 서버를 프록시라고 부릅니다. 프록시가 신원 정보를 요구할 때는 이 헤더 대신 Proxy-Authenticate 를 씁니다. 상태 코드도 401 대신 407 을 씁니다.
| 서버가 요구할 때 | 프록시가 요구할 때 | |
|---|---|---|
| 상태 코드 | 401 | 407 |
| 요구하는 헤더 | WWW-Authenticate | Proxy-Authenticate |
| 답하는 헤더 | Authorization | Proxy-Authorization |
헤더 이름이 갈려 있어서 요청 하나가 프록시와 서버에 각각 다른 신원 정보를 실을 수 있습니다.
브라우저가 이 헤더를 받았을 때
브라우저는 Basic 챌린지를 담은 401 을 받으면 아이디와 비밀번호를 묻는 창을 스스로 띄웁니다. 웹 페이지가 그린 화면이 아니라 브라우저에 들어 있는 창이라 모양을 바꿀 수 없습니다.
그래서 웹 페이지가 부르는 백엔드 서버는 Basic 챌린지를 보내지 않는 경우가 많습니다. 토큰을 쓰는 서버라면 Bearer 챌린지만 보내서 이 창이 뜨지 않게 합니다.
웹 페이지의 스크립트가 다른 출처의 서버를 부를 때는 이 헤더를 읽지 못할 수 있습니다. 출처는 주소의 프로토콜과 도메인과 포트를 한데 묶은 것입니다. 셋 가운데 하나라도 다르면 다른 출처입니다.
출처가 다른 서버와 주고받는 규칙을 CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)라고 부릅니다. 이 규칙은 스크립트가 읽을 수 있는 응답 헤더를 몇 개로 묶어 둡니다. WWW-Authenticate 는 거기 들지 않습니다.
스크립트가 이 헤더를 읽게 하려면 서버가 Access-Control-Expose-Headers 에 이 이름을 적어 보냅니다. Access-Control-Expose-Headers 를 빠뜨리면 스크립트는 401 만 보고 error 값은 못 봅니다.
서버가 챙길 것
이 소절은 이 헤더를 보내는 서버가 흔히 놓치는 셋을 봅니다.
먼저 401 에 이 헤더를 빠뜨리지 않습니다. 본문에 오류 메시지만 넣으면 사람은 읽어도 클라이언트 코드는 다음에 할 일을 못 정합니다.
다음으로 error_description 에 너무 많은 것을 적지 않습니다. 「없는 아이디입니다」와 「비밀번호가 틀렸습니다」를 가르면 공격자가 어떤 아이디가 가입돼 있는지 알아냅니다. 이렇게 가입된 계정을 캐내는 것을 계정 열거라고 부릅니다.
끝으로 Basic 챌린지는 암호화된 연결에서만 씁니다. Base64 는 누구나 되돌릴 수 있는 인코딩입니다. 중간에서 읽히면 비밀번호가 드러납니다. HTTPS(HyperText Transfer Protocol Secure, 보안 하이퍼텍스트 전송 프로토콜)가 오가는 내용을 암호화해 이것을 막습니다.
관련 항목
WWW-Authenticate 를 정의하는 표준 문서
RFC 9110 · RFC 6750 · RFC 7617 · RFC 7616 · RFC 7235 · HTTP
WWW-Authenticate 와 짝을 이루는 헤더
Authorization 헤더 · Proxy-Authenticate · Proxy-Authorization · Authentication-Info · Access-Control-Expose-Headers
WWW-Authenticate 가 실리는 상태 코드
HTTP 401 · HTTP 403 · HTTP 407 · HTTP 400 · 상태 코드
WWW-Authenticate 가 알리는 인증 스킴
인증 스킴 · Basic 인증 · Bearer 토큰 · Digest 인증 · Negotiate 인증 · HOBA · Mutual 인증
WWW-Authenticate 를 이루는 구성 요소
챌린지 · 보호 영역 · realm · 인증 매개변수 · token68 · 스코프
WWW-Authenticate 가 속하는 상위 분류
HTTP 인증 · 챌린지-응답 · 인증 · 인증과 인가 · 헤더 · 응답 헤더
WWW-Authenticate 를 받고 클라이언트가 싣는 자격 증명
자격증명 · 액세스 토큰 · 리프레시 토큰 · JWT · API 키 · Base64 · 해시
WWW-Authenticate 를 쓰는 인증 체계
OAuth 2.0 · OpenID Connect · Kerberos · NTLM · SPNEGO
WWW-Authenticate 를 받아 읽는 클라이언트와 중간 서버
브라우저 · 프록시 · CORS · HTTP 클라이언트 · 리버스 프록시
WWW-Authenticate 가 부르는 신원 정보를 지키는 방어
다른 이름: WWW-Authenticate 헤더 · WWW-Authenticate header · 인증 요구 헤더