GraphQL 서비스에 적당한 인증 붙이기
문제의 시작
아주 예전에 공개키 기반 로그인을 구현해본 적이 있으니 이걸 활용해서 GraphQL 서버에 적용해보려고 한다.
대충의 설계
- Agent는 Subtle crypto를 이용해 개인키를 생성한다.
- 서버에서 challenge를 제공한다.
- Agent는 자신의 개인키를 이용해 서명해서 돌려준다.
- 서버는 이제 Agent를 인식할 수 있다.
- 유저는 Agent를 신뢰할 수 있는 Agent 목록에 추가한다.
- 이제 Agent는 서버에 유저를 대신해서 요청을 보낼 수 있다.
요구사항 분석
- 모든 API에 대해 일괄적으로 인증을 의무화하는 방법을 생각해봤는데, 이러면 challenge를 제공하는 API 또한 인증을 받아야 호출할 수 있어서 곤란하다.
- 또한 iGraphQL 같은 툴에선 schema discovery API같은걸 사용해서 API 목록을 생성하는데, 이러한 기능도 막아버리면 곤란하다.
- 따라서 특정 API에 대해서만 인증을 요구하고, 그 외의 API는 public으로 유지할 필요가 있었다.
- 또한 한번 인증에 성공한 후, 인증에 성공한 정보를 서버에서 기억해둘 필요가 있다. 매 요청마다 공개키 인증을 요구할…순 있겠지만 너무 느리지 않겠는가
- 보통 인증 정보를 보내는건
cookie
헤더를 쓰거나authorization
헤더를 쓰는 것인데…
Cookie냐 Authorization이냐 그것이 문제로다
클라이언트와 서버 프레임워크가 해당 로그인 구현을 얼마나 잘 구현하냐에 따라 난이도가 달라진다. 어지간해서 개발의 편의성을 위해 브라우저에서 바로 GraphiQL을 사용하는 방법을 우선순위에 놓고 보니, 공개키 기반 로그인은 적용이 어렵고 간단한 basic auth만 적용하는 편이 낫다는 결론이 나왔다. 내가 필요로 하는 클라이언트의 목록과 요구사항은 다음과 같다.
Apollo
https://www.apollographql.com/docs/react/networking/authentication
Deno
개발 중에 API 테스트 목적으로 종종 Deno에서 바로 fetch를 호출할 때가 있었다. 다만, Deno는 Cookie jar를 잘 지원하지 않아서, 해당 기능을 구현한 라이브러리를 사용해줄 필요가 있다.
https://github.com/denoland/deno/issues/5862
나의 경우엔 another_cookiejar 라이브러리를 사용했다.
import { wrapFetch } from "https://deno.land/x/another_cookiejar@v5.0.7/mod.ts";
const fetch = wrapFetch();
GraphiQL
온전히 브라우저에서만 돌아가기 때문에 임의의 인증을 붙일 여지가 많지 않다. 다만 요청 헤더를 설정할 수 있으니 거기에 authorization 헤더를 추가해주면 기본 인증 정도는 해결할 수 있다.
서버 구현
Basic auth만 쓰는것이기 때문에 일단은 reverse proxy 역할을 하는 Caddy가 인증도 같이 처리하도록 했다. 나만 쓰는 API이니 authentication 외의 authorization이나 다른 기능은 일단 포기하자.
React 클라이언트 구현
인증에 필요한 ID/password를 입력하는 폼을 간단하게 만들고, 해당 폼 입력 여부를 Context에서 관리하도록 하고 로그인이 되어있지 않으면 그냥 로그인 폼만 보여주도록 간단하게 구현했다.
결론
안전해지긴 했는데 아직 로그인 유지 기능같은건 미흡하다. 여전히 유저가 암호를 입력해야 하니 내가 처음 원하던 모습과는 크게 모자라다…