Skip to content

Rails 8

Rails 8 + omniauth 스타일을 쓰지 않고 직접 OAuth 클라이언트를 구현합니다 (의존성 최소화).

시작 전 체크

프로덕션 배포 시 LOGI_API_URL / LOGI_CLIENT_ID / LOGI_CLIENT_SECRET 3개를 호스팅 플랫폼 env에 모두 주입해야 합니다. 누락되면 코드는 localhost:3000으로 fallback해서 prod에서 연결 실패합니다 — 가장 흔한 실패 케이스입니다. 상세: Troubleshooting

ruby
# app/controllers/sessions_controller.rb
class LogiSessionsController < ApplicationController
  def start
    verifier = SecureRandom.urlsafe_base64(32).delete("=")
    challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier), padding: false)
    state = SecureRandom.hex(16)

    session[:logi_pkce] = verifier
    session[:logi_state] = state

    redirect_to "#{ENV['LOGI_API_URL']}/oauth/authorize?" + {
      client_id: ENV["LOGI_CLIENT_ID"],
      redirect_uri: logi_callback_url,
      response_type: "code",
      scope: "profile email",
      state: state,
      code_challenge: challenge,
      code_challenge_method: "S256",
    }.to_query, allow_other_host: true
  end

  def callback
    return render_error("state mismatch") if params[:state] != session[:logi_state]

    res = Net::HTTP.post_form(
      URI("#{ENV['LOGI_API_URL']}/oauth/token"),
      grant_type: "authorization_code",
      code: params[:code],
      redirect_uri: logi_callback_url,
      code_verifier: session[:logi_pkce],
      client_id: ENV["LOGI_CLIENT_ID"],
      client_secret: ENV["LOGI_CLIENT_SECRET"]
    )
    tokens = JSON.parse(res.body)

    session.delete(:logi_pkce); session.delete(:logi_state)
    cookies.signed.permanent[:logi_rt] = {
      value: tokens["refresh_token"], httponly: true, secure: Rails.env.production?, same_site: :strict
    }
    redirect_to root_path
  end
end

JWT 검증:

ruby
gem "jwt"

jwks = JSON.parse(Net::HTTP.get(URI("#{ENV['LOGI_API_URL']}/.well-known/jwks.json")))
payload, = JWT.decode(
  access_token, nil, true,
  algorithms: ["RS256"],
  jwks: JWT::JWK::Set.new(jwks),
  iss: "logi", verify_iss: true,
  aud: ENV["LOGI_CLIENT_ID"], verify_aud: true
)

Scope 환경변수화 (권장)

위 예시는 scope: "profile email" 가 하드코딩돼 있습니다. 실제 운영에서는 RP 별로 요구 scope 가 다르므로 env 로 분리하세요:

ruby
scope: ENV.fetch("LOGI_SCOPES", "openid profile email")

표준 조합:

  • 일반 SSO: openid profile email
  • 식별번호 게이팅 포함: openid profile:basic email
  • ID Token 만 사용 (서버에서 토큰 검증 안 함): openid

Scope 는 logi 앱 등록 시 allowed_scopes 와 일치해야 합니다 — 등록되지 않은 scope 를 요청하면 invalid_scope. 자세한 매트릭스: Scope 레퍼런스.

Post-deploy 검증 (가장 자주 빠지는 단계)

배포 직후 브라우저로 로그인 흘러 들어가기 전에, CLI 한 줄로 env 누락을 잡아냅니다:

bash
curl -sIL "https://yourapp.com/auth/logi/start" | grep -iE "^(HTTP|location)"

기대 출력:

HTTP/2 302
location: https://api.1pass.dev/oauth/authorize?client_id=logi_xxx&...&code_challenge=...&state=...

체크 포인트:

  1. status 302 — initiate 라우트 자체가 살아있는지
  2. location host = api.1pass.devLOGI_API_URL 누락 시 localhost:3000 이 박혀서 옴 (가장 흔한 사고)
  3. client_id 쿼리LOGI_CLIENT_ID 누락 시 빈 값 또는 누락
  4. scope 쿼리LOGI_SCOPES 누락 시 코드 default 사용

자동화 (CI/배포 후 smoke test):

bash
curl -sI "https://yourapp.com/auth/logi/start" | grep -i "^location:" | grep -q "api.1pass.dev" \
  && echo "✓ logi env OK" || { echo "✗ LOGI_API_URL 누락 — env 점검"; exit 1; }

302 만 봐선 안 됩니다

status 302 만 체크하면 localhost:3000 으로 떨어지는 케이스를 못 잡습니다. 반드시 location 의 host 까지 확인 하세요. 실제 운영 사고 다수가 이 단계에서 발생.

기존 앱에 RP 를 추가하는 절차 + 트러블슈팅: Troubleshooting.

MIT License · Identity가 제품의 신뢰를 만듭니다.