├── app └── .keep ├── .ruby-version ├── spec ├── dummy │ ├── log │ │ └── .keep │ ├── app │ │ ├── mailers │ │ │ └── .keep │ │ ├── models │ │ │ ├── .keep │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ └── account.rb │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── stylesheets │ │ │ │ ├── accounts.css │ │ │ │ └── application.css │ │ │ └── javascripts │ │ │ │ ├── accounts.js │ │ │ │ └── application.js │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── application_controller.rb │ │ │ └── accounts_controller.rb │ │ ├── helpers │ │ │ ├── accounts_helper.rb │ │ │ └── application_helper.rb │ │ └── views │ │ │ ├── accounts │ │ │ ├── index.html.erb │ │ │ └── show.html.erb │ │ │ └── layouts │ │ │ └── application.html.erb │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── .rspec │ ├── db │ │ ├── test.sqlite3 │ │ ├── migrate │ │ │ ├── 20150210215727_add_password_reset_tokens.rb │ │ │ ├── 20150211185152_add_activation_token_to_account.rb │ │ │ └── 20150127045508_create_accounts.rb │ │ └── schema.rb │ ├── bin │ │ ├── rake │ │ ├── bundle │ │ ├── rails │ │ └── setup │ ├── config.ru │ ├── config │ │ ├── initializers │ │ │ ├── cookies_serializer.rb │ │ │ ├── session_store.rb │ │ │ ├── mime_types.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── assets.rb │ │ │ ├── wrap_parameters.rb │ │ │ └── inflections.rb │ │ ├── environment.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── secrets.yml │ │ ├── locales │ │ │ └── en.yml │ │ ├── application.rb │ │ ├── routes.rb │ │ └── environments │ │ │ ├── test.rb │ │ │ └── development.rb │ ├── Rakefile │ ├── README.rdoc │ └── spec │ │ └── spec_helper.rb ├── support │ └── helpers │ │ └── authentication_helper.rb ├── factories │ └── accounts.rb ├── spec_helper.rb ├── controllers │ └── accounts_controller_spec.rb ├── rails_helper.rb └── models │ └── account_spec.rb ├── .rspec ├── Gemfile ├── config └── routes.rb ├── lib ├── pillowfort │ ├── version.rb │ ├── engine.rb │ ├── helpers │ │ └── deprecation_helper.rb │ ├── config.rb │ └── concerns │ │ ├── models │ │ ├── resource │ │ │ ├── password_reset.rb │ │ │ ├── activation.rb │ │ │ └── base.rb │ │ └── token │ │ │ └── base.rb │ │ └── controllers │ │ └── base.rb ├── tasks │ └── pillowfort_tasks.rake └── pillowfort.rb ├── docs └── assets │ └── pillowfort.gif ├── .gitignore ├── bin └── rails ├── db └── migrate │ ├── 20160204201918_create_pillowfort_resources.rb │ └── 20160204201924_create_pillowfort_tokens.rb ├── Rakefile ├── MIT-LICENSE ├── pillowfort.gemspec ├── Gemfile.lock └── README.md /app/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.0 2 | -------------------------------------------------------------------------------- /spec/dummy/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /spec/dummy/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/accounts_helper.rb: -------------------------------------------------------------------------------- 1 | module AccountsHelper 2 | end 3 | -------------------------------------------------------------------------------- /lib/pillowfort/version.rb: -------------------------------------------------------------------------------- 1 | module Pillowfort 2 | VERSION = '2.1.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /docs/assets/pillowfort.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/pillowfort/master/docs/assets/pillowfort.gif -------------------------------------------------------------------------------- /spec/dummy/db/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coroutine/pillowfort/master/spec/dummy/db/test.sqlite3 -------------------------------------------------------------------------------- /spec/dummy/app/views/accounts/index.html.erb: -------------------------------------------------------------------------------- 1 |

Accounts#index

2 |

Find me in app/views/accounts/index.html.erb

3 | -------------------------------------------------------------------------------- /spec/dummy/app/views/accounts/show.html.erb: -------------------------------------------------------------------------------- 1 |

Accounts#show

2 |

Find me in app/views/accounts/show.html.erb

3 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | spec/dummy/log/*.log 4 | 5 | pkg/ 6 | 7 | .DS_Store 8 | 9 | pillowfort-*.gem 10 | -------------------------------------------------------------------------------- /lib/tasks/pillowfort_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :pillowfort do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/accounts.css: -------------------------------------------------------------------------------- 1 | /* 2 | Place all the styles related to the matching controller here. 3 | They will automatically be included in application.css. 4 | */ 5 | -------------------------------------------------------------------------------- /spec/dummy/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 Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/accounts.js: -------------------------------------------------------------------------------- 1 | // Place all the behaviors and hooks related to the matching controller here. 2 | // All this logic will automatically be available in application.js. 3 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/dummy/app/models/account.rb: -------------------------------------------------------------------------------- 1 | class Account < ActiveRecord::Base 2 | include Pillowfort::Concerns::ModelAuthentication 3 | include Pillowfort::Concerns::ModelPasswordReset 4 | include Pillowfort::Concerns::ModelActivation 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | # Prevent CSRF attacks by raising an exception. 3 | # For APIs, you may want to use :null_session instead. 4 | protect_from_forgery with: :exception 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20150210215727_add_password_reset_tokens.rb: -------------------------------------------------------------------------------- 1 | class AddPasswordResetTokens < ActiveRecord::Migration 2 | def change 3 | change_table :accounts do |t| 4 | t.string :password_reset_token 5 | t.datetime :password_reset_token_expires_at 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/pillowfort/engine.rb: -------------------------------------------------------------------------------- 1 | module Pillowfort 2 | class Engine < ::Rails::Engine 3 | 4 | # isolation 5 | isolate_namespace Pillowfort 6 | 7 | # generators 8 | config.generators do |g| 9 | g.test_framework :rspec 10 | g.fixture_replacement :factory_girl, dir: 'spec/factories' 11 | end 12 | 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20150211185152_add_activation_token_to_account.rb: -------------------------------------------------------------------------------- 1 | class AddActivationTokenToAccount < ActiveRecord::Migration 2 | def change 3 | change_table :accounts do |t| 4 | t.datetime :activated_at 5 | t.string :activation_token 6 | t.datetime :activation_token_expires_at 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20150127045508_create_accounts.rb: -------------------------------------------------------------------------------- 1 | class CreateAccounts < ActiveRecord::Migration 2 | def change 3 | create_table :accounts do |t| 4 | t.string :email 5 | t.string :password_digest 6 | t.string :auth_token 7 | t.datetime :auth_token_expires_at 8 | 9 | t.timestamps null: false 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/accounts_controller.rb: -------------------------------------------------------------------------------- 1 | class AccountsController < ApplicationController 2 | include Pillowfort::Concerns::ControllerAuthentication 3 | include Pillowfort::Concerns::ControllerActivation 4 | 5 | skip_filter :authenticate_from_account_token!, only: [:index] 6 | skip_filter :enforce_activation!, only: [:index] 7 | 8 | def index 9 | head :ok 10 | end 11 | 12 | def show 13 | head :ok 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/dummy/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/support/helpers/authentication_helper.rb: -------------------------------------------------------------------------------- 1 | module AuthenticationHelper 2 | def authenticate_with(account) 3 | return unless account 4 | 5 | email = account.email 6 | token = account.auth_token 7 | 8 | request.env['HTTP_AUTHORIZATION'] = 9 | ActionController::HttpAuthentication::Basic.encode_credentials(email, token) 10 | end 11 | end 12 | 13 | RSpec.configure do |config| 14 | config.include AuthenticationHelper, :type => :controller 15 | end 16 | -------------------------------------------------------------------------------- /spec/factories/accounts.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | sequence :email do |n| 3 | "foo.bar.#{n}@baz.org" 4 | end 5 | 6 | factory :account do 7 | email 8 | password { "SuperSafe123" } 9 | activation_token { "thisismytoken" } 10 | activation_token_expires_at { 1.hour.from_now } 11 | activated_at nil 12 | 13 | trait :activated do 14 | activation_token nil 15 | activation_token_expires_at nil 16 | activated_at { Time.now } 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/pillowfort/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /spec/dummy/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 | # 7 | default: &default 8 | adapter: 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 | <<: *default 17 | database: db/test.sqlite3 18 | 19 | development: 20 | <<: *default 21 | database: db/development.sqlite3 22 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /db/migrate/20160204201918_create_pillowfort_resources.rb: -------------------------------------------------------------------------------- 1 | # This migration was generated by Pillowfort. It assumes the default 2 | # name for authenticable model. Modify as needed before running 3 | # `rake db:migrate`. 4 | # 5 | class CreatePillowfortResources < ActiveRecord::Migration[5.1] 6 | def change 7 | create_table :users do |t| 8 | t.string :email, null: false 9 | t.string :password_digest, null: true 10 | 11 | t.timestamps null: false 12 | end 13 | 14 | add_index :users, 15 | [:email], 16 | unique: true, 17 | name: :udx_users_on_email 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/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 any plugin's vendor/assets/javascripts directory 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 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /lib/pillowfort/helpers/deprecation_helper.rb: -------------------------------------------------------------------------------- 1 | # This module establishes convenience methods related to adding 2 | # deprecation messages. 3 | # 4 | 5 | module Pillowfort 6 | module Helpers 7 | module DeprecationHelper 8 | 9 | # This method prints a deprecation warning for the specified 10 | # method name and refers the user to the new method name. 11 | # 12 | def self.warn(klass_name, bad, good) 13 | head = "********** PILLOWFORT WARNING - #{ klass_name }" 14 | msg = "The method `#{ bad }` will be deprecated in the next major release. Please use `#{ good }` instead." 15 | 16 | puts "#{ head }: #{ msg }" 17 | end 18 | 19 | end 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | test: 14 | secret_key_base: a0f1d3fd6b4dd881bf33ba3ab5984676168123f5dd60e21358d94c34a9eeb46b02e5c63b288fdb98f4372ab1163f4b55de63620541750310025116f8d0b16efa 15 | -------------------------------------------------------------------------------- /spec/dummy/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. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy/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 any plugin's vendor/assets/stylesheets directory 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 bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | 3 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 4 | 5 | require 'rspec/rails' 6 | require 'rspec/autorun' 7 | require 'factory_girl_rails' 8 | require 'rspec/its' 9 | 10 | Rails.backtrace_cleaner.remove_silencers! 11 | 12 | # Load support files 13 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 14 | 15 | RSpec.configure do |config| 16 | config.mock_with :rspec 17 | config.use_transactional_fixtures = true 18 | config.infer_base_class_for_anonymous_controllers = false 19 | config.expect_with :rspec do |expectations| 20 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 21 | end 22 | config.mock_with :rspec do |mocks| 23 | mocks.verify_partial_doubles = true 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'Pillowfort' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | load 'rails/tasks/statistics.rake' 21 | 22 | Bundler::GemHelper.install_tasks 23 | 24 | require 'rspec/core' 25 | require 'rspec/core/rake_task' 26 | 27 | desc "Run all the specs in the dummy app" 28 | RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare') 29 | 30 | task :default => :spec 31 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Coroutine 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /db/migrate/20160204201924_create_pillowfort_tokens.rb: -------------------------------------------------------------------------------- 1 | # This migration was generated by Pillowfort. It creates a default 2 | # implementation of the token model. Modify as needed before running 3 | # `rake db:migrate`. 4 | # 5 | class CreatePillowfortTokens < ActiveRecord::Migration[5.1] 6 | def change 7 | create_table :pillowfort_tokens do |t| 8 | t.integer :resource_id, null: false 9 | t.string :type, null: false, default: 'session' 10 | t.string :secret, null: false 11 | t.string :realm, null: false, default: 'application' 12 | t.datetime :created_at, null: false 13 | t.datetime :expires_at, null: false 14 | t.datetime :confirmed_at, null: true 15 | end 16 | 17 | add_index :pillowfort_tokens, 18 | [:resource_id, :type, :realm], 19 | unique: true, 20 | name: :udx_pillowfort_tokens_on_rid_type_and_realm 21 | 22 | add_index :pillowfort_tokens, 23 | [:type, :secret], 24 | unique: true, 25 | name: :udx_pillowfort_tokens_on_type_and_secret 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/dummy/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 "action_view/railtie" 8 | require "sprockets/railtie" 9 | # require "rails/test_unit/railtie" 10 | 11 | Bundler.require(*Rails.groups) 12 | require "pillowfort" 13 | 14 | module Dummy 15 | class Application < Rails::Application 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 21 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 22 | # config.time_zone = 'Central Time (US & Canada)' 23 | 24 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 25 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 26 | # config.i18n.default_locale = :de 27 | 28 | # Do not swallow errors in after_commit/after_rollback callbacks. 29 | config.active_record.raise_in_transactional_callbacks = true 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20150211185152) do 15 | 16 | create_table "accounts", force: :cascade do |t| 17 | t.string "email" 18 | t.string "password_digest" 19 | t.string "auth_token" 20 | t.datetime "auth_token_expires_at" 21 | t.datetime "created_at", null: false 22 | t.datetime "updated_at", null: false 23 | t.string "password_reset_token" 24 | t.datetime "password_reset_token_expires_at" 25 | t.datetime "activated_at" 26 | t.string "activation_token" 27 | t.datetime "activation_token_expires_at" 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/pillowfort.rb: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------- 2 | # Dependencies 3 | #---------------------------------------------------------- 4 | 5 | require 'scrypt' 6 | 7 | 8 | #---------------------------------------------------------- 9 | # Autoloads 10 | #---------------------------------------------------------- 11 | 12 | module Pillowfort 13 | 14 | # errors 15 | class NotActivatedError < StandardError; end 16 | class NotAuthenticatedError < StandardError; end 17 | class TokenStateError < StandardError; end 18 | 19 | # concerns 20 | module Concerns 21 | module Controllers 22 | autoload :Base, 'pillowfort/concerns/controllers/base' 23 | end 24 | module Models 25 | module Resource 26 | autoload :Base, 'pillowfort/concerns/models/resource/base' 27 | autoload :Activation, 'pillowfort/concerns/models/resource/activation' 28 | autoload :PasswordReset, 'pillowfort/concerns/models/resource/password_reset' 29 | end 30 | module Token 31 | autoload :Base, 'pillowfort/concerns/models/token/base' 32 | end 33 | end 34 | end 35 | 36 | # helpers 37 | module Helpers 38 | autoload :DeprecationHelper, 'pillowfort/helpers/deprecation_helper' 39 | end 40 | 41 | end 42 | 43 | 44 | #---------------------------------------------------------- 45 | # Requires 46 | #---------------------------------------------------------- 47 | 48 | require "pillowfort/config" 49 | require "pillowfort/engine" 50 | -------------------------------------------------------------------------------- /pillowfort.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('../lib', __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require 'pillowfort/version' 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = 'pillowfort' 9 | s.version = Pillowfort::VERSION 10 | s.authors = ['Tim Lowrimore','John Dugan'] 11 | s.email = ['tlowrimore@coroutine.com'] 12 | s.homepage = 'https://github.com/coroutine/pillowfort' 13 | s.summary = 'Pillowfort is a opinionated, no bullshit, session-less authentication engine for Rails APIs.' 14 | s.description = 'Pillowfort is nothing more than a handful of Rails API authentication concerns, bundled up for distribution and reuse. It has absolutely no interest in your application. All it cares about is token management. How you integrate Pillowfort\'s tokens into your application is entirely up to you. You are, presumably, paid handsomely to make decisions like that.' 15 | s.license = 'MIT' 16 | s.required_ruby_version = '>= 2.2' 17 | 18 | s.files = Dir['{app,config,db,lib}/**/*', 'MIT-LICENSE', 'Rakefile', 'README.rdoc'] 19 | s.test_files = Dir['spec/**/*'] 20 | 21 | s.add_dependency 'rails', '>= 5.1', '< 6.0' 22 | s.add_dependency 'scrypt', '>= 2.0', '< 4.0' 23 | 24 | s.add_development_dependency 'sqlite3' 25 | s.add_development_dependency 'rspec-rails' 26 | s.add_development_dependency 'rspec-its' 27 | s.add_development_dependency 'factory_girl_rails' 28 | s.add_development_dependency 'pry-nav' 29 | end 30 | -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

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

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

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

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | get 'accounts/index' 3 | 4 | get 'accounts/show' 5 | 6 | # The priority is based upon order of creation: first created -> highest priority. 7 | # See how all your routes lay out with "rake routes". 8 | 9 | # You can have the root of your site routed with "root" 10 | # root 'welcome#index' 11 | 12 | # Example of regular route: 13 | # get 'products/:id' => 'catalog#view' 14 | 15 | # Example of named route that can be invoked with purchase_url(id: product.id) 16 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 17 | 18 | # Example resource route (maps HTTP verbs to controller actions automatically): 19 | # resources :products 20 | 21 | # Example resource route with options: 22 | # resources :products do 23 | # member do 24 | # get 'short' 25 | # post 'toggle' 26 | # end 27 | # 28 | # collection do 29 | # get 'sold' 30 | # end 31 | # end 32 | 33 | # Example resource route with sub-resources: 34 | # resources :products do 35 | # resources :comments, :sales 36 | # resource :seller 37 | # end 38 | 39 | # Example resource route with more complex sub-resources: 40 | # resources :products do 41 | # resources :comments 42 | # resources :sales do 43 | # get 'recent', on: :collection 44 | # end 45 | # end 46 | 47 | # Example resource route with concerns: 48 | # concern :toggleable do 49 | # post 'toggle' 50 | # end 51 | # resources :posts, concerns: :toggleable 52 | # resources :photos, concerns: :toggleable 53 | 54 | # Example resource route within a namespace: 55 | # namespace :admin do 56 | # # Directs /admin/products/* to Admin::ProductsController 57 | # # (app/controllers/admin/products_controller.rb) 58 | # resources :products 59 | # end 60 | end 61 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = true 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = true 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Randomize the order test cases are executed. 35 | config.active_support.test_order = :random 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /lib/pillowfort/config.rb: -------------------------------------------------------------------------------- 1 | module Pillowfort 2 | 3 | # These methods extend the Pillowfort module to allow an initializer 4 | # access to a simple configuration object. 5 | # 6 | class << self 7 | def configure(&block) 8 | yield config 9 | end 10 | 11 | def config 12 | @config ||= Pillowfort::Configuration.new 13 | end 14 | end 15 | 16 | # This is the configuration object for Pillowfort. It is a simple 17 | # class that extends ActiveSupport::Configurable so we can add 18 | # options and default values easily. 19 | # 20 | class Configuration 21 | include ActiveSupport::Configurable 22 | 23 | #========== CLASSES =================================== 24 | 25 | # resource_class: :user 26 | config_accessor :resource_class do 27 | :user 28 | end 29 | 30 | # token_class: :pillowfort_token 31 | config_accessor :token_class do 32 | :pillowfort_token 33 | end 34 | 35 | 36 | #========== LENGTHS =================================== 37 | 38 | # activation_token_length: 40 39 | config_accessor :activation_token_length do 40 | 40 41 | end 42 | 43 | # password_min_length: 8 44 | config_accessor :password_min_length do 45 | 8 46 | end 47 | 48 | # password_reset_token_length: 40 49 | config_accessor :password_reset_token_length do 50 | 40 51 | end 52 | 53 | # session_token_length: 40 54 | config_accessor :session_token_length do 55 | 40 56 | end 57 | 58 | 59 | #========== TTLS ====================================== 60 | 61 | # activation_token_ttl: 7.days 62 | config_accessor :activation_token_ttl do 63 | 7.days 64 | end 65 | 66 | # password_reset_token_ttl: 7.days 67 | config_accessor :password_reset_token_ttl do 68 | 7.days 69 | end 70 | 71 | # session_token_ttl: 1.day 72 | config_accessor :session_token_ttl do 73 | 1.day 74 | end 75 | 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/controllers/accounts_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe AccountsController, :type => :controller do 4 | describe 'its response' do 5 | subject { response } 6 | 7 | context 'when authenticated' do 8 | let(:account) { FactoryGirl.create :account } 9 | before { 10 | account.activate! 11 | authenticate_with account 12 | } 13 | 14 | describe 'unprotected #index' do 15 | before { get :index } 16 | it { should have_http_status :success } 17 | end 18 | 19 | describe 'protected #show' do 20 | before { get :show, id: 1 } 21 | it { should have_http_status :success } 22 | end 23 | end 24 | 25 | context 'when not authenticated' do 26 | describe 'unprotected #index' do 27 | before { get :index } 28 | it { should have_http_status :success } 29 | end 30 | 31 | describe 'protected #show' do 32 | before { get :show, id: 1 } 33 | it { should have_http_status :unauthorized } 34 | end 35 | end 36 | 37 | context 'when not activated' do 38 | let(:account) { FactoryGirl.create :account } 39 | before { authenticate_with account } 40 | 41 | describe 'unprotected #index' do 42 | before { get :index } 43 | it { should have_http_status :success } 44 | end 45 | 46 | describe 'protected #show' do 47 | before { get :show, id: 1 } 48 | it { should have_http_status :forbidden } 49 | end 50 | end 51 | end 52 | 53 | describe 'its methods' do 54 | describe '#current_account' do 55 | it { should respond_to(:current_account) } 56 | 57 | context 'when authenticated' do 58 | let(:account) { FactoryGirl.create :account } 59 | before do 60 | authenticate_with account 61 | get :show, id: 1 62 | end 63 | 64 | it 'should return the account when current_account is called' do 65 | expect(subject.current_account).to eq(account) 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require 'spec_helper' 4 | require File.expand_path("../dummy/config/environment", __FILE__) 5 | require 'rspec/rails' 6 | require 'factory_girl_rails' 7 | 8 | # Add additional requires below this line. Rails is not loaded until this point! 9 | 10 | # Requires supporting ruby files with custom matchers and macros, etc, in 11 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 12 | # run as spec files by default. This means that files in spec/support that end 13 | # in _spec.rb will both be required and run as specs, causing the specs to be 14 | # run twice. It is recommended that you do not name files matching this glob to 15 | # end with _spec.rb. You can configure this pattern with the --pattern 16 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 17 | # 18 | # The following line is provided for convenience purposes. It has the downside 19 | # of increasing the boot-up time by auto-requiring all files in the support 20 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 21 | # require only the support files necessary. 22 | # 23 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 24 | 25 | # Checks for pending migrations before tests are run. 26 | # If you are not using ActiveRecord, you can remove this line. 27 | ActiveRecord::Migration.maintain_test_schema! 28 | 29 | RSpec.configure do |config| 30 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 31 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 32 | 33 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 34 | # examples within a transaction, remove the following line or assign false 35 | # instead of true. 36 | config.use_transactional_fixtures = true 37 | 38 | # RSpec Rails can automatically mix in different behaviours to your tests 39 | # based on their file location, for example enabling you to call `get` and 40 | # `post` in specs under `spec/controllers`. 41 | # 42 | # You can disable this behaviour by removing the line below, and instead 43 | # explicitly tag your specs with their type, e.g.: 44 | # 45 | # RSpec.describe UsersController, :type => :controller do 46 | # # ... 47 | # end 48 | # 49 | # The different available types are documented in the features, such as in 50 | # https://relishapp.com/rspec/rspec-rails/docs 51 | config.infer_spec_type_from_file_location! 52 | end 53 | -------------------------------------------------------------------------------- /lib/pillowfort/concerns/models/resource/password_reset.rb: -------------------------------------------------------------------------------- 1 | module Pillowfort 2 | module Concerns 3 | module Models 4 | module Resource 5 | 6 | # This module is designed to be included in the model you configure 7 | # as the `resource_class`. It provides helper methods for 8 | # creating and confirming password reset tokens. 9 | # 10 | module PasswordReset 11 | extend ActiveSupport::Concern 12 | 13 | 14 | #------------------------------------------------ 15 | # Class Methods 16 | #------------------------------------------------ 17 | 18 | class_methods do 19 | 20 | # This method locates the user record and returns 21 | # it if the supplied secret matches and is 22 | # resettable. 23 | # 24 | def find_by_password_reset_secret(email, secret, &block) 25 | email = email.to_s.downcase.strip 26 | secret = secret.to_s.strip 27 | 28 | if resource = self.where(email: email).first 29 | if resource.password_resettable? 30 | if resource.password_reset_token.secure_compare(secret) 31 | yield resource 32 | else 33 | raise Pillowfort::NotAuthenticatedError # token invalid 34 | end 35 | else 36 | raise Pillowfort::NotAuthenticatedError # not resettable 37 | end 38 | else 39 | raise Pillowfort::NotAuthenticatedError # no resource 40 | end 41 | end 42 | 43 | # DEPRECATED: This method should be removed in the next 44 | # major release of the library. 45 | # 46 | def find_by_password_reset_token(email, secret, &block) 47 | Pillowfort::Helpers::DeprecationHelper.warn(self.name, :find_by_password_reset_token, :find_by_password_reset_secret) 48 | find_by_password_reset_secret(email, secret, &block) 49 | end 50 | 51 | end 52 | 53 | 54 | #------------------------------------------------ 55 | # Public Methods 56 | #------------------------------------------------ 57 | 58 | # This method determines whether or not the resource 59 | # is valid for password reset. 60 | # 61 | def password_resettable? 62 | token = password_reset_token 63 | !!(token && !token.expired? && !token.confirmed?) 64 | end 65 | 66 | # This method is a public interface for accepting a 67 | # password reset of the resource. 68 | # 69 | def confirm_password_reset! 70 | if password_resettable? 71 | password_reset_token.confirm! 72 | self 73 | else 74 | raise Pillowfort::TokenStateError 75 | end 76 | end 77 | 78 | # This method is a public interface that allows 79 | # controllers to interact with the password reset token 80 | # in a relatively decoupled way. 81 | # 82 | def require_password_reset! 83 | token = password_reset_token || build_password_reset_token 84 | token.reset! 85 | token 86 | end 87 | 88 | end 89 | 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/pillowfort/concerns/models/resource/activation.rb: -------------------------------------------------------------------------------- 1 | module Pillowfort 2 | module Concerns 3 | module Models 4 | module Resource 5 | 6 | # This module is designed to be included in the model you configure 7 | # as the `resource_class`. It provides helper methods for 8 | # creating and confirming activation tokens. 9 | # 10 | module Activation 11 | extend ActiveSupport::Concern 12 | 13 | 14 | #------------------------------------------------ 15 | # Class Methods 16 | #------------------------------------------------ 17 | 18 | class_methods do 19 | 20 | # This method locates the user record and returns 21 | # it if the supplied secret matches and is 22 | # activatable. 23 | # 24 | def find_by_activation_secret(email, secret, &block) 25 | email = email.to_s.downcase.strip 26 | secret = secret.to_s.strip 27 | 28 | if resource = self.where(email: email).first 29 | if resource.activatable? 30 | if resource.activation_token.secure_compare(secret) 31 | yield resource 32 | else 33 | raise Pillowfort::NotAuthenticatedError # token invalid 34 | end 35 | else 36 | raise Pillowfort::NotAuthenticatedError # not activatable 37 | end 38 | else 39 | raise Pillowfort::NotAuthenticatedError # no resource 40 | end 41 | end 42 | 43 | # DEPRECATED: This method should be removed in the next 44 | # major release of the library. 45 | # 46 | def find_by_activation_token(email, secret, &block) 47 | Pillowfort::Helpers::DeprecationHelper.warn(self.name, :find_by_activation_token, :find_by_activation_secret) 48 | find_by_activation_secret(email, secret, &block) 49 | end 50 | 51 | end 52 | 53 | 54 | #------------------------------------------------ 55 | # Public Methods 56 | #------------------------------------------------ 57 | 58 | # This method determines whether or not the resource 59 | # is valid for activation. 60 | # 61 | def activatable? 62 | token = activation_token 63 | !!(token && !token.expired? && !token.confirmed?) 64 | end 65 | 66 | # This method determines whether or not the resource 67 | # has already been activated. 68 | # 69 | def activated? 70 | activation_token && activation_token.confirmed? 71 | end 72 | 73 | # This method is a public interface for activating 74 | # the resource. 75 | # 76 | def confirm_activation! 77 | if activatable? 78 | activation_token.confirm! 79 | self 80 | else 81 | raise Pillowfort::TokenStateError 82 | end 83 | end 84 | 85 | # This method is a public interface that allows 86 | # controllers to interact with the activation token 87 | # in a relatively decoupled way. 88 | # 89 | def require_activation! 90 | token = activation_token || build_activation_token 91 | token.reset! 92 | token 93 | end 94 | 95 | end 96 | 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/dummy/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 4 | # file to always be loaded, without a need to explicitly require it in any files. 5 | # 6 | # Given that it is always loaded, you are encouraged to keep this file as 7 | # light-weight as possible. Requiring heavyweight dependencies from this file 8 | # will add to the boot time of your test suite on EVERY test run, even for an 9 | # individual file that may not need all of that loaded. Instead, consider making 10 | # a separate helper file that requires the additional dependencies and performs 11 | # the additional setup, and require it from the spec files that actually need it. 12 | # 13 | # The `.rspec` file also contains a few flags that are not defaults but that 14 | # users commonly want. 15 | # 16 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 17 | RSpec.configure do |config| 18 | require 'rspec/its' 19 | # rspec-expectations config goes here. You can use an alternate 20 | # assertion/expectation library such as wrong or the stdlib/minitest 21 | # assertions if you prefer. 22 | config.expect_with :rspec do |expectations| 23 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 24 | end 25 | 26 | # rspec-mocks config goes here. You can use an alternate test double 27 | # library (such as bogus or mocha) by changing the `mock_with` option here. 28 | config.mock_with :rspec do |mocks| 29 | mocks.verify_partial_doubles = true 30 | end 31 | 32 | # The settings below are suggested to provide a good initial experience 33 | # with RSpec, but feel free to customize to your heart's content. 34 | =begin 35 | # These two settings work together to allow you to limit a spec run 36 | # to individual examples or groups you care about by tagging them with 37 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 38 | # get run. 39 | config.filter_run :focus 40 | config.run_all_when_everything_filtered = true 41 | 42 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 43 | # For more details, see: 44 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 45 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 46 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 47 | config.disable_monkey_patching! 48 | 49 | # Many RSpec users commonly either run the entire suite or an individual 50 | # file, and it's useful to allow more verbose output when running an 51 | # individual spec file. 52 | if config.files_to_run.one? 53 | # Use the documentation formatter for detailed output, 54 | # unless a formatter has already been configured 55 | # (e.g. via a command-line flag). 56 | config.default_formatter = 'doc' 57 | end 58 | 59 | # Print the 10 slowest examples and example groups at the 60 | # end of the spec run, to help surface which specs are running 61 | # particularly slow. 62 | config.profile_examples = 10 63 | 64 | # Run specs in random order to surface order dependencies. If you find an 65 | # order dependency and want to debug it, you can fix the order by providing 66 | # the seed, which is printed after each run. 67 | # --seed 1234 68 | config.order = :random 69 | 70 | # Seed global randomization in this process using the `--seed` CLI option. 71 | # Setting this allows you to use `--seed` to deterministically reproduce 72 | # test failures related to randomization by passing the same `--seed` value 73 | # as the one that triggered the failure. 74 | Kernel.srand config.seed 75 | =end 76 | end 77 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | pillowfort (3.0.0) 5 | rails (>= 5.1, < 6.0) 6 | scrypt (>= 2.0, < 4.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | actioncable (5.1.1) 12 | actionpack (= 5.1.1) 13 | nio4r (~> 2.0) 14 | websocket-driver (~> 0.6.1) 15 | actionmailer (5.1.1) 16 | actionpack (= 5.1.1) 17 | actionview (= 5.1.1) 18 | activejob (= 5.1.1) 19 | mail (~> 2.5, >= 2.5.4) 20 | rails-dom-testing (~> 2.0) 21 | actionpack (5.1.1) 22 | actionview (= 5.1.1) 23 | activesupport (= 5.1.1) 24 | rack (~> 2.0) 25 | rack-test (~> 0.6.3) 26 | rails-dom-testing (~> 2.0) 27 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 28 | actionview (5.1.1) 29 | activesupport (= 5.1.1) 30 | builder (~> 3.1) 31 | erubi (~> 1.4) 32 | rails-dom-testing (~> 2.0) 33 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 34 | activejob (5.1.1) 35 | activesupport (= 5.1.1) 36 | globalid (>= 0.3.6) 37 | activemodel (5.1.1) 38 | activesupport (= 5.1.1) 39 | activerecord (5.1.1) 40 | activemodel (= 5.1.1) 41 | activesupport (= 5.1.1) 42 | arel (~> 8.0) 43 | activesupport (5.1.1) 44 | concurrent-ruby (~> 1.0, >= 1.0.2) 45 | i18n (~> 0.7) 46 | minitest (~> 5.1) 47 | tzinfo (~> 1.1) 48 | arel (8.0.0) 49 | builder (3.2.3) 50 | coderay (1.1.1) 51 | concurrent-ruby (1.0.5) 52 | diff-lcs (1.3) 53 | erubi (1.6.0) 54 | factory_girl (4.8.0) 55 | activesupport (>= 3.0.0) 56 | factory_girl_rails (4.8.0) 57 | factory_girl (~> 4.8.0) 58 | railties (>= 3.0.0) 59 | ffi (1.9.18) 60 | ffi-compiler (1.0.1) 61 | ffi (>= 1.0.0) 62 | rake 63 | globalid (0.4.0) 64 | activesupport (>= 4.2.0) 65 | i18n (0.8.1) 66 | loofah (2.0.3) 67 | nokogiri (>= 1.5.9) 68 | mail (2.6.5) 69 | mime-types (>= 1.16, < 4) 70 | method_source (0.8.2) 71 | mime-types (3.1) 72 | mime-types-data (~> 3.2015) 73 | mime-types-data (3.2016.0521) 74 | mini_portile2 (2.1.0) 75 | minitest (5.10.2) 76 | nio4r (2.0.0) 77 | nokogiri (1.7.2) 78 | mini_portile2 (~> 2.1.0) 79 | pry (0.10.4) 80 | coderay (~> 1.1.0) 81 | method_source (~> 0.8.1) 82 | slop (~> 3.4) 83 | pry-nav (0.2.4) 84 | pry (>= 0.9.10, < 0.11.0) 85 | rack (2.0.2) 86 | rack-test (0.6.3) 87 | rack (>= 1.0) 88 | rails (5.1.1) 89 | actioncable (= 5.1.1) 90 | actionmailer (= 5.1.1) 91 | actionpack (= 5.1.1) 92 | actionview (= 5.1.1) 93 | activejob (= 5.1.1) 94 | activemodel (= 5.1.1) 95 | activerecord (= 5.1.1) 96 | activesupport (= 5.1.1) 97 | bundler (>= 1.3.0, < 2.0) 98 | railties (= 5.1.1) 99 | sprockets-rails (>= 2.0.0) 100 | rails-dom-testing (2.0.3) 101 | activesupport (>= 4.2.0) 102 | nokogiri (>= 1.6) 103 | rails-html-sanitizer (1.0.3) 104 | loofah (~> 2.0) 105 | railties (5.1.1) 106 | actionpack (= 5.1.1) 107 | activesupport (= 5.1.1) 108 | method_source 109 | rake (>= 0.8.7) 110 | thor (>= 0.18.1, < 2.0) 111 | rake (12.0.0) 112 | rspec-core (3.6.0) 113 | rspec-support (~> 3.6.0) 114 | rspec-expectations (3.6.0) 115 | diff-lcs (>= 1.2.0, < 2.0) 116 | rspec-support (~> 3.6.0) 117 | rspec-its (1.2.0) 118 | rspec-core (>= 3.0.0) 119 | rspec-expectations (>= 3.0.0) 120 | rspec-mocks (3.6.0) 121 | diff-lcs (>= 1.2.0, < 2.0) 122 | rspec-support (~> 3.6.0) 123 | rspec-rails (3.6.0) 124 | actionpack (>= 3.0) 125 | activesupport (>= 3.0) 126 | railties (>= 3.0) 127 | rspec-core (~> 3.6.0) 128 | rspec-expectations (~> 3.6.0) 129 | rspec-mocks (~> 3.6.0) 130 | rspec-support (~> 3.6.0) 131 | rspec-support (3.6.0) 132 | scrypt (3.0.5) 133 | ffi-compiler (>= 1.0, < 2.0) 134 | slop (3.6.0) 135 | sprockets (3.7.1) 136 | concurrent-ruby (~> 1.0) 137 | rack (> 1, < 3) 138 | sprockets-rails (3.2.0) 139 | actionpack (>= 4.0) 140 | activesupport (>= 4.0) 141 | sprockets (>= 3.0.0) 142 | sqlite3 (1.3.13) 143 | thor (0.19.4) 144 | thread_safe (0.3.6) 145 | tzinfo (1.2.3) 146 | thread_safe (~> 0.1) 147 | websocket-driver (0.6.5) 148 | websocket-extensions (>= 0.1.0) 149 | websocket-extensions (0.1.2) 150 | 151 | PLATFORMS 152 | ruby 153 | 154 | DEPENDENCIES 155 | factory_girl_rails 156 | pillowfort! 157 | pry-nav 158 | rspec-its 159 | rspec-rails 160 | sqlite3 161 | 162 | BUNDLED WITH 163 | 1.14.6 164 | -------------------------------------------------------------------------------- /lib/pillowfort/concerns/controllers/base.rb: -------------------------------------------------------------------------------- 1 | module Pillowfort 2 | module Concerns 3 | module Controllers 4 | 5 | # This module is designed to be included in whichever controller acts 6 | # as the base class for your project. In most Rails projects, this will 7 | # be the ApplicationController. 8 | # 9 | module Base 10 | extend ActiveSupport::Concern 11 | 12 | #-------------------------------------------------- 13 | # Configuration 14 | #-------------------------------------------------- 15 | 16 | included do 17 | 18 | # callbacks 19 | before_action :remove_response_headers! 20 | before_action :authenticate_from_resource_secret! 21 | 22 | # mixins 23 | include ActionController::HttpAuthentication::Basic::ControllerMethods 24 | 25 | # errors 26 | rescue_from Pillowfort::NotActivatedError, with: :render_pillowfort_activation_error 27 | rescue_from Pillowfort::NotAuthenticatedError, with: :render_pillowfort_authentication_error 28 | rescue_from Pillowfort::TokenStateError, with: :render_pillowfort_token_state_error 29 | 30 | # helpers 31 | helper_method :pillowfort_realm 32 | helper_method :pillowfort_resource 33 | helper_method :pillowfort_session_token 34 | 35 | end 36 | 37 | 38 | #-------------------------------------------------- 39 | # Private Methods 40 | #-------------------------------------------------- 41 | private 42 | 43 | #========== AUTHENTICATION ======================== 44 | 45 | # This method reads the email, secret, and realm from 46 | # the request headers, determines the authenticable class, 47 | # and defers lookup to the authenticable class itself. 48 | # 49 | # If you wish to support multiple sessions per client 50 | # application, your client can provide a identifying value 51 | # in a custom http header named `X-Realm`. If no such 52 | # header is provided, Pillowfort will use the default Rails 53 | # realm of `Application`. 54 | # 55 | def authenticate_from_resource_secret! 56 | klass = Pillowfort.config.resource_class.to_s.classify.constantize 57 | 58 | authenticate_with_http_basic do |email, secret| 59 | klass.authenticate_securely(email, secret, pillowfort_realm) do |resource| 60 | @pillowfort_resource = resource 61 | end 62 | end 63 | end 64 | 65 | # DEPRECATED: This method should be removed in the next 66 | # major release of the library. 67 | # 68 | def authenticate_from_resource_token! 69 | Pillowfort::Helpers::DeprecationHelper.warn(self, :authenticate_from_resource_token!, :authenticate_from_resource_secret!) 70 | authenticate_from_resource_secret! 71 | end 72 | 73 | 74 | #========== CURRENT =============================== 75 | 76 | # This method returns the specified realm for this 77 | # request. 78 | # 79 | def pillowfort_realm 80 | @pillowfort_realm ||= begin 81 | (request.headers['HTTP_X_REALM'] || 'Application').to_s.underscore.strip 82 | end 83 | end 84 | 85 | # This method returns the current instance of the 86 | # authenticable resource. 87 | # 88 | def pillowfort_resource 89 | @pillowfort_resource 90 | end 91 | 92 | # This method returns the current session token of the 93 | # authenticable resource for the current realm. 94 | # 95 | def pillowfort_session_token 96 | pillowfort_resource && 97 | pillowfort_resource.session_tokens.where(realm: pillowfort_realm).first 98 | end 99 | 100 | 101 | #========== HEADERS ================================= 102 | 103 | # This is necessary, as it allows Cordova to properly delegate 104 | # 401 response handling to our application. If we keep this header, 105 | # Cordova will defer handling to iOS, and we'll never see the 401 106 | # status in the app... it'll just do nothing. 107 | # 108 | def remove_response_headers! 109 | headers.delete('WWW-Authenticate') 110 | end 111 | 112 | 113 | #========== RENDERING =============================== 114 | 115 | # This method renders a standard response for resources 116 | # that are not activated. 117 | # 118 | def render_pillowfort_activation_error 119 | head :unauthorized 120 | end 121 | 122 | # This method renders a standard response for resources 123 | # that are not authenticated. 124 | # 125 | def render_pillowfort_authentication_error 126 | head :unauthorized 127 | end 128 | 129 | # This method renders a standard response for resources 130 | # that attempt to modify tokens in illegal ways. 131 | # 132 | def render_pillowfort_token_state_error 133 | head :unauthorized 134 | end 135 | 136 | end 137 | 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /lib/pillowfort/concerns/models/token/base.rb: -------------------------------------------------------------------------------- 1 | module Pillowfort 2 | module Concerns 3 | module Models 4 | module Token 5 | 6 | # This module is designed to be included in the model you configure 7 | # as the `token_class`. It handles establishing the appropriate 8 | # validations and provides helper methods for creating and comparing 9 | # secrets. 10 | # 11 | module Base 12 | extend ActiveSupport::Concern 13 | 14 | #------------------------------------------------ 15 | # Configuration 16 | #------------------------------------------------ 17 | 18 | included do 19 | 20 | # constants 21 | TOKEN_TYPES ||= %w{ activation password_reset session } 22 | 23 | # callbacks 24 | before_validation :normalize_type 25 | before_validation :normalize_realm 26 | before_validation :reset_secret, on: :create 27 | 28 | # turn off fucking sti 29 | self.inheritance_column = :_sti_disabled 30 | 31 | # associations 32 | belongs_to :resource, class_name: Pillowfort.config.resource_class.to_s.classify 33 | 34 | # validations 35 | validates :resource, presence: true 36 | validates :type, presence: true, inclusion: { in: TOKEN_TYPES } 37 | validates :secret, presence: true, uniqueness: { scope: [:type] } 38 | validates :realm, presence: true, uniqueness: { scope: [:resource_id, :type] } 39 | 40 | end 41 | 42 | 43 | #------------------------------------------------ 44 | # Class Methods 45 | #------------------------------------------------ 46 | 47 | class_methods do 48 | 49 | # This method provides an application-wide utility 50 | # for generating friendly secrets. 51 | # 52 | def friendly_secret(length=40) 53 | SecureRandom.base64(length).tr('+/=lIO0', 'pqrsxyz').first(length) 54 | end 55 | 56 | # DEPRECATED: This method should be removed in the next 57 | # major release of the library. 58 | # 59 | def friendly_token(length=40) 60 | Pillowfort::Helpers::DeprecationHelper.warn(self.name, :friendly_token, :friendly_secret) 61 | friendly_secret(length) 62 | end 63 | 64 | end 65 | 66 | 67 | #------------------------------------------------ 68 | # Public Methods 69 | #------------------------------------------------ 70 | 71 | #========== ATTRIBUTES ========================== 72 | 73 | # DEPRECATED: This method should be removed in the next 74 | # major release of the library. 75 | # 76 | def token=(value) 77 | Pillowfort::Helpers::DeprecationHelper.warn(self.class.name, :token=, :secret=) 78 | send(:secret=, value) 79 | end 80 | 81 | # DEPRECATED: This method should be removed in the next 82 | # major release of the library. 83 | # 84 | def token 85 | Pillowfort::Helpers::DeprecationHelper.warn(self.class.name, :token, :secret) 86 | send(:secret) 87 | end 88 | 89 | 90 | #========== COMPARISONS ========================= 91 | 92 | # This method performs a constant-time comparison 93 | # of pillowfort secrets in an effort to confound 94 | # timing attacks. 95 | # 96 | # This was lifted verbatim from Devise. 97 | # 98 | def secure_compare(value) 99 | a = self.secret 100 | b = value 101 | 102 | return false if a.blank? || b.blank? || a.bytesize != b.bytesize 103 | l = a.unpack "C#{a.bytesize}" 104 | 105 | res = 0 106 | b.each_byte { |byte| res |= byte ^ l.shift } 107 | res == 0 108 | end 109 | 110 | 111 | #========== CONFIRMATION ======================== 112 | 113 | def confirm 114 | unless confirmed? 115 | self.confirmed_at = Time.now 116 | end 117 | end 118 | 119 | def confirm! 120 | confirm 121 | save! 122 | end 123 | 124 | def confirmed? 125 | confirmed_at? 126 | end 127 | 128 | 129 | #========== EXPIRATION ========================== 130 | 131 | def expire 132 | unless expired? 133 | self.expires_at = Time.now - 1.second 134 | end 135 | end 136 | 137 | def expire! 138 | expire 139 | save! 140 | end 141 | 142 | def expired? 143 | Time.now > expires_at 144 | end 145 | 146 | 147 | #========== RESETS ============================== 148 | 149 | # This method is a public interface that allows the 150 | # associated resource to extend the token's expiry. 151 | # 152 | def refresh! 153 | refresh_expiry 154 | save! 155 | end 156 | 157 | # This method is a public interface that allows the 158 | # associated resource to reset the token completely. 159 | # 160 | def reset! 161 | reset_secret 162 | refresh_expiry 163 | reset_confirmation 164 | save! 165 | end 166 | 167 | 168 | #------------------------------------------------ 169 | # Private Methods 170 | #------------------------------------------------ 171 | private 172 | 173 | #========== RESETS ============================== 174 | 175 | # This method extends the expiry according to the 176 | # ttl for the token's type. 177 | # 178 | def refresh_expiry 179 | self.expires_at = Time.now + ttl 180 | end 181 | 182 | # This method will nullify the token's confirmation 183 | # timestamp. 184 | # 185 | def reset_confirmation 186 | self.confirmed_at = nil 187 | end 188 | 189 | # This method will create new tokens in a loop until 190 | # one is generated that is unique for the token's type. 191 | # 192 | def reset_secret 193 | loop do 194 | self.secret = friendly_secret 195 | break self.secret unless self.class.where(type: self.type, secret: self.secret).first 196 | end 197 | end 198 | 199 | 200 | #========== NORMALIZATION ======================= 201 | 202 | # This method ensures all realms are stored in a 203 | # similar string format to facilitate lookups. 204 | # 205 | def normalize_realm 206 | if self.realm.present? 207 | self.realm = self.realm.to_s.underscore.strip 208 | end 209 | end 210 | 211 | # This method ensures all types are stored in a 212 | # similar string format to facilitate lookups. 213 | # 214 | def normalize_type 215 | if self.type.present? 216 | self.type = self.type.to_s.underscore.strip 217 | end 218 | end 219 | 220 | 221 | #========== TOKEN =============================== 222 | 223 | # This method produces a random, base64 secret and 224 | # replaces any potentially problematic characters 225 | # with nice characters. 226 | # 227 | # This was lifted verbatim from Devise. 228 | # 229 | def friendly_secret 230 | self.class.friendly_secret(length) 231 | end 232 | 233 | 234 | #========== TTL ================================= 235 | 236 | # This method determines the configured secret length 237 | # for this token's type. 238 | # 239 | def length 240 | config = Pillowfort.config 241 | 242 | case self.type 243 | when 'activation' then config.activation_token_length 244 | when 'password_reset' then config.password_reset_token_length 245 | else config.session_token_length 246 | end 247 | end 248 | 249 | # This method determines the configured ttl for this 250 | # token's type. 251 | # 252 | def ttl 253 | config = Pillowfort.config 254 | 255 | case self.type 256 | when 'activation' then config.activation_token_ttl 257 | when 'password_reset' then config.password_reset_token_ttl 258 | else config.session_token_ttl 259 | end 260 | end 261 | 262 | end 263 | 264 | end 265 | end 266 | end 267 | end 268 | -------------------------------------------------------------------------------- /lib/pillowfort/concerns/models/resource/base.rb: -------------------------------------------------------------------------------- 1 | module Pillowfort 2 | module Concerns 3 | module Models 4 | module Resource 5 | 6 | # This module is designed to be included in the model you configure 7 | # as the `resource_class`. It handles establishing the appropriate 8 | # validations and provides helper methods for authenticating sessions 9 | # and interacting with session tokens. 10 | # 11 | # Behaviors related to activation and password resets are handled 12 | # in seaparate modules. 13 | # 14 | module Base 15 | extend ActiveSupport::Concern 16 | 17 | #-------------------------------------------------- 18 | # Configuration 19 | #-------------------------------------------------- 20 | 21 | included do 22 | 23 | # callbacks 24 | before_validation :normalize_email 25 | 26 | # attributes 27 | attr_reader :password 28 | attr_reader :password_confirmation 29 | 30 | # associations 31 | has_one :activation_token, -> { where(type: 'activation', realm: 'application') }, 32 | class_name: Pillowfort.config.token_class.to_s.classify, 33 | foreign_key: :resource_id 34 | has_one :password_reset_token, -> { where(type: 'password_reset', realm: 'application') }, 35 | class_name: Pillowfort.config.token_class.to_s.classify, 36 | foreign_key: :resource_id 37 | has_many :session_tokens, -> { where(type: 'session') }, 38 | class_name: Pillowfort.config.token_class.to_s.classify, 39 | foreign_key: :resource_id 40 | 41 | # validations 42 | validates_presence_of :email 43 | validates_uniqueness_of :email 44 | validates_presence_of :password, unless: :password_digest? 45 | validates_length_of :password, minimum: Pillowfort.config.password_min_length, allow_nil: true 46 | validates_confirmation_of :password, allow_nil: true 47 | 48 | end 49 | 50 | 51 | #-------------------------------------------------- 52 | # Class Methods 53 | #-------------------------------------------------- 54 | 55 | class_methods do 56 | 57 | # This method accepts authentication information and checks 58 | # the database for the email and session token. If all goes 59 | # well, we reset the session token with the realm; otherwise 60 | # we raise the appropriate error. 61 | # 62 | def authenticate_securely(email, secret, realm='application') 63 | email = email.to_s.downcase.strip 64 | secret = secret.to_s.strip 65 | realm = realm.to_s.downcase.strip 66 | 67 | if email.blank? || secret.blank? 68 | raise Pillowfort::NotAuthenticatedError # no anything 69 | else 70 | transaction do 71 | if resource = self.where(email: email).first 72 | if resource.activated? 73 | if session_token = resource.session_tokens.where(realm: realm).first 74 | 75 | if session_token.expired? 76 | session_token.reset! 77 | raise Pillowfort::NotAuthenticatedError # token expired 78 | else 79 | if session_token.secure_compare(secret) 80 | session_token.refresh! 81 | yield resource # success! 82 | else 83 | raise Pillowfort::NotAuthenticatedError # bad token 84 | end 85 | end 86 | 87 | else 88 | raise Pillowfort::NotAuthenticatedError # no token 89 | end 90 | else 91 | raise Pillowfort::NotActivatedError # not activated 92 | end 93 | else 94 | raise Pillowfort::NotAuthenticatedError # no resource 95 | end 96 | end 97 | end 98 | end 99 | 100 | # This method accepts authentication information and checks 101 | # the database for the email and password digest. If all goes 102 | # well, we reset the session token with the realm; otherwise 103 | # we raise the appropriate error. 104 | # 105 | def find_and_authenticate(email, password, realm='application') 106 | resource = self.where(email: email.to_s.downcase).first 107 | 108 | if resource && resource.authenticate(password) 109 | if resource.activated? 110 | resource.reset_session!(realm) 111 | resource 112 | else 113 | raise Pillowfort::NotActivatedError 114 | end 115 | else 116 | raise Pillowfort::NotAuthenticatedError 117 | end 118 | end 119 | 120 | end 121 | 122 | 123 | #-------------------------------------------------- 124 | # Public Methods 125 | #-------------------------------------------------- 126 | 127 | #========== PASSWORDS ============================= 128 | 129 | # This method accepts an unencrypted password value, 130 | # stores it in an instance variable, and uses it 131 | # to generate a secure one-way hash. 132 | # 133 | # For now, we just uses default costs. ~200ms, 1MB 134 | # 135 | def password=(unencrypted) 136 | pword = unencrypted.to_s.strip 137 | 138 | if pword.blank? 139 | self.password_digest = nil 140 | else 141 | @password = pword 142 | self.password_digest = SCrypt::Password.create(pword) 143 | end 144 | end 145 | 146 | # This method stores the unencrypted password 147 | # confirmation value in an instance variable to 148 | # facilitate validations. 149 | # 150 | def password_confirmation=(unencrypted) 151 | @password_confirmation = unencrypted.to_s.strip 152 | end 153 | 154 | # This method resets the resource's password by 155 | # assigning a random token to :password and 156 | # :password_confirmation. 157 | # 158 | def reset_password 159 | klass = Pillowfort.config.token_class.to_s.classify.constantize 160 | random = klass.friendly_secret(40) 161 | 162 | self.password = random 163 | self.password_confirmation = random 164 | end 165 | 166 | 167 | #========== ACTIVATION ============================ 168 | 169 | # This method always returns true in its default form. 170 | # If you want actual activation behavior, please see 171 | # the Activation model concern, which will override 172 | # this method and add several others. 173 | # 174 | def activated? 175 | true 176 | end 177 | 178 | 179 | #========== SESSION =============================== 180 | 181 | # This method accepts a plain text password and 182 | # determines whether or not it matches the 183 | # password digest for the current user. 184 | # 185 | def authenticate(unencrypted) 186 | SCrypt::Password.new(password_digest) == unencrypted && self 187 | end 188 | 189 | # This method delegates the token reset process to the 190 | # model's session token. A session token will be created 191 | # if none exists. 192 | # 193 | def reset_session!(realm='application') 194 | token = session_tokens.where(realm: realm).first_or_initialize 195 | token.reset! 196 | token.secret 197 | end 198 | 199 | 200 | #-------------------------------------------------- 201 | # Private Methods 202 | #-------------------------------------------------- 203 | private 204 | 205 | #========== NORMALIZATION ========================= 206 | 207 | # This method ensures all emails are stored in a 208 | # similar string format to facilitate lookups. 209 | # 210 | def normalize_email 211 | if self.email.present? 212 | self.email = self.email.to_s.downcase.strip 213 | end 214 | end 215 | 216 | end 217 | 218 | end 219 | end 220 | end 221 | end 222 | -------------------------------------------------------------------------------- /spec/models/account_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | # ------------------------------------------------------------------------------ 4 | # Shared Examples 5 | # ------------------------------------------------------------------------------ 6 | 7 | RSpec.shared_examples 'an auth token resetter' do 8 | describe 'its affect on the auth_token' do 9 | subject { account.auth_token } 10 | 11 | describe 'before the call' do 12 | it { should eq(auth_token) } 13 | end 14 | 15 | describe 'after the call' do 16 | before { call_the_method } 17 | it { should_not eq(auth_token) } 18 | end 19 | end 20 | 21 | describe 'its affect on the auth_token_expires_at' do 22 | subject { account.auth_token_expires_at } 23 | 24 | describe 'before the call' do 25 | it { should eq(auth_token_expires_at) } 26 | end 27 | 28 | describe 'after the call' do 29 | before { call_the_method } 30 | it { should be > auth_token_expires_at } 31 | end 32 | end 33 | end 34 | 35 | # ------------------------------------------------------------------------------ 36 | # The Spec! 37 | # ------------------------------------------------------------------------------ 38 | 39 | RSpec.describe Account, :type => :model do 40 | 41 | describe 'its validations' do 42 | before { account.save } 43 | subject { account.errors.messages } 44 | 45 | describe 'email validations' do 46 | let(:account) { FactoryGirl.build(:account, email: email) } 47 | 48 | context 'presence_of' do 49 | let(:email) { nil } 50 | 51 | it { should include(email: ["can't be blank"]) } 52 | end 53 | 54 | context 'uniqueness' do 55 | let(:email) { 'foobar@baz.com' } 56 | let(:dup_account) { FactoryGirl.build(:account, email: email) } 57 | before { dup_account.save } 58 | subject { dup_account.errors.messages} 59 | 60 | it { should include(email: ["has already been taken"]) } 61 | end 62 | end 63 | 64 | describe 'password validations' do 65 | let(:account) { FactoryGirl.build(:account, password: password) } 66 | 67 | context 'presence_of' do 68 | let(:password) { nil } 69 | 70 | it { should include(password: [/can't be blank/]) } 71 | end 72 | 73 | context 'length of' do 74 | context "when it's too short" do 75 | let(:password) { "x"*3 } 76 | 77 | it { should include(password: [/is too short/]) } 78 | end 79 | 80 | context "when it's too long" do 81 | let(:password) { "x"*80 } 82 | 83 | it { should include(password: [/is too long/]) } 84 | end 85 | 86 | context "when the record is persisted" do 87 | let(:password) { 'foobarbaz' } 88 | before { account.save } 89 | 90 | context "when the password is updated with a nil value" do 91 | before {account.update_attribute :password, nil } 92 | it { should be_empty } 93 | end 94 | 95 | context "when the password is updated with a short value" do 96 | before {account.update_attributes password: '3' } 97 | it { should include(password: [/is too short/]) } 98 | end 99 | 100 | context "when the password is updated with a short value" do 101 | before {account.update_attributes password: "x"*80 } 102 | it { should include(password: [/is too long/]) } 103 | end 104 | end 105 | end 106 | 107 | describe 'activation validations' do 108 | let(:activation_token) { "my_token" } 109 | let(:activation_token_expires_at) { 1.hour.from_now } 110 | let(:activated_at) { nil } 111 | 112 | let(:account) { 113 | FactoryGirl.build(:account, 114 | activation_token: activation_token, 115 | activation_token_expires_at: activation_token_expires_at, 116 | activated_at: activated_at 117 | ) 118 | } 119 | 120 | before { account.save } 121 | subject { account.errors.messages } 122 | 123 | it { should be_empty } 124 | 125 | context 'all fields set' do 126 | let(:activated_at) { 1.day.ago } 127 | 128 | it { should include( activation_token_expires_at: [/must be blank/] ) } 129 | it { should include( activated_at: [/must be blank/] ) } 130 | end 131 | 132 | context 'no fields set' do 133 | let(:activation_token) { nil } 134 | let(:activation_token_expires_at) { nil } 135 | 136 | it { should include( activation_token: [/can't be blank/] ) } 137 | it { should include( activated_at: [/can't be blank/] ) } 138 | end 139 | 140 | context 'duplicate activation token' do 141 | let(:dup_account) { 142 | FactoryGirl.build(:account, activation_token: activation_token) 143 | } 144 | before { dup_account.save } 145 | subject { dup_account.errors.messages } 146 | 147 | it { should include activation_token: [/has already been taken/] } 148 | end 149 | 150 | context 'account activated under the old rules...' do 151 | let(:activation_token) { nil } 152 | let(:activated_at) { 1.day.ago } 153 | 154 | it { should_not have_key :activation_token } 155 | end 156 | end 157 | end 158 | end 159 | 160 | describe 'its callbacks' do 161 | describe 'email normalization' do 162 | let(:account) { FactoryGirl.create :account, email: email } 163 | let(:email) { ' HotCarl@Gmail.Com ' } 164 | let(:expected) { 'hotcarl@gmail.com' } 165 | 166 | subject { account.email } 167 | 168 | it { should eq(expected) } 169 | end 170 | end 171 | 172 | describe 'the instance methods' do 173 | let(:account) { 174 | FactoryGirl.create :account, 175 | auth_token: auth_token, 176 | auth_token_expires_at: auth_token_expires_at, 177 | password_reset_token: password_reset_token, 178 | password_reset_token_expires_at: password_reset_token_expires_at, 179 | activation_token: activation_token, 180 | activation_token_expires_at: activation_token_expires_at 181 | } 182 | 183 | let(:auth_token) { 'abc123def456' } 184 | let(:auth_token_expires_at) { 1.day.from_now } 185 | let(:password_reset_token) { '123abc456def' } 186 | let(:password_reset_token_expires_at) { 1.hour.from_now } 187 | let(:activation_token) { 'activateme' } 188 | let(:activation_token_expires_at) { 1.hour.from_now } 189 | 190 | describe '#ensure_auth_token' do 191 | subject { account.auth_token } 192 | before { account.ensure_auth_token } 193 | 194 | context 'when the token is nil' do 195 | let(:auth_token) { nil } 196 | it { should_not be_nil } 197 | end 198 | 199 | context 'when the token is not nil' do 200 | let(:auth_token) { 'deadbeef' } 201 | it { should eq('deadbeef') } 202 | end 203 | end 204 | 205 | describe '#reset_auth_token' do 206 | let(:call_the_method) { account.reset_auth_token } 207 | it_behaves_like 'an auth token resetter' 208 | 209 | describe 'its persistence' do 210 | subject { account } 211 | after { call_the_method } 212 | it { should_not receive(:save) } 213 | end 214 | end 215 | 216 | describe '#reset_auth_token!' do 217 | let(:call_the_method) { account.reset_auth_token! } 218 | it_behaves_like 'an auth token resetter' 219 | 220 | describe 'its persistence' do 221 | subject { account } 222 | after { call_the_method } 223 | it { should receive(:save) } 224 | end 225 | end 226 | 227 | describe '#auth_token_expired?' do 228 | subject { account.auth_token_expired? } 229 | 230 | context 'when the token expiration is in the future' do 231 | let(:auth_token_expires_at) { 1.minute.from_now } 232 | it { should be_falsey } 233 | end 234 | 235 | context 'when the token expiration is in the past' do 236 | let(:auth_token_expires_at) { 1.minute.ago } 237 | it { should be_truthy } 238 | end 239 | end 240 | 241 | describe '#password=' do 242 | let!(:current_password) { account.password.to_s } 243 | subject { account.password.to_s } 244 | 245 | describe 'before the call' do 246 | it { should == (current_password) } 247 | end 248 | 249 | describe 'after the call' do 250 | before { account.password = 'fudge_knuckles_45' } 251 | it { should_not eq(current_password) } 252 | end 253 | end 254 | 255 | # ------------------------------------------------------------------------ 256 | # Password reset tokens 257 | # ------------------------------------------------------------------------ 258 | describe '#password_token_expired?' do 259 | subject { account.password_token_expired? } 260 | describe 'an unexpired token' do 261 | it { should be_falsey } 262 | end 263 | 264 | describe 'an expired token' do 265 | let(:password_reset_token_expires_at) { 1.hour.ago } 266 | it { should be_truthy } 267 | end 268 | end 269 | 270 | shared_examples_for 'password token creator' do 271 | describe '#password_reset_token' do 272 | subject { account.password_reset_token } 273 | it { should_not eq(password_reset_token) } 274 | end 275 | 276 | describe '#password_reset_token_expires_at' do 277 | subject { account.password_reset_token_expires_at } 278 | it { should_not eq(password_reset_token_expires_at) } 279 | end 280 | end 281 | 282 | describe '#create_password_reset_token with default expiration time' do 283 | before { account.create_password_reset_token } 284 | it_behaves_like 'password token creator' 285 | 286 | describe '#password_reset_token_expires_at' do 287 | subject { account.password_reset_token_expires_at } 288 | it { should be_within(5.seconds).of 1.hour.from_now } 289 | end 290 | end 291 | 292 | describe '#create_password_reset_token with specific expiration time' do 293 | before { account.create_password_reset_token(expiry: 10.minutes.from_now) } 294 | it_behaves_like 'password token creator' 295 | describe '#password_reset_token_expires_at' do 296 | subject { account.password_reset_token_expires_at } 297 | it {should be_within(5.seconds).of 10.minutes.from_now } 298 | end 299 | end 300 | 301 | describe '#clear_password_reset_token' do 302 | before { account.clear_password_reset_token } 303 | subject { account } 304 | its(:password_reset_token) { should be_blank } 305 | its(:password_reset_token_expires_at) { should be_blank } 306 | its(:password_token_expired?) { should be_truthy } 307 | end 308 | 309 | # ------------------------------------------------------------------------ 310 | # Activation 311 | # ------------------------------------------------------------------------ 312 | 313 | describe '#actived?' do 314 | subject { account } 315 | it { should_not be_activated } 316 | its(:activated_at) { should be_blank } 317 | its(:activation_token) { should eq(activation_token) } 318 | its(:activation_token_expires_at) { should eq(activation_token_expires_at) } 319 | its(:activation_token_expired?) { should be_falsey } 320 | 321 | context 'already activated' do 322 | before do 323 | subject.activate! 324 | end 325 | 326 | it { should be_activated } 327 | its(:activated_at) { should be_within(5.seconds).of Time.now } 328 | its(:activation_token) { should_not be_blank } 329 | its(:activation_token_expires_at) { should be_blank } 330 | its(:activation_token_expired?) { should be_truthy } 331 | end 332 | end 333 | 334 | describe '#create_activation_token' do 335 | subject { account } 336 | before { subject.create_activation_token } 337 | 338 | its(:activation_token) { should_not be_blank } 339 | its(:activation_token_expires_at) { should be_within(5.seconds).of 1.hour.from_now } 340 | its(:activation_token_expired?) { should be_falsey } 341 | 342 | context 'with specific expiration' do 343 | before { subject.create_activation_token(expiry: 5.minutes.from_now) } 344 | its(:activation_token_expires_at) { should be_within(5.seconds).of 5.minutes.from_now } 345 | its(:activation_token_expired?) { should be_falsey } 346 | end 347 | 348 | context 'with expiration in the past' do 349 | before { subject.create_activation_token(expiry: 10.minutes.ago) } 350 | its(:activation_token_expired?) { should be_truthy } 351 | end 352 | end 353 | end 354 | 355 | describe 'the class methods' do 356 | let(:email) { 'foobar@baz.com' } 357 | let(:token) { 'deadbeef' } 358 | let(:password) { 'admin4lolz' } 359 | let(:auth_token_expires_at) { 1.day.from_now } 360 | let(:activation_token) { 'activateme' } 361 | let(:activation_token_expires_at) { 1.hour.from_now } 362 | let(:password_reset_token) { 'resetme' } 363 | let(:password_reset_token_expires_at) { 1.hour.from_now } 364 | 365 | let!(:account) { 366 | FactoryGirl.create :account, 367 | email: email, 368 | auth_token: token, 369 | password: password, 370 | auth_token_expires_at: auth_token_expires_at, 371 | password_reset_token: password_reset_token, 372 | password_reset_token_expires_at: password_reset_token_expires_at, 373 | activation_token: activation_token, 374 | activation_token_expires_at: activation_token_expires_at 375 | } 376 | 377 | describe '.find_by_email_case_insensitive' do 378 | subject { Account.find_by_email_case_insensitive(search_email) } 379 | 380 | context 'when an email is provided' do 381 | let(:search_email) { email } 382 | it { should_not be_nil} 383 | end 384 | 385 | context 'when no email is provided' do 386 | let(:search_email) { nil } 387 | it { should be_nil } 388 | end 389 | end 390 | 391 | describe '.authenticate_securely' do 392 | let(:email_param) { email } 393 | let(:token_param) { token } 394 | let(:block) { ->(resource) {} } 395 | 396 | subject { Account.authenticate_securely(email_param, token_param, &block) } 397 | 398 | context 'when email is nil' do 399 | let(:email_param) { nil } 400 | it { should be_falsey } 401 | end 402 | 403 | context 'when token is nil' do 404 | let(:token_param) { nil } 405 | it { should be_falsey } 406 | end 407 | 408 | context 'when email and token are provided' do 409 | 410 | context 'email case-sensitivity' do 411 | describe 'when an uppercased email address is provided' do 412 | let(:email_param) { email.upcase } 413 | 414 | it 'should yield the matched account' do 415 | expect { |b| Account.authenticate_securely(email_param, token_param, &b) }.to yield_with_args(account) 416 | end 417 | end 418 | 419 | describe 'when a downcased email address is provided' do 420 | let(:email_param) { email.downcase } 421 | 422 | it 'should yield the matched account' do 423 | expect { |b| Account.authenticate_securely(email_param, token_param, &b) }.to yield_with_args(account) 424 | end 425 | end 426 | end 427 | 428 | context 'when the resource is located' do 429 | 430 | context 'when the auth_token is expired' do 431 | let(:auth_token_expires_at) { 1.week.ago } 432 | 433 | it 'should reset the account auth_token' do 434 | allow(Account).to receive(:find_by_email_case_insensitive) { account } 435 | expect(account).to receive(:reset_auth_token!) 436 | subject 437 | end 438 | 439 | it { should be_falsey } 440 | end 441 | 442 | context 'when the auth_token is current' do 443 | 444 | context 'when the auth_token matches' do 445 | it 'should yield the matched account' do 446 | expect { |b| Account.authenticate_securely(email_param, token_param, &b) }.to yield_with_args(account) 447 | end 448 | end 449 | 450 | context 'when the auth_token does not match' do 451 | it { should be_falsey } 452 | end 453 | end 454 | end 455 | 456 | context 'when the resource is not located' do 457 | it { should be_falsey } 458 | end 459 | 460 | end 461 | end 462 | 463 | describe '.find_and_activate' do 464 | let(:email_param) { email } 465 | let(:token_param) { activation_token } 466 | let(:block) { ->(resource) {} } 467 | 468 | subject { Account.find_and_activate(email_param, token_param) } 469 | 470 | context 'when the resource is located' do 471 | context 'when the token matches' do 472 | it 'should activate the matched account' do 473 | expect_any_instance_of(Account).to receive(:activate!) 474 | subject 475 | end 476 | 477 | it 'should yield the matched account' do 478 | expect { |b| Account.find_and_activate(email_param, token_param, &b) }.to yield_with_args(account) 479 | end 480 | 481 | it { should == account } 482 | 483 | context "when the activation_token is expired" do 484 | let(:activation_token_expires_at) { 1.day.ago } 485 | it { should be_falsey } 486 | end 487 | end 488 | 489 | context "when the activation_token doesn't match" do 490 | let(:token_param) { 'notmytoken' } 491 | it { should be_falsey } 492 | end 493 | end 494 | 495 | context "when the email doesn't match" do 496 | let(:email_param) { 'notmyemail@gmail.com' } 497 | it { should be_falsey } 498 | end 499 | end 500 | 501 | describe '.find_and_validate_password_reset_token' do 502 | let(:token_param) { password_reset_token } 503 | subject { Account.find_and_validate_password_reset_token(token_param) } 504 | 505 | context "when the token doesn't match" do 506 | let(:token_param) { 'notmytoken' } 507 | it { should be_falsey } 508 | end 509 | 510 | it 'should yield the matched account' do 511 | expect { |b| Account.find_and_validate_password_reset_token(token_param, &b) }.to yield_with_args(account) 512 | end 513 | 514 | it { should == account } 515 | end 516 | 517 | describe '.find_and_authenticate' do 518 | let(:email_param) { email } 519 | let(:password_param) { password } 520 | 521 | subject { Account.find_and_authenticate(email_param, password_param) } 522 | 523 | 524 | context 'when the resource is located' do 525 | 526 | context 'when the password matches' do 527 | it { should eq(account) } 528 | end 529 | 530 | context 'when the password does not match' do 531 | let(:password_param) { "#{password}_bad" } 532 | it { should be_falsey } 533 | end 534 | end 535 | 536 | context 'when the resource is not located' do 537 | let(:email_param) { "#{email}_evil" } 538 | it { should be_falsey } 539 | end 540 | end 541 | end 542 | end 543 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | # WARNING! 3 | 4 | Use of this library is strongly discouraged, as many of the tests are now broken. 5 | 6 | If you are looking for a simple, well tested gem for API auth, let me recommend [Knock](https://github.com/nsarno/knock) 7 | 8 | --- 9 | 10 | # Pillowfort 11 | 12 | [![Build Status](https://travis-ci.org/coroutine/pillowfort.svg?branch=master)](https://travis-ci.org/coroutine/pillowfort) 13 | 14 | Pillowfort is a opinionated, no bullshit, session-less authentication engine for Rails APIs. If you want a lot of configurability, get the fuck out. If you want lightweight, zero-config authentication for your API, you've come to the right place. 15 | 16 | Pillowfort is nothing more than a handful of concerns, bundled up for distribution and reuse. It has absolutely no interest in your application. All it cares about is token management. How you integrate Pillowfort's tokens into your application is entirely up to you. You are, presumably, paid handsomely to make decisions like that. 17 | 18 | You may find yourself wiring up controller and view code and thinking to yourself, *I wonder if Pillowfort has a method for handling this thing I find tedious*. Now ask yourself whether the functionality you want is related to token creation, retrieval, or validation. If not, please allow us to spare everyone some trouble and assure you right now that Pillowfort gives exactly zero fucks about that thing you're doing. Godspeed. 19 | 20 | Here's the break down: 21 | 22 | ![Pillowfort](docs/assets/pillowfort.gif) 23 | 24 | 25 | ## Basic Principles 26 | 27 | Pillowfort has been optimized for API clients, but it fundamentally works like any Rails application—credentials are exchanged for an expirable token that is provided on all subsequent requests. 28 | 29 | A client application creates a user session with Pillowfort by providing a email and password that matches a known set of credentials in the database. (Pillowfort uses `scrypt` to digest passwords.) When a match is found, Pillowfort returns a random, expirable secret token to the client application. 30 | 31 | On all subsequent requests, the client application provides the email and secret token in the request header, which Pillowfort processes via basic HTTP authentication. 32 | 33 | If your API has more than one client application, each can specify a custom `x-realm` header to instruct Pillowfort to create separate sessions. This will allow a user to be logged into more than one client application simultaneously. 34 | 35 | If you don't like that outcome, you can use the same realm value everywhere and Pillowfort will log users out of one application when they log into the other. 36 | 37 | As with all things Pillowfort, it's up to you. 38 | 39 | 40 | ## Model Concerns 41 | 42 | ### Pillowfort Resource 43 | 44 | Pillowfort doesn't care what authenticable model your application uses, but unless you're some kind of weirdo, it'll be your `User` model. 45 | 46 | At a minimum, you'll need to add the `Base` concern to enable session token management. 47 | 48 | If you want to enable email confirmation of new resource records, you should include the `Activation` concern. If you want to enable the resetting of forgotten passwords, you should include the `PasswordReset` concern. 49 | 50 | Here's a model with the kitchen sink. 51 | 52 | 53 | ``` ruby 54 | # == Schema Information 55 | # 56 | # Table name: users 57 | # 58 | # id :integer not null, primary key 59 | # email :string not null 60 | # password_digest :string 61 | # created_at :datetime not null 62 | # updated_at :datetime not null 63 | # 64 | # Indexes 65 | # 66 | # udx_users_on_email (email) UNIQUE 67 | # 68 | 69 | class User < ApplicationRecord 70 | 71 | #-------------------------------------------------------- 72 | # Configuration 73 | #-------------------------------------------------------- 74 | 75 | # callbacks 76 | before_validation :ensure_password, on: :create 77 | 78 | # mixins 79 | include Pillowfort::Concerns::Models::Resource::Base 80 | include Pillowfort::Concerns::Models::Resource::Activation 81 | include Pillowfort::Concerns::Models::Resource::PasswordReset 82 | 83 | 84 | #-------------------------------------------------------- 85 | # Private Methods 86 | #-------------------------------------------------------- 87 | private 88 | 89 | #========== CALLBACKS =================================== 90 | 91 | def ensure_password 92 | unless password_digest.present? 93 | reset_password 94 | end 95 | end 96 | 97 | end 98 | ``` 99 | 100 | #### Class Methods 101 | 102 | **authenticate_securely(email, secret, realm='application')** 103 | 104 | Accepts the email, secret, and realm from the client and returns the associated Pillowfort resource (or a suitable error). If found, extends the expiry of the session token for the specified realm. 105 | 106 | **find\_and\_authenticate(email, password, realm='application')** 107 | 108 | Accepts the email, password, and realm from the client and returns the associated Pillowfort resource (or a suitable error). If found, creates a new session token for the specified realm. 109 | 110 | #### Public Methods 111 | 112 | **activated?** 113 | 114 | Returns true in all cases. Is overridden by including the `Activation` concern. 115 | 116 | **authenticate(unencrypted)** 117 | 118 | Accepts an unencrypted password value from the client and uses `scrypt` to determine whether or not it matches the `password_digest` attribute. 119 | 120 | **password=(unencrypted)** 121 | 122 | Accepts an unencrypted password and uses `scrypt` to perform a one-way hashing of the value. 123 | 124 | **password_confirmation=(unencrypted)** 125 | 126 | Accepts an unencrypted password confirmation and stores it in an instance variable to facilitate validations. 127 | 128 | **reset_password** 129 | 130 | Sets the resource model password (and confirmation) to randomly generated token string. Does not save the change to the model. 131 | 132 | **reset_session!(realm='application')** 133 | 134 | Resets the session token for the specified realm. Returns the new token secret value. 135 | 136 | 137 | ### Activation Concern 138 | 139 | #### Class Methods 140 | 141 | **find_by_activation_secret(email, secret)** 142 | 143 | Accepts the email and activation token secret from the client and returns the associated Pillowfort resource (or a suitable error). 144 | 145 | #### Public Methods 146 | 147 | **activatable?** 148 | 149 | Returns `true` if the resource model has a valid, unconfirmed activation token; otherwise, `false`. 150 | 151 | **activated?** 152 | 153 | Returns `true` if the resource model has a confirmed activation token; otherwise, `false`. 154 | 155 | **confirm_activation!** 156 | 157 | Marks the current activation token as confirmed and returns the resource model (or a suitable error). This method completes the activation process. 158 | 159 | **require_activation!** 160 | 161 | Sets a new activation token secret and returns the activation token. This method starts the activation process. 162 | 163 | 164 | ### PasswordReset Concern 165 | 166 | #### Class Methods 167 | 168 | **find_by_activation_secret(email, secret)** 169 | 170 | Accepts the email and activation token secret from the client and returns the associated Pillowfort resource (or a suitable error). 171 | 172 | #### Public Methods 173 | 174 | **password_resettable?** 175 | 176 | Returns `true` if the resource model has a valid, unconfirmed password reset token; otherwise, `false`. 177 | 178 | **confirm_password_reset!** 179 | 180 | Marks the current password reset token as confirmed and returns the resource model (or a suitable error). This method completes the password reset process. 181 | 182 | 183 | **require_password_reset!** 184 | 185 | Sets a new password reset token secret and returns the password reset token. This method starts the password reset process. 186 | 187 | --- 188 | 189 | ### Pillowfort Token 190 | 191 | At its core, Pillowfort is a token manager. You need a table to hold the token records and a model to manage them. 192 | 193 | Unless you've read the source code thoroughly and know what you're doing, we **strongly** recommend you accept the default migration and simply add the corresponding concern to the token model. 194 | 195 | Don't be a busybody. Help us help you. 196 | 197 | ``` ruby 198 | # == Schema Information 199 | # 200 | # Table name: pillowfort_tokens 201 | # 202 | # id :integer not null, primary key 203 | # resource_id :integer not null 204 | # type :string default("session"), not null 205 | # secret :string not null 206 | # realm :string default("application"), not null 207 | # created_at :datetime not null 208 | # expires_at :datetime not null 209 | # confirmed_at :datetime 210 | # 211 | # Indexes 212 | # 213 | # udx_pillowfort_tokens_on_rid_type_and_realm (resource_id,type,realm) UNIQUE 214 | # udx_pillowfort_tokens_on_type_and_token (type,token) UNIQUE 215 | # 216 | 217 | class PillowfortToken < ApplicationRecord 218 | 219 | #-------------------------------------------------------- 220 | # Configuration 221 | #-------------------------------------------------------- 222 | 223 | # mixins 224 | include Pillowfort::Concerns::Models::Token::Base 225 | 226 | end 227 | ``` 228 | 229 | #### Public Methods 230 | 231 | *These methods are documented for the sake of being thorough, but in truth, you likely will not use them. They exist mostly for the internal use of the Pillowfort resource model. Your code will likely only ever need to invoke methods on the Pillowfort resource.* 232 | 233 | **confirm** 234 | 235 | Sets the `confirmed_at` attribute but does not save the token model. 236 | 237 | **confirm!** 238 | 239 | Sets the `confirmed_at` attribute and also saves the token model. 240 | 241 | **confirmed?** 242 | 243 | Returns `true` if the token model has been confirmed; otherwise, `false`. 244 | 245 | **expire** 246 | 247 | Sets the `expires_at` attribute but does not save the token model. 248 | 249 | **expire!** 250 | 251 | Sets the `expires_at` attribute and also saves the token model. 252 | 253 | **expired?** 254 | 255 | Returns `true` if the token model is expired; otherwise, `false`. 256 | 257 | **refresh!** 258 | 259 | Extends the `expires_at` value by the configured TTL for the token model's type and saves the token model. 260 | 261 | **reset!** 262 | 263 | Sets a new `secret` value, resets all timestamps, and saves the token model. 264 | 265 | **secure_compare(value)** 266 | 267 | Accepts a secret passed from the client and determines whether or not it matches the `secret` attribute value. This comparison is relatively slow in an effort to confound certain kinds of timing attacks. 268 | 269 | 270 | 271 | ## Controller Concerns 272 | 273 | ### Application Controller 274 | 275 | Pillowfort has a single controller concern that teaches your `ApplicationController` to stop being such a jerk and to be cool for once in its life. 276 | 277 | The controller concern does pretty much exactly what you would expect the authentication controller to do. It authenticates all actions by default; it understands how to use Rails' basic HTTP authentication methods to get the client's email and secret; it knows how to determine the specified realm; it knows how to pass all that wiz biz to the Pillowfort resource class; and it knows how to handle any errors that the whole process might throw. 278 | 279 | ``` ruby 280 | class ApplicationController < ActionController::API 281 | 282 | #-------------------------------------------------- 283 | # Configuration 284 | #-------------------------------------------------- 285 | 286 | # mixins 287 | include Pillowfort::Concerns::Controllers::Base 288 | 289 | # helpers 290 | helper_method :current_user 291 | 292 | 293 | #-------------------------------------------------- 294 | # Private Methods 295 | #-------------------------------------------------- 296 | private 297 | 298 | def current_user 299 | pillowfort_resource 300 | end 301 | 302 | end 303 | ``` 304 | 305 | 306 | ## Sample Endpoints 307 | 308 | ### Sessions 309 | 310 | In this example, the endpoint supports three session-related actions. 311 | 312 | - `show`: Returns information on the current session. 313 | - `create`: Constructs a new session token for the associated resource (i.e., signs in). 314 | - `destroy`: Deletes the current session token for the associated resource (i.e., signs out). 315 | 316 | Because no session token exists for the `create` action, we need to skip token secret authentication and instead perform password authentication. 317 | 318 | ``` ruby 319 | module V1 320 | class SessionsController < ApplicationController 321 | 322 | #------------------------------------------------------ 323 | # Configuration 324 | #------------------------------------------------------ 325 | 326 | # callbacks 327 | skip_before_action :authenticate_from_resource_secret!, only: [:create] 328 | 329 | 330 | #------------------------------------------------------ 331 | # Public Methods 332 | #------------------------------------------------------ 333 | 334 | #========== READ ====================================== 335 | 336 | def show; end 337 | 338 | 339 | #========== CREATE ==================================== 340 | 341 | def create 342 | email = params[:email].to_s.strip 343 | pword = params[:password].to_s.strip 344 | realm = pillowfort_realm 345 | 346 | @pillowfort_resource = User.find_and_authenticate(email, pword, realm) 347 | 348 | render :show 349 | end 350 | 351 | 352 | #========== DESTROY ==================================== 353 | 354 | def destroy 355 | pillowfort_session_token.reset! 356 | head :ok 357 | end 358 | 359 | end 360 | end 361 | ``` 362 | 363 | ### Activations 364 | 365 | In this example, the endpoint supports two actions for ensuring actual people are using your API. 366 | 367 | - `show`: Allows the client to verify the activation token before bothering to present a password change form. 368 | - `create`: Processes the password change request and creates a new session (i.e., signs in). 369 | 370 | Because no session token exists when these actions are invoked, we need to skip token secret authentication in favor of activation token lookups. 371 | 372 | ``` ruby 373 | module V1 374 | class ActivationsController < ApplicationController 375 | 376 | #------------------------------------------------------ 377 | # Configuration 378 | #------------------------------------------------------ 379 | 380 | # callbacks 381 | skip_before_action :authenticate_from_resource_secret! 382 | 383 | 384 | #------------------------------------------------------ 385 | # Public Methods 386 | #------------------------------------------------------ 387 | 388 | #========== READ ====================================== 389 | 390 | def show 391 | email = params[:email].to_s.strip 392 | secret = params[:secret].to_s.strip 393 | 394 | User.find_by_activation_secret(email, secret) do |resource| 395 | @user = resource 396 | end 397 | end 398 | 399 | 400 | #========== CREATE ==================================== 401 | 402 | def create 403 | email = params[:email].to_s.strip 404 | secret = params[:secret].to_s.strip 405 | 406 | User.transaction do 407 | User.find_by_activation_secret(email, secret) do |resource| 408 | @pillowfort_resource = resource 409 | @pillowfort_resource.attributes = create_params 410 | 411 | if @pillowfort_resource.save 412 | @pillowfort_resource.reset_session!(pillowfort_realm) 413 | @pillowfort_resource.confirm_activation! 414 | else 415 | render_unprocessable_error(@pillowfort_resource) 416 | end 417 | end 418 | end 419 | end 420 | 421 | 422 | #------------------------------------------------------ 423 | # Private Methods 424 | #------------------------------------------------------ 425 | private 426 | 427 | #========== PARAMS ==================================== 428 | 429 | def create_params 430 | params.permit( 431 | :password, 432 | :password_confirmation 433 | ) 434 | end 435 | 436 | end 437 | end 438 | ``` 439 | 440 | ### Password Requests 441 | 442 | In this example, the endpoint supports a single action for the kind of person who has not yet heard of password managers. 443 | 444 | - `create`: Accepts an email and locates the associated resource model. If found, a new password reset token is created and instructions are sent to the email address. 445 | 446 | Because no session token exists when this action is invoked, we need to skip token secret authentication in favor of a simple email lookup. 447 | 448 | ``` ruby 449 | module V1 450 | class PasswordRequestsController < ApplicationController 451 | 452 | #------------------------------------------------------ 453 | # Configuration 454 | #------------------------------------------------------ 455 | 456 | # callbacks 457 | skip_before_action :authenticate_from_resource_secret! 458 | 459 | 460 | #------------------------------------------------------ 461 | # Public Methods 462 | #------------------------------------------------------ 463 | 464 | #========== CREATE ==================================== 465 | 466 | def create 467 | if user.persisted? 468 | user.require_password_reset! 469 | PasswordRequestMailerJob.perform_later(user.id) 470 | head :ok 471 | else 472 | user.errors.add(:email, 'address is invalid or unrecognized.') 473 | render_unprocessable_error(user) 474 | end 475 | end 476 | 477 | 478 | #------------------------------------------------------ 479 | # Private Methods 480 | #------------------------------------------------------ 481 | private 482 | 483 | #========== HELPERS =================================== 484 | 485 | def user 486 | @user ||= begin 487 | email = params[:email].to_s.strip.downcase 488 | User.where(email: email).first_or_initialize 489 | end 490 | end 491 | 492 | end 493 | end 494 | ``` 495 | 496 | ### Password Resets 497 | 498 | In this example, the endpoint supports two actions for allowing users to regain control of their accounts. 499 | 500 | - `show`: Allows the client to verify the password reset token before bothering to present a password change form. 501 | - `create`: Processes the password change request and creates a new session (i.e., signs in). 502 | 503 | Because no session token exists when these actions are invoked, we need to skip token secret authentication in favor of password reset token lookups. 504 | 505 | ``` ruby 506 | module V1 507 | class PasswordResetsController < ApplicationController 508 | 509 | #------------------------------------------------------ 510 | # Configuration 511 | #------------------------------------------------------ 512 | 513 | # callbacks 514 | skip_before_action :authenticate_from_resource_secret! 515 | 516 | 517 | #------------------------------------------------------ 518 | # Public Methods 519 | #------------------------------------------------------ 520 | 521 | #========== GET ======================================= 522 | 523 | def show 524 | email = params[:email].to_s.strip 525 | secret = params[:secret].to_s.strip 526 | 527 | begin 528 | User.find_by_password_reset_secret(email, secret) do |resource| 529 | @user = resource 530 | end 531 | end 532 | end 533 | 534 | 535 | #========== CREATE ==================================== 536 | 537 | def create 538 | email = params[:email].to_s.strip 539 | secret = params[:secret].to_s.strip 540 | 541 | begin 542 | User.transaction do 543 | User.find_by_password_reset_secret(email, secret) do |resource| 544 | @pillowfort_resource = resource 545 | @pillowfort_resource.attributes = create_params 546 | 547 | if @pillowfort_resource.save 548 | @pillowfort_resource.reset_session!(pillowfort_realm) 549 | @pillowfort_resource.confirm_password_reset! 550 | else 551 | render_unprocessable_error(@pillowfort_resource) 552 | end 553 | end 554 | end 555 | end 556 | end 557 | 558 | 559 | #------------------------------------------------------ 560 | # Private Methods 561 | #------------------------------------------------------ 562 | private 563 | 564 | #========== PARAMS ==================================== 565 | 566 | def create_params 567 | params.permit( 568 | :password, 569 | :password_confirmation 570 | ) 571 | end 572 | 573 | end 574 | end 575 | ``` 576 | 577 | ## Configuration 578 | 579 | Pillowfort comes preconfigured with sane defaults. But you may not like these values. 580 | 581 | First, why are you being so mean? Second, you can override any of Pillowfort's default configurations in an initializer. The following example sets all available options to their default values: 582 | 583 | ``` ruby 584 | Pillowfort.configure do |config| 585 | 586 | # classes 587 | config.resource_class = :user 588 | config.token_class = :pillowfort_token 589 | 590 | # token lengths 591 | config.activation_token_length = 40 592 | config.password_reset_token_length = 40 593 | config.session_token_length = 40 594 | 595 | # token ttls 596 | config.activation_token_ttl = 7.days 597 | config.password_reset_token_ttl = 7.days 598 | config.session_token_ttl = 1.day 599 | 600 | end 601 | ``` 602 | 603 | 604 | ## Errors 605 | 606 | Pillowfort throws three basic errors, which the controller concern automatically traps and routes to private handlers. By default, the handlers simply return an HTTP status of 401 (unauthorized). Like all things Rails, you are welcome to override the methods however you please. 607 | 608 | The error handlers are: 609 | 610 | ``` ruby 611 | # This method renders a standard response for resources 612 | # that are not activated. 613 | # 614 | def render_pillowfort_activation_error 615 | head :unauthorized 616 | end 617 | 618 | # This method renders a standard response for resources 619 | # that are not authenticated. 620 | # 621 | def render_pillowfort_authentication_error 622 | head :unauthorized 623 | end 624 | 625 | # This method renders a standard response for resources 626 | # that attempt to modify tokens in illegal ways. 627 | # 628 | def render_pillowfort_token_state_error 629 | head :unauthorized 630 | end 631 | ``` 632 | 633 | 634 | ## FAQs 635 | 636 | **I expect Pillowfort to do something, but I can't figure out the right method calls.** 637 | 638 | Probably Pillowfort doesn't do the thing you expect. Pillowfort is tightly focused on a handful of authentication and token management functions. If a feature is not documented above, you can pretty safely assume it is not supported. 639 | 640 | If that doesn't convince you, please peruse the `lib` directory at your leisure. Pillowfort is truly tiny and has been crafted lovingly by the capital fellows at Coroutine. Few experiences in your life will compare to the pleasure of reading our source code directly. 641 | 642 | **I see that all the tests for this gem are hopelessly broken.** 643 | 644 | Alas, that is true. When Tim Lowrimore wrote the original version of Pillowfort, he provided a comprehensive set of probing, automated tests. Magical tests, really. 645 | 646 | Some time later, John Dugan modified Pillowfort to allow for each resource to have multiple session tokens. At the time, he argued that he didn't have the bandwidth to rewrite the test suite, but the truth is he's a giant asshole. 647 | 648 | In summary, please remember that Tim Lowrimore is a prince among men and John Dugan is your nemesis. 649 | 650 | **This documentation is sort of absurd. I don't know if I can trust you clowns.** 651 | 652 | These are excellent points, but Pillowfort is, we believe, well-designed and perfectly secure for most applications. We take our code very seriously; we take ourselves somewhat less so. 653 | 654 | **None of these FAQs are actually questions.** 655 | 656 | That's true. Thank you for actually reading this documentation. Your sacrifice shall be recorded in the Annuls of Ruby Heroes. 657 | 658 | 659 | ## Contributing 660 | 661 | If you have an idea for improving Pillowfort, we would love to hear it. You are, no doubt, an expert in all things known and unknown in the universe, whereas we are mere mortals, toiling away at our data machines like so many monkeys at typewriters. 662 | 663 | Having written that—it only took 1,000 years!—we suggest you open an issue on Github to discuss your idea with us before you haul off and author a PR. 664 | 665 | Like Pillowfort, we are also opinionated. 666 | --------------------------------------------------------------------------------