AshAuthentication을 이용하여 API 리소스 보호 구현하기 (2편)

들어가며

전편에서 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 매크로 부분을 더 살펴보면 충분히 실전에서도 활용이 가능할 것 같다.

댓글 남기기