DAV 1. DAVx⁵ 로그인이 조용히 실패할 때: forward auth 뒤 CalDAV 서버는 401 과 302 를 나눠야 한다
캘린더와 연락처를 직접 운영하려고 Go 로 CalDAV/CardDAV 서버를 만들었습니다. 로그인은 서버가 직접 하지 않고, 같이 쓰는 authentik 의 프록시 아웃포스트에 forward auth 로 물어봅니다. 요청이 오면 아웃포스트에 “이 요청의 주인이 누구냐” 를 묻고, 돌아온 X-authentik-* 헤더만 믿습니다.
손님이 둘입니다. 브라우저로 여는 웹 화면, 그리고 휴대폰의 DAVx⁵ 입니다. 둘은 로그인 실패를 다르게 다뤄야 했습니다.
브라우저는 로그인이 안 됐으면 authentik 로그인 페이지로 보내면 됩니다. 302 로 아웃포스트의 로그인 시작 주소로 넘깁니다.
DAVx⁵ 는 다릅니다. DAVx⁵ 는 사용자 이름과 비밀번호를 Basic 인증으로 보내는데, 처음 접속할 때는 아직 보내지 않고 서버가 401 과 WWW-Authenticate: Basic 을 줄 때 비로소 보냅니다. 여기서 302 를 받으면 로그인 페이지 HTML 을 받아 들고 “이건 DAV 서버가 아니네” 하고 끝납니다. 사용자 화면에는 그럴듯한 오류도 안 나옵니다.
그래서 실패 응답을 이렇게 나눴습니다.
/dav/, /.well-known/ 아래 → 401 + WWW-Authenticate: Basic realm="dav"
경로와 상관없이 WebDAV 메서드 → 401 + Basic
(PROPFIND, PROPPATCH, REPORT, MKCOL, MKCALENDAR)
/api/ 아래(웹 화면의 JSON API) → 401 JSON, 챌린지 없음
그 밖의 웹 경로 → 302 아웃포스트 로그인 시작
두 번째 줄이 중요했습니다. DAVx⁵ 에 서버 주소만 넣으면 / 에 PROPFIND 를 보냅니다. / 는 웹 화면 주소라 첫 규칙만 있으면 302 를 받습니다. 메서드로도 나눠야 DAVx⁵ 가 401 을 받습니다.
DAVx⁵ 가 서버를 찾는 순서는 이렇습니다. /.well-known/caldav 로 물어서 리다이렉트를 따라가고, current-user-principal 을 묻고, 거기서 캘린더 홈셋을 받고, 그 안의 캘린더 목록을 받습니다. /.well-known/caldav 와 /carddav 는 /dav/ 로 가라는 307 뿐이라 인증 밖에 두었습니다. 드러나는 정보가 없고 아웃포스트에 한 번 덜 물어도 됩니다. DNS 에 _caldavs._tcp, _carddavs._tcp SRV 레코드를 넣어 두면 도메인만 넣어도 찾아옵니다.
하나 더 있습니다. 아웃포스트가 죽었거나 이상한 응답을 줄 때는 401 이 아니라 502 를 줍니다. 401 을 주면 DAVx⁵ 는 비밀번호가 틀렸다고 생각하고 사용자에게 다시 입력하라고 하니까요. 서버 쪽 문제는 서버 쪽 문제로 보여야 합니다.
DAVx⁵ 에는 “URL 과 사용자 이름으로 로그인” 을 고르고, 사용자 이름에 authentik 사용자 이름(이메일 아님)과 authentik 에서 만든 앱 비밀번호를 넣습니다. 아웃포스트의 “Intercept header authentication” 이 이 Basic 헤더를 authentik 에서 검증해 줍니다.
보안상 하나만 덧붙이면, 클라이언트가 직접 보낸 X-authentik-* 헤더는 대소문자나 _ 변형까지 전부 지우고 시작합니다. 안 그러면 누구나 헤더 하나로 다른 사람이 될 수 있습니다.
참고: DAVx⁵, https://www.davx5.com/