├── .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 | [](https://travis-ci.org/halogenandtoast/oath)
9 | [](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 |
--------------------------------------------------------------------------------