From 252250e731a15ea9eff89b97dfd78063b6774da6 Mon Sep 17 00:00:00 2001 From: Katie Delfin Date: Wed, 19 Aug 2015 21:03:13 -0400 Subject: [PATCH] Add authentication using Google OAuth - Add users table - Whitelist user email domains - Add environment variable configuration --- .env.example | 16 ++++++++++ .gitignore | 1 + Gemfile | 4 ++- Gemfile.lock | 27 +++++++++++++++++ app/controllers/application_controller.rb | 31 +++++++++++++++++++ app/controllers/sessions_controller.rb | 33 ++++++++++++++++++++ app/models/user.rb | 37 +++++++++++++++++++++++ config/initializers/omniauth.rb | 3 ++ config/routes.rb | 6 ++++ db/schema.rb | 12 +++++++- 10 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 app/controllers/sessions_controller.rb create mode 100644 config/initializers/omniauth.rb diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5180d1e --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Orientation configuration +# +# set this to 'codeschool.com:pluralsight.com' to only authorize +# emails from these two domains to sign in to Orientation +APP_DOMAIN=compliments.dev +EMAIL_WHITELIST= + +# Google OAuth 2 +# +# Create a new project on Google's API console here: https://console.developers.google.com/ +# Then enable the Google+ and Contacts APIs for this project. +# +# e.g. 831a244758x7.apps.googleusercontent.com +GOOGLE_KEY= +# e.g. 5Ac5Fcigyty0tRO6b4c4Zh4E +GOOGLE_SECRET= diff --git a/.gitignore b/.gitignore index 5b61ab0..d372318 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /log/* !/log/.keep /tmp +.env diff --git a/Gemfile b/Gemfile index a72e420..a0a6c38 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,9 @@ gem 'jbuilder', '~> 2.0' # bundle exec rake doc:rails generates the API under doc/api. gem 'sdoc', '~> 0.4.0', group: :doc +gem 'omniauth-google-oauth2' +gem 'dotenv-rails' + # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' @@ -42,4 +45,3 @@ group :development, :test do # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' end - diff --git a/Gemfile.lock b/Gemfile.lock index e6d2d61..0f690bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -49,10 +49,17 @@ GEM execjs coffee-script-source (1.9.1.1) debug_inspector (0.0.2) + dotenv (2.0.2) + dotenv-rails (2.0.2) + dotenv (= 2.0.2) + railties (~> 4.0) erubis (2.7.0) execjs (2.6.0) + faraday (0.9.1) + multipart-post (>= 1.2, < 3) globalid (0.3.6) activesupport (>= 4.1.0) + hashie (3.4.2) i18n (0.7.0) jbuilder (2.3.1) activesupport (>= 3.0.0, < 5) @@ -62,6 +69,7 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (1.8.3) + jwt (1.5.1) loofah (2.0.3) nokogiri (>= 1.5.9) mail (2.6.3) @@ -70,8 +78,25 @@ GEM mini_portile (0.6.2) minitest (5.8.0) multi_json (1.11.2) + multi_xml (0.5.5) + multipart-post (2.0.0) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) + oauth2 (1.0.0) + faraday (>= 0.8, < 0.10) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (~> 1.2) + omniauth (1.2.2) + hashie (>= 1.2, < 4) + rack (~> 1.0) + omniauth-google-oauth2 (0.2.6) + omniauth (> 1.0) + omniauth-oauth2 (~> 1.1) + omniauth-oauth2 (1.3.1) + oauth2 (~> 1.0) + omniauth (~> 1.2) pg (0.18.2) rack (1.6.4) rack-test (0.6.3) @@ -142,8 +167,10 @@ PLATFORMS DEPENDENCIES byebug coffee-rails (~> 4.1.0) + dotenv-rails jbuilder (~> 2.0) jquery-rails + omniauth-google-oauth2 pg rails (= 4.2.3) sass-rails (~> 5.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d83690e..ba8f826 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,4 +2,35 @@ class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception + before_filter :authenticate_user! + + private + + def current_user + @current_user ||= User.find(session[:user_id]) if session[:user_id].present? + end + helper_method :current_user + + def user_signed_in? + current_user.present? + end + helper_method :user_signed_in? + + def authenticate_user! + if current_user + true + else + session["return_to"] ||= request.url + redirect_to login_path unless login_redirect? or oauth_callback? + end + end + helper_method :authenticate_user! + + def login_redirect? + request.path == login_path + end + + def oauth_callback? + request.path == oauth_callback_path("google_oauth2") + end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..6ffca96 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,33 @@ +class SessionsController < ApplicationController + def new + origin = { origin: session.delete(:return_to) }.to_query + redirect_to("/auth/google_oauth2?#{origin}") + end + + def create + user = User.find_or_create_from_omniauth(auth_hash) + if user.valid? + session[:user_id] = user.id + flash[:notice] = "Signed in!" + # OmniAuth automatically saves the HTTP_REFERER when you begin the auth process + redirect_to request.env['omniauth.origin'] || root_url + else + flash[:error] = "You need a #{ENV.fetch('APP_DOMAIN')} account to sign in." + redirect_to root_url + end + end + + def destroy + session[:user_id] = nil + redirect_to root_url, notice: "Signed out!" + end + + protected + + def auth_hash + # calling to_h because Strong Parameters don't allow direct access + # to request parameters, even when passed to a class outside the + # controller scope. + request.env['omniauth.auth'].to_h + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 556c12b..ccd06b0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,9 +1,46 @@ class User < ActiveRecord::Base validates :email, presence: true + validate :whitelisted_email, if: -> { self.class.email_whitelist? } + + def self.find_or_create_from_omniauth(auth) + find_and_update_from_omniauth(auth) or create_from_omniauth(auth) + end + + def self.create_from_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"] + end + end + + def self.find_and_update_from_omniauth(auth) + find_by(auth.slice("provider","uid")).tap do |user| + user && user.update_attribute(:image, auth["info"]["image"]) + end + end def to_s self.name || self.email end + private + + def self.email_whitelist? + !!ENV['EMAIL_WHITELIST'] + end + + def email_whitelist + ENV["EMAIL_WHITELIST"].split(":") + end + + def whitelisted_email + if email_whitelist.none? { |email| self.email.include?(email) } + errors.add(:email, "doesn't match the email domain whitelist: #{email_whitelist}") + end + end + end diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb new file mode 100644 index 0000000..ba7da45 --- /dev/null +++ b/config/initializers/omniauth.rb @@ -0,0 +1,3 @@ +Rails.application.config.middleware.use OmniAuth::Builder do + provider :google_oauth2, ENV["GOOGLE_KEY"], ENV["GOOGLE_SECRET"] +end diff --git a/config/routes.rb b/config/routes.rb index 3f66539..1ece82b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,6 +5,12 @@ # You can have the root of your site routed with "root" # root 'welcome#index' + resources :sessions, only: [:new, :create, :destroy] + get 'auth/:provider/callback', to: 'sessions#create', as: :oauth_callback + get 'auth/failure', to: redirect('/') + get 'login', to: 'sessions#new', as: :login + get 'logout', to: 'sessions#destroy', as: :logout + # Example of regular route: # get 'products/:id' => 'catalog#view' diff --git a/db/schema.rb b/db/schema.rb index ea89ed5..386bd5d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,9 +11,19 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 0) do +ActiveRecord::Schema.define(version: 20150819222233) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "users", force: :cascade do |t| + t.string "provider" + t.string "uid" + t.string "name" + t.string "email" + t.string "image" + t.datetime "created_at" + t.datetime "updated_at" + end + end