이제 우리는 OAuth 2 protocol의 도움을 받아 소셜 네트워크를 통한 인증에 대해 논의할 것이다. OmniAuth와 그것의 네가지 전략: Twitter, Facebook, Google+ 그리고 LinkedIn, 에 대해 논의할 것이다. 이것은 사용자들이 그들이 좋아하는 네트워크를 가지고 로그인할 수 있도록 한다. 나는 각각의 전략을 어떻게 통합하고 셋팅하고 에러를 다룰지 알려줄 것이다.
이 문서의 소스 코드는 GitHub 에 있다.
동작하는 데모는 sitepoint-oauth2.herokuapp.com 이다.
OAuth 2 and OmniAuth
OAuth2 는 써드 파티 앱이 HTTP 서비스에 대한 제한된 액세스를 얻을 수 있도록 하는 인증 프로토콜이다. 이 프로토콜의 주요 측면들 중 하나는 엡으로 부터 발행된 access token 이다. 이 토큰은 앱이 사용자를 대신하여 다양한 액션을 수행하도록 하는데 사용된다. 그러나 승인받지 못한 행위는 할 수 없다(예를 들어, 사용자는 오직 앱이 친구들에 대한 정보만 받아가고 담벼락에 글을 게시하지는 못하게 할 수 있다).
써드 파티 앱이 당신의 패스워드에 접근할 수 없는 상황에서 굉장한 것이다. 써드 파티 앱은 얼마간의 시간 뒤에는 만료되는 특별한 토큰을 받을 뿐이다(대체로, 사용자들은 또한 직접 액세스를 철회할 수도 있다) 그리고 앱은 스코프라고 불리는 승인된 태스크의 리스트를 수행하는 데만 사용될 수도 있다.
써드 앱은 어떻게 확인할 수 있을까? 소셜 네트워크에 대해 얘기하자면 앱은 먼저 등록되어야 한다. 개발자는 사용자들에게 노출될 앱의 URL, name, contact data, 그리고 다른 정보들을 제공한다. 그러면 authorization provider(Twitter 또는 Facebook) 소셜 네트워크/프로바이더가 앱을 확인할 수 있는 키쌍을 발생한다.
여기 사용자가 앱을 방문하고 소셜 네트웍을 통해 인증하고자 할 때 어떤 일이 일어나는지 간략화한 개요가 있다:
전략에 대해 표준대로 접근하는 것은 당신이 원하는 만큼 많은 것들을 이슈없이 통합할 수 있기 때문에 대단하다. 여전히, 몇몇 소셜 네트웍은 사용자를 인증하는 것에 대해 다른 데이터를 리턴할 수도 있다는 것을 기억해야 하며 당신의 앱이 어떻게 동작하는지 테스트하는 것이 절대적으로 필요하다(여기서 몇몇 예를 보여줄 것이다).
만약 앱에 OAuth2를 통합해본 적이 없다면 이 모든 것이 복잡해 보일 것이다. 그러나 걱정하지 마라, 곧 매우 분명해질 것이고 예제로 배우는 것이 좋다. 그러면 코드로 뛰어들어 무언가 고급스러운 것을 만들어보자.
이 데모를 위해 Rails 4 가 사용되었다. 그러나 Rails 3도 잘 동작할 것이다.
앱을 스타일링하기 위해 Bootstrap을 연결하자(optional):
Gemfile
그리고 다음을 실행하자:
필요한 파일들을 임포트한다:
stylesheets/application.html.erb
레이아웃을 변경하자:
layouts/application.html.erb
이 코드는 메인 메뉴(아직은 아무것도 없다)와 flash message를 렌더링할 영역을 셋업한다.
root page를 생성해보자:
config/routes.rb
pages_controller.rb
views/pages/index.html.erb
Gemfile
그리고 다음을 실행하자
모든 OmniAuth 전략들에 대한 설정은 omniauth.rb initializer 파일에 있다. 그러므로 파일을 생성하자:
config/initializers/omniauth.rb
새로운 전략(프로바이더)를 등록하는 것은 프로바이더에 대한 앱을 확인하는 키 페어를 제공하는 것이다. 물론, 우리는 이 두 키를 습득할 필요가 있다. 다음을 수행해보자:
이제 Callback URL로 돌아가보자. 이것은 사용자가 인증이 성공하고 인증이 승인된 후(요청은 또한 사용자의 데이터와 토큰을 포함하고 있을 것이다) 앱 내부로 리다이렉트될 URL이다. 모든 OmniAuth 전략들은 initializer에 리스트된 것처럼 "/auth/:provider/callback" 과 동일한 형식의 callback URL을 기대한다. : provider 는 전략의 이름이 들어간다("twitter", "facebook", "linkedin", 등등.)
이 정보와 함께, 대응되는 routes를 셋팅해보자.
config/routes.rb
그리고 메인 메뉴에 첫 링크를 추가해보자.
layouts/application.html.erb
/auth/twitter route 는 전략에 의해 제공되고 트위터 로긴 페이지로 리다이렉트된다.
sessions_controller.rb
request.env['omniauth.auth'] 는 사용자에 대한 모든 데이터를 가지고 있는 Authentication Hash를 포함하고 있다.
서버를 다시 읽어들이고 트위터를 통해 인증을 시도해보자. 이와 같은 무언가를 보게 될 것이다. 당신의 name, location, avatar 그리고 다른 기본적인 정보와 더불어, follower들 그리고 tweets count와 같은 몇몇 특별한 데이터들도 있다. 물론, 이들 데이터는 provider에 따라 바뀔 것이다(그리고 몇몇 소셜 네트워크는 디폴트로 기본적인 데이터만을 전송한다).
이 데모를 위해서 다음을 저장해보자:
우리는 실제로 사용자를 대신해서 어떤 액션을 수행하지 않을 것이기 때문에 사용자의 토큰을 저장하지는 않을 것이다. 만약 이 값을 필요로 한다면 token 과 secret 필드를 string 데이터 타입으로 생성해라(몇몇 소셜 네트웍들은 secret을 함께 제공한다). 이들 값을 API request를 서비스로 전달할 때 사용해라.
좋다. 이제 적당한 migration을 생성할 수 있다.
xxx_create_users.rb
provider 와 uid 는 null 이 될 수 없고 인덱싱 되어야 한다. add_index :users, [:provider, :uid]. unique: true 는 이들 두 필드에 clusted index를 추가한다. 그리고 그들의 조합이 unique한지 확실히 한다.
migration을 적용한다.
이제 create action을 재작성하자.
sessions_controller.rb
from_omniauth 는 아직 존재하지 않는 메소드이다. 이 메소드는 authentication hash를 파싱하고 사용자 레코드를 리턴할 것이다. 다음으로, 사용자의 id를 세션에 저장하고 메인 페이지로 리다이렉팅한다.
from_omniauth 클래스 메소드를 살펴보자.
user.rb
find_or_create_by 는 동일한 user를 여러번 생성하지 않는 것을 보장한다. 메소드는 모든 필요한 데이터를 보관하고 있고 user를 저장하고 user를 리턴한다. 만약 관심있다면, 사용자의 토큰은 일반적으로 auth_hash['credentials']['token'](secret의 경우auth_hash['credentials']['secret']) 표현으로 접근할 수 있다.
application_controller.rb
뿐만아니라 helper_method :current_user 는 뷰로부터 잘 호출될 수 있다는 것을 보장한다. 좋다. 지금은 사용자가 로그인할 수는 있지만 로그아웃 할 수는 없다. logout 링크를 추가해보자.
- 사용자가 "Login" 링크를 클릭한다.
- 사용자는 소셜 네트워크의 웹사이트로 리다이렉트된다. 앱의 데이터(client_id)가 확인을 위해 보내진다.
- 사용자는 앱의 상세사항과(이름, 로고, 설명 등) 자신을 대신해서 어떤 액션이 수행되는지 확인한다. 사용자에게 가서 다음과 같이 얘기하는 것이라고 생각하면 된다: "헤이, 내 이름은 Jack이고 나는 니가 동의한다면 너의 모든 친구 목록을 갖길 원한다. 내가 친구들에 대한 흥미로운 통계를 보여줄께"
- 만약 사용자가 이 앱을 신뢰하지 않는다면, 그들은 인증을 취소할 것이다.
- 만약 사용자가 앱을 신뢰하면 인증이 승인되고 사용자는 앱으로 다시 돌아가게 된다(콜백 URL을 통해서). 인증된 사용자의 정보와 부여받은 토큰(때때로 secret key와 함께)들도 보내진다.
OAuth2 인증을 지원하는 많은 서비스들이 있다. 그래서 인증을 생성하는 절차를 표준화하기 위해 OmniAuth 솔루션이 Intridea, Inc.에 의해 만들어졌다. 그것은 OAuth 2에서 LDAP 까지 어떤 것이든 지원한다. 이 문서에서는 추상 OAuth 2 전략인 omniauth-oauth2 에 초점을 둘 것이다. 기본적으로 그것은 상위에 인증 전략을 쉽게 갖추기 위한 건축 블록으로써 사용된다. 여기 OmniAuth가 사용가능한 모든 전략의 리스트가 있다(또한 그것은 OAuth 2와 관련되지 않은 것들도 포함한다).
전략에 대해 표준대로 접근하는 것은 당신이 원하는 만큼 많은 것들을 이슈없이 통합할 수 있기 때문에 대단하다. 여전히, 몇몇 소셜 네트웍은 사용자를 인증하는 것에 대해 다른 데이터를 리턴할 수도 있다는 것을 기억해야 하며 당신의 앱이 어떻게 동작하는지 테스트하는 것이 절대적으로 필요하다(여기서 몇몇 예를 보여줄 것이다).
만약 앱에 OAuth2를 통합해본 적이 없다면 이 모든 것이 복잡해 보일 것이다. 그러나 걱정하지 마라, 곧 매우 분명해질 것이고 예제로 배우는 것이 좋다. 그러면 코드로 뛰어들어 무언가 고급스러운 것을 만들어보자.
Preparing the Demo App
좋다, 사용자가 제시되는 소셜 네트웍 중 하나를 통해 인증할 수 있도록하는 간단한 앱을 만들어 보자. 아는 이 앱을 SocialFreak 라고 부르겠다:$ rails new SocialFreak -T
이 데모를 위해 Rails 4 가 사용되었다. 그러나 Rails 3도 잘 동작할 것이다.
앱을 스타일링하기 위해 Bootstrap을 연결하자(optional):
Gemfile
[...] gem 'bootstrap-sass' [...]
그리고 다음을 실행하자:
$ bundle install
필요한 파일들을 임포트한다:
stylesheets/application.html.erb
@import "bootstrap-sprockets"; @import "bootstrap"; @import 'bootstrap/theme';
레이아웃을 변경하자:
layouts/application.html.erb
<nav class="navbar navbar-inverse"> <div class="container"> <div class="navbar-header"> <%= link_to 'Social Freak', root_path, class: 'navbar-brand' %> </div> <div id="navbar"> <ul class="nav navbar-nav"> </ul> </div> </div> </nav> <div class="container"> <% flash.each do |key, value| %> <div class="alert alert-<%= key %>"> <%= value %> </div> <% end %> <%= yield %> </div>
이 코드는 메인 메뉴(아직은 아무것도 없다)와 flash message를 렌더링할 영역을 셋업한다.
root page를 생성해보자:
config/routes.rb
[...] root to: 'pages#index' [...]
pages_controller.rb
class PagesController < ApplicationController def index end end
views/pages/index.html.erb
<div class="jumbotron"> <h1>Welcome!</h1> <p>Authenticate via one of the social networks to get started.</p></div>
Authentication Via Twitter
Adding a Strategy
Arun Agrawal이 만든 omniauth-twitter gem을 사용해보자. 이것은 OmniAuth를 위한 수많은 전략 중의 하나이다. Gemfile에 넣어보자:Gemfile
[...] gem 'omniauth-twitter' [...]
그리고 다음을 실행하자
$ bundle install
모든 OmniAuth 전략들에 대한 설정은 omniauth.rb initializer 파일에 있다. 그러므로 파일을 생성하자:
config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET'] end
새로운 전략(프로바이더)를 등록하는 것은 프로바이더에 대한 앱을 확인하는 키 페어를 제공하는 것이다. 물론, 우리는 이 두 키를 습득할 필요가 있다. 다음을 수행해보자:
- apps.twitter.com 으로 이동하자.
- "Create new app"을 클릭한다.
- 폼을 채운다. callback URL의 경우 사이트 주소에 "/auth/twitter/callback"을 추가하여 제공한다. 만약 로컬 머신에 있다면 "http://localhost:3000/auth/twitter/callback"을 제공한다. 우리는 이 callback URL을 짧게 논의할 것이다.
- "Create"를 클릭한다.
- 당신은 트위터상의 앱의 정보 페이지로 리다이렉트될 것이다. "Keys and Access Tokens" 탭으로 이동한다.
- Consumer Key와 Consumer Secret를 복사해서 initialize file에 붙여넣는다.
- 다른 탭으로 이동할 수 있지만 이 데모는 다른 내용들을 변경하지 않을 것이다. "Permissions" 내에 사용자가 인증받은 후 앱이 어떤 액션을 수행할 수 있는지 셋팅할 수 있다. 예들 들어서 필요하면, 사용자 입장에서 트윗을 포스팅할 수 있게 하기 위해, 그에 따른 퍼미션을 변경해야 할 것이다. 이는 앞서 말한 인증의 "scope"를 제어한다.
나중에 여기에 설명된 데로 이 전략을 셋업할 수 있을 것이다. 이 데모의 경우, 기본 값들이 잘 동작할 것이다.
이제 Callback URL로 돌아가보자. 이것은 사용자가 인증이 성공하고 인증이 승인된 후(요청은 또한 사용자의 데이터와 토큰을 포함하고 있을 것이다) 앱 내부로 리다이렉트될 URL이다. 모든 OmniAuth 전략들은 initializer에 리스트된 것처럼 "/auth/:provider/callback" 과 동일한 형식의 callback URL을 기대한다. : provider 는 전략의 이름이 들어간다("twitter", "facebook", "linkedin", 등등.)
이 정보와 함께, 대응되는 routes를 셋팅해보자.
config/routes.rb
[...] get '/auth/:provider/callback', to: 'sessions#create' [...]
그리고 메인 메뉴에 첫 링크를 추가해보자.
layouts/application.html.erb
[...] <ul class="nav navbar-nav"> <li><%= link_to 'Twitter', '/auth/twitter' %></li> </ul> [...]
/auth/twitter route 는 전략에 의해 제공되고 트위터 로긴 페이지로 리다이렉트된다.
Authentication Hash
SessionsController 의 create 메소드는 user data를 파싱하고 그것을 데이터베이스에 저장할 것이다. 그리고 앱에 대한 sign in을 수행한다. 그런데 user data는 어떻게 보일까? 체크해보는 것은 쉽다:sessions_controller.rb
class SessionsController < ApplicationController def create render text: request.env['omniauth.auth'].to_yaml end end
request.env['omniauth.auth'] 는 사용자에 대한 모든 데이터를 가지고 있는 Authentication Hash를 포함하고 있다.
서버를 다시 읽어들이고 트위터를 통해 인증을 시도해보자. 이와 같은 무언가를 보게 될 것이다. 당신의 name, location, avatar 그리고 다른 기본적인 정보와 더불어, follower들 그리고 tweets count와 같은 몇몇 특별한 데이터들도 있다. 물론, 이들 데이터는 provider에 따라 바뀔 것이다(그리고 몇몇 소셜 네트워크는 디폴트로 기본적인 데이터만을 전송한다).
Saving User Data
이제 authentication hash가 어떻게 보이는지 그리고 어떤 데이터들을 가져올 수 있는지 알았다. 그러면 어떤 정보들을 저장해야할지 결정할 시간이다. 우리는 multi-provider authentication을 만들고자 하므로 트위트 카운트와 같은 정보를 저장하는 것은 최고의 우선순위를 갖는 것은 아니다(그러나, 당신은 단지 사용자의 authentication hash "extra" 부분을 직렬화해서 분리된 필드에 저장할 수 있다).이 데모를 위해서 다음을 저장해보자:
- 프로바이더의 이름, 이르는 다수의 프로파이더를 가질 것이므로 이 정보는 필수적으로 필요하다.
- 사용자의 유일한 identifier. 이 identifier는 소셜 네트워크에 의해 생성되고 다양한 심폴을 포함할 수 있다. 다른 네트웍들은 숫자와 문자를 모두 사용하는 반면 어떤 네트웍들은 오직 숫자만을 사용할 수 있다. 프로바이더의 이름과 uID의 조합은 앱 내부에서 사용자를 유일하게 확인할 수 있도록 할것이다.
- 사용자의 전체 이름. 어떤 네트웍들은 또한 이름과 성을 분리해서 제공한다(그리고 트위터와 같은 몇몇 프로바이더들은 또한 분리되니 이름과 nickname을 제공하기도 한다). 그러나 우리는 그러한 복잡성은 필요로하지 않는다.
- 사용자의 위치. 모든 네트웍들이 이를 제공하는 것은 아니다. 그러나 어떤 네트웍이 제공하고 어떤 포맷으로 제공하는지 알아보자.
- 사용자의 avatar URL. 우리는 메인 메뉴에 사용자의 아바타를 제공할 것이다. 그러므로 그것은 매우 작아야 한다. 다행히도 대부분의 소셜 네트웍들은 당신이 여러 사용가능한 사이즈들 중에 고를수 있도록 하고 있다. 트위터의 경우 이를 제어하기 위해(기본은 48x48이고 이것은 우리에게 정확히 알맞다) image_size 옵션을 사용해라. 또한 디폴트로 아바타 URL은 http를 사용한다는 것을 주지해라. 만약 당신이 당신 페이지의 모든 것에 https를 사용하기 원한다면, 많은 소셜 네트웍들이 이를 지원한다. 예를 들어, 트위터는 secure_image_url을 true로 셋팅해야 한다.
- 사용자의 프로파일 URL. 모든 소셜 네트웍은 이것을 제공한다. 그러나 많은 경우에 urls key 는 다음과 같이 중첩된 hash를 갖는다:
:urls => {
:Website => 'http://example.com',
:Twitter => "https://twitter.com/xxx"
}
우리는 실제로 사용자를 대신해서 어떤 액션을 수행하지 않을 것이기 때문에 사용자의 토큰을 저장하지는 않을 것이다. 만약 이 값을 필요로 한다면 token 과 secret 필드를 string 데이터 타입으로 생성해라(몇몇 소셜 네트웍들은 secret을 함께 제공한다). 이들 값을 API request를 서비스로 전달할 때 사용해라.
좋다. 이제 적당한 migration을 생성할 수 있다.
xxx_create_users.rb
[...] t.string :provider, null: false t.string :uid, null: false add_index :users, :provider add_index :users, :uid add_index :users, [:provider, :uid], unique: true [...]
provider 와 uid 는 null 이 될 수 없고 인덱싱 되어야 한다. add_index :users, [:provider, :uid]. unique: true 는 이들 두 필드에 clusted index를 추가한다. 그리고 그들의 조합이 unique한지 확실히 한다.
migration을 적용한다.
$ rake db:migrate
이제 create action을 재작성하자.
sessions_controller.rb
[...] def create begin @user = User.from_omniauth(request.env['omniauth.auth']) session[:user_id] = @user.id flash[:success] = "Welcome, #{@user.name}!" rescue flash[:warning] = "There was an error while trying to authenticate you..." end redirect_to root_path end [...]
from_omniauth 는 아직 존재하지 않는 메소드이다. 이 메소드는 authentication hash를 파싱하고 사용자 레코드를 리턴할 것이다. 다음으로, 사용자의 id를 세션에 저장하고 메인 페이지로 리다이렉팅한다.
from_omniauth 클래스 메소드를 살펴보자.
user.rb
[...] class << self def from_omniauth(auth_hash) user = find_or_create_by(uid: auth_hash['uid'], provider: auth_hash['provider']) user.name = auth_hash['info']['name'] user.location = auth_hash['info']['location'] user.image_url = auth_hash['info']['image'] user.url = auth_hash['info']['urls']['Twitter'] user.save! user end end [...]
find_or_create_by 는 동일한 user를 여러번 생성하지 않는 것을 보장한다. 메소드는 모든 필요한 데이터를 보관하고 있고 user를 저장하고 user를 리턴한다. 만약 관심있다면, 사용자의 토큰은 일반적으로 auth_hash['credentials']['token'](secret의 경우auth_hash['credentials']['secret']) 표현으로 접근할 수 있다.
Current User and Logging Out
우리는 사용자가 로그인 했는지 로그아웃 했는지 알아낼 방법이 필요하다. current_user 는 편리한 방법으로 사용자 레코드를 리턴하거나 nil을 리턴하는 메소드이다.application_controller.rb
[...] private def current_user @current_user ||= User.find_by(id: session[:user_id]) end helper_method :current_user [...]
뿐만아니라 helper_method :current_user 는 뷰로부터 잘 호출될 수 있다는 것을 보장한다. 좋다. 지금은 사용자가 로그인할 수는 있지만 로그아웃 할 수는 없다. logout 링크를 추가해보자.
댓글 없음:
댓글 쓰기