├── 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 | [](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 | 
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 |
--------------------------------------------------------------------------------