DAV 2. fetch 가 로그인 302 를 따라가다 CORS 오류로 끝날 때: API 는 챌린지 없는 401 JSON 으로
앞 글의 캘린더 서버 이야기입니다. 웹 화면은 React 로 만들었고 서버의 /api/ 아래 JSON API 를 fetch 로 부릅니다. 인증 게이트웨이를 처음 만들 때는 /api/ 도 다른 웹 경로처럼 로그인이 안 됐으면 302 로 authentik 로그인 시작 주소를 주게 되어 있었습니다.
웹 API 를 붙이는 단계에서 이걸 다시 봤습니다. 화면을 열어 둔 채 로그인 세션이 만료되면, 그다음 API 호출은 302 를 받습니다. fetch 는 리다이렉트를 자동으로 따라가고, 로그인 페이지는 인증 서버 도메인이라 다른 출처입니다. 그 응답에는 CORS 헤더가 없으니 브라우저가 막고, 화면 코드는 의미 없는 네트워크 오류 하나만 받게 됩니다. 사용자는 달력이 왜 안 뜨는지 모르고요.
redirect: 'manual' 로 받으면 opaqueredirect 라서 어디로 가라는 건지 읽을 수 없습니다.
그래서 /api/ 아래만 다르게 답하게 바꿨습니다.
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{"error":"unauthenticated","login":"/outpost.goauthentik.io/start?rd=https://dav.example.com/"}
화면은 401 을 받으면 login 값으로 location.href 를 바꿉니다. 로그인이 끝나면 rd 의 주소로 돌아옵니다.
if (res.status === 401) {
const body = await res.json();
location.href = body.login;
return;
}
여기에 WWW-Authenticate: Basic 은 붙이지 않았습니다. 붙이면 브라우저가 401 을 보고 아이디·비밀번호 창을 띄웁니다. DAV 경로에서는 그게 필요하지만(DAVx⁵ 가 그걸 보고 Basic 인증을 보냅니다), 웹 화면에서는 엉뚱한 창이 하나 뜨는 셈입니다. 같은 401 이라도 경로마다 챌린지를 붙일지 말지가 다릅니다.
rd 에는 경로만이 아니라 전체 URL 을 넣었습니다. 아웃포스트를 single application 모드로 쓰면 rd 에 외부 호스트가 들어 있어야 받아 줍니다.
이 구분은 처음 인증 게이트웨이를 만들 때 “웹 API 를 만들 때 정한다” 로 남겨 두었던 것입니다. 게이트웨이 작업 기록에 미뤄 둔 질문으로 적어 두지 않았다면 그대로 지나쳤을 것 같습니다.