├── .rspec ├── spec ├── rails_app │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── log │ │ └── development.log │ ├── app │ │ ├── views │ │ │ ├── posts │ │ │ │ └── index.html.erb │ │ │ ├── invalid_sessions │ │ │ │ └── new.html.erb │ │ │ ├── sessions │ │ │ │ └── new.html.erb │ │ │ ├── users │ │ │ │ └── new.html.erb │ │ │ └── layouts │ │ │ │ └── application.html.erb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── controllers │ │ │ ├── invalid_sessions_controller.rb │ │ │ ├── constrained_to_users_controller.rb │ │ │ ├── posts_controller.rb │ │ │ ├── application_controller.rb │ │ │ ├── constrained_to_visitors_controller.rb │ │ │ ├── failures_controller.rb │ │ │ ├── basic_auth_controller.rb │ │ │ ├── users_controller.rb │ │ │ └── sessions_controller.rb │ │ ├── assets │ │ │ ├── images │ │ │ │ └── rails.png │ │ │ ├── stylesheets │ │ │ │ └── application.css │ │ │ └── javascripts │ │ │ │ └── application.js │ │ └── models │ │ │ └── user.rb │ ├── config.ru │ ├── config │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── initializers │ │ │ ├── backtrace_silencers.rb │ │ │ ├── secret_token.rb │ │ │ └── inflections.rb │ │ ├── database.yml │ │ ├── routes.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ │ └── application.rb │ ├── Rakefile │ ├── script │ │ └── rails │ └── db │ │ └── seeds.rb ├── features │ ├── visitor │ │ ├── visitor_is_unauthorized_spec.rb │ │ ├── visitor_fails_to_sign_up_spec.rb │ │ ├── visitor_signs_in_via_invalid_form_spec.rb │ │ ├── visitor_uses_remember_token_spec.rb │ │ ├── visitor_tries_to_access_constrained_routes_spec.rb │ │ └── visitor_signs_up_spec.rb │ └── user │ │ ├── user_signs_in_through_back_door_spec.rb │ │ ├── user_tries_to_access_http_auth_page_spec.rb │ │ ├── user_signs_in_spec.rb │ │ └── user_tries_to_access_constrained_routes_spec.rb ├── oath │ ├── configuration_spec.rb │ ├── services │ │ ├── sign_in_spec.rb │ │ ├── sign_out_spec.rb │ │ ├── password_reset_spec.rb │ │ ├── authentication_spec.rb │ │ └── sign_up_spec.rb │ ├── field_map_spec.rb │ ├── strategies │ │ └── password_strategy_spec.rb │ ├── test_controller_helpers_spec.rb │ ├── test_helpers_spec.rb │ └── controller_helpers_spec.rb ├── oath_spec.rb └── spec_helper.rb ├── .travis.yml ├── Gemfile ├── lib ├── oath │ ├── version.rb │ ├── services.rb │ ├── railtie.rb │ ├── constraints │ │ ├── signed_in.rb │ │ └── signed_out.rb │ ├── services │ │ ├── sign_out.rb │ │ ├── sign_in.rb │ │ ├── password_reset.rb │ │ ├── authentication.rb │ │ └── sign_up.rb │ ├── test │ │ ├── helpers.rb │ │ └── controller_helpers.rb │ ├── failure_app.rb │ ├── param_transformer.rb │ ├── strategies │ │ └── password_strategy.rb │ ├── warden_setup.rb │ ├── field_map.rb │ ├── back_door.rb │ ├── configuration.rb │ └── controller_helpers.rb └── oath.rb ├── .gitignore ├── Rakefile ├── LICENSE.txt ├── oath.gemspec ├── NEWS.rdoc ├── Gemfile.lock └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /spec/rails_app/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/rails_app/log/development.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.4.2 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/posts/index.html.erb: -------------------------------------------------------------------------------- 1 | Posts 2 | -------------------------------------------------------------------------------- /lib/oath/version.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | # 1.1.0 3 | VERSION = "1.1.0" 4 | end 5 | -------------------------------------------------------------------------------- /spec/rails_app/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | spec/rails_app/log/* 2 | spec/rails_app/tmp/* 3 | *.sqlite3 4 | pkg 5 | /.yardoc/ 6 | /_yardoc/ 7 | /doc/ 8 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/invalid_sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class InvalidSessionsController < SessionsController 2 | end 3 | -------------------------------------------------------------------------------- /spec/rails_app/app/assets/images/rails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halogenandtoast/oath/HEAD/spec/rails_app/app/assets/images/rails.png -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/invalid_sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for :session do |f| %> 2 | <%= f.text_field :password %> 3 | <%= f.submit 'go' %> 4 | <% end %> 5 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/constrained_to_users_controller.rb: -------------------------------------------------------------------------------- 1 | class ConstrainedToUsersController < ApplicationController 2 | def show 3 | head :ok 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class PostsController < ApplicationController 2 | before_filter :require_login 3 | 4 | def index 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Oath::ControllerHelpers 3 | protect_from_forgery 4 | end 5 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/constrained_to_visitors_controller.rb: -------------------------------------------------------------------------------- 1 | class ConstrainedToVisitorsController < ApplicationController 2 | def show 3 | head :ok 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for :session do |f| %> 2 | <%= f.text_field :email %> 3 | <%= f.text_field :password %> 4 | <%= f.submit 'go' %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /spec/rails_app/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run RailsApp::Application 5 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/failures_controller.rb: -------------------------------------------------------------------------------- 1 | class FailuresController < ApplicationController 2 | def show 3 | render status: :unauthorized, plain: "Unauthorized" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/users/new.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for @user, url: users_path do |f| %> 2 | <%= f.text_field :email %> 3 | <%= f.text_field :password %> 4 | <%= f.submit 'go' %> 5 | <% end %> 6 | -------------------------------------------------------------------------------- /spec/rails_app/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | RailsApp::Application.initialize! 6 | -------------------------------------------------------------------------------- /lib/oath/services.rb: -------------------------------------------------------------------------------- 1 | require 'oath/services/sign_in' 2 | require 'oath/services/sign_out' 3 | require 'oath/services/sign_up' 4 | require 'oath/services/authentication' 5 | require 'oath/services/password_reset' 6 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/basic_auth_controller.rb: -------------------------------------------------------------------------------- 1 | class BasicAuthController < ApplicationController 2 | http_basic_authenticate_with name: "admin", password: "password" 3 | 4 | def show 5 | render plain: "Hello" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/rails_app/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 5 | 6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 7 | -------------------------------------------------------------------------------- /spec/features/visitor/visitor_is_unauthorized_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Visitor is unauthorzed' do 4 | scenario 'when visiting a resource' do 5 | visit failure_path 6 | expect(page.status_code).to eq(401) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/features/visitor/visitor_fails_to_sign_up_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Visitor signs up' do 4 | scenario 'with an email and password' do 5 | visit sign_up_path 6 | click_on 'go' 7 | 8 | expect(page).not_to have_content("Sign out") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/rails_app/app/models/user.rb: -------------------------------------------------------------------------------- 1 | require 'active_hash' 2 | class User < ActiveHash::Base 3 | include ActiveModel::Validations 4 | attr_accessor :email, :password_digest, :password 5 | validates :email, presence: true 6 | 7 | def self.find_by(params) 8 | where(params).first 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/rails_app/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | RailsApp::Application.load_tasks 8 | -------------------------------------------------------------------------------- /lib/oath/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'warden' 2 | 3 | module Oath 4 | # Railtie for Oath. Injects the Warden middleware and initializes Oath. 5 | # @since 0.0.15 6 | class Railtie < Rails::Railtie 7 | config.app_middleware.use Warden::Manager do |config| 8 | Oath.initialize(config) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/rails_app/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/features/user/user_signs_in_through_back_door_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'User signs in through the back-door' do 4 | scenario 'with the configured lookup field' do 5 | user = User.create! 6 | 7 | visit constrained_to_users_path(as: user) 8 | 9 | expect(current_path).to eq constrained_to_users_path 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/features/user/user_tries_to_access_http_auth_page_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'User tries to access constrained routes' do 4 | scenario 'they can access a route constrained to users' do 5 | page.driver.browser.basic_authorize("admin", "password") 6 | visit basic_auth_path 7 | expect(page.status_code).to eq(200) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/oath/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'warden' 3 | 4 | module Oath 5 | describe Configuration do 6 | it 'sets the no login redirect to a resonable default' do 7 | configuration = Configuration.new 8 | expect(configuration.no_login_redirect).to eq({ controller: "/sessions", action: "new" }) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/rails_app/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) 7 | # Mayor.create(name: 'Emanuel', city: cities.first) 8 | -------------------------------------------------------------------------------- /spec/oath/services/sign_in_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'oath/services/sign_in' 3 | 4 | describe Oath::Services::SignIn, '#perform' do 5 | it 'signs the user in' do 6 | user = double() 7 | warden = double() 8 | allow(warden).to receive(:set_user) 9 | 10 | Oath::Services::SignIn.new(user, warden).perform 11 | expect(warden).to have_received(:set_user).with(user) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/oath/services/sign_out_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'oath/services/sign_out' 3 | 4 | describe Oath::Services::SignOut, '#perform' do 5 | it 'signs out the user' do 6 | warden = double() 7 | allow(warden).to receive(:logout) 8 | allow(warden).to receive(:user).and_return(double()) 9 | 10 | Oath::Services::SignOut.new(warden).perform 11 | expect(warden).to have_received(:logout) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/features/visitor/visitor_signs_in_via_invalid_form_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Visitor signs in with invalid form' do 4 | scenario 'is not signed in' do 5 | Oath::Services::SignUp.new(email: 'email@example.com', password: 'password').perform 6 | visit invalid_sign_in_path 7 | fill_in "session_password", with: 'password' 8 | click_button 'go' 9 | expect(page).to have_content("Sign in") 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/oath/constraints/signed_in.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | module Constraints 3 | # Rails route constraint for signed in users 4 | class SignedIn 5 | # Checks to see if the constraint is matched by having a user signed in 6 | # 7 | # @param request [Rack::Request] A rack request 8 | def matches?(request) 9 | warden = request.env["warden"] 10 | warden && warden.authenticated? 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/features/visitor/visitor_uses_remember_token_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Visitor signs in' do 4 | scenario 'with remember token' do 5 | pending 6 | Oath::SignUp.new(email: "email@example.com", password: "password").perform 7 | visit sign_in_path 8 | fill_in 'session_email', with: 'email@example.com' 9 | fill_in 'session_password', with: 'password' 10 | check 'Remember me' 11 | click_on 'go' 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/oath/constraints/signed_out.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | module Constraints 3 | # Rails route constraint for signed out users 4 | class SignedOut 5 | # Checks to see if the constraint is matched by not having a user signed in 6 | # 7 | # @param request [Rack::Request] A rack request 8 | def matches?(request) 9 | warden = request.env["warden"] 10 | warden && warden.unauthenticated? 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/features/user/user_signs_in_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'User signs in' do 4 | scenario 'with mismatched email case' do 5 | user = User.create!(email: "example@example.com", password_digest: "password") 6 | 7 | visit sign_in_path 8 | fill_in "session[email]", with: "Example@example.com" 9 | fill_in "session[password]", with: "password" 10 | click_button "go" 11 | 12 | expect(current_path).to eq posts_path 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | def new 3 | @user = User.new 4 | end 5 | 6 | def create 7 | @user = sign_up(user_params) 8 | 9 | if @user.valid? 10 | sign_in(@user) 11 | redirect_to posts_path 12 | else 13 | render :new 14 | end 15 | end 16 | 17 | private 18 | 19 | def user_params 20 | params.require(:user).permit(:email, :password) 21 | end 22 | end 23 | 24 | -------------------------------------------------------------------------------- /spec/rails_app/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | RailsApp 5 | <%= stylesheet_link_tag "application", :media => "all" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | <% if signed_in? %> 11 | <%= link_to "Sign out", sign_out_path, method: :delete %> 12 | <% else %> 13 | <%= link_to "Sign in", sign_in_path %> 14 | <% end %> 15 | <%= yield %> 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /spec/features/visitor/visitor_tries_to_access_constrained_routes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Visitor tries to access constrained routes' do 4 | scenario 'they can access a route constrained to visitors' do 5 | visit constrained_to_visitors_path 6 | expect(page.status_code).to eq(200) 7 | end 8 | 9 | scenario 'they cannot access a route constrained to users' do 10 | expect { 11 | visit constrained_to_users_path 12 | }.to raise_error ActionController::RoutingError 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | RailsApp::Application.config.secret_token = '840b3262c6147ca69157ebd545ff37817922cd1315d688d7ec3a864629ae89398604efeca261c8e40273b0306dbcf5c7f609f7b0dd4d12d213f1d6bd3dc169db' 8 | -------------------------------------------------------------------------------- /lib/oath/services/sign_out.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | module Services 3 | # Sign out service. Signs the user out via warden 4 | # @since 0.0.15 5 | class SignOut 6 | # Initialize service 7 | # 8 | # @param warden [Warden] warden 9 | def initialize warden 10 | @warden = warden 11 | @user = warden.user 12 | end 13 | 14 | # Perform the service 15 | def perform 16 | warden.logout 17 | end 18 | 19 | private 20 | 21 | attr_reader :warden, :user 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/features/user/user_tries_to_access_constrained_routes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'User tries to access constrained routes' do 4 | scenario 'they can access a route constrained to users' do 5 | sign_in User.new 6 | 7 | visit constrained_to_users_path 8 | expect(page.status_code).to eq(200) 9 | end 10 | 11 | scenario 'they cannot access a route constrained to visitors' do 12 | sign_in User.new 13 | 14 | expect { 15 | visit constrained_to_visitors_path 16 | }.to raise_error ActionController::RoutingError 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/rails_app/app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < ApplicationController 2 | def new 3 | end 4 | 5 | def create 6 | user = authenticate_session(session_params) 7 | 8 | if sign_in(user) 9 | redirect_to posts_path 10 | else 11 | redirect_to root_path, notice: "Invalid email or password" 12 | end 13 | end 14 | 15 | def destroy 16 | sign_out 17 | redirect_to root_path 18 | end 19 | 20 | private 21 | 22 | def session_params 23 | params.require(:session).permit(:email, :password) 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /lib/oath/services/sign_in.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | module Services 3 | # Sign in service. Signs the user in via warden 4 | # @since 0.0.15 5 | class SignIn 6 | # Initialize service 7 | # 8 | # @param user [User] A user object 9 | # @param warden [Warden] warden 10 | def initialize user, warden 11 | @user = user 12 | @warden = warden 13 | end 14 | 15 | # Perform the service 16 | def perform 17 | warden.set_user(user) 18 | end 19 | 20 | private 21 | 22 | attr_reader :warden, :user 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/oath/test/helpers.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | module Test 3 | # Helpers for integration or feature specs 4 | # @note these have only been tested with rspec integration and feature specs 5 | # @since 0.0.15 6 | module Helpers 7 | include Warden::Test::Helpers 8 | 9 | # Sign a user in 10 | # @param user [User] user to sign in 11 | # @returns user [User] signed in user 12 | def sign_in user 13 | login_as user 14 | 15 | user 16 | end 17 | 18 | # Sign a user out 19 | def sign_out 20 | logout 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/rails_app/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /spec/rails_app/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /spec/rails_app/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // the compiled file. 9 | // 10 | // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD 11 | // GO AFTER THE REQUIRES BELOW. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /spec/oath/field_map_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Oath 4 | describe FieldMap do 5 | it 'returns the params with symbolized keys without a field map' do 6 | params = double() 7 | allow(params).to receive(:inject).and_return(params) 8 | field_map = FieldMap.new(params, nil) 9 | expect(field_map.to_fields).to eq(params) 10 | end 11 | 12 | it 'returns mapped params with a field map' do 13 | params = { email_or_username: 'foo' } 14 | map = { email_or_username: [:email, :username] } 15 | field_map = FieldMap.new(params, map) 16 | expect(field_map.to_fields).to eq(["email = ? OR username = ?", 'foo', 'foo']) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/rails_app/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | development: 7 | adapter: sqlite3 8 | database: db/development.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | # Warning: The database defined as "test" will be erased and 13 | # re-generated from your development database when you run "rake". 14 | # Do not set this db to the same as development or production. 15 | test: 16 | adapter: sqlite3 17 | database: db/test.sqlite3 18 | pool: 5 19 | timeout: 5000 20 | 21 | production: 22 | adapter: sqlite3 23 | database: db/production.sqlite3 24 | pool: 5 25 | timeout: 5000 26 | -------------------------------------------------------------------------------- /spec/rails_app/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /lib/oath/services/password_reset.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | module Services 3 | # Password reset service. Updates the password on a User 4 | # @since 0.0.15 5 | class PasswordReset 6 | # Initialize service 7 | # 8 | # @param user [User] A user object 9 | # @param new_password [String] The new undigested password for a user 10 | def initialize user, new_password 11 | @user = user 12 | @new_password = new_password 13 | end 14 | 15 | # Perform the service. 16 | def perform 17 | field = Oath.config.user_token_store_field 18 | digested_password = Oath.hash_token(new_password) 19 | user[field] = digested_password 20 | end 21 | 22 | private 23 | 24 | attr_reader :user, :new_password 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/oath/services/password_reset_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'oath/services/password_reset' 3 | require 'ostruct' 4 | 5 | describe Oath::Services::PasswordReset do 6 | before do 7 | Oath.config.hashing_method = ->(password) { password + "secret" } 8 | end 9 | 10 | it 'updates the password with the hashing strategy' do 11 | password_digest = Oath.hash_token('password') 12 | user = double() 13 | field = Oath.config.user_token_store_field 14 | allow(user).to receive(:[]=) 15 | password_reset = Oath::Services::PasswordReset.new(user, 'password') 16 | 17 | password_reset.perform 18 | expect(user).to have_received(:[]=).with(field, 'passwordsecret') 19 | end 20 | 21 | after do 22 | Oath.config.hashing_method = Oath.config.default_hashing_method 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/rails_app/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/oath/strategies/password_strategy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Oath 4 | module Strategies 5 | describe PasswordStrategy do 6 | it "bases lookup and token on config values" do 7 | params = HashWithIndifferentAccess.new(username: 'test', the_password: 'password') 8 | 9 | with_oath_config(user_lookup_field: "username", user_token_field: "the_password") do 10 | env = Rack::MockRequest.env_for("/", params: params) 11 | strategy = PasswordStrategy.new(env) 12 | expect(strategy).to be_valid 13 | end 14 | end 15 | 16 | it "it doesn't trigger if params are not provided" do 17 | env = Rack::MockRequest.env_for("/") 18 | strategy = PasswordStrategy.new(env) 19 | expect(strategy).not_to be_valid 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/rails_app/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/rails_app/config/routes.rb: -------------------------------------------------------------------------------- 1 | require 'oath/constraints/signed_in' 2 | require 'oath/constraints/signed_out' 3 | 4 | RailsApp::Application.routes.draw do 5 | constraints Oath::Constraints::SignedIn.new do 6 | resource :constrained_to_users, only: [:show] 7 | end 8 | 9 | constraints Oath::Constraints::SignedOut.new do 10 | resource :constrained_to_visitors, only: [:show] 11 | end 12 | 13 | resources :posts, only: [:index] 14 | resources :users, only: [:create] 15 | resource :failure, only: [:show] 16 | root to: "users#new" 17 | get "sign_in" => "sessions#new" 18 | post "sign_in" => "sessions#create" 19 | delete "sign_out" => "sessions#destroy" 20 | get "sign_up" => "users#new" 21 | get "invalid_sign_in" => "invalid_sessions#new" 22 | post "invalid_sign_in" => "invalid_sessions#create" 23 | get "basic_auth" => "basic_auth#show" 24 | end 25 | -------------------------------------------------------------------------------- /spec/oath_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'warden' 3 | 4 | describe 'Oath' do 5 | it "stores the warden config" do 6 | expect(Oath.warden_config).to be_a Warden::Config 7 | end 8 | 9 | it "provides a .test_mode!" do 10 | Oath.test_mode! 11 | expect(Oath.hash_token('password')).to eql('password') 12 | expect(Oath.compare_token('password', 'password')).to be_truthy 13 | end 14 | 15 | it "does not lookup with empty params" do 16 | allow(Oath::FieldMap).to receive(:new).and_return(fake_field_map) 17 | with_oath_config(find_method: -> (conditions) { raise }) do 18 | expect(-> { Oath.lookup({}, {}) }).not_to raise_exception 19 | end 20 | end 21 | 22 | def fake_field_map 23 | double(Oath::FieldMap).tap do |field_map| 24 | allow(field_map).to receive(:to_fields).and_return(["foo=1 OR bar=1"]) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | $LOAD_PATH.unshift File.dirname(__FILE__) 3 | 4 | require 'rails_app/config/environment' 5 | require 'rspec/rails' 6 | require 'warden' 7 | require 'oath' 8 | require 'capybara' 9 | 10 | Oath.test_mode! 11 | Warden.test_mode! 12 | 13 | RSpec.configure do |config| 14 | config.include Warden::Test::Helpers 15 | config.include Oath::Test::Helpers, type: :feature 16 | config.order = "random" 17 | config.after :each do 18 | Oath.test_reset! 19 | end 20 | end 21 | 22 | def with_oath_config(hash, &block) 23 | begin 24 | old_config = {} 25 | hash.each do |key, value| 26 | old_config[key] = Oath.config.send(key) 27 | Oath.config.send(:"#{key}=", value) 28 | end 29 | 30 | yield 31 | ensure 32 | 33 | old_config.each do |key, value| 34 | Oath.config.send(:"#{key}=", old_config[key]) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/oath/services/authentication_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'oath/services/authentication' 3 | 4 | describe Oath::Services::Authentication, '#authentication' do 5 | it 'is authenticated for a valid password' do 6 | password_digest = BCrypt::Password.create('password') 7 | user = double(password_digest: password_digest) 8 | auth = Oath::Services::Authentication.new(user, 'password') 9 | 10 | expect(auth.perform).to eq(user) 11 | end 12 | 13 | it 'is not authenticated for the wrong password' do 14 | password_digest = BCrypt::Password.create('password') 15 | user = double(password_digest: password_digest) 16 | auth = Oath::Services::Authentication.new(user, 'drowssap') 17 | 18 | expect(auth.perform).to eq(false) 19 | end 20 | 21 | it 'is not authenticated without a user' do 22 | auth = Oath::Services::Authentication.new(nil, 'password') 23 | expect(auth.perform).to eq(false) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/oath/failure_app.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | class FailureApp 3 | def self.call(env) 4 | request = Rack::Request.new(env) 5 | new(request).response 6 | end 7 | 8 | def initialize(request) 9 | @request = request 10 | end 11 | 12 | def response 13 | [401, headers, body] 14 | end 15 | 16 | private 17 | 18 | attr_reader :request 19 | 20 | def headers 21 | if http_auth_header? 22 | basic_headers.merge(auth_headers) 23 | else 24 | basic_headers 25 | end 26 | end 27 | 28 | def basic_headers 29 | { 30 | "Content-Type" => request.content_type.to_s 31 | } 32 | end 33 | 34 | def auth_headers 35 | { 36 | "WWW-Authenticate" => 'Basic realm="Application"' 37 | } 38 | end 39 | 40 | def body 41 | ["Authorization Failed"] 42 | end 43 | 44 | def http_auth_header? 45 | !request.xhr? 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/oath/param_transformer.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | # Parameter transformer. Sanitizes and transforms parameter values 3 | # @since 1.0.0 4 | class ParamTransformer 5 | # Initialize parameter transformer 6 | # 7 | # @param params [ActionController::Parameters] parameters to be altered 8 | def initialize(params, transformations) 9 | @params = params 10 | @transformations = transformations 11 | end 12 | 13 | # Returns the transformed parameters 14 | def to_h 15 | sanitized_params.each_with_object({}) do |(key, value), hash| 16 | hash[key] = transform(key, value) 17 | end 18 | end 19 | 20 | private 21 | 22 | attr_reader :params, :transformations 23 | 24 | def sanitized_params 25 | params.to_h 26 | end 27 | 28 | def transform(key, value) 29 | return value unless value.is_a? String 30 | 31 | if transformations.key?(key) 32 | transformations[key].call(value) 33 | else 34 | value 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/oath/services/authentication.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | module Services 3 | # Authentication service. Checks to see if the credentials provided are valid 4 | # @since 0.0.15 5 | class Authentication 6 | # Initialize service 7 | # 8 | # @param user [User] A user object 9 | # @param undigested_token [String] An undigested password 10 | def initialize user, undigested_token 11 | @user = user 12 | @undigested_token = undigested_token 13 | end 14 | 15 | # Perform the service 16 | # 17 | # @return [User] if authentication succeeds 18 | # @return [false] if authentication fails 19 | def perform 20 | if authenticated? 21 | user 22 | else 23 | false 24 | end 25 | end 26 | 27 | private 28 | 29 | attr_reader :user, :undigested_token 30 | 31 | def authenticated? 32 | user && Oath.compare_token(user.send(token_store_field), undigested_token) 33 | end 34 | 35 | def token_store_field 36 | Oath.config.user_token_store_field 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/oath/strategies/password_strategy.rb: -------------------------------------------------------------------------------- 1 | require 'warden' 2 | 3 | module Oath 4 | module Strategies 5 | # Password strategy for warden 6 | # @since 0.0.15 7 | class PasswordStrategy < ::Warden::Strategies::Base 8 | 9 | # Checks if strategy should be executed 10 | # @return [Boolean] 11 | def valid? 12 | lookup_field_value || token_field_value 13 | end 14 | 15 | 16 | # Authenticates for warden 17 | def authenticate! 18 | user = Oath.config.user_class.find_by(lookup_field => lookup_field_value) 19 | auth = Oath.config.authentication_service.new(user, token_field_value) 20 | auth.authenticated? ? success!(user) : fail!("Could not log in") 21 | end 22 | 23 | private 24 | 25 | def lookup_field_value 26 | params[lookup_field] 27 | end 28 | 29 | def token_field_value 30 | params[token_field] 31 | end 32 | 33 | def lookup_field 34 | Oath.config.user_lookup_field 35 | end 36 | 37 | def token_field 38 | Oath.config.user_token_field 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 halogenandtoast 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/oath/services/sign_up.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | module Services 3 | # Sign up service. Signs the user up 4 | # @since 0.0.15 5 | class SignUp 6 | # Initialize service 7 | # 8 | # @param user_params [Hash] A hash of user credentials. Should contain the lookup and token fields 9 | def initialize user_params 10 | digested_token = token_digest(user_params) 11 | @user_params = user_params. 12 | except(token_field). 13 | merge(token_store_field.to_sym => digested_token) 14 | end 15 | 16 | # Performs the service 17 | # @see Oath::Configuration.default_creation_method 18 | def perform 19 | Oath.config.creation_method.call(user_params) 20 | end 21 | 22 | private 23 | 24 | attr_reader :user_params 25 | 26 | def token_digest(user_params) 27 | undigested_token = user_params[token_field] 28 | unless undigested_token.blank? 29 | Oath.hash_token(undigested_token) 30 | end 31 | end 32 | 33 | def token_store_field 34 | Oath.config.user_token_store_field 35 | end 36 | 37 | def token_field 38 | Oath.config.user_token_field 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /oath.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'oath/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "oath" 8 | gem.version = Oath::VERSION 9 | gem.authors = ["halogenandtoast", "calebthompson"] 10 | gem.email = ["halogenandtoast@gmail.com"] 11 | gem.description = %q{simple rails authentication} 12 | gem.summary = %q{Making rails authentication as simple as possible} 13 | gem.homepage = "https://github.com/halogenandtoast/oath" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | 20 | gem.add_dependency 'rails' 21 | gem.add_dependency 'bcrypt' 22 | gem.add_dependency 'warden' 23 | 24 | gem.add_development_dependency 'rake' 25 | gem.add_development_dependency 'rspec' 26 | gem.add_development_dependency 'rspec-rails' 27 | gem.add_development_dependency 'capybara' 28 | gem.add_development_dependency 'sqlite3' 29 | gem.add_development_dependency 'active_hash' 30 | end 31 | -------------------------------------------------------------------------------- /lib/oath/test/controller_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'warden' 2 | 3 | module Oath 4 | module Test 5 | # These are test helpers for controller specs 6 | # @note these have only been tested with rspec controller specs 7 | # @since 0.0.15 8 | module ControllerHelpers 9 | def self.included(base) 10 | base.class_eval do 11 | setup :store_controller_for_warden, :warden if respond_to?(:setup) 12 | end 13 | end 14 | 15 | # Signs a user in for tests 16 | # @param user [User] the user to sign in 17 | def sign_in(user) 18 | @controller.sign_in(user) 19 | end 20 | 21 | # Signs the user out in tests 22 | def sign_out 23 | @controller.sign_out 24 | end 25 | 26 | # A mock of warden for tests 27 | def warden 28 | @warden ||= begin 29 | manager = Warden::Manager.new(nil) do |config| 30 | config.merge! Oath.warden_config 31 | end 32 | @request.env['warden'] = Warden::Proxy.new(@request.env, manager) 33 | end 34 | end 35 | 36 | private 37 | def store_controller_for_warden 38 | @request.env['action_controller.instance'] = @controller 39 | end 40 | 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/features/visitor/visitor_signs_up_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Visitor signs up' do 4 | scenario 'with an email and password' do 5 | visit sign_up_path 6 | fill_in 'user_email', with: 'email@example.com' 7 | fill_in 'user_password', with: 'password' 8 | click_on 'go' 9 | 10 | expect(page.current_path).to eq(posts_path) 11 | end 12 | 13 | scenario 'with uppercase email' do 14 | visit sign_up_path 15 | fill_in 'user_email', with: 'Email@example.com' 16 | fill_in 'user_password', with: 'password' 17 | click_on 'go' 18 | 19 | expect(User.last.email).to eq('email@example.com') 20 | end 21 | 22 | scenario 'multiple users' do 23 | visit sign_up_path 24 | fill_in 'user_email', with: 'email@example.com' 25 | fill_in 'user_password', with: 'password' 26 | click_on 'go' 27 | click_on 'Sign out' 28 | visit sign_up_path 29 | fill_in 'user_email', with: 'email2@example.com' 30 | fill_in 'user_password', with: 'password2' 31 | click_on 'go' 32 | click_on 'Sign out' 33 | visit sign_in_path 34 | fill_in 'session_email', with: 'email@example.com' 35 | fill_in 'session_password', with: 'password' 36 | click_on 'go' 37 | 38 | expect(page.current_path).to eq(posts_path) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Print deprecation notices to the Rails logger 17 | config.active_support.deprecation = :log 18 | 19 | # Only use best-standards-support built into browsers 20 | config.action_dispatch.best_standards_support = :builtin 21 | 22 | # Raise exception on mass assignment protection for Active Record models 23 | config.active_record.mass_assignment_sanitizer = :strict 24 | 25 | # Log the query plan for queries taking more than this (works 26 | # with SQLite, MySQL, and PostgreSQL) 27 | config.active_record.auto_explain_threshold_in_seconds = 0.5 28 | 29 | end 30 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | config.middleware.insert_after Warden::Manager, Oath::BackDoor 4 | 5 | # The test environment is used exclusively to run your application's 6 | # test suite. You never need to work with it otherwise. Remember that 7 | # your test database is "scratch space" for the test suite and is wiped 8 | # and recreated between test runs. Don't rely on the data there! 9 | config.cache_classes = true 10 | 11 | # Configure static asset server for tests with Cache-Control for performance 12 | config.serve_static_assets = true 13 | config.static_cache_control = "public, max-age=3600" 14 | 15 | # Show full error reports and disable caching 16 | config.consider_all_requests_local = true 17 | config.action_controller.perform_caching = false 18 | 19 | # Raise exceptions instead of rendering exception templates 20 | config.action_dispatch.show_exceptions = false 21 | 22 | # Disable request forgery protection in test environment 23 | config.action_controller.allow_forgery_protection = false 24 | 25 | # Print deprecation notices to the stderr 26 | config.active_support.deprecation = :stderr 27 | config.eager_load = false 28 | config.secret_key_base = "1" 29 | end 30 | -------------------------------------------------------------------------------- /lib/oath/warden_setup.rb: -------------------------------------------------------------------------------- 1 | require 'warden' 2 | require "oath/strategies/password_strategy" 3 | 4 | module Oath 5 | # Sets up warden specifics for working with oath 6 | class WardenSetup 7 | def initialize(warden_config) 8 | @warden_config = warden_config 9 | end 10 | 11 | # Sets up warden specifics for working with oath: 12 | # * Session serialization 13 | # * Strategy 14 | # * Failure app 15 | def call 16 | setup_warden_manager 17 | setup_warden_strategies 18 | setup_warden_config 19 | end 20 | 21 | private 22 | attr_reader :warden_config 23 | 24 | def setup_warden_manager 25 | Warden::Manager.serialize_into_session(&serialize_into_session_method) 26 | Warden::Manager.serialize_from_session(&serialize_from_session_method) 27 | end 28 | 29 | def setup_warden_strategies 30 | Warden::Strategies.add(:password_strategy, Oath.config.authentication_strategy) 31 | end 32 | 33 | def setup_warden_config 34 | warden_config.tap do |config| 35 | config.failure_app = Oath.config.failure_app 36 | end 37 | end 38 | 39 | def serialize_into_session_method 40 | Oath.config.warden_serialize_into_session 41 | end 42 | 43 | def serialize_from_session_method 44 | Oath.config.warden_serialize_from_session 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/oath/field_map.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | # FieldMap is used to allow multiple lookup fields. For instance if you 3 | # wanted to allow a user to sign in via email or username. This is used 4 | # internally by the authenticate_session controller helper 5 | # @since 0.0.15 6 | class FieldMap 7 | # @param params [Hash] hash of parameters 8 | # @param field_map [Hash] hash of values to map 9 | def initialize params, field_map 10 | @params = params 11 | @field_map = field_map 12 | end 13 | 14 | # converts params into values that can be passed into a where clause 15 | # 16 | # @return [Array] if initialized with field_map 17 | # @return [Hash] if not initialized with field_map 18 | def to_fields 19 | if @field_map 20 | params_from_field_map 21 | else 22 | params_with_symbolized_keys 23 | end 24 | end 25 | 26 | private 27 | 28 | def params_with_symbolized_keys 29 | @params.inject(default_fields){|hash,(key,value)| hash.merge(key.to_sym => value) } 30 | end 31 | 32 | def default_fields 33 | { Oath.config.user_lookup_field => nil } 34 | end 35 | 36 | def params_from_field_map 37 | [query_string, *([value] * lookup_keys.length)] 38 | end 39 | 40 | def query_string 41 | lookup_keys.map { |key| "#{key} = ?" }.join(" OR ") 42 | end 43 | 44 | def session_key 45 | @field_map.keys.first 46 | end 47 | 48 | def lookup_keys 49 | @field_map.values.first 50 | end 51 | 52 | def value 53 | @params[session_key] 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/oath/back_door.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | # Middleware used in tests to allow users to be signed in directly, without 3 | # having to load and submit the sign in form. The user should be provided by 4 | # using the key :as in a hash passed to the path. 5 | # 6 | # @note This should only be used for testing purposes 7 | # @since 0.0.15 8 | # @example Using the backdoor in an rspec feature spec 9 | # feature "User dashboard" do 10 | # scenario "user visits dashboard" do 11 | # user = create(:user) 12 | # visit dashboard_path(as: user) 13 | # expect(page).to have_css("#dashboard") 14 | # end 15 | # end 16 | class BackDoor 17 | # Create the a new BackDoor middleware for test purposes 18 | # @return [BackDoor] 19 | def initialize(app, &block) 20 | @app = app 21 | 22 | if block 23 | @sign_in_block = block 24 | end 25 | end 26 | 27 | # Execute the BackDoor middleware signing in the user specified with :as 28 | def call(env) 29 | sign_in_through_the_back_door(env) 30 | @app.call(env) 31 | end 32 | 33 | private 34 | 35 | def sign_in_through_the_back_door(env) 36 | params = Rack::Utils.parse_query(env['QUERY_STRING']) 37 | user_id = params['as'] 38 | 39 | if user_id.present? 40 | user = find_user(user_id) 41 | env["warden"].set_user(user) 42 | end 43 | end 44 | 45 | def find_user(user_id) 46 | if @sign_in_block 47 | @sign_in_block.call(user_id) 48 | else 49 | Oath.config.user_class.find(user_id) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/oath/test_controller_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class FakeRequest 4 | attr_reader :env 5 | def initialize 6 | @env = {} 7 | end 8 | end 9 | 10 | class FakeController 11 | attr_reader :user 12 | def sign_in(user) 13 | @user = user 14 | end 15 | 16 | def sign_out 17 | @user = nil 18 | end 19 | end 20 | 21 | class FakeSpec 22 | cattr_reader :setup_methods 23 | attr_reader :request, :controller 24 | def initialize(controller = FakeController.new) 25 | @request = FakeRequest.new 26 | @controller = controller 27 | end 28 | 29 | def self.setup(*methods) 30 | @@setup_methods = methods 31 | end 32 | include Oath::Test::ControllerHelpers 33 | end 34 | 35 | module Oath 36 | module Test 37 | describe ControllerHelpers do 38 | it 'sets up warden' do 39 | expect(FakeSpec.setup_methods).to eq([:store_controller_for_warden, :warden]) 40 | end 41 | 42 | it 'creates a warden manager' do 43 | fake_spec = FakeSpec.new 44 | expect(fake_spec.warden).to be_a(Warden::Proxy) 45 | end 46 | 47 | it 'calls the sign in method on the controller' do 48 | controller = FakeController.new 49 | fake_spec = FakeSpec.new(controller) 50 | fake_spec.sign_in("user") 51 | expect(controller.user).to eq("user") 52 | end 53 | 54 | it 'calls the sign out method on the controller' do 55 | controller = FakeController.new 56 | fake_spec = FakeSpec.new(controller) 57 | fake_spec.sign_in("user") 58 | fake_spec.sign_out 59 | expect(controller.user).to eq(nil) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/oath/services/sign_up_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'oath/services/sign_up' 3 | 4 | describe Oath::Services::SignUp, '#perform' do 5 | it 'creates a user with the right parameters' do 6 | allow(User).to receive(:create) 7 | user_params = { email: 'email@example.com', password: 'password' } 8 | 9 | Oath::Services::SignUp.new(user_params).perform 10 | expect(User).to have_received(:create) do |args| 11 | expect(args[:email]).to eq(user_params[:email]) 12 | expect(Oath.compare_token(args[:password_digest], 'password')).to be_truthy 13 | end 14 | end 15 | 16 | it 'creates a user from the configured user creation method' do 17 | user_create_double = double(Proc, call: true) 18 | 19 | user_params = { email: 'email@example.com', password: 'password' } 20 | 21 | with_oath_config(creation_method: user_create_double) do 22 | Oath::Services::SignUp.new(user_params).perform 23 | end 24 | 25 | expect(user_create_double).to have_received(:call) do |args| 26 | expect(Oath.compare_token(args[:password_digest], 'password')).to be_truthy 27 | end 28 | end 29 | 30 | it 'does not create a user with an empty password' do 31 | allow(User).to receive(:create) 32 | user_params = { email: 'email@example.com', password: '' } 33 | 34 | Oath::Services::SignUp.new(user_params).perform 35 | expect(User).to have_received(:create) do |args| 36 | expect(args[:password_digest]).to be_nil 37 | end 38 | end 39 | 40 | it 'does not create a user with a nil password' do 41 | allow(User).to receive(:create) 42 | user_params = { email: nil, password: nil } 43 | 44 | Oath::Services::SignUp.new(user_params).perform 45 | expect(User).to have_received(:create) do |args| 46 | expect(args[:password_digest]).to be_nil 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/rails_app/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | RailsApp::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | 15 | # Specifies the header that your server uses for sending files 16 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 17 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 18 | 19 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 20 | # config.force_ssl = true 21 | 22 | # See everything in the log (default is :info) 23 | # config.log_level = :debug 24 | 25 | # Prepend all log lines with the following tags 26 | # config.log_tags = [ :subdomain, :uuid ] 27 | 28 | # Use a different logger for distributed setups 29 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 30 | 31 | # Use a different cache store in production 32 | # config.cache_store = :mem_cache_store 33 | 34 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 35 | # config.action_controller.asset_host = "http://assets.example.com" 36 | 37 | 38 | # Disable delivery errors, bad email addresses will be ignored 39 | # config.action_mailer.raise_delivery_errors = false 40 | 41 | # Enable threaded mode 42 | # config.threadsafe! 43 | 44 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 45 | # the I18n.default_locale when a translation can not be found) 46 | config.i18n.fallbacks = true 47 | 48 | # Send deprecation notices to registered listeners 49 | config.active_support.deprecation = :notify 50 | 51 | # Log the query plan for queries taking more than this (works 52 | # with SQLite, MySQL, and PostgreSQL) 53 | # config.active_record.auto_explain_threshold_in_seconds = 0.5 54 | end 55 | -------------------------------------------------------------------------------- /spec/rails_app/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | # require "action_mailer/railtie" 7 | # require "active_resource/railtie" 8 | # require "sprockets/railtie" 9 | # require "rails/test_unit/railtie" 10 | 11 | if defined?(Bundler) 12 | # If you precompile assets before deploying to production, use this line 13 | Bundler.require(*Rails.groups(:assets => %w(development test))) 14 | # If you want your assets lazily compiled in production, use this line 15 | # Bundler.require(:default, :assets, Rails.env) 16 | end 17 | 18 | require 'oath' 19 | 20 | module RailsApp 21 | class Application < Rails::Application 22 | # Settings in config/environments/* take precedence over those specified here. 23 | # Application configuration should go into files in config/initializers 24 | # -- all .rb files in that directory are automatically loaded. 25 | 26 | # Custom directories with classes and modules you want to be autoloadable. 27 | # config.autoload_paths += %W(#{config.root}/extras) 28 | 29 | # Only load the plugins named here, in the order given (default is alphabetical). 30 | # :all can be used as a placeholder for all plugins not explicitly named. 31 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 32 | 33 | # Activate observers that should always be running. 34 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 35 | 36 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 37 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 38 | # config.time_zone = 'Central Time (US & Canada)' 39 | 40 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 41 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 42 | # config.i18n.default_locale = :de 43 | 44 | # Configure the default encoding used in templates for Ruby 1.9. 45 | config.encoding = "utf-8" 46 | 47 | # Configure sensitive parameters which will be filtered from the log file. 48 | config.filter_parameters += [:password] 49 | 50 | # Enable escaping HTML in JSON. 51 | config.active_support.escape_html_entities_in_json = true 52 | 53 | # Use SQL instead of Active Record's schema dumper when creating the database. 54 | # This is necessary if your schema can't be completely dumped by the schema dumper, 55 | # like if you have constraints or database-specific column types 56 | # config.active_record.schema_format = :sql 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/oath/test_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'warden' 3 | require 'oath/test/helpers' 4 | 5 | module Warden::Spec 6 | module Helpers 7 | FAILURE_APP = lambda{|e|[401, {"Content-Type" => "text/plain"}, ["You Fail!"]] } 8 | 9 | def env_with_params(path = "/", params = {}, env = {}) 10 | method = params.delete(:method) || "GET" 11 | env = { 'HTTP_VERSION' => '1.1', 'REQUEST_METHOD' => "#{method}" }.merge(env) 12 | Rack::MockRequest.env_for("#{path}?#{Rack::Utils.build_query(params)}", env) 13 | end 14 | 15 | def setup_rack(app = nil, opts = {}, &block) 16 | app ||= block if block_given? 17 | 18 | opts[:failure_app] ||= failure_app 19 | opts[:default_strategies] ||= [:password] 20 | opts[:default_serializers] ||= [:session] 21 | blk = opts[:configurator] || proc{} 22 | 23 | Rack::Builder.new do 24 | use opts[:session] || Warden::Spec::Helpers::Session unless opts[:nil_session] 25 | use Warden::Manager, opts, &blk 26 | run app 27 | end 28 | end 29 | 30 | def valid_response 31 | Rack::Response.new("OK").finish 32 | end 33 | 34 | def failure_app 35 | Warden::Spec::Helpers::FAILURE_APP 36 | end 37 | 38 | def success_app 39 | lambda{|e| [200, {"Content-Type" => "text/plain"}, ["You Win"]]} 40 | end 41 | 42 | class Session 43 | attr_accessor :app 44 | def initialize(app,configs = {}) 45 | @app = app 46 | end 47 | 48 | def call(e) 49 | e['rack.session'] ||= {} 50 | @app.call(e) 51 | end 52 | end # session 53 | end 54 | end 55 | 56 | module Oath 57 | module Test 58 | describe Helpers do 59 | include Oath::Test::Helpers 60 | include Warden::Spec::Helpers 61 | 62 | before { $captures = [] } 63 | after { Oath.test_reset! } 64 | 65 | it 'performs a sign in' do 66 | user = double(id: 1) 67 | return_value = sign_in(user) 68 | app = lambda do |env| 69 | $captures << :run 70 | expect(env['warden']).to be_authenticated 71 | expect(env['warden'].user).to eq(user) 72 | valid_response 73 | end 74 | setup_rack(app).call(env_with_params) 75 | 76 | expect(return_value).to eq(user) 77 | expect($captures).to eq([:run]) 78 | end 79 | 80 | it 'performs a sign out' do 81 | user = double(id: 1) 82 | sign_in(user) 83 | sign_out 84 | 85 | app = lambda do |env| 86 | $captures << :run 87 | warden = env['warden'] 88 | expect(warden.user).to be_nil 89 | expect(warden).not_to be_authenticated 90 | end 91 | 92 | setup_rack(app).call(env_with_params) 93 | expect($captures).to eq([:run]) 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /NEWS.rdoc: -------------------------------------------------------------------------------- 1 | == 1.1.0 2 | * `Oath.config.sign_in_notice` should now be a callable object in order for I18n to work correctly 3 | * [DEPRECATED] you should no longer set `Oath.config.sign_in_notice` to a string value 4 | 5 | == 1.0.1 6 | * Wrap helper_method calls in respond_to?(:helper_method) 7 | * param_transformations now correctly use string keys 8 | 9 | == 1.0.0 10 | * Do not perform lookup if no params are passed to lookup 11 | * Add param transformer for sanitizing and normalizing 12 | 13 | == 0.3.1 14 | * Extend private interface for services 15 | 16 | == 0.3.0 17 | * Warden serialization is now configurable 18 | 19 | == 0.2.1 20 | * Updated documentation for test helpers 21 | * Updated documentation for using oath in console 22 | * Fixed bug with failure app now allowing actual http auth to occur 23 | 24 | == 0.2.0 25 | * `sign_in` test helper now returns the user 26 | * `authenticate_session` arguments are coerced to Hash 27 | * Oath::BackDoor can be configured with a block 28 | * Document validation suggestions 29 | * Document locale suggestions 30 | * Deprecate usage of Oath.user_class in favor of Oath.config.user_class 31 | * Make warden strategy configurable via Oath.config.authentication_strategy 32 | * Extract warden setup into the WardenSetup class 33 | * Document layout suggestions 34 | 35 | == 0.1.1 36 | * Link to Rubydoc in documentation 37 | * Fix header in documentation 38 | * Fix no_login_redirect to default to a properly named controller 39 | * Fix documentation reference to sign_up to mention user_params instead of user 40 | 41 | == 0.1.0 42 | * Fix PasswordStrategy to use configuration options 43 | * Documentation 44 | * Renamed encryption to hashing 45 | * Renamed encrypted to digested 46 | * Renamed unencrypted to undigested 47 | * A configuration for `no_login_redirect` was added. This accepts anything that 48 | can be passed to `redirect_to` and is used when `require_login` is called with 49 | no logged in user. 50 | * A configuration for `no_login_handler` was added. This allows developers to 51 | completely customize the response when `require_login` is called with no 52 | logged in user. 53 | 54 | == 0.0.15 55 | * Delegate user_class correctly so that config returns class 56 | * Fixed issue authenticate session not allowing for multiple fields 57 | * Do not hardcode User class 58 | * Add signed out routing constraint 59 | * Backfill NEWS.md 60 | 61 | == 0.0.14 62 | * Encryption of empty string is empty string. 63 | * Remove last trace of generators. 64 | 65 | == 0.0.13 66 | * Oath requires Rails 4+. 67 | * Move generators to the oath-generators gem. 68 | 69 | == 0.0.12 70 | * Ensure forms can't be tampered with by providing no username. 71 | * Prevent hashing of empty string passwords. 72 | * Memoize the configuration. 73 | 74 | == 0.0.11 75 | * Add `Oath::Backdoor` for easier tests. 76 | 77 | == 0.0.10 78 | * Add Oath::Test::ControllerHelpers for controller specs. 79 | * Depend on the bcrypt gem, not the bcrypt-ruby gem. 80 | 81 | == 0.0.9 82 | * Make user creation method configurable. 83 | * Redirect to SessionsController#new, ignoring namespace. 84 | * Add `Oath.config.creation_method`. 85 | 86 | == 0.0.8 87 | * Now configurable via `Oath.configure`: 88 | * sign in service 89 | * sign up service 90 | * authentication service 91 | * user_token_store_field 92 | * user_token_field 93 | * Add PasswordReset service. 94 | * Rename controller_helpers to services. 95 | * Allos blocks to be passed into sign_in and sign_up. 96 | * Fix error on trying to respond with HTTP 401. 97 | * Oath does not generate a User model for you. 98 | * Add `Oath.test_mode!` and `Oath.test_reset!`. 99 | * Add a lot of tests. 100 | 101 | == 0.0.7 102 | * Check for Rails 4 or the strong_parameters gem, not just the strong_parameters gem 103 | 104 | == 0.0.6 105 | * [FIX] require_login should use controller and action for routing. 106 | 107 | == 0.0.5 108 | * [FIX] Scaffolded SessionsController should have respond_to. 109 | * [FIX] SignUp should get the value instead of slicing. 110 | 111 | == 0.0.4 112 | * Cleaned up generated controllers. 113 | * Use find_by_id instead of find so invalid sessions don't cause apps to crash. 114 | * Hashes passed in are no longer mutated via delete. 115 | 116 | == 0.0.3 117 | 118 | * Fixed bug where password wasn't deleted from session params which would cause lookup to fail. 119 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | oath (1.1.0) 5 | bcrypt 6 | rails 7 | warden 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | actioncable (5.1.4) 13 | actionpack (= 5.1.4) 14 | nio4r (~> 2.0) 15 | websocket-driver (~> 0.6.1) 16 | actionmailer (5.1.4) 17 | actionpack (= 5.1.4) 18 | actionview (= 5.1.4) 19 | activejob (= 5.1.4) 20 | mail (~> 2.5, >= 2.5.4) 21 | rails-dom-testing (~> 2.0) 22 | actionpack (5.1.4) 23 | actionview (= 5.1.4) 24 | activesupport (= 5.1.4) 25 | rack (~> 2.0) 26 | rack-test (>= 0.6.3) 27 | rails-dom-testing (~> 2.0) 28 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 29 | actionview (5.1.4) 30 | activesupport (= 5.1.4) 31 | builder (~> 3.1) 32 | erubi (~> 1.4) 33 | rails-dom-testing (~> 2.0) 34 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 35 | active_hash (1.5.2) 36 | activesupport (>= 2.2.2) 37 | activejob (5.1.4) 38 | activesupport (= 5.1.4) 39 | globalid (>= 0.3.6) 40 | activemodel (5.1.4) 41 | activesupport (= 5.1.4) 42 | activerecord (5.1.4) 43 | activemodel (= 5.1.4) 44 | activesupport (= 5.1.4) 45 | arel (~> 8.0) 46 | activesupport (5.1.4) 47 | concurrent-ruby (~> 1.0, >= 1.0.2) 48 | i18n (~> 0.7) 49 | minitest (~> 5.1) 50 | tzinfo (~> 1.1) 51 | addressable (2.5.2) 52 | public_suffix (>= 2.0.2, < 4.0) 53 | arel (8.0.0) 54 | bcrypt (3.1.11) 55 | builder (3.2.3) 56 | capybara (2.16.1) 57 | addressable 58 | mini_mime (>= 0.1.3) 59 | nokogiri (>= 1.3.3) 60 | rack (>= 1.0.0) 61 | rack-test (>= 0.5.4) 62 | xpath (~> 2.0) 63 | concurrent-ruby (1.0.5) 64 | crass (1.0.3) 65 | diff-lcs (1.3) 66 | erubi (1.7.0) 67 | globalid (0.4.1) 68 | activesupport (>= 4.2.0) 69 | i18n (0.9.1) 70 | concurrent-ruby (~> 1.0) 71 | loofah (2.1.1) 72 | crass (~> 1.0.2) 73 | nokogiri (>= 1.5.9) 74 | mail (2.7.0) 75 | mini_mime (>= 0.1.1) 76 | method_source (0.9.0) 77 | mini_mime (1.0.0) 78 | mini_portile2 (2.3.0) 79 | minitest (5.10.3) 80 | nio4r (2.1.0) 81 | nokogiri (1.8.1) 82 | mini_portile2 (~> 2.3.0) 83 | public_suffix (3.0.1) 84 | rack (2.0.3) 85 | rack-test (0.8.2) 86 | rack (>= 1.0, < 3) 87 | rails (5.1.4) 88 | actioncable (= 5.1.4) 89 | actionmailer (= 5.1.4) 90 | actionpack (= 5.1.4) 91 | actionview (= 5.1.4) 92 | activejob (= 5.1.4) 93 | activemodel (= 5.1.4) 94 | activerecord (= 5.1.4) 95 | activesupport (= 5.1.4) 96 | bundler (>= 1.3.0) 97 | railties (= 5.1.4) 98 | sprockets-rails (>= 2.0.0) 99 | rails-dom-testing (2.0.3) 100 | activesupport (>= 4.2.0) 101 | nokogiri (>= 1.6) 102 | rails-html-sanitizer (1.0.3) 103 | loofah (~> 2.0) 104 | railties (5.1.4) 105 | actionpack (= 5.1.4) 106 | activesupport (= 5.1.4) 107 | method_source 108 | rake (>= 0.8.7) 109 | thor (>= 0.18.1, < 2.0) 110 | rake (12.3.0) 111 | rspec (3.7.0) 112 | rspec-core (~> 3.7.0) 113 | rspec-expectations (~> 3.7.0) 114 | rspec-mocks (~> 3.7.0) 115 | rspec-core (3.7.0) 116 | rspec-support (~> 3.7.0) 117 | rspec-expectations (3.7.0) 118 | diff-lcs (>= 1.2.0, < 2.0) 119 | rspec-support (~> 3.7.0) 120 | rspec-mocks (3.7.0) 121 | diff-lcs (>= 1.2.0, < 2.0) 122 | rspec-support (~> 3.7.0) 123 | rspec-rails (3.7.2) 124 | actionpack (>= 3.0) 125 | activesupport (>= 3.0) 126 | railties (>= 3.0) 127 | rspec-core (~> 3.7.0) 128 | rspec-expectations (~> 3.7.0) 129 | rspec-mocks (~> 3.7.0) 130 | rspec-support (~> 3.7.0) 131 | rspec-support (3.7.0) 132 | sprockets (3.7.1) 133 | concurrent-ruby (~> 1.0) 134 | rack (> 1, < 3) 135 | sprockets-rails (3.2.1) 136 | actionpack (>= 4.0) 137 | activesupport (>= 4.0) 138 | sprockets (>= 3.0.0) 139 | sqlite3 (1.3.13) 140 | thor (0.20.0) 141 | thread_safe (0.3.6) 142 | tzinfo (1.2.4) 143 | thread_safe (~> 0.1) 144 | warden (1.2.7) 145 | rack (>= 1.0) 146 | websocket-driver (0.6.5) 147 | websocket-extensions (>= 0.1.0) 148 | websocket-extensions (0.1.3) 149 | xpath (2.1.0) 150 | nokogiri (~> 1.3) 151 | 152 | PLATFORMS 153 | ruby 154 | 155 | DEPENDENCIES 156 | active_hash 157 | capybara 158 | oath! 159 | rake 160 | rspec 161 | rspec-rails 162 | sqlite3 163 | 164 | BUNDLED WITH 165 | 1.16.0 166 | -------------------------------------------------------------------------------- /lib/oath.rb: -------------------------------------------------------------------------------- 1 | require 'warden' 2 | require "oath/version" 3 | require "oath/configuration" 4 | require "oath/services" 5 | require "oath/controller_helpers" 6 | require "oath/railtie" 7 | require "oath/failure_app" 8 | require "oath/back_door" 9 | require "oath/warden_setup" 10 | require "oath/field_map" 11 | require "oath/param_transformer" 12 | require "oath/strategies/password_strategy" 13 | require "active_support/core_ext/module/attribute_accessors" 14 | 15 | # Oath is an authentication toolkit designed to allow developers to create their own 16 | # authentication solutions. If you're interested in a default implementation try 17 | # {http://github.com/halogenandtoast/oath-generators Oath Generators} 18 | # @since 0.0.15 19 | module Oath 20 | mattr_accessor :warden_config 21 | mattr_accessor :config 22 | 23 | module Test 24 | autoload :Helpers, "oath/test/helpers" 25 | autoload :ControllerHelpers, "oath/test/controller_helpers" 26 | end 27 | 28 | # initialize Oath. Sets up warden and the default configuration. 29 | # 30 | # @note This is used in {Oath::Railtie} in order to bootstrap Oath 31 | # @param warden_config [Warden::Config] the configuration from warden 32 | # @see Oath::Railtie 33 | # @see Oath::Configuration 34 | def self.initialize warden_config 35 | setup_config 36 | setup_warden_config(warden_config) 37 | end 38 | 39 | # compares the token (undigested password) to a digested password 40 | # 41 | # @param digest [String] A digested password 42 | # @param token [String] An undigested password 43 | # @see Oath::Configuration#default_token_comparison 44 | # @return [Boolean] whether the token and digest match 45 | def self.compare_token(digest, token) 46 | config.token_comparison.call(digest, token) 47 | end 48 | 49 | # hashes a token 50 | # 51 | # @param token [String] the password in undigested form 52 | # @see Oath::Configuration#default_hashing_method 53 | # @return [String] a digest of the token 54 | def self.hash_token(token) 55 | config.hashing_method.call(token) 56 | end 57 | 58 | # performs transformations on params for signing up and 59 | # signing in 60 | # 61 | # @param params [Hash] hash of parameters to transform 62 | # @see Oath::Configuration#param_transofmrations 63 | # @return [Hash] hash with transformed parameters 64 | def self.transform_params(params) 65 | ParamTransformer.new(params, config.param_transformations).to_h 66 | end 67 | 68 | # the user class 69 | # 70 | # @see Oath::Configuration#setup_class_defaults 71 | # @deprecated Use Oath.config.user_class instead 72 | # @return [Class] the User class 73 | def self.user_class 74 | warn "#{Kernel.caller.first}: [DEPRECATION] " + 75 | 'Accessing the user class through the Oath module is deprecated. Use Oath.config.user_class instead.' 76 | config.user_class 77 | end 78 | 79 | # finds a user based on their credentials 80 | # 81 | # @param params [Hash] a hash of user parameters 82 | # @param field_map [FieldMap] a field map in order to allow multiple lookup fields 83 | # @see Oath::Configuration#default_find_method 84 | # @return [User] if user is found 85 | # @return [nil] if no user is found 86 | def self.lookup(params, field_map) 87 | if params.present? 88 | fields = FieldMap.new(params, field_map).to_fields 89 | self.config.find_method.call(fields) 90 | end 91 | end 92 | 93 | # Puts oath into test mode. This will disable hashing passwords 94 | # @note You must call this if you want to use oath in your tests 95 | def self.test_mode! 96 | Warden.test_mode! 97 | self.config ||= Oath::Configuration.new 98 | config.hashing_method = ->(password) { password } 99 | config.token_comparison = ->(digest, undigested_password) do 100 | digest == undigested_password 101 | end 102 | end 103 | 104 | # Configures oath 105 | # 106 | # @yield [configuration] Yield the current configuration 107 | # @example A custom configuration 108 | # Oath.configure do |config| 109 | # config.user_lookup_field = :username 110 | # config.user_token_store_field = :hashed_password 111 | # end 112 | def self.configure(&block) 113 | self.config ||= Oath::Configuration.new 114 | yield self.config 115 | end 116 | 117 | # Resets oath in between tests. 118 | # @note You must call this between tests 119 | def self.test_reset! 120 | Warden.test_reset! 121 | end 122 | 123 | private 124 | 125 | def self.setup_config 126 | self.config ||= Oath::Configuration.new 127 | end 128 | 129 | def self.setup_warden_config(warden_config) 130 | self.warden_config = WardenSetup.new(warden_config).call 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /lib/oath/configuration.rb: -------------------------------------------------------------------------------- 1 | module Oath 2 | # Configuration options for Oath 3 | # @since 0.0.15 4 | class Configuration 5 | attr_accessor :user_token_field, :user_token_store_field 6 | attr_accessor :hashing_method, :token_comparison, :user_lookup_field 7 | attr_accessor :sign_in_notice 8 | attr_accessor :sign_in_service, :sign_up_service, :sign_out_service 9 | attr_accessor :authentication_service, :password_reset_service 10 | attr_accessor :failure_app 11 | attr_accessor :creation_method, :find_method 12 | attr_accessor :no_login_handler, :no_login_redirect 13 | attr_accessor :authentication_strategy 14 | attr_accessor :warden_serialize_into_session, :warden_serialize_from_session 15 | attr_accessor :param_transformations 16 | 17 | attr_writer :user_class 18 | 19 | def initialize 20 | setup_class_defaults 21 | setup_token_hashing 22 | setup_notices 23 | setup_services 24 | setup_warden 25 | setup_param_transformations 26 | end 27 | 28 | # Default creation method. Can be overriden via {Oath.configure} 29 | # 30 | # @see #creation_method= 31 | def default_creation_method 32 | ->(params) do 33 | updated_params = Oath.transform_params(params) 34 | Oath.config.user_class.create(updated_params) 35 | end 36 | end 37 | 38 | # Default hashing method. Can be overriden via {Oath.configure} 39 | # 40 | # @see #hashing_method= 41 | def default_hashing_method 42 | ->(token) do 43 | if token.present? 44 | BCrypt::Password.create(token) 45 | else 46 | token 47 | end 48 | end 49 | end 50 | 51 | # Default find method. Can be overriden via {Oath.configure} 52 | # 53 | # @see #find_method= 54 | # @see Oath.config.user_class 55 | def default_find_method 56 | ->(params) do 57 | updated_params = Oath.transform_params(params) 58 | Oath.config.user_class.find_by(updated_params) 59 | end 60 | end 61 | 62 | # Default token comparison method. Can be overriden via {Oath.configure} 63 | # 64 | # @see #token_comparison= 65 | def default_token_comparison 66 | ->(digest, undigested_token) do 67 | BCrypt::Password.new(digest) == undigested_token 68 | end 69 | end 70 | 71 | # Default handler when user is not logged in. Can be overriden via {Oath.configure} 72 | # 73 | # @see #no_login_handler= 74 | # @see #sign_in_notice 75 | # @see #no_login_redirect 76 | def default_no_login_handler 77 | ->(controller) do 78 | notice = Oath.config.sign_in_notice 79 | 80 | if notice.respond_to?(:call) 81 | controller.flash.notice = notice.call 82 | else 83 | warn "[DEPRECATION] `Oath.config.sign_in_notice` should be a lambda instead of a string" 84 | controller.flash.notice = notice 85 | end 86 | 87 | controller.redirect_to Oath.config.no_login_redirect 88 | end 89 | end 90 | 91 | # User class. Can be overriden via {Oath.configure} 92 | # 93 | # @see #user_class= 94 | def user_class 95 | @user_class.constantize 96 | end 97 | 98 | private 99 | 100 | def setup_token_hashing 101 | @hashing_method = default_hashing_method 102 | @token_comparison = default_token_comparison 103 | end 104 | 105 | def setup_notices 106 | @sign_in_notice = -> { 'You must be signed in' } 107 | end 108 | 109 | def setup_class_defaults 110 | @user_class = 'User' 111 | @user_token_field = :password 112 | @user_token_store_field = :password_digest 113 | @user_lookup_field = :email 114 | @creation_method = default_creation_method 115 | @find_method = default_find_method 116 | @no_login_redirect = { controller: '/sessions', action: 'new' } 117 | @no_login_handler = default_no_login_handler 118 | end 119 | 120 | def setup_services 121 | @authentication_service = Oath::Services::Authentication 122 | @sign_in_service = Oath::Services::SignIn 123 | @sign_up_service = Oath::Services::SignUp 124 | @sign_out_service = Oath::Services::SignOut 125 | @password_reset_service = Oath::Services::PasswordReset 126 | end 127 | 128 | def setup_warden 129 | setup_warden_requirements 130 | setup_warden_serialization 131 | end 132 | 133 | def setup_warden_requirements 134 | @failure_app = Oath::FailureApp 135 | @authentication_strategy = Oath::Strategies::PasswordStrategy 136 | end 137 | 138 | def setup_warden_serialization 139 | @warden_serialize_into_session = -> (user) { user.id } 140 | @warden_serialize_from_session = -> (id) { Oath.config.user_class.find_by(id: id) } 141 | end 142 | 143 | def setup_param_transformations 144 | @param_transformations = { 145 | "email" => ->(value) { value.downcase } 146 | } 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/oath/controller_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'bcrypt' 2 | require 'active_support/concern' 3 | 4 | module Oath 5 | # Mixin to be included in Rails controllers. 6 | # @since 0.0.15 7 | module ControllerHelpers 8 | extend ActiveSupport::Concern 9 | included do 10 | if respond_to?(:helper_method) 11 | helper_method :current_user, :signed_in? 12 | end 13 | end 14 | 15 | # Sign in a user 16 | # 17 | # @note Uses the {Oath::Services::SignIn} service to create a session 18 | # 19 | # @param user [User] the user object to sign in 20 | # @yield Yields to the block if the user is successfully signed in 21 | # @return [Object] returns the value from calling perform on the {Oath::Services::SignIn} service 22 | def sign_in user 23 | Oath.config.sign_in_service.new(user, warden).perform.tap do |status| 24 | if status && block_given? 25 | yield 26 | end 27 | end 28 | end 29 | 30 | # Sign out the current session 31 | # 32 | # @note Uses the {Oath::Services::SignOut} service to destroy the session 33 | # 34 | # @return [Object] returns the value from calling perform on the {Oath::Services::SignOut} service 35 | def sign_out 36 | Oath.config.sign_out_service.new(warden).perform 37 | end 38 | 39 | # Sign up a user 40 | # 41 | # @note Uses the {Oath::Services::SignUp} service to create a user 42 | # 43 | # @param user_params [Hash] params containing lookup and token fields 44 | # @yield Yields to the block if the user is signed up successfully 45 | # @return [Object] returns the value from calling perform on the {Oath::Services::SignUp} service 46 | def sign_up user_params 47 | Oath.config.sign_up_service.new(user_params).perform.tap do |status| 48 | if status && block_given? 49 | yield 50 | end 51 | end 52 | end 53 | 54 | # Authenticates a session. 55 | # 56 | # @note Uses the {Oath::Services::Authentication} service to verify the user's details 57 | # 58 | # @param session_params [Hash] params containing lookup and token fields 59 | # @param field_map [Hash] Field map used for allowing users to sign in with multiple fields e.g. email and username 60 | # @return [User] if authentication succeeded 61 | # @return [nil] if authentication failed 62 | # @example Basic usage 63 | # class SessionsController < ApplicationController 64 | # def create 65 | # user = authenticate_session(session_params) 66 | # 67 | # if sign_in(user) 68 | # redirect_to(root_path) 69 | # else 70 | # render :new 71 | # end 72 | # end 73 | # 74 | # private 75 | # 76 | # def session_params 77 | # params.require(:session).permit(:email, :password) 78 | # end 79 | # 80 | # end 81 | # @example Using the field map to authenticate using multiple lookup fields 82 | # class SessionsController < ApplicationController 83 | # def create 84 | # user = authenticate_session(session_params, email_or_username: [:email, :username]) 85 | # 86 | # if sign_in(user) 87 | # redirect_to(root_path) 88 | # else 89 | # render :new 90 | # end 91 | # end 92 | # 93 | # private 94 | # 95 | # def session_params 96 | # params.require(:session).permit(:email_or_username, :password) 97 | # end 98 | # 99 | # end 100 | 101 | def authenticate_session session_params, field_map = nil 102 | token_field = Oath.config.user_token_field 103 | params_hash = Oath.transform_params(session_params).symbolize_keys 104 | password = params_hash.fetch(token_field) 105 | user = Oath.lookup(params_hash.except(token_field), field_map) 106 | authenticate(user, password) 107 | end 108 | 109 | # Authenticates a user given a password 110 | # 111 | # @note Uses the {Oath::Services::Authentication} service to verify the user's credentials 112 | # 113 | # @param user [User] the user 114 | # @param password [String] the password 115 | # @return [User] if authentication succeeded 116 | # @return [nil] if authentication failed 117 | def authenticate user, password 118 | Oath.config.authentication_service.new(user, password).perform 119 | end 120 | 121 | # Resets a user's password 122 | # 123 | # @note Uses the {Oath::Services::PasswordReset} service to change a user's password 124 | # 125 | # @param user [User] the user 126 | # @param password [String] the password 127 | def reset_password user, password 128 | Oath.config.password_reset_service.new(user, password).perform 129 | end 130 | 131 | # @api private 132 | def warden 133 | request.env['warden'] 134 | end 135 | 136 | # helper_method that returns the current user 137 | # 138 | # @return [User] if user is signed in 139 | # @return [nil] if user is not signed in 140 | def current_user 141 | @current_user ||= warden.user 142 | end 143 | 144 | # helper_method that checks if there is a user signed in 145 | # 146 | # @return [User] if user is signed in 147 | # @return [nil] if user is not signed in 148 | def signed_in? 149 | warden.user 150 | end 151 | 152 | # before_action that determines what to do when the user is not signed in 153 | # 154 | # @note Uses the no login handler 155 | def require_login 156 | unless signed_in? 157 | Oath.config.no_login_handler.call(self) 158 | end 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /spec/oath/controller_helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'warden' 3 | 4 | module Oath 5 | describe ControllerHelpers do 6 | class WardenMock 7 | def user; end 8 | end 9 | class Flash < Struct.new(:notice) 10 | end 11 | class FakeRequest 12 | attr_reader :env 13 | def initialize(env) 14 | @env = env 15 | end 16 | end 17 | 18 | class Dummy 19 | attr_reader :redirected, :redirected_to, :flash, :request 20 | def initialize warden 21 | @warden = warden 22 | @flash = Flash.new 23 | @redirected = false 24 | @request = FakeRequest.new(env) 25 | end 26 | def redirect_to path 27 | @redirected = true 28 | @redirected_to = path 29 | end 30 | def env 31 | { "warden" => @warden } 32 | end 33 | end 34 | 35 | before(:each) do 36 | @warden = WardenMock.new 37 | @dummy = Dummy.new(@warden) 38 | @dummy.extend(ControllerHelpers) 39 | end 40 | 41 | it 'performs a sign in' do 42 | user = stub_sign_in 43 | @dummy.sign_in user 44 | end 45 | 46 | it 'runs the block when user is signed in' do 47 | user = stub_sign_in 48 | expectation = double() 49 | allow(expectation).to receive(:success) 50 | @dummy.sign_in(user) { expectation.success } 51 | expect(expectation).to have_received(:success) 52 | end 53 | 54 | it 'does not run the block when user can not be signed in' do 55 | user = stub_sign_in(false) 56 | expectation = double() 57 | allow(expectation).to receive(:failure) 58 | @dummy.sign_in(user) { expectation.failure } 59 | expect(expectation).not_to have_received(:failure) 60 | end 61 | 62 | it 'performs a sign out' do 63 | sign_out = double() 64 | allow(sign_out).to receive(:perform) 65 | allow(Services::SignOut).to receive(:new).with(@warden).and_return(sign_out) 66 | @dummy.sign_out 67 | expect(sign_out).to have_received(:perform) 68 | end 69 | 70 | it 'performs a sign_up' do 71 | user_params = stub_sign_up 72 | @dummy.sign_up user_params 73 | end 74 | 75 | it 'runs the block when user is signed up' do 76 | user_params = stub_sign_up 77 | expectation = double() 78 | allow(expectation).to receive(:success) 79 | @dummy.sign_up(user_params) { expectation.success } 80 | expect(expectation).to have_received(:success) 81 | end 82 | 83 | it 'does not run the block when user can not be signed up' do 84 | user_params = stub_sign_up(false) 85 | expectation = double() 86 | allow(expectation).to receive(:failure) 87 | @dummy.sign_up(user_params) { expectation.failure } 88 | expect(expectation).not_to have_received(:failure) 89 | end 90 | 91 | it 'authenticates a session' do 92 | session_params = { password: 'password', email: 'a@b.com' } 93 | user = double() 94 | authentication = double() 95 | allow(authentication).to receive(:perform).and_return(user) 96 | allow(Oath).to receive(:lookup).with({email: 'a@b.com'}, nil).and_return(user) 97 | allow(Services::Authentication).to receive(:new).with(user, 'password').and_return(authentication) 98 | expect(@dummy.authenticate_session(session_params)).to eq user 99 | end 100 | 101 | it 'authenticates a session against multiple fields' do 102 | session_params = { email_or_username: 'foo', password: 'password' } 103 | field_map = { email_or_username: [:email, :username] } 104 | user = double() 105 | authentication = double() 106 | allow(authentication).to receive(:perform).and_return(user) 107 | allow(Oath).to receive(:lookup).with(session_params.except(:password), field_map).and_return(user) 108 | allow(Services::Authentication).to receive(:new).with(user, 'password').and_return(authentication) 109 | expect(@dummy.authenticate_session(session_params, field_map)).to eq user 110 | end 111 | 112 | it 'returns false when it could not authenticate the user' do 113 | session_params = { password: "password", lookup_key: "lookup_key" } 114 | user = double() 115 | authentication = double() 116 | allow(authentication).to receive(:perform).and_return(false) 117 | allow(Oath).to receive(:lookup).with({ lookup_key: "lookup_key" }, nil).and_return(user) 118 | allow(Services::Authentication).to receive(:new).with(user, 'password').and_return(authentication) 119 | expect(@dummy.authenticate_session(session_params)).to be_falsey 120 | end 121 | 122 | it 'performs an authenticate' do 123 | user = double() 124 | password = double() 125 | authentication = double() 126 | allow(authentication).to receive(:perform) 127 | allow(Services::Authentication).to receive(:new).with(user, password).and_return(authentication) 128 | @dummy.authenticate user, password 129 | expect(authentication).to have_received(:perform) 130 | end 131 | 132 | it 'returns the current user' do 133 | current_user = double() 134 | allow(@warden).to receive(:user).and_return(current_user) 135 | expect(@dummy.current_user).to eq current_user 136 | end 137 | 138 | it 'returns signed_in?' do 139 | allow(@warden).to receive(:user) 140 | allow(@dummy).to receive(:current_user) 141 | @dummy.signed_in? 142 | expect(@warden).to have_received(:user) 143 | expect(@dummy).not_to have_received(:current_user) 144 | end 145 | 146 | it 'redirects when not signed_in' do 147 | allow(@warden).to receive(:user).and_return(false) 148 | @dummy.require_login 149 | expect(@dummy.redirected).to eq(true) 150 | expect(@dummy.redirected_to).to eq(Oath.config.no_login_redirect) 151 | expect(@dummy.flash.notice).to eq(Oath.config.sign_in_notice.call) 152 | end 153 | 154 | it 'does not redirect when signed_in' do 155 | allow(@warden).to receive(:user).and_return(true) 156 | @dummy.require_login 157 | expect(@dummy.redirected).to eq(false) 158 | end 159 | 160 | it 'returns warden' do 161 | expect(@dummy.warden).to eq @warden 162 | end 163 | 164 | def stub_sign_in(success = true) 165 | user = double() 166 | sign_in = double() 167 | allow(sign_in).to receive(:perform).and_return(success) 168 | allow(Services::SignIn).to receive(:new).with(user, @warden).and_return(sign_in) 169 | user 170 | end 171 | 172 | def stub_sign_up(success = true) 173 | user_params = double() 174 | sign_up = double() 175 | allow(sign_up).to receive(:perform).and_return(success) 176 | allow(Services::SignUp).to receive(:new).with(user_params).and_return(sign_up) 177 | user_params 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NOTICE 2 | 3 | Monban is currently in the process of being renamed Oath: https://github.com/halogenandtoast/oath 4 | 5 | 6 | # Oath 7 | 8 | [![Build Status](https://travis-ci.org/halogenandtoast/oath.png?branch=master)](https://travis-ci.org/halogenandtoast/oath) 9 | [![Code Climate](https://codeclimate.com/github/halogenandtoast/oath.png)](https://codeclimate.com/github/halogenandtoast/oath) 10 | 11 | Oath is designed to be a very simple and extensible user authentication 12 | library for rails. Its goal is to give all the power to the developer instead 13 | of forcing them to make Oath work with their system. 14 | 15 | ## Why use Oath? 16 | 17 | Oath makes authentication simple: 18 | 19 | - Easy to use in tests with dependency injection 20 | - Provides convenient controller helpers 21 | - Very customizable 22 | 23 | Oath doesn't do the following: 24 | 25 | - Doesn't automatically add routes to your application 26 | - Doesn't force you to use engine based controllers or views 27 | - Doesn't require you to make changes to your user model 28 | 29 | ## Documentation 30 | 31 | You can read the full documentation at [rubydoc](http://rubydoc.info/github/halogenandtoast/oath) 32 | 33 | ## Installation 34 | 35 | Oath was designed to work with Rails > 4.0. Add this line to your Gemfile: 36 | 37 | gem 'oath' 38 | 39 | Then inside of your ApplicationController add the following: 40 | 41 | include Oath::ControllerHelpers 42 | 43 | And you're ready to start designing your authentication system. 44 | 45 | ## Generators 46 | 47 | If you'd like a good starting point for building an app using Oath, it is suggested to use the [oath generators] 48 | 49 | ## Usage 50 | 51 | Oath does currently have some out-of-the-box expectations, but you can 52 | configure and change any of these: 53 | 54 | - By default the model should be called `User` 55 | - Oath expects your user model to respond to `create`, `id`, and `find_by` 56 | - You should have an `email` and `password_digest` column on your `User` 57 | - Passwords will be handled with BCrypt 58 | 59 | ### Suggestions 60 | 61 | #### Console Usage 62 | 63 | If you're trying to sign up a User in a console you won't be able to call User#new or User#create because the User model does not know how to encrypt passwords. 64 | You should instead use the sign up service in order to create the user: 65 | 66 | ```ruby 67 | Oath.config.sign_up_service.new(email: "foo@example.com", password: "password").perform 68 | ``` 69 | 70 | #### Validations 71 | 72 | Oath doesn't add validations to your user model unless you're using [oath generators] so it's suggested to add the following validations: 73 | 74 | ```ruby 75 | validates :email, presence: true, uniqueness: true 76 | validates :password_digest, presence: true 77 | ``` 78 | 79 | In addition to that you'll want to add the following to your `config/locale/en.yml`: 80 | 81 | ```yaml 82 | en: 83 | activerecord: 84 | attributes: 85 | user: 86 | password_digest: "Password" 87 | ``` 88 | 89 | Which will generate the error message `Password can't be blank` instead of `Password digest can't be blank`. 90 | 91 | #### Layout changes 92 | 93 | It is suggested you add something like this to your application layout: 94 | 95 | ```erb 96 | <% if signed_in? %> 97 | <%= link_to "Sign out", session_path, method: :delete %> 98 | <% else %> 99 | <%= link_to "Sign in", new_session_path %> 100 | <%= link_to "Sign up", new_user_path %> 101 | <% end %> 102 | ``` 103 | 104 | #### Guest user 105 | 106 | If you want to introduce a Guest object when a user is not signed in, you can override Oath's `current_user` method in your `ApplicationController`: 107 | 108 | ```ruby 109 | def current_user 110 | super || Guest.new 111 | end 112 | ``` 113 | 114 | In `app/models/`, define a `Guest` class: 115 | 116 | ```ruby 117 | class Guest 118 | def name 119 | "Guest" 120 | end 121 | end 122 | ``` 123 | 124 | This article on the [Null Object Pattern](http://robots.thoughtbot.com/handling-associations-on-null-objects) provides a good explanation of why you might want to do this. 125 | 126 | #### Using I18n for sign in notice 127 | 128 | If you want to use I18n for the notice instructing users to sign in when they try to access an unauthorized page you can do so with the following configuration: 129 | 130 | ```ruby 131 | Oath.configure do |config| 132 | config.sign_in_notice = -> { I18n.t("sign_in_notice") } 133 | end 134 | ``` 135 | 136 | It is suggested to store this file at `config/initializers/oath.rb` 137 | 138 | ### Controller Additions 139 | 140 | Oath provides the following controller methods: 141 | 142 | - `sign_in(user)` 143 | - `sign_out` 144 | - `sign_up(user_params)` 145 | - `authenticate(user, password)` 146 | - `authenticate_session(session_params)` 147 | - `reset_password(user, password)` 148 | 149 | These helpers: 150 | 151 | - `current_user` 152 | - `signed_in?` 153 | 154 | And this filter: 155 | 156 | - `require_login` 157 | 158 | ### Routing Constraints 159 | 160 | To authorize users in `config/routes.rb`: 161 | 162 | ```ruby 163 | require "oath/constraints/signed_in" 164 | require "oath/constraints/signed_out" 165 | 166 | Blog::Application.routes.draw do 167 | constraints Oath::Constraints::SignedIn.new do 168 | root "dashboards#show", as: :dashboard 169 | end 170 | 171 | constraints Oath::Constraints::SignedOut.new do 172 | root "landings#show" 173 | end 174 | end 175 | ``` 176 | 177 | ## Usage in Tests 178 | 179 | ### Test mode 180 | 181 | Oath provides the following: 182 | 183 | ```ruby 184 | Oath.test_mode! 185 | ``` 186 | 187 | Which will change password hashing method to provide plaintext responses instead of using BCrypt. This will allow you to write factories using the password_digest field: 188 | 189 | ```ruby 190 | FactoryBot.define do 191 | factory :user do 192 | sequence(:email) { |n| "user#{n}@example.com" } 193 | password_digest 'password' 194 | end 195 | end 196 | ``` 197 | 198 | ### Spec helpers 199 | 200 | A couple of convenience methods are available in your tests. In order to set this up you'll want to add the following to `rails_helper.rb` or if that doesn't exist `spec_helper.rb` 201 | 202 | ```ruby 203 | Oath.test_mode! 204 | 205 | RSpec.configure do |config| 206 | config.include Oath::Test::Helpers, type: :feature 207 | config.after :each do 208 | Oath.test_reset! 209 | end 210 | end 211 | ``` 212 | 213 | Then you can use any of the [test helpers] in your scenarios 214 | 215 | ```ruby 216 | feature "A feature spec" do 217 | scenario "that requires login" do 218 | user = create(:user) 219 | sign_in(user) 220 | # do something 221 | sign_out 222 | # do something else 223 | end 224 | end 225 | ``` 226 | 227 | ### Oath Backdoor 228 | 229 | Similar to clearance's backdoor you can visit a path and sign in quickly via 230 | 231 | ```ruby 232 | user = create(:user) 233 | visit dashboard_path(as: user) 234 | ``` 235 | 236 | To enable this functionality you'll want to add the following to `config/environments/test.rb`: 237 | 238 | ```ruby 239 | config.middleware.insert_after Warden::Manager, Oath::BackDoor 240 | ``` 241 | 242 | If you'd like to find your User model by a field other than `id`, insert the 243 | middleware with a block that accepts the `as` query parameter and returns an 244 | instance of your User model: 245 | 246 | ```ruby 247 | config.middleware.insert_after Warden::Manager, Oath::BackDoor do |user_param| 248 | User.find_by(username: user_param) 249 | end 250 | ``` 251 | 252 | ### Controller Specs 253 | 254 | If you are going to write controller tests, helpers are provided for those as well: 255 | 256 | ```ruby 257 | Oath.test_mode! 258 | 259 | RSpec.configure do |config| 260 | config.include Oath::Test::ControllerHelpers, type: :controller 261 | config.after :each do 262 | Oath.test_reset! 263 | end 264 | end 265 | ``` 266 | 267 | ```ruby 268 | require 'spec_helper' 269 | 270 | describe ProtectedController do 271 | 272 | describe "GET 'index'" do 273 | it "returns http success when signed in" do 274 | user = create(:user) 275 | sign_in(user) 276 | get 'index' 277 | response.should be_success 278 | end 279 | 280 | it "redirects when not signed in" do 281 | get 'index' 282 | response.should be_redirect 283 | end 284 | end 285 | end 286 | ``` 287 | 288 | ## Advanced Functionality 289 | 290 | ### Authentication with username instead of email 291 | 292 | If you want to sign in with username instead of email just change the configuration option 293 | 294 | ```ruby 295 | # config/initializers/oath.rb 296 | Oath.configure do |config| 297 | config.user_lookup_field = :username 298 | end 299 | ``` 300 | 301 | If you used the oath:scaffold generator from [oath generators] you'll have to change the following four references to email. 302 | 303 | * In SessionsController#session_params 304 | * In UsersController#user_params 305 | * The email form field on sessions#new 306 | * The email form field on users#new 307 | 308 | ### Using multiple lookup fields 309 | 310 | You may perform a look up on a user using multiple fields by doing something like the following: 311 | 312 | ```ruby 313 | class SessionsController < ApplicationController 314 | def create 315 | user = authenticate_session(session_params, email_or_username: [:email, :username]) 316 | 317 | if sign_in(user) 318 | redirect_to(root_path) 319 | else 320 | render :new 321 | end 322 | end 323 | 324 | private 325 | 326 | def session_params 327 | params.require(:session).permit(:email_or_username, :password) 328 | end 329 | 330 | end 331 | ``` 332 | 333 | This will allow the user to enter either their username or email to login 334 | 335 | ## Configuration 336 | 337 | Oath::Configuration has lots of options for changing how oath works. Currently the options you can change are as follows: 338 | 339 | ### User values 340 | 341 | * **user_lookup_field**: (default `:email`) Field in the database to lookup a user by. 342 | * **user_token_field**: (default `:password`) Field the form submits containing the undigested password. 343 | * **user_token_store_field**: (default: `:password_digest`) Field in the database that stores the user's digested password. 344 | * **user_class**: (default: `'User'`) The user class. 345 | 346 | ### Services 347 | 348 | * **sign_in_notice**: (default: `You must be signed in`) Rails flash message to set when user signs in. 349 | * **sign_in_service**: (default: `Oath::Services::SignIn`) Service for signing a user in. 350 | * **sign_up_service**: (default: `Oath::Services::SignUp`) Service for signing a user up. 351 | * **sign_out_service**: (default: `Oath::Services::SignOut`) Service for signing a user out. 352 | * **authentication_service**: (default: `Oath::Services::Authentication`) Service for authenticated a user. 353 | * **password_reset_service**: (default: `Oath::Services::PasswordReset`) Service for resetting a user's password. 354 | 355 | ### Rails values 356 | 357 | * **no_login_handler**: A before_action for rails that handles when a user is not signed in. 358 | * **no_login_redirect**: Used by the no_login_handler to redirect the user 359 | 360 | ### Methods 361 | 362 | * **hashing_method**: Method to hash an undigested password. 363 | * **token_comparison**: Method to compare a digested and undigested password. 364 | * **creation_method**: Method for creating a user. 365 | * **find_method**: Method for finding a user. 366 | 367 | ### Warden Settings 368 | 369 | * **failure_app**: Necessary for warden to work. A rack app that handles failures in authentication. 370 | 371 | ## Limitations 372 | 373 | Here are a few of the current limitations of oath: 374 | 375 | - Oath assumes you only have one user model. 376 | 377 | ## Contributing 378 | 379 | 1. [Fork it](https://github.com/halogenandtoast/oath/fork) 380 | 2. Create your feature branch (`git checkout -b my-new-feature`) 381 | 3. Commit your changes (`git commit -am 'Add some feature'`) 382 | 4. Push to the branch (`git push origin my-new-feature`) 383 | 5. Create new Pull Request 384 | 385 | [oath generators]: https://github.com/halogenandtoast/oath-generators 386 | [test helpers]: https://github.com/halogenandtoast/oath/blob/master/lib/oath/test/helpers.rb 387 | --------------------------------------------------------------------------------