Junhee Park

Rails has_secure_password로 안전한 인증 구현하기

Rails ActiveModel에는 has_secure_password라는 메소드가 존재하는데, 이 메소드를 사용하면 Rails에서 인증 기능을 쉽게 구현할 수 있다.

has_secure_password를 사용하기 위해서는 먼저 암호화 라이브러리인 bcrypt를 설치해야 한다. Rails 프로젝트를 생성하면 기본적으로 주석 처리된 상태로 추가되어 있기 때문에 주석을 해제하고 bundle install을 실행하면 된다.

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
gem "bcrypt", "~> 3.1.7"

이제 모델에 has_secure_password를 추가하고, 암호화된 비밀번호 값을 저장하기 위한 password_digest 컬럼을 테이블에 추가해준다.

class User < ApplicationRecord
  has_secure_password
end
class AddPasswordDigestToUsers < ActiveRecord::Migration[8.1]
  def change
    add_column :users, :password_digest, :string
  end
end

이제 has_secure_password가 제공하는 몇 가지 유용한 메소드를 사용해서 비밀번호 인증을 구현하면 끝이다. 크게 회원가입, 로그인, 비밀번호 재설정 세 가지 사용 케이스로 나누어볼 수 있는데 각 케이스에 대한 사용방법을 정리하면 아래와 같다:

회원가입

User.create(email:, password:)로 새로운 사용자를 생성한다. 실제 테이블에는 password_digest 컬럼만 존재하지만 has_secure_passwordpassword= 메소드를 추가해 가상 속성(attribute)으로 사용할 수 있도록 해준다. password에 값을 지정하면 bcrypt를 통해 암호화한 후 password_digest에 저장하는 방식으로 동작한다.

비밀번호 확인값을 받으려는 경우, User.create(password:, password_confirmation:)과 같이 password_confirmation을 넘기면 passwordpassword_confirmation의 값이 일치하는지 확인한다.

로그인

user.authenticate(password) 또는 user.authenticate_password(password)로 비밀번호가 일치하는지 검증할 수 있다.

다만 실제 로그인 기능을 구현할 때는 has_secure_password가 제공하는 기능은 아니지만 ActiveRecord가 제공하는 authenticated_by를 사용하는 것이 보안상 더 유리하다. 입력값이 nil이거나 빈 문자열인 경우 아예 쿼리를 수행하지 않는 검증 로직을 포함하므로 불필요한 쿼리를 줄일 수 있다.

또한 타이밍 기반 열거 공격(timing-based enumeration attack)을 방지할 수 있는데, 검증하려는 레코드가 존재하지 않는 경우 결과를 바로 리턴해버리면 실제 레코드가 존재하는 경우 비밀번호를 해싱하는데 걸리는 시간으로 인해 응답 시간에 차이가 발생하게 된다. 따라서 공격자는 어떤 레코드가 DB에 존재하는지 유추할 수 있게 된다. 이 문제를 방지하기 위해 레코드가 존재하지 않더라도 입력된 비밀번호를 무조건 한 번 해싱하는 작업을 진행해 응답 시간을 레코드 유무에 상관없이 일관되게 유지해준다.

비밀번호 재설정

비밀번호 재설정은 두 가지 경우를 생각해볼 수 있다:

  1. 현재 비밀번호를 알고 있는 상태에서 다른 비밀번호로 변경하려는 경우
  2. 현재 비밀번호를 알지 못해서 비밀번호를 재설정하려는 경우

첫 번째의 경우, has_secure_password가 제공하는 password_challenge 속성을 사용하면 된다. user.update(password: "new_password", password_challenge: "current_password")와 같이 변경하려는 비밀번호는 password 속성으로, 현재 비밀번호는 password_challenge 속성으로 전달하면 실제 비밀번호 변경을 수행하기 전에 현재 비밀번호가 입력된 password_challenge와 일치하는지를 검증한다.

비밀번호를 알지 못해서 비밀번호를 재설정하는 경우 has_secure_passwordpassword_reset_token 메소드를 제공한다. user.password_reset_token으로 일정 시간(기본 15분)의 유효 기간을 가지는 토큰을 발급받을 수 있으며, User.find_by_password_reset_token(token)으로 입력된 토큰이 유효한 경우에만 레코드를 반환하도록 할 수 있다. 따라서 사용자가 올바른 토큰을 전달했을 때에만 사용자가 요청한 새로운 비밀번호로 해당 레코드를 업데이트할 수 있다. password_reset_tokenpassword_digest에 저장된 salt 값을 포함하여 생성되기 때문에 사용자가 토큰을 발급한 이후 비밀번호를 변경하면 토큰이 자동으로 무효화된다.


has_secure_password를 사용하면 Rails에서 별도의 라이브러리나 인증 서비스를 사용하지 않고도 쉽게 비밀번호 인증을 구현할 수 있다. 물론 실제 서비스에서는 세션 관리, OAuth 인증, 2FA 등 더 많은 기능을 필요로 하는 경우가 많다. 하지만 간단한 인증만 필요하거나 인증 로직을 직접 제어하고 싶다면 has_secure_password만으로도 충분히 안전하고 효과적인 인증 시스템을 구축할 수 있다.