이 시리즈의 첫 번째 글에서는 예제 앱을 통해 Devise의 모듈, 헬퍼, 뷰, 컨트롤러, 라우트 등 기본 개념을 살펴보았습니다.
이번 두 번째 파트에서는 Devise의 고급 활용법을 다룹니다. 구체적으로 OmniAuth를 이용한 소셜 로그인, API 인증, 그리고 Authtrail을 활용한 로그인 추적 방법까지 알아보겠습니다.
바로 시작해 보겠습니다!
OmniAuth를 활용한 Ruby 인증
요즘 거의 모든 웹 애플리케이션은 트위터, 페이스북 같은 소셜 네트워크는 물론 Google, GitHub 등 다양한 인증 제공자를 통해 로그인할 수 있는 옵션을 제공합니다.
이렇게 편리한 멀티 프로바이더 인증을 가능하게 해주는 것이 바로 OmniAuth라는 라이브러리입니다. OmniAuth는 Ruby용 유연하고 강력한 인증 라이브러리로, 여러 외부 제공자와의 통합을 손쉽게 구현할 수 있습니다.
OmniAuth는 다양한 OAuth 제공자에 연결할 수 있는 간단하고 통일된 API를 제공합니다. 특히 사용자가 소셜 미디어 계정으로 회원가입하거나 로그인할 수 있도록 하고 싶을 때 매우 유용합니다. OmniAuth를 사용하면 Rails 애플리케이션에 소셜 로그인 기능을 쉽게 추가할 수 있습니다.
Devise 젬과 함께 사용하면 사용자 인증 및 권한 관리가 한층 더 쉬워집니다. Devise의 내장 인증 기능을 활용하면서 동시에 OmniAuth로 외부 제공자 로그인 옵션을 제공하는 방식입니다.
OmniAuth와 Devise 시작하기
앞서 언급했듯이 OmniAuth는 다양한 서드파티 인증 제공자와 통합할 수 있습니다. 이 글에서는 GitHub을 예시로 사용하겠습니다.
OmniAuth 젬 설치하기
앱의 Gemfile에 다음 라인들을 추가합니다:
OmniAuth 2.0 이상 버전을 사용한다면 아래 젬도 함께 추가해야 합니다:
omniauth-rails_csrf_protection 젬은 OAuth 플로우로 향하는 GET 요청을 비활성화합니다. 또한 OAuth 요청 단계 전에 Rails CSRF 토큰 검증기를 삽입합니다. 이 두 가지 조치는 OAuth 인증 플로우를 겨냥한 크로스 사이트 위조 공격(CSRF)을 방지하기 위한 것입니다.
다음으로 bundle install을 실행하여 젬들을 설치합니다.
새 GitHub OAuth 앱 생성하기
이제 GitHub에서 새로운 OAuth 앱을 생성해야 합니다. 이 앱은 인증 권한을 가진 사용자 역할을 하며, 필요할 경우 언제든지 쉽게 권한을 회수할 수 있습니다.
먼저 GitHub 계정 프로필의 설정 페이지로 이동합니다. 그다음 왼쪽 메뉴에서 "Developer settings"를 클릭합니다. 아래와 같은 화면이 나타나면 새 OAuth 앱을 생성할 수 있습니다:

"Register new application"을 클릭하면 다음과 같은 화면이 표시됩니다:

폼을 아래와 같이 작성합니다:
- Application name — 새 OAuth 앱에 적절한 이름을 지정합니다.
- Homepage url — 우선은
https://localhost:3000/을 사용하세요. 실제 운영 환경에서는 앱의 실제 홈페이지 URL을 입력해야 합니다. - Application description — 필수는 아니지만, 앱이 여러 개일 때 구분하기 위해 작성해두면 좋습니다.
- Authorization callback url — 필수 입력 항목이며, 일반적으로
https://<app-url>/users/auth/<application-provider>/callback형식의 OAuth 콜백 URL을 따릅니다. 다만 Google 같은 일부 OAuth 제공자는 이 형식을 따르지 않으니 주의해야 합니다.
작성을 마쳤으면 "Register application"을 클릭합니다. 다음 화면에서 새 앱 시크릿을 생성하고 안전한 곳에 기록해두세요(시크릿은 딱 한 번만 표시됩니다).
Devise 초기화 파일 설정하기
Devise 초기화 파일인 config/initializers/devise.rb를 열고 GitHub 관련 OmniAuth 섹션으로 이동합니다. 해당 부분은 주석 처리되어 있을 가능성이 높으므로, 주석을 해제하고 새로 만든 GitHub OAuth 앱의 ID와 시크릿을 입력합니다:
OmniAuth 콜백 컨트롤러 생성하기
Devise 컨트롤러를 이미 생성했다면 OmniauthCallbacksController가 준비되어 있어 바로 커스터마이징할 수 있습니다. 없다면 직접 하나 만들고 아래와 같이 수정합니다:
위 코드에서 주목할 부분들:
from_omniauth— User 모델 내부에 구현할 메서드입니다.sign_in_and_redirect— OAuth 내장 메서드입니다.
User 모델 수정용 마이그레이션 추가하기
User 모델에 몇 가지 컬럼을 추가해야 합니다. 구체적으로 provider 컬럼과 uid 컬럼입니다:
bundle exec rails db:migrate를 실행하여 이 단계를 마무리합니다.
Devise 모델을 Omniauthable로 만들기
이제 User 모델에 Devise의 Omniauthable 모듈을 추가합니다:
그다음 from_omniauth 메서드를 추가합니다. 이 메서드는 방금 설정한 Users::OmniauthCallbacksController에서 호출됩니다:
이제 마지막 단계 하나만 남았습니다. 바로 Devise 뷰에 로그인 링크를 추가하는 것입니다.
로그인 링크 설정하기
기본적으로 Devise는 사용자 등록 및 로그인 뷰에 해당 제공자의 로그인 링크를 자동으로 추가해줍니다. 이 링크는 GET 메서드를 사용하지만, OmniAuth 2.0 이상 버전은 POST 요청을 선호합니다. 따라서 자동 생성된 링크를 비활성화하고 POST 요청을 사용하는 링크를 직접 삽입해야 합니다:
이것으로 Devise와 GitHub OAuth 인증이 적용된 Ruby on Rails 7 앱 세팅이 완료되었습니다. 전체 소스 코드는 데모 앱 저장소에서 확인할 수 있습니다.
다음으로 또 다른 고급 사용 사례인 Devise를 활용한 API 인증을 살펴보겠습니다.
Devise를 이용한 Ruby API 인증
오늘날 사용자들은 API를 통해 여러분의 앱에 연결할 수 있기를 기대하는 경우가 많습니다. 이 섹션에서는 Devise를 사용해 이러한 사용자 요청을 안전하게 인증하는 방법을 알아봅니다.
브라우저 기반 인증이 대체로 쿠키 기반인 반면, 대부분의 API 인증은 헤더에 담아 전달되는 JSON Web Token(JWT)이라 불리는 토큰을 통해 이루어집니다.
팁: 이 섹션에서는 Rails API 전용 앱을 사용한다고 가정합니다. 따라오려면 rails new app_name --api 명령으로 앱을 생성하세요.
JWT 기반 인증 플로우
앞서 언급했듯이 API 인증은 JWT 토큰을 기반으로 합니다. JWT 기반 인증 플로우가 어떻게 진행되는지 이해하는 것이 중요합니다. 기본적으로 다음과 같은 순서로 진행됩니다:
- 사용자 클라이언트가 API 앱에 호출을 보냅니다.
- API 앱이 쿠키 대신 사용할 수 있는 인증 토큰인 JSON Web Token(JWT)으로 응답합니다.
- 이후 사용자 클라이언트의 요청에는 이 토큰이
Authorization헤더에 담겨 전송됩니다. - 사용자는 Devise의 '세션 삭제' 액션을 호출할 수 있으며, 이로써 토큰이 파기되고 사용자가 로그아웃됩니다.
이제 이 플로우를 실제로 구현해 보겠습니다. 먼저 CORS(Cross-Origin Resource Sharing) 설정부터 시작합니다.
CORS 설정하기
CORS는 API 앱이 외부 소스로부터의 요청을 허용하도록 설정하는 것입니다. CORS는 외부 요청이 애플리케이션에서 어떻게 처리될지 정의하는 HTTP 기반 보안 정책입니다. 기본적으로 CORS는 최초 요청을 보낸 도메인과 다른 도메인(즉, 다른 '오리진'에서 온 요청)이 보낸 요청을 차단합니다.
CORS를 올바르게 처리하기 위해 rack-cors라는 유용한 젬을 사용합니다. Gemfile에서 아래 라인의 주석을 해제한 후 bundle install을 실행합니다:
또한 해당 CORS 초기화 파일을 열어 아래와 같이 수정합니다:
방금 수행한 설정에 대한 중요 참고 사항:
origins "*"— API 앱이 이제 어떤 소스에서든 요청을 받을 수 있다는 의미입니다.expose: %w[Authorization Uid]— 기본적으로rack-cors젬은 authorization 및 UID 헤더를 노출하지 않습니다. 하지만 우리는 인증 토큰을 전달해야 하므로 이 헤더들이 필요합니다.
설정이 끝났으니 이제 Devise와 이에 딸린 Devise-JWT 젬을 설치하겠습니다.
Rails 앱에 Devise와 Devise-JWT 젬 추가하기
devise-jwt 젬은 JWT 토큰을 다룰 수 있게 해주는 Devise 확장입니다. Gemfile에 젬들을 추가한 후 bundle install을 실행합니다:
Devise 설치 제너레이터인 bundle exec rails g devise:install을 실행합니다.
모델 생성 및 설정하기
두 개의 모델을 설정해야 합니다. 하나는 일반적인 Devise 사용자 모델(bundle exec rails g devise User)이고, 다른 하나는 폐기(revocation) 전략에 사용할 모델입니다(즉, 사용자가 API에서 로그아웃하는 방식을 정의합니다):
JWT 토큰 인증 가능 모듈을 추가하고 토큰 폐기 전략이 두 번째 모델인 JwtDenylist를 사용하도록 정의함으로써, 일반 Devise 사용자 모델을 API 인증에 맞게 수정합니다:
다음으로 두 번째 모델에 폐기 전략과 사용할 폐기 테이블을 참조하도록 설정합니다:
다음 섹션에서는 토큰 폐기가 무엇이고 왜 필요한지 설명하겠습니다.
토큰 폐기(Token Revocation)의 중요성
왜 토큰 폐기가 중요할까요? JWT 토큰은 상태 비저장(stateless)이기 때문입니다. 서버는 토큰에 서명하는 것 외에는 토큰에 대해 아무것도 알지 못합니다. 이런 상황에서는 서버가 해당 토큰을 폐기하는 방식으로 사용자를 로그아웃시킬 방법이 없습니다. 개별 토큰을 폐기할 방법이 없기 때문에, 직접 폐기 메커니즘을 구축하고 서버가 이를 사용하도록 지정해야 합니다.
토큰을 폐기할 때 내부적으로 실제로 일어나는 일은, 토큰의 고유한 조각인 jti(JWT ID)를 추출하여 정의된 폐기 전략에 따라 사용하는 것입니다.
물론 여기서 '토큰 폐기 전략이란 무엇인가'라는 의문이 생깁니다. 간단히 말해, 토큰 폐기를 서버가 어떻게 처리할지에 대한 정의입니다. 기본적인 폐기 전략은 세 가지가 있습니다:
- JTIMatcher 전략 — 사용자 모델에 "jti"라는 고유 컬럼을 추가하며, 이 모델이 곧 폐기 테이블 역할도 합니다. 사용자가 요청할 때마다 헤더의
jti를 저장된 토큰과 대조하고, 일치하는 항목이 발견될 때만 접근을 허용합니다. - Denylist(거부 목록) 전략 —
jti와 폐기된 토큰의 만료 시점(exp)을 데이터베이스 테이블에 저장합니다. 사용자가 요청할 때마다 이 테이블과 대조하여 현재 토큰의jti가 데이터베이스의 폐기된 토큰들과 일치하는지 확인합니다. 일치하는 항목이 있으면 해당 사용자의 요청은 거부됩니다. - Allowlist(허용 목록) 전략 — 첫 번째 전략과 어느 정도 유사하지만, JWT ID를 저장하는 테이블이 사용자 토큰을 저장하는 다른 테이블과 일대다 관계를 맺고 있습니다. 요청이 들어올 때마다 Allowlist 테이블에 저장된 사용자의
jti를 토큰 매칭 테이블의 값과 대조합니다. 일치하는 항목이 있을 때만 접근이 허용됩니다.
물론 이것은 토큰 폐기에 대한 매우 간소화된 개요이며, 더 자세한 내용은 관련 문서에서 확인할 수 있습니다.
JWT 토큰 서명 키 설정하기
사용자와 그들의 요청을 인증하는 데 보안 토큰을 사용할 것이므로, 토큰에 서명할 방법이 필요합니다. 바로 이때 시크릿 키가 필요합니다. Rails의 시크릿 키인 secret_key_base와는 다른 새로운 키를 생성하는 것이 권장됩니다.
bundle exec rake secret을 실행하여 고유한 키를 생성한 후, Devise 초기화 파일에 이 키를 포함시킵니다:
마지막으로 컨트롤러를 설정하겠습니다.
컨트롤러 설정하기
Devise를 API 인증에 적용하는 마지막 단계는 컨트롤러 설정입니다. 간단하게 하기 위해 두 개의 컨트롤러를 설정하겠습니다. 하나는 회원가입(registration)을 처리하고, 다른 하나는 세션(sessions)을 담당합니다.
먼저 회원가입 컨트롤러부터 직접 만들어 보겠습니다:
이 컨트롤러의 동작 방식은 다음과 같습니다:
- JSON으로 요청에 응답하도록 설정합니다.
- 회원가입 성공 또는 실패 결과를 반환하는
respond_with액션을 지정합니다.
이제 세션 컨트롤러입니다:
회원가입 컨트롤러와 마찬가지로 이 컨트롤러도 JSON으로 응답하도록 지정합니다. 또한 사용자가 성공적으로 로그인했을 때를 위한 respond_with 액션과, 사용자가 로그아웃할 때를 처리하는 respond_to_on_destroy를 정의합니다.
여기까지 완료하셨다면, Devise와 JWT 토큰으로 구동되는 API 인증 플로우가 완성된 것입니다!
마지막 섹션에서는 Devise와 Authtrail을 사용하여 사용자 로그인을 추적하는 방법을 간단히 살펴보겠습니다.
Authtrail로 Devise 로그인 추적하기
누군가 사용자 계정에 로그인할 때마다 IP 주소와 로그인 시간 등의 정보를 담아 알림 이메일을 보내고 싶다고 가정해 봅시다. 어떻게 구현할 수 있을까요?
사용자 로그인을 추적한 다음, 그 정보를 알림 이메일에 활용해야 합니다. 하지만 먼저 로그인을 추적할 방법이 필요합니다. 이것은 Authtrail이라는 훌륭한 젬으로 구현할 수 있으며, Devise와도 잘 연동됩니다.
Authtrail 설치하기
먼저 bundle add authtrail 명령으로 젬을 설치합니다. 추가로, 이메일과 IP 주소 같은 사용자 식별 정보를 앱 데이터베이스에 저장하게 되므로, 운영 환경에서는 Lockbox와 Blindindex 젬을 조합하여 이 데이터를 암호화하는 것이 강력히 권장됩니다.
다음으로 Authtrail 제너레이터를 실행하여 초기화 파일과 로그인 데이터를 저장할 마이그레이션 테이블을 생성합니다:
Authtrail의 작동 원리
사용자가 로그인을 시도할 때마다 다음과 같은 중요 정보를 담은 새 Authtrail 레코드가 생성됩니다:
- 로그인에 사용된 이메일 주소
- 로그인 성공 여부
- 로그인 실패 시 그 실패 이유
- 사용자의 IP 주소,
referrer등 훨씬 더 많은 정보
이 정보를 원하는 대로 활용할 수 있습니다. 예를 들어, 사용자에게 이메일과 IP 주소 정보를 포함한 알림 이메일을 보내 계정에 로그인 시도가 있었음을 알려줄 수 있습니다.
Authtrail 문서를 살펴보면 활용 가능한 모든 기능을 확인할 수 있습니다.
마치며
이 시리즈에서는 Devise 젬을 깊이 있게 다루었습니다.
첫 번째 파트에서는 모듈, 헬퍼, 뷰, 컨트롤러, 라우트의 작동 방식 등 Devise의 기본기를 다졌습니다. 이번 두 번째이자 마지막 파트에서는 Devise를 OAuth, Authtrail, API 인증과 함께 사용하는 방법을 살펴보았습니다.
이 시리즈가 Devise 인증과 관련된 모든 작업에 유용한 가이드가 되기를 바랍니다.
즐거운 코딩 되세요!
P.S. Ruby Magic의 글을 발행 즉시 읽고 싶으시다면 Ruby Magic 뉴스레터를 구독하고 어떤 글도 놓치지 마세요!