들어가며
전편에서 AshAuthentication을 설치하고 사용자 및 API key 를 만들었다. 이제 실제로 리소스를 보호하고 만들어진 API key 를 통해 접근하도록 만들어 보자. 해야 할 일은 다음과 같다.
목표
- API Key 없는 요청은 401 Unauthorized로 차단
- 유효한 API Key를 가진 요청만 리소스 접근 허용
핵심 개념
API 보호는 두 단계로 구성된다:
1단계: 인증 (Authentication) – Phoenix Plug에서 처리
- “누구인가?” 확인
- API Key 존재 여부 및 유효성 검증
2단계: 권한 (Authorization) – Ash Policy에서 처리
- “무엇을 할 수 있는가?” 결정
- 리소스별 접근 권한 제어
인증 처리
라우터에서 ApiKey.Plug 설정을 required?: true 로 변경
# lib/helpdesk_web/router.ex
pipeline :api do
plug :accepts, ["json"]
plug AshAuthentication.Strategy.ApiKey.Plug,
resource: Helpdesk.Accounts.User,
required?: false # false에서 true로 변경
plug :load_from_bearer
plug :set_actor, :user
end
변경 후 API 를 호출 해 보면 API key 가 없는 요청은 거부하는 것을 확인할 수 있다.
% curl -X GET http://localhost:4000/api/json/posts
Unauthorized
% curl -X GET http://localhost:4000/api/json/tickets \
-H "Authorization: Bearer helpdesk_FlhUEreQrABNZ2XvyDIC5bUeJljjHiOGL9jeiwHPpjeMSmYosY4TupfzhIOF339E_ckbhkf"
{"data":[{"attributes":{"status":"open","subject":"This ticket was created through the JSON API"},"id":"e2ee4c2a-dd70-4a2e-b799-8d16b8a9e2b2","links":{},"meta":{},"type":"ticket","relationships":{}}],"links":{"self":"http://localhost:4000/api/json/tickets"},"meta":{},"jsonapi":{"version":"1.0"}}
% curl -X GET http://localhost:4000/api/json/tickets \
-H "Authorization: Bearer invalid_key"
Unauthorized
권한 적용
이제 리소스에 접근 권한을 설정할 차례다. 권한 설정에 앞서 권한 관련 로그를 자세히 볼 수 있도록 전역 설정을 변경할 수 있다. 권한 적용 과정을 좀 더 살펴보고 싶다면 설정을 다음과 같이 변경한다. 설정을 변경한다면 서버를 재시작해야 한다.
# config/dev.exs
config :ash,
policies: [
show_policy_breakdowns?: true,
log_policy_breakdowns: :error, # 허용되지 않는 정책으로의 접근은 error
log_successful_policy_breakdowns: :info # 허용되는 정책으로의 접근은 info
]
기본 정책 설정
보호하고자 하는 Ticket 리소스에 인증 정책을 추가 한다.
defmodule Helpdesk.Support.Ticket do
use Ash.Resource,
domain: Helpdesk.Support,
data_layer: AshPostgres.DataLayer,
extensions: [AshJsonApi.Resource],
authorizers: [Ash.Policy.Authorizer] # 권한 검사 활성화
# ... 기존 설정들 ...
policies do
policy always() do
description "API Key로 인증된 사용자만 티켓에 접근 가능"
authorize_if actor_present()
end
end
# ... 나머지 코드 ...
end
변경된 부분의 내용은 다음과 같다.
authorizers: [Ash.Policy.Authorizer]: 정책 기반 인증 활성화actor_present(): 현재 요청에 인증된 사용자(actor)가 있는지 확인- 모든 액션에 동일한 정책 적용
policy 매크로에서 권한 관련 내용을 작성하면 된다. 지금은 인증된 사용자라면 모두 접근이 가능하도록 되어 있기 때문에 실제로 권한이 적용되어 있는지 구분하기가 어렵다(인증이 되지 않는 사용자라면 인증 단계부터 거부되기 때문에 정책 검사까지 도달하지 않으므로). 테스트 편의를 위해 정책을 모든 요청에 대해 거부하는 정책으로 변경해 보자. 그러면 인증된 사용자라도(401 Unauthorized 를 통과하더라도) 리소스 응답을 거부(Forbidden 또는 필터링된 응답)하게 된다.
defmodule Helpdesk.Support.Ticket do
# ... 기존 설정들 ...
policies do
# policy always() do
# description "API Key로 인증된 사용자만 티켓에 접근 가능"
# authorize_if actor_present()
# end
policy always() do
# access_type :strict
description "시스템 점검 중 - 모든 접근 차단"
forbid_if always()
end
end
# ... 나머지 코드 ...
end
테스트 결과는 다음과 같다.
% curl -X GET http://localhost:4000/api/json/tickets \
-H "Authorization: Bearer helpdesk_FlhUEreQrABNZ2XvyDIC5bUeJljjHiOGL9jeiwHPpjeMSmYosY4TupfzhIOF339E_ckbhkf"
{"data":[],"links":{"self":"http://localhost:4000/api/json/tickets"},"meta":{},"jsonapi":{"version":"1.0"}}
결과가 흥미로운데 응답은 거부되지 않고 구조도 올바르다. 다만 실제 데이터 부분이 빈 리스트로 응답되었다. 이는 보안 설계의 일종으로 어떤 API가 거부 또는 허용으로 정책이 구성되었는지 알기 어렵게 하기 위한 것이다. Forbidden 응답을 명시적으로 응답하려면 access_type :strict 로 설정을 변경한다.
% curl -X GET http://localhost:4000/api/json/tickets \
-H "Authorization: Bearer helpdesk_FlhUEreQrABNZ2XvyDIC5bUeJljjHiOGL9jeiwHPpjeMSmYosY4TupfzhIOF339E_ckbhkf"
{"errors":[{"code":"forbidden","id":"94afea0d-e6f6-492b-867b-d9a2c9924c14","meta":{},"status":"403","title":"Forbidden","detail":"forbidden"}],"jsonapi":{"version":"1.0"}}
응답 구조도 바뀌었고 서버 로그상에도 Sent 403 로그가 있을 것이다.
AshAuthentication 을 가지고 비교적 쉽게 API 보안을 추가할 수 있었다. 보안 자체가 기본적으로 어려운 개념이 많기 때문에 자동으로 추가되는 코드도 많긴 하지만 그래도 어디서 어느 부분이 어떤 역할을 하는지 명확하게 구분되어 있다는 느낌을 받았다. 이 예제는 보안이 동작한다 수준의 아주 단순한 예제이긴 하지만 실 서비스에 적용하는데 있어서 출발점이 될 수 있다고 생각한다. policy 매크로 부분을 더 살펴보면 충분히 실전에서도 활용이 가능할 것 같다.