Autoryzacja przez GitHub

Jednym z kluczowych założeń portalu apki.org jest nauka (a wręcz wbijanie do głowy) idei open source oraz programowania "społecznościowego". Patrząc na niesamowity trend wyznaczony przez portal GitHub można zauważyć że otwarty kod to (w większości przypadków) kod żywy i dynamicznie rozwijany, nawet gdy pierwotny twórca przestanie się nim interesować. Następnym powodem do otwierania swojego kodu jest podejście do programowania w kwestii biznesowej. Każdy programista na pewnym etapie swojej kariery musi sobie uświadomić ważną rzecz.

Kod nie jest produktem za który programista dostaje wynagrodzenie. Programowanie jest usługą!

Gdy już ta idea stanie się dla nas zrozumiała i oczywista to dzielenie kodu ze społeczeństwem będzie działaniem naturalnym i wręcz odruchowym.

Co z tego wynika?

Zatem, chcemy nauczyć użytkownika obsługi narzędzia git oraz portalu GitHub. Co jest pierwszym krokiem w tym przypadku? Oczywiście założenie konta. To jest pierwszy i podstawowy powód dla którego apki.org nie posiada własnego systemu użytkowników. Autoryzacja na naszym serwisie bazuje w pełni na serwisie GitHub. Jedyne dane które przechowujemy to:

  • email
  • username na portalu GitHub
  • Pełna nazwa użytkownika GitHub (jeżeli została podana)
  • uid użytkownika GitHub
  • Hash urls (czyli url'e które użytkownik zdefiniował na portalu GitHub)

Jak widać na temat użytkownika nie przechowujemy żadnych informacji poza tymi podanymi nam przez logowanie GitHub. I nic też więcej nie potrzebujemy.

Wady

Oczywiście, git nie jest narzędziem prostym. Korzystanie z GitHub w sporej części także wymaga wiedzy z git'a, aczkolwiek podstawowe czynności są w sporym stopniu uproszczone. Dlatego też jedynym kursem który można odbyć bez posiadania konta będzie kurs odnośnie rejestracji na portalu GitHub.

Jak to zrobiliśmy u nas

Autoryzacja GitHub jest w zasadzie bardzo prostą operacją. Jedyne co musieliśmy zrobić to stworzyć aplikację na portalu GitHub i zdefiniować jej adresy do odpowiedzi zwrotnych. W zamian otrzymaliśmy ClientID oraz SecretID. Te 2 ciągi znaków pozwolą nam korzystać zdanie z wszystkich funkcji api portalu GitHub.

Ciągi te musimy zapisać do konfiguracji gema omniauth-github w pliku omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do  
  provider :github, ENV['GITHUB_KEY'], ENV['GITHUB_SECRET'], scope: 'user:email'
end  

omniauth.rb

Klucze pobieramy ze zmiennych środowiskowych GITHUB_KEY oraz GITHUB_SECRET. Dzięki temu możemy wystawić naszą aplikację jako open source nie narażając informacji poufnych na kradzież oraz wykorzystanie. Natomiast parametr scope określa wszystkie uprawnienia których nasza aplikacja będzie żądać od użytkownika. Wartość user:email sprawia że nasza aplikacja żąda tylko i wyłącznie podstawowych informacji autoryzacyjnych.

nowa aplikacja github

Jak widzimy na screenie naszym adresem do opowiedzi zwrotnej jest ciąg /auth/github/callback. W związku z tym musimy zdefiniować route który obsłuży nam to żądanie. Wygląda to następująco:

# routes.rb
Rails.application.routes.draw do  
  get '/auth/:provider/callback' => 'sessions#create'
  get '/signout' => 'sessions#destroy', :as => :signout
  # inne routingi
end  

routes.rb

Widać tutaj że oba routingi odwołują się tutaj do kontrolera Sessions. Tak wygląda jego treść:

class SessionsController < ApplicationController  
  def create
    auth = request.env['omniauth.auth']
    if User.where(provider: auth['provider'], uid: auth['uid']).exists?
      user = User.find_by(provider: auth['provider'], uid: auth['uid'])
    else
      user = User.create_with_omniauth(auth)
    end
    session[:user_id] = user.id
    redirect_to root_url, :notice => 'Zalogowano!'
  end

  def destroy
    session[:user_id] = nil
    redirect_to root_url, :notice => 'Wylogowano!'
  end
end  

sessions_controller.rb

Akcja destroy w zasadzie jedyne co robi to czyści zawartość zmiennej sesyjnej :user_id, dzięki czemu aplikacja wie że użytkownik został wylogowany. To co jest ciekawsze to treść akcji create.

Pod zmienną request.env['omniauth.auth'] kryją się dane zwrócone przez portal GitHub na żądanie logowania. 2 klucze które nas interesują na tym etapie to provider oraz uid.

  • provider - na obecnym etapie i przy naszych założeniach klucz ten może zostać pominięty. Jedyne co oznacza to jaki provider został użyty do autoryzacji. Zatem na chwilę obecną można przyjąć wartość tylko i wyłącznie github. Jednak nie wykluczamy większej ilości providerów w przyszłości (np. bitbucket). Z tego powodu na wszelki wypadek sobie tą informację zapisujemy.
  • uid - liczbowy identyfikator użytkownika GitHub. Raczej nie musimy wyjaśniać do czego ta wartość służy.

if User.where(provider: auth['provider'], uid: auth['uid']).exists? Jeżeli użytkownik o zwrócony przez GitHub uid istnieje to wystarczy że pobierzemy go z bazy danych a jego id wpiszemy do sesji. Natomiast jeżeli nie istnieje to należy go stworzyć. Za tą operację odpowiada metoda create_with_omniauth modelu User.

class User  
  include Mongoid::Document

  validates :nickname, :uid, :account_type, presence: true

  field :nickname, type: String
  field :email, type: String
  field :name, type: String
  field :account_type, type: Symbol # :student :teacher :moderator :admin
  field :uid, type: String
  field :image, type: String
  field :urls, type: Hash
  field :provider, type: String

  def self.create_with_omniauth(auth)
    create! do |user|
      user.provider = auth['provider']
      user.uid = auth['uid']
      user.name = auth['info']['name']
      user.email = auth['info']['email']
      user.image = auth['info']['image']
      user.nickname = auth['info']['nickname']
      user.urls = auth['info']['urls']
      user.account_type ||= :student
    end
  end
end  

User.rb

Metoda create_with_omniauth jest bardzo prosta. Przypisuje dane zwrócone przez GitHub do odpowiadających im pól w naszej bazie danych. Jedyną ciekawostką jest tu konstrukcja user.account_type ||= :student. Oznacza ona że kontu zostanie przypisana rola :student (reprezentująca ucznia lub zwykłego użytkownika).

Teraz posiadamy już w bazie wszystkie potrzebne nam informacje na temat użytkownika. Zatem jedyne czego nam teraz brakuje to możliwości pobrania danych aktualnie zalogowanego użytkownika. Realizujemy to poprzez helper_method w podstawowym kontrolerze Application z którego dziedziczą wszystkie pozostałe.

class ApplicationController < ActionController::Base  
  helper_method :current_user

  private
  def current_user
    @current_user ||= User.find(session[:user_id]['$oid']) if session[:user_id]
  end
end

application_controller.rb

Metoda current_user będzie nam teraz zwracać aktualnie zalogowanego użytkownika lub nil jeżeli będzie brak sesji. Warto zauważyć w metodzie find został wykorzystany nietypowy sposób dostępu do klucza id [:user_id]['$oid']. Związane jest to z tym, że klucz podstawowy id w bazie MongoDB jest obiektem, którego klucz $oid przechowuje ciąg znaków wymagany przez metodę find.

I to wszystko, teraz aby zalogować użytkownika wystarczy przekierować go na adres /auth/github

Nekromancer

Programista z zawodu i zamiłowania. W fundacji zajmuje się głównie rozwiązaniami backendowymi oraz aplikacjami mobilnymi. Współtwórca portalu apki.org oraz mentor na forum

Jastrzębie-Zdrój http://ownvision.pl/