├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .release-please-manifest.json
├── .rubocop.yml
├── Appraisals
├── CHANGELOG.md
├── Gemfile
├── MIT-LICENSE
├── README.md
├── authie.gemspec
├── bin
├── appraisal
├── console
├── rspec
└── rubocop
├── db
└── migrate
│ ├── 20141012174250_create_authie_sessions.rb
│ ├── 20141013115205_add_indexes_to_authie_sessions.rb
│ ├── 20150109144120_add_parent_id_to_authie_sessions.rb
│ ├── 20150305135400_add_two_factor_auth_fields_to_authie.rb
│ ├── 20170417170000_add_token_hashes_to_authie_sessions.rb
│ ├── 20170421174100_add_index_to_token_hashes_on_authie_sessions.rb
│ ├── 20180215152200_add_host_to_authie_sessions.rb
│ ├── 20220502180100_add_two_factor_required_to_sessions.rb
│ └── 20230627165500_add_countries_to_authie_sessions.rb
├── gemfiles
├── .bundle
│ └── config
├── rails_7.1.gemfile
├── rails_7.2.gemfile
└── rails_8.0.gemfile
├── lib
├── authie.rb
└── authie
│ ├── config.rb
│ ├── controller_delegate.rb
│ ├── controller_extension.rb
│ ├── engine.rb
│ ├── error.rb
│ ├── rack_controller.rb
│ ├── session.rb
│ ├── session_model.rb
│ ├── user.rb
│ └── version.rb
├── release-please-config.json
└── spec
├── dummy
├── Rakefile
├── app
│ └── controllers
│ │ ├── application_controller.rb
│ │ └── pages_controller.rb
├── bin
│ ├── rails
│ ├── rake
│ └── setup
├── config.ru
├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── database.yml
│ ├── environment.rb
│ ├── environments
│ │ └── test.rb
│ ├── initializers
│ │ └── filter_parameter_logging.rb
│ └── routes.rb
└── public
│ ├── 404.html
│ ├── 422.html
│ ├── 500.html
│ ├── apple-touch-icon-precomposed.png
│ ├── apple-touch-icon.png
│ └── favicon.ico
├── integration
└── controller_extension_spec.rb
├── lib
├── config_spec.rb
├── controller_delegate_spec.rb
├── session_model_spec.rb
├── session_spec.rb
└── user_spec.rb
├── spec_helper.rb
└── support
├── controller_helpers.rb
└── user_model.rb
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: push
3 | jobs:
4 | release-please:
5 | runs-on: ubuntu-latest
6 | if: github.ref == 'refs/heads/main'
7 | outputs:
8 | release_created: ${{ steps.release-please.outputs.release_created }}
9 | tag_name: ${{ steps.release-please.outputs.tag_name }} # e.g. v1.0.0
10 | version: ${{ steps.release-please.outputs.version }} # e.g. 1.0.0
11 | all: ${{ toJSON(steps.release-please.outputs) }}
12 | steps:
13 | - uses: google-github-actions/release-please-action@v3
14 | id: release-please
15 | with:
16 | command: manifest
17 |
18 | lint:
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v3
22 |
23 | - uses: ruby/setup-ruby@v1
24 | with:
25 | ruby-version: 3.3
26 |
27 | - name: Install dependencies
28 | run: bundle install
29 |
30 | - name: Run linter
31 | run: bundle exec rubocop
32 |
33 | test:
34 | runs-on: ubuntu-latest
35 | strategy:
36 | fail-fast: false
37 | matrix:
38 | ruby_version:
39 | - 3.1
40 | - 3.2
41 | - 3.3
42 | - 3.4
43 | gemfile:
44 | - rails_7.1
45 | - rails_7.2
46 | - rails_8.0
47 | exclude:
48 | - ruby_version: 3.1
49 | gemfile: rails_8.0
50 | env:
51 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
52 | steps:
53 | - uses: actions/checkout@v3
54 | - uses: ruby/setup-ruby@v1
55 | with:
56 | ruby-version: ${{ matrix.ruby_version }}
57 |
58 | - name: Install dependencies
59 | run: bundle install
60 |
61 | - name: Run tests
62 | run: bundle exec rspec
63 |
64 | coverage:
65 | runs-on: ubuntu-latest
66 | steps:
67 | - uses: actions/checkout@v3
68 | - uses: ruby/setup-ruby@v1
69 | with:
70 | ruby-version: 3.1
71 |
72 | - name: Install dependencies
73 | run: bundle install
74 |
75 | - name: Generate and publish coverage
76 | uses: paambaati/codeclimate-action@v3.0.0
77 | env:
78 | COVERAGE: "1"
79 | CC_TEST_REPORTER_ID: "${{ secrets.CC_TEST_REPORTER_ID }}"
80 | with:
81 | coverageCommand: bundle exec rspec
82 | prefix: "${{ github.workspace }}"
83 | coverageLocations: |
84 | ${{ github.workspace }}/coverage/coverage.json:simplecov
85 |
86 | release:
87 | runs-on: ubuntu-latest
88 | needs: [test, release-please]
89 | if: needs.release-please.outputs.release_created
90 | steps:
91 | - uses: actions/checkout@v3
92 |
93 | - name: Set up Ruby
94 | uses: ruby/setup-ruby@v1
95 | with:
96 | ruby-version: 3.1
97 |
98 | - name: Export version from tag name
99 | run: echo ${{ needs.release-please.outputs.version }} > VERSION
100 |
101 | - name: Build Gem
102 | run: gem build *.gemspec
103 |
104 | - name: Setup credentials
105 | run: |
106 | mkdir -p $HOME/.gem
107 | touch $HOME/.gem/credentials
108 | chmod 0600 $HOME/.gem/credentials
109 | printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}\n" > $HOME/.gem/credentials
110 | printf -- ":github: Bearer ${GITHUB_API_KEY}\n" >> $HOME/.gem/credentials
111 | env:
112 | RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
113 | GITHUB_API_KEY: ${{secrets.GITHUB_TOKEN}}
114 |
115 | - name: Publish to RubyGems
116 | run: |
117 | gem push *.gem
118 |
119 | - name: Publish to GPR
120 | run: |
121 | gem push --key github --host https://rubygems.pkg.github.com/adamcooke *.gem
122 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | spec/dummy/log/*
3 | spec/dummy/tmp/*
4 | coverage/*
5 | Gemfile.lock
6 | gemfiles/*.gemfile.lock
7 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "5.0.0"
3 | }
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | SuggestExtensions: false
3 | NewCops: enable
4 | Exclude:
5 | - gemfiles/*
6 | - bin/*
7 |
8 | Style/AccessorGrouping:
9 | EnforcedStyle: separated
10 |
11 | Metrics/MethodLength:
12 | Enabled: false
13 |
14 | Metrics/AbcSize:
15 | Enabled: false
16 |
17 | Style/Documentation:
18 | Enabled: false
19 |
20 | Metrics/ClassLength:
21 | Enabled: false
22 |
23 | Metrics/CyclomaticComplexity:
24 | Enabled: false
25 |
26 | Gemspec/RequiredRubyVersion:
27 | Enabled: false
28 |
29 | Metrics/BlockLength:
30 | Enabled: false
31 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | appraise 'rails-7.1' do
4 | gem 'rails', '~> 7.1.0'
5 | end
6 |
7 | appraise 'rails-7.2' do
8 | gem 'rails', '~> 7.2.0'
9 | end
10 |
11 | appraise 'rails-8.0' do
12 | gem 'rails', '~> 8.0.0'
13 | end
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [5.0.0](https://github.com/adamcooke/authie/compare/v4.1.6...v5.0.0) (2025-02-20)
4 |
5 |
6 | ### ⚠ BREAKING CHANGES
7 |
8 | * Authie no longer supports Rails <= 7.0. Official support for Rails 7.0 ended over a year ago, we don't need to continue supporting it in Authie 5.0 and higher. Rails 7.0 can continue to use the 4.x releases.
9 | * If a coder was set using `config.active_record.default_column_serializer` then it will no longer be respected. You will need to set `Authie.config.serialize_coder` to override the coder for Authie sessions.
10 |
11 | ### Features
12 |
13 | * allow data serialization encoder to be defined using `Authie.config.serialize_coder`. ([588b5e7](https://github.com/adamcooke/authie/commit/588b5e7b0cc0e03fa3282b4fc088a18b866b0932))
14 |
15 |
16 | ### Miscellaneous Chores
17 |
18 | * remove support for Rails 7.0 ([1b09a80](https://github.com/adamcooke/authie/commit/1b09a80efaf1525a4079abf793bdfb93d9246917))
19 |
20 |
21 | ### Tests
22 |
23 | * use the >= 7.0 migration context ([7daabf1](https://github.com/adamcooke/authie/commit/7daabf1aff3cf06fec544f6b6e6f8fb9f7f55f6d))
24 |
25 | ## [4.1.6](https://github.com/adamcooke/authie/compare/v4.1.5...v4.1.6) (2025-02-04)
26 |
27 |
28 | ### Miscellaneous Chores
29 |
30 | * update action dependencies for release ([c070a1e](https://github.com/adamcooke/authie/commit/c070a1e3a197c04dff8631839f4430596e8ab581))
31 |
32 | ## [4.1.5](https://github.com/adamcooke/authie/compare/v4.1.4...v4.1.5) (2025-02-04)
33 |
34 |
35 | ### Miscellaneous Chores
36 |
37 | * add additional changelog sections to release please ([e6c763d](https://github.com/adamcooke/authie/commit/e6c763d1acb4f33ae2a988952329c815d5bb6d0c))
38 | * exclude 3.1/8.0 and 3.4/7.0 ([8456d8d](https://github.com/adamcooke/authie/commit/8456d8ded1accbda3d8bad838ec20bf09af42eb2))
39 | * unpin sqlite ([74043d6](https://github.com/adamcooke/authie/commit/74043d68a8d8aa6ce397255c84f7592be03b27a0))
40 | * update ci to include rails 8 and ruby 3.3/3.4 ([1326f9e](https://github.com/adamcooke/authie/commit/1326f9e49b9edf050a24046b732907f56d1a893a))
41 |
42 |
43 | ### Tests
44 |
45 | * require 'logger' in tests ([e1f55d3](https://github.com/adamcooke/authie/commit/e1f55d31315137b87ed2473deeb307ff1acee963))
46 |
47 | ## [4.1.4](https://github.com/adamcooke/authie/compare/v4.1.3...v4.1.4) (2024-01-29)
48 |
49 |
50 | ### Bug Fixes
51 |
52 | * fixes RackController session lookup ([#40](https://github.com/adamcooke/authie/issues/40)) ([89395b4](https://github.com/adamcooke/authie/commit/89395b4e23d33193b3ca4a30c3ed0bb19fd533f3))
53 |
54 | ## [4.1.3](https://github.com/adamcooke/authie/compare/v4.1.2...v4.1.3) (2023-11-02)
55 |
56 |
57 | ### Bug Fixes
58 |
59 | * fix dependency constraints ([0268de8](https://github.com/adamcooke/authie/commit/0268de87c02acd3ab0a1c01b70ed8153cb11d075))
60 |
61 | ## [4.1.2](https://github.com/adamcooke/authie/compare/v4.1.1...v4.1.2) (2023-11-02)
62 |
63 |
64 | ### Bug Fixes
65 |
66 | * don't provide Schema::Migration in Rails >= 7.0 ([57f2857](https://github.com/adamcooke/authie/commit/57f2857ba9c38bbc2078ca45be6ca13c55ed9373))
67 | * specify type of object for serialise data attribute ([48263f8](https://github.com/adamcooke/authie/commit/48263f84bae4a3e00ca67c878019503e83b09e34))
68 |
69 | ## [4.1.1](https://github.com/adamcooke/authie/compare/v4.1.0...v4.1.1) (2023-06-27)
70 |
71 |
72 | ### Bug Fixes
73 |
74 | * expose Config#lookup_ip_country_backend ([8473337](https://github.com/adamcooke/authie/commit/8473337fce552cb4d1ae5788e304347b2266b3d9))
75 |
76 | ## [4.1.0](https://github.com/adamcooke/authie/compare/v4.0.0...v4.1.0) (2023-06-27)
77 |
78 |
79 | ### Features
80 |
81 | * support for storing ip address countries ([90b2394](https://github.com/adamcooke/authie/commit/90b2394c7080feb9b355de0dec4e46e6683c64a2))
82 |
83 | ## [4.0.0](https://github.com/adamcooke/authie/compare/v3.4.0...v4.0.0) (2023-05-02)
84 |
85 |
86 | ### Features
87 |
88 | * ability to have expiry times increased on session activity ([a67dbbe](https://github.com/adamcooke/authie/commit/a67dbbed0d7e6d322e2516dc296b25d339c51a6a))
89 | * ability to pass session options to ControllerDelegate#create_auth_session ([38922f4](https://github.com/adamcooke/authie/commit/38922f4ac941dcebba5043dbf6ec8682dc213102))
90 | * ability to skip session touching within a request ([593eacf](https://github.com/adamcooke/authie/commit/593eacf83c4d2fd5ce50f0703c88914a4971a9b7))
91 | * active support notifications ([ce0c895](https://github.com/adamcooke/authie/commit/ce0c89574208091b0165c8133e4dd274f65aae4f))
92 | * add boolean for storing two factor skip state ([ec834df](https://github.com/adamcooke/authie/commit/ec834dff52fb54d07f718e1e5fb5669ecde300d7))
93 | * add notification on session invalidation ([cf9af97](https://github.com/adamcooke/authie/commit/cf9af97d5d76bf8539a54256a5975e7722e0cb9d))
94 | * add session to validity exceptions ([9e23f19](https://github.com/adamcooke/authie/commit/9e23f19e4cf4c9ba25941f1104e4ee3d8e2580e7))
95 | * allow persistent sessions to be created ([9ed6b6d](https://github.com/adamcooke/authie/commit/9ed6b6d759bc2ee7e68180a2e1bd52e64e8a7e43))
96 | * customisable token lengths ([41431a6](https://github.com/adamcooke/authie/commit/41431a66cf943f5f70abe1fa6dc059271b5f46cd))
97 | * separate touching & validating auth sessions ([e688762](https://github.com/adamcooke/authie/commit/e688762662215c823d9fe8bbf2cc6e1cef815b24))
98 | * support for resetting a token to a new value ([ed6f138](https://github.com/adamcooke/authie/commit/ed6f1381a4a69913cede483bd3c947320ac3b543))
99 |
100 |
101 | ### Bug Fixes
102 |
103 | * do not invalidate inactive sessions when invalidating others ([56a659b](https://github.com/adamcooke/authie/commit/56a659bec5438966fec24c3b8b48da5c68c7d5c9))
104 | * don't inspect sessions when invalidating others ([5a81581](https://github.com/adamcooke/authie/commit/5a81581d66a8e56200bd67726f27f76a265593e4))
105 | * don't override skip_two_factor whenever calling #mark_as_two_factored ([7e5c8a0](https://github.com/adamcooke/authie/commit/7e5c8a032c1383574f9c1f96d7f7007ff791130a))
106 | * maintain Authie::Session.cleanup ([5776421](https://github.com/adamcooke/authie/commit/5776421bb4d2f8f4cbe71bae927b3a132d877b58))
107 | * only add helper methods if the controller supports them ([bbeca3b](https://github.com/adamcooke/authie/commit/bbeca3b055b7b4ea0934d82e8ee4a3356dfe62de)), closes [#24](https://github.com/adamcooke/authie/issues/24)
108 | * require all of active record for the session model ([c042e34](https://github.com/adamcooke/authie/commit/c042e34f9002feaac9448de0cd9d4e58fbaec029))
109 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 | gemspec
5 |
6 | gem 'appraisal', '2.4.1'
7 | gem 'rails', '>= 5.0', '< 9.0'
8 | gem 'rspec'
9 | gem 'rspec-core'
10 | gem 'rspec-expectations'
11 | gem 'rspec-mocks'
12 | gem 'rspec-rails'
13 | gem 'rubocop'
14 | gem 'simplecov'
15 | gem 'simplecov-console'
16 | gem 'solargraph'
17 | gem 'sqlite3'
18 | gem 'timecop'
19 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2014 Adam Cooke.
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Authie
2 |
3 |
4 |
5 | This is a Rails library which provides applications with a database-backed user
6 | sessions. This ensures that user sessions can be invalidated from the server and
7 | users activity can be easily tracked.
8 |
9 | The "traditional" way of simply setting a user ID in your session is insecure
10 | and unwise. If you simply do something like the example below, it means that anyone
11 | with access to the session cookie can login as the user whenever and wherever they wish.
12 |
13 | To clarify: while by default Rails session cookies are encrypted, there is
14 | nothing to allow them to be invalidated if someone were to "steal" an encrypted
15 | cookie from an authenticated user. This could be stolen using a MITM attack or
16 | simply by stealing it directly from their browser when they're off getting a coffee.
17 |
18 | ```ruby
19 | if user = User.authenticate(params[:username], params[:password])
20 | # Don't do this...
21 | session[:user_id] = user.id
22 | redirect_to root_path, :notice => "Logged in successfully!"
23 | end
24 | ```
25 |
26 | The design goals behind Authie are:
27 |
28 | - Any session can be invalidated instantly from the server without needing to make
29 | changes to remote cookies.
30 | - We can see who is logged in to our application at any point in time.
31 | - Sessions should automatically expire after a certain period of inactivity.
32 | - Sessions can be either permanent or temporary.
33 |
34 | ## Installation
35 |
36 | As usual, just pop this in your Gemfile:
37 |
38 | ```ruby
39 | gem 'authie', '~> 4.0'
40 | ```
41 |
42 | You will then need add the database tables Authie needs to your database. You
43 | should copy Authie's migrations and then migrate.
44 |
45 | ```
46 | rake authie:install:migrations
47 | rake db:migrate
48 | ```
49 |
50 | ## Usage
51 |
52 | Authie is just a session manager and doesn't provide any functionality for your authentication or User models. Your `User` model should implement any methods needed to authenticate a username & password.
53 |
54 | ### Creating a new session
55 |
56 | When a user has been authenticated, you can simply set `current_user` to the user
57 | you wish to login. You may have a method like this in a controller.
58 |
59 | ```ruby
60 | class SessionsController < ApplicationController
61 |
62 | def create
63 | if user = User.authenticate(params[:username], params[:password])
64 | create_auth_session(user)
65 | redirect_to root_path
66 | else
67 | flash.now[:alert] = "Username/password was invalid"
68 | end
69 | end
70 |
71 | end
72 | ```
73 |
74 | ### Checking whether user's are logged in
75 |
76 | On any subsequent request, you should make sure that your user is logged in.
77 | You may wish to implement a `login_required` controller method which is called
78 | before every action in your application.
79 |
80 | ```ruby
81 | class ApplicationController < ActionController::Base
82 |
83 | before_action :login_required
84 |
85 | private
86 |
87 | def login_required
88 | return if logged_in?
89 |
90 | redirect_to login_path, :alert => "You must login to view this resource"
91 | end
92 |
93 | end
94 | ```
95 |
96 | ### Accessing the current user (and session)
97 |
98 | There are a few controller methods which you can call which will return information about the current session:
99 |
100 | - `current_user` - returns the currently logged in user
101 | - `auth_session` - returns the current auth session
102 | - `logged_in?` - returns a true if there's a session or false if no user is logged in
103 |
104 | ### Catching session errors
105 |
106 | If there is an issue with an auth session, an error will be raised which you need
107 | to catch within your application. The errors which will be raised are:
108 |
109 | - `Authie::Session::InactiveSession` - is raised when a session has been de-activated.
110 | - `Authie::Session::ExpiredSession` - is raised when a session expires.
111 | - `Authie::Session::BrowserMismatch` - is raised when the browser ID provided does
112 | not match the browser ID associated with the session token provided.
113 | - `Authie::Session::HostMismatch` - is raised when the session is used on a hostname
114 | that does not match that which created the session
115 |
116 | The easiest way to rescue these to use a `rescue_from`. For example:
117 |
118 | ```ruby
119 | class ApplicationController < ActionController::Base
120 |
121 | rescue_from Authie::Session::ValidityError, :with => :auth_session_error
122 |
123 | private
124 |
125 | def auth_session_error
126 | redirect_to login_path, :alert => "Your session is no longer valid. Please login again to continue..."
127 | end
128 |
129 | end
130 | ```
131 |
132 | ### Logging out
133 |
134 | In order to invalidate a session you can simply invalidate it.
135 |
136 | ```ruby
137 | def logout
138 | auth_session.invalidate
139 | redirect_to login_path, :notice => "Logged out successfully."
140 | end
141 | ```
142 |
143 | ### Default session length
144 |
145 | By default, a session will last for however long it is being actively used in
146 | browser. If the user stops using your application, the session will last for
147 | 12 hours before becoming invalid. You can change this:
148 |
149 | ```ruby
150 | Authie.config.session_inactivity_timeout = 2.hours
151 | ```
152 |
153 | This does not apply if the session is marked as persistent. See below.
154 |
155 | ### Persisting sessions
156 |
157 | In some cases, you may wish users to have a permanent sessions. In this case,
158 | you should ask users after they have logged in if they wish to "persist" their
159 | session across browser restarts. If they do wish to do this, just do something
160 | like this:
161 |
162 | ```ruby
163 | def persist_session
164 | auth_session.persist
165 | redirect_to root_path, :notice => "You will now be remembered!"
166 | end
167 | ```
168 |
169 | By default, persistent sessions will last for 2 months before requring the user
170 | logs in again. You can increase (or decrease) this if needed:
171 |
172 | ```ruby
173 | Authie.config.persistent_session_length = 12.months
174 | ```
175 |
176 | ### Accessing all user sessions
177 |
178 | If you want to provide users with a list of their sessions, you can access all
179 | active sessions for a user. The best way to do this will be to add a `has_many`
180 | association to your User model.
181 |
182 | ```ruby
183 | class User < ActiveRecord::Base
184 | has_many :sessions, :class_name => 'Authie::SessionModel', :as => :user, :dependent => :destroy
185 | end
186 | ```
187 |
188 | ### Storing additional data in the user session
189 |
190 | If you need to store additional information in your database-backed database
191 | session, then you can use the following methods to achieve this:
192 |
193 | ```ruby
194 | auth_session.set :two_factor_seen_at, Time.now
195 | auth_session.get :two_factor_seen_at
196 | ```
197 |
198 | ### Invalidating all but current session
199 |
200 | You may wish to allow users to easily invalidate all sessions which aren't their
201 | current one. Some applications invalidate old sessions whenever a user changes
202 | their password. The `invalidate_others!` method can be called on any
203 | `Authie::Session` object and will invalidate all sessions which aren't itself.
204 |
205 | ```ruby
206 | def change_password
207 | @user.change_password(params[:new_password])
208 | auth_session.invalidate_others!
209 | end
210 | ```
211 |
212 | ### Sudo functions
213 |
214 | In some applications, you may want to require that the user has recently provided
215 | their password to you before executing certain sensitive actions. Authie provides
216 | some methods which can help you keep track of when a user last provided their
217 | password in a session and whether you need to prompt them before continuing.
218 |
219 | ```ruby
220 | # When the user logs into your application, run the see_password method to note
221 | # that we have just seen their password.
222 | def login
223 | if user = User.authenticate(params[:username], params[:password])
224 | create_auth_session(user, see_password: true)
225 | redirect_to root_path
226 | end
227 | end
228 |
229 | # Before executing any dangerous actions, check to see whether the password has
230 | # recently been seen.
231 | def change_password
232 | if auth_session.recently_seen_password?
233 | # Allow the user to change their password as normal.
234 | else
235 | # Redirect the user a page which allows them to re-enter their password.
236 | # The method here should verify the password is correct and call the
237 | # see_password method as above. Once verified, you can return them back to
238 | # this page.
239 | redirect_to reauth_path(:return_to => request.fullpath)
240 | end
241 | end
242 | ```
243 |
244 | By default, a password will be said to have been recently seen if it has been
245 | seen in the last 10 minutes. You can change this configuration if needed:
246 |
247 | ```ruby
248 | Authie.config.sudo_session_timeout = 30.minutes
249 | ```
250 |
251 | ### Working with two factor authentication
252 |
253 | Authie provides a couple of methods to help you determine when two factor
254 | authentication is required for a request. Whenever a user logs in and has
255 | enabled two factor authentication, you can mark sessions as being permitted.
256 |
257 | You can add the following to your application controller and ensure that it runs
258 | on every request to your application.
259 |
260 | ```ruby
261 | class ApplicationController < ActionController::Base
262 |
263 | before_action :check_two_factor_auth
264 |
265 | def check_two_factor_auth
266 | if logged_in? && current_user.has_two_factor_auth? && !auth_session.two_factored?
267 | # If the user has two factor auth enabled, and we haven't already checked it
268 | # in this auth session, redirect the user to an action which prompts the user
269 | # to do their two factor auth check.
270 | redirect_to two_factor_auth_path
271 | end
272 | end
273 |
274 | end
275 | ```
276 |
277 | Then, on your two factor auth action, you need to ensure that you mark the auth
278 | session as being verified with two factor auth.
279 |
280 | ```ruby
281 | class LoginController < ApplicationController
282 |
283 | skip_before_action :check_two_factor_auth
284 |
285 | def two_factor_auth
286 | if user.verify_two_factor_token(params[:token])
287 | auth_session.mark_as_two_factored
288 | redirect_to root_path, :notice => "Logged in successfully!"
289 | end
290 | end
291 |
292 | end
293 | ```
294 |
295 | ## Storing IP address countries
296 |
297 | Authie has support for storing the country that an IP address is located in whenever they are saved to the database. To use this, you need to specify a backend to use in the Authie configuration. The backend should respond to `#call(ip_address)`.
298 |
299 | ```ruby
300 | Authie.config.lookup_ip_country_backend = proc do |ip_address|
301 | SomeService.lookup_country_from_ip(ip_address)
302 | end
303 | ```
304 |
305 | ## Instrumentation/Notification
306 |
307 | Authie will publish events to the ActiveSupport::Notification instrumentation system. The following events are published
308 | with the given attributes.
309 |
310 | - `set_browser_id.authie` - when a new browser ID is set for a user. Provides `:browser_id` and `:controller` arguments.
311 | - `cleanup.authie` - when session cleanup is run. Provides no arguments.
312 | - `touch.authie` - when a session is touched. Provides `:session` argument.
313 | - `see_password.authie` - when a session sees a password. Provides `:session` argument.
314 | - `mark_as_two_factor.authie` - when a session has two factor credentials provided. Provides `:session` argument.
315 | - `session_start.authie` - when a session is started. Provides `:session` argument.
316 | - `session_invalidate.authie` - when a session is intentionally invalidated. Provides `:session` argument with session model instance.
317 | - `browser_id_mismatch_error.authie` - when a session is validated when the browser ID does not match. Provides `:session` argument.
318 | - `invalid_session_error.authie` - when a session is validated when invalid. Provides `:session` argument.
319 | - `expired_session_error.authie` - when a session is validated when expired. Provides `:session` argument.
320 | - `inactive_session_error.authie` - when a session is validated when inactive. Provides `:session` argument.
321 | - `host_mismatch_error.authie` - when a session is validated and the host does not match. Provides `:session` argument.
322 |
323 | ## Differences for Authie 4.0
324 |
325 | Authie 4.0 introduces a number of changes to the library which are worth noting when upgrading from any version less than 4.
326 |
327 | - Authie 4.0 removes the impersonation features which may make a re-appearance in a futre version.
328 | - All previous callback/events have been replaced with standard ActiveSupport instrumentation notifications.
329 | - `Authie::SessionModel` has been introduced to represent the instance of the underlying database record.
330 | - Various methods on Authie::Session (more commonly known as `auth_session`) have been renamed as follows.
331 | - `check_security!` is now `validate`
332 | - `persist!` is now `persist`
333 | - `invalidate!` is now `invalidate`
334 | - `touch!` is now `touch`
335 | - `set_cookie!` is now `set_cookie` and is now a private method and should not be called directly.
336 | - `see_password!` is now `see_password`
337 | - `mark_as_two_factored!` is now `mark_as_two_factored`
338 | - A new `Authie::Session#reset_token` has been added which will generate a new token for a session, save it and update the cookie.
339 | - When starting a session using `Authie::Session.start` or `create_auth_session` you can provide the following additional options:
340 | - `persistent: true` to mark the session as persistent (i.e. give it an expiry time)
341 | - `see_password: true` to set the password seen timestamp at the same time as creation
342 | - If the `extend_session_expiry_on_touch` config option is set to true (default is false), the expiry time for a persistent session will be extended whenver a session is touched.
343 | - When making a request, the session will be touched **after** the action rather than before. Previously, the `touch_auth_session` method was added before every action and it both validated the session and touched it. Now, there are two separate methods - `validate_auth_session` which is run before every action and `touch_auth_session` runs after every action. If you don't want to touch a session in a request you can either use `skip_around_action :touch_auth_session` or call `skip_touch_auth_session!` anywhere in the action.
344 | - A new config option called `session_token_length` is available which allows you to change the length of the random token used for sessions (default 64).
345 |
--------------------------------------------------------------------------------
/authie.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require File.expand_path('lib/authie/version', __dir__)
4 |
5 | # rubocop:disable Gemspec/RequireMFA
6 | Gem::Specification.new do |s|
7 | s.name = 'authie'
8 | s.description = 'A Rails library for storing user sessions in a backend database'
9 | s.summary = s.description
10 | s.homepage = 'https://github.com/adamcooke/authie'
11 | s.licenses = ['MIT']
12 | s.version = Authie::VERSION
13 | s.files = Dir.glob('{lib,db}/**/*')
14 | s.require_paths = ['lib']
15 | s.authors = ['Adam Cooke']
16 | s.email = ['me@adamcooke.io']
17 |
18 | s.add_dependency 'activerecord', '>= 6.1', '< 9.0'
19 | end
20 | # rubocop:enable Gemspec/RequireMFA
21 |
--------------------------------------------------------------------------------
/bin/appraisal:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'appraisal' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12 |
13 | bundle_binstub = File.expand_path("bundle", __dir__)
14 |
15 | if File.file?(bundle_binstub)
16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17 | load(bundle_binstub)
18 | else
19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21 | end
22 | end
23 |
24 | require "rubygems"
25 | require "bundler/setup"
26 |
27 | load Gem.bin_path("appraisal", "appraisal")
28 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'bundler/setup'
5 | require 'authie'
6 |
7 | require 'irb'
8 | IRB.start(__FILE__)
9 |
--------------------------------------------------------------------------------
/bin/rspec:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'rspec' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("../bundle", __FILE__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("rspec-core", "rspec")
30 |
--------------------------------------------------------------------------------
/bin/rubocop:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'rubocop' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "pathname"
12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13 | Pathname.new(__FILE__).realpath)
14 |
15 | bundle_binstub = File.expand_path("../bundle", __FILE__)
16 |
17 | if File.file?(bundle_binstub)
18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19 | load(bundle_binstub)
20 | else
21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23 | end
24 | end
25 |
26 | require "rubygems"
27 | require "bundler/setup"
28 |
29 | load Gem.bin_path("rubocop", "rubocop")
30 |
--------------------------------------------------------------------------------
/db/migrate/20141012174250_create_authie_sessions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class CreateAuthieSessions < ActiveRecord::Migration[6.1]
4 | def change
5 | create_table :authie_sessions do |t|
6 | t.string :token, :browser_id
7 | t.bigint :user_id
8 | t.boolean :active, default: true
9 | t.text :data
10 | t.datetime :expires_at
11 | t.datetime :login_at
12 | t.string :login_ip
13 | t.datetime :last_activity_at
14 | t.string :last_activity_ip, :last_activity_path
15 | t.string :user_agent
16 | t.timestamps null: true
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/db/migrate/20141013115205_add_indexes_to_authie_sessions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddIndexesToAuthieSessions < ActiveRecord::Migration[6.1]
4 | def change
5 | add_column :authie_sessions, :user_type, :string
6 | add_index :authie_sessions, :token, length: 10
7 | add_index :authie_sessions, :browser_id, length: 10
8 | add_index :authie_sessions, :user_id
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20150109144120_add_parent_id_to_authie_sessions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddParentIdToAuthieSessions < ActiveRecord::Migration[6.1]
4 | def change
5 | add_column :authie_sessions, :parent_id, :bigint
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20150305135400_add_two_factor_auth_fields_to_authie.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddTwoFactorAuthFieldsToAuthie < ActiveRecord::Migration[6.1]
4 | def change
5 | add_column :authie_sessions, :two_factored_at, :datetime
6 | add_column :authie_sessions, :two_factored_ip, :string
7 | add_column :authie_sessions, :requests, :integer, default: 0
8 | add_column :authie_sessions, :password_seen_at, :datetime
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20170417170000_add_token_hashes_to_authie_sessions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddTokenHashesToAuthieSessions < ActiveRecord::Migration[6.1]
4 | def change
5 | add_column :authie_sessions, :token_hash, :string
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20170421174100_add_index_to_token_hashes_on_authie_sessions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddIndexToTokenHashesOnAuthieSessions < ActiveRecord::Migration[6.1]
4 | def change
5 | add_index :authie_sessions, :token_hash, length: 10
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20180215152200_add_host_to_authie_sessions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddHostToAuthieSessions < ActiveRecord::Migration[6.1]
4 | def change
5 | add_column :authie_sessions, :host, :string
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20220502180100_add_two_factor_required_to_sessions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddTwoFactorRequiredToSessions < ActiveRecord::Migration[6.1]
4 | def change
5 | add_column :authie_sessions, :skip_two_factor, :boolean, default: false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20230627165500_add_countries_to_authie_sessions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddCountriesToAuthieSessions < ActiveRecord::Migration[6.1]
4 | def change
5 | add_column :authie_sessions, :login_ip_country, :string
6 | add_column :authie_sessions, :two_factored_ip_country, :string
7 | add_column :authie_sessions, :last_activity_ip_country, :string
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/gemfiles/.bundle/config:
--------------------------------------------------------------------------------
1 | ---
2 | BUNDLE_RETRY: "1"
3 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", "2.4.1"
6 | gem "rails", "~> 7.1.0"
7 | gem "rspec"
8 | gem "rspec-core"
9 | gem "rspec-expectations"
10 | gem "rspec-mocks"
11 | gem "rspec-rails"
12 | gem "rubocop"
13 | gem "simplecov"
14 | gem "simplecov-console"
15 | gem "solargraph"
16 | gem "sqlite3"
17 | gem "timecop"
18 |
19 | gemspec path: "../"
20 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", "2.4.1"
6 | gem "rails", "~> 7.2.0"
7 | gem "rspec"
8 | gem "rspec-core"
9 | gem "rspec-expectations"
10 | gem "rspec-mocks"
11 | gem "rspec-rails"
12 | gem "rubocop"
13 | gem "simplecov"
14 | gem "simplecov-console"
15 | gem "solargraph"
16 | gem "sqlite3"
17 | gem "timecop"
18 |
19 | gemspec path: "../"
20 |
--------------------------------------------------------------------------------
/gemfiles/rails_8.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal", "2.4.1"
6 | gem "rails", "~> 8.0.0"
7 | gem "rspec"
8 | gem "rspec-core"
9 | gem "rspec-expectations"
10 | gem "rspec-mocks"
11 | gem "rspec-rails"
12 | gem "rubocop"
13 | gem "simplecov"
14 | gem "simplecov-console"
15 | gem "solargraph"
16 | gem "sqlite3"
17 | gem "timecop"
18 |
19 | gemspec path: "../"
20 |
--------------------------------------------------------------------------------
/lib/authie.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'authie/version'
4 | require 'authie/config'
5 | require 'authie/error'
6 | require 'authie/user'
7 |
8 | require 'authie/engine' if defined?(Rails)
9 |
--------------------------------------------------------------------------------
/lib/authie/config.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Authie
4 | class Config
5 | attr_accessor :session_inactivity_timeout
6 | attr_accessor :persistent_session_length
7 | attr_accessor :sudo_session_timeout
8 | attr_accessor :browser_id_cookie_name
9 | attr_accessor :session_token_length
10 | attr_accessor :extend_session_expiry_on_touch
11 | attr_accessor :lookup_ip_country_backend
12 | attr_accessor :serialize_coder
13 |
14 | def initialize
15 | set_defaults
16 | end
17 |
18 | def lookup_ip_country(ip)
19 | return nil if @lookup_ip_country_backend.nil?
20 |
21 | @lookup_ip_country_backend.call(ip)
22 | end
23 |
24 | def set_defaults
25 | @session_inactivity_timeout = 12.hours
26 | @persistent_session_length = 2.months
27 | @sudo_session_timeout = 10.minutes
28 | @browser_id_cookie_name = :browser_id
29 | @session_token_length = 64
30 | @extend_session_expiry_on_touch = false
31 | @lookup_ip_country_backend = nil
32 | @serialize_coder = ActiveRecord::Coders::YAMLColumn
33 | end
34 | end
35 |
36 | class << self
37 | def config
38 | @config ||= Config.new
39 | end
40 |
41 | def configure(&block)
42 | block.call(config)
43 | config
44 | end
45 |
46 | def notify(event, args = {}, &block)
47 | ActiveSupport::Notifications.instrument("#{event}.authie", args, &block)
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/authie/controller_delegate.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'securerandom'
4 | require 'authie/session'
5 | require 'authie/config'
6 | require 'authie/session_model'
7 |
8 | module Authie
9 | # The controller delegate implements methods that can be used by a controller. These are then
10 | # extended into controllers as needed (see ControllerExtension).
11 | class ControllerDelegate
12 | attr_accessor :touch_auth_session_enabled
13 |
14 | # @param controller [ActionController::Base]
15 | # @return [Authie::ControllerDelegate]
16 | def initialize(controller)
17 | @controller = controller
18 | @touch_auth_session_enabled = true
19 | end
20 |
21 | # Sets a browser ID. This must be performed on any page request where AUthie will be used.
22 | # It should be triggered before any other Authie provided methods. This will ensure that
23 | # the given browser ID is unique.
24 | #
25 | # @return [String] the generated browser ID
26 | def set_browser_id
27 | until cookies[Authie.config.browser_id_cookie_name]
28 | proposed_browser_id = SecureRandom.uuid
29 | next if Authie::SessionModel.where(browser_id: proposed_browser_id).exists?
30 |
31 | cookies[Authie.config.browser_id_cookie_name] = {
32 | value: proposed_browser_id,
33 | expires: 5.years.from_now,
34 | httponly: true,
35 | secure: @controller.request.ssl?
36 | }
37 | Authie.notify(:set_browser_id,
38 | browser_id: proposed_browser_id,
39 | controller: @controller)
40 | end
41 | proposed_browser_id
42 | end
43 |
44 | # Validate the auth session to ensure that it is current validate and raise an error if it
45 | # is not suitable for use.
46 | #
47 | # @return [Authie::Session, false]
48 | def validate_auth_session
49 | return false unless logged_in?
50 |
51 | auth_session.validate
52 | end
53 |
54 | # Touch the session to update details on the latest activity.
55 | #
56 | # @return [Authie::Session, false]
57 | def touch_auth_session
58 | yield if block_given?
59 | ensure
60 | auth_session.touch if @touch_auth_session_enabled && logged_in?
61 | end
62 |
63 | # Return the user for the currently logged in user or nil if no user is logged in
64 | #
65 | # @return [ActiveRecord::Base, nil]
66 | def current_user
67 | return nil unless logged_in?
68 |
69 | auth_session.session.user
70 | end
71 |
72 | # Create a new session for the given user. If nil is provided as a user, the existing session
73 | # will be invalidated.
74 | #
75 | # @return [Authie::Session, nil]
76 | def create_auth_session(user, **kwargs)
77 | if user.nil?
78 | invalidate_auth_session
79 | return nil
80 | end
81 |
82 | @auth_session = Authie::Session.start(@controller, user: user, **kwargs)
83 | end
84 |
85 | # Invalidate the existing auth session if one exists. Return true if a sesion has been invalidated
86 | # otherwise return false.
87 | #
88 | # @return [Boolean]
89 | def invalidate_auth_session
90 | return false unless logged_in?
91 |
92 | auth_session.invalidate
93 | @auth_session = nil
94 | true
95 | end
96 |
97 | # Is anyone currently logged in? Return true if there is an auth session present.
98 | #
99 | # Note: this does not check the validatity of the session. You must always ensure that the `validate`
100 | # or `touch` method is invoked to ensure that the session that has been found is active.
101 | #
102 | # @return [Boolean]
103 | def logged_in?
104 | auth_session.is_a?(Session)
105 | end
106 |
107 | # Return an auth session that has been found in the current cookies.
108 | #
109 | # @return [Authie::Session]
110 | def auth_session
111 | return @auth_session if instance_variable_defined?('@auth_session')
112 |
113 | @auth_session = Authie::Session.get_session(@controller)
114 | end
115 |
116 | private
117 |
118 | def cookies
119 | @controller.send(:cookies)
120 | end
121 | end
122 | end
123 |
--------------------------------------------------------------------------------
/lib/authie/controller_extension.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'authie/controller_delegate'
4 |
5 | module Authie
6 | module ControllerExtension
7 | class << self
8 | def included(base)
9 | base.helper_method :logged_in?, :current_user, :auth_session if base.respond_to?(:helper_method)
10 |
11 | base.before_action :set_browser_id, :validate_auth_session
12 | base.around_action :touch_auth_session
13 |
14 | base.delegate :set_browser_id, to: :auth_session_delegate
15 | base.delegate :validate_auth_session, to: :auth_session_delegate
16 | base.delegate :touch_auth_session, to: :auth_session_delegate
17 | base.delegate :current_user, to: :auth_session_delegate
18 | base.delegate :create_auth_session, to: :auth_session_delegate
19 | base.delegate :invalidate_auth_session, to: :auth_session_delegate
20 | base.delegate :logged_in?, to: :auth_session_delegate
21 | base.delegate :auth_session, to: :auth_session_delegate
22 | end
23 | end
24 |
25 | private
26 |
27 | def auth_session_delegate
28 | @auth_session_delegate ||= Authie::ControllerDelegate.new(self)
29 | end
30 |
31 | def skip_touch_auth_session!
32 | auth_session_delegate.touch_auth_session_enabled = false
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/authie/engine.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Authie
4 | class Engine < ::Rails::Engine
5 | engine_name 'authie'
6 |
7 | initializer 'authie.initialize' do |_app|
8 | ActiveSupport.on_load :active_record do
9 | require 'authie/session'
10 | end
11 |
12 | ActiveSupport.on_load :action_controller do
13 | require 'authie/controller_extension'
14 | include Authie::ControllerExtension
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/authie/error.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Authie
4 | class Error < StandardError
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/authie/rack_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # If you're dealing with your authentication in a middleware and you only have
4 | # access to your rack environment, this will wrap around rack and make it look
5 | # close enough to an ActionController to work with Authie
6 | #
7 | # Usage:
8 | #
9 | # controller = Authie::RackController.new(@env)
10 | # controller.current_user = user
11 |
12 | module Authie
13 | class RackController
14 | attr_reader :request
15 |
16 | def initialize(env)
17 | @env = env
18 | @request = ActionDispatch::Request.new(@env)
19 | set_browser_id
20 | end
21 |
22 | def cookies
23 | @request.cookie_jar
24 | end
25 |
26 | # Set a random browser ID for this browser.
27 | def set_browser_id
28 | until cookies[:browser_id]
29 | proposed_browser_id = SecureRandom.uuid
30 | unless SessionModel.where(browser_id: proposed_browser_id).exists?
31 | cookies[:browser_id] = { value: proposed_browser_id, expires: 20.years.from_now }
32 | end
33 | end
34 | end
35 |
36 | def current_user=(user)
37 | Session.start(self, user: user)
38 | end
39 |
40 | def current_user
41 | auth_session.user if auth_session.is_a?(Session)
42 | end
43 |
44 | def auth_session
45 | @auth_session ||= Session.get_session(self)
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/lib/authie/session.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'authie/session_model'
4 | require 'authie/error'
5 | require 'authie/config'
6 | require 'active_support/core_ext/module/delegation'
7 |
8 | module Authie
9 | class Session
10 | # The underlying session model instance
11 | #
12 | # @return [Authie::SessionModel]
13 | attr_reader :session
14 |
15 | # A parent class that encapsulates all session validity errors.
16 | class ValidityError < Error
17 | attr_reader :session
18 |
19 | def initialize(message, session = nil)
20 | super(message)
21 | @session = session
22 | end
23 | end
24 |
25 | # Raised when a session is used but it is no longer active
26 | class InactiveSession < ValidityError; end
27 |
28 | # Raised when a session is used but it has expired
29 | class ExpiredSession < ValidityError; end
30 |
31 | # Raised when a session is used but the browser ID does not match
32 | class BrowserMismatch < ValidityError; end
33 |
34 | # Raised when a session is used but the hostname does not match
35 | # the session hostname
36 | class HostMismatch < ValidityError; end
37 |
38 | # Initialize a new session object
39 | #
40 | # @param controller [ActionController::Base] any controller
41 | # @param session [Authie::SessionModel] an Authie session model instance
42 | # @return [Authie::Session]
43 | def initialize(controller, session)
44 | @controller = controller
45 | @session = session
46 | end
47 |
48 | # Validate that the session is valid and raise and error if not
49 | #
50 | # @raises [Authie::Session::BrowserMismatch]
51 | # @raises [Authie::Session::InactiveSession]
52 | # @raises [Authie::Session::ExpiredSession]
53 | # @raises [Authie::Session::HostMismatch]
54 | # @return [Authie::Session]
55 | def validate
56 | validate_browser_id
57 | validate_active
58 | validate_expiry
59 | validate_inactivity
60 | validate_host
61 | self
62 | end
63 |
64 | # Mark the current session as persistent. Will set the expiry time of the underlying
65 | # session and update the cookie.
66 | #
67 | # @raises [ActiveRecord::RecordInvalid]
68 | # @return [Authie::Session]
69 | def persist
70 | @session.expires_at = Authie.config.persistent_session_length.from_now
71 | @session.save!
72 | set_cookie
73 | self
74 | end
75 |
76 | # Invalidates the current session by marking it inactive and removing the current cookie.
77 | #
78 | # @raises [ActiveRecord::RecordInvalid]
79 | # @return [Authie::Session]
80 | def invalidate
81 | @session.invalidate!
82 | cookies.delete(:user_session)
83 | self
84 | end
85 |
86 | # Touches the current session to ensure it is currently valid and to update attributes
87 | # which should be updatd on each request. This will raise the same errors as the #validate
88 | # method. It will set the last activity time, IP and path as well as incrementing
89 | # the request counter.
90 | #
91 | # @raises [Authie::Session::BrowserMismatch]
92 | # @raises [Authie::Session::InactiveSession]
93 | # @raises [Authie::Session::ExpiredSession]
94 | # @raises [Authie::Session::HostMismatch]
95 | # @raises [ActiveRecord::RecordInvalid]
96 | # @return [Authie::Session]
97 | def touch
98 | @session.last_activity_at = Time.now
99 | if @controller.request.ip != @session.last_activity_ip
100 | @session.last_activity_ip_country = Authie.config.lookup_ip_country(@controller.request.ip)
101 | end
102 | @session.last_activity_ip = @controller.request.ip
103 |
104 | @session.last_activity_path = @controller.request.path
105 | @session.requests += 1
106 | extend_session_expiry_if_appropriate
107 | @session.save!
108 | Authie.notify(:touch, session: self)
109 | self
110 | end
111 |
112 | # Mark the session's password as seen at the current time
113 | #
114 | # @raises [ActiveRecord::RecordInvalid]
115 | # @return [Authie::Session]
116 | def see_password
117 | @session.password_seen_at = Time.now
118 | @session.save!
119 | Authie.notify(:see_password, session: self)
120 | self
121 | end
122 |
123 | # Mark this request as two factored by setting the time and the current
124 | # IP address.
125 | #
126 | # @raises [ActiveRecord::RecordInvalid]
127 | # @return [Authie::Session]
128 | def mark_as_two_factored(skip: nil)
129 | @session.two_factored_at = Time.now
130 | @session.two_factored_ip = @controller.request.ip
131 | @session.two_factored_ip_country = Authie.config.lookup_ip_country(@controller.request.ip)
132 | @session.skip_two_factor = skip unless skip.nil?
133 | @session.save!
134 | Authie.notify(:mark_as_two_factor, session: self)
135 | self
136 | end
137 |
138 | # Starts a new session by setting the cookie. This should be invoked whenever
139 | # a new session begins. It usually does not need to be called directly as it
140 | # will be taken care of by the class-level start method.
141 | #
142 | # @return [Authie::Session]
143 | def start
144 | set_cookie
145 | Authie.notify(:session_start, session: self)
146 | self
147 | end
148 |
149 | # Resets the token for the currently active session to a new string
150 | #
151 | # @return [Authie::Session]
152 | def reset_token
153 | @session.reset_token
154 | set_cookie
155 | self
156 | end
157 |
158 | private
159 |
160 | def set_cookie(value = @session.temporary_token)
161 | cookies[:user_session] = {
162 | value: value,
163 | secure: @controller.request.ssl?,
164 | httponly: true,
165 | expires: @session.expires_at
166 | }
167 | Authie.notify(:cookie_updated, session: session)
168 | true
169 | end
170 |
171 | def cookies
172 | @controller.send(:cookies)
173 | end
174 |
175 | def validate_browser_id
176 | if cookies[:browser_id] != @session.browser_id
177 | Authie.notify(:browser_id_mismatch_error, session: self)
178 | invalidate
179 | raise BrowserMismatch.new('Browser ID mismatch', self)
180 | end
181 |
182 | self
183 | end
184 |
185 | def validate_active
186 | unless @session.active?
187 | invalidate
188 | Authie.notify(:invalid_session_error, session: self)
189 | raise InactiveSession.new('Session is no longer active', self)
190 | end
191 |
192 | self
193 | end
194 |
195 | def validate_expiry
196 | if @session.expired?
197 | invalidate
198 | Authie.notify(:expired_session_error, session: self)
199 | raise ExpiredSession.new('Persistent session has expired', self)
200 | end
201 |
202 | self
203 | end
204 |
205 | def validate_inactivity
206 | if @session.inactive?
207 | invalidate
208 | Authie.notify(:inactive_session_error, session: self)
209 | raise InactiveSession.new('Non-persistent session has expired', self)
210 | end
211 |
212 | self
213 | end
214 |
215 | def validate_host
216 | if @session.host && @session.host != @controller.request.host
217 | invalidate
218 | Authie.notify(:host_mismatch_error, session: self)
219 | raise HostMismatch.new("Session was created on #{@session.host} but accessed using #{@controller.request.host}",
220 | self)
221 | end
222 |
223 | self
224 | end
225 |
226 | def extend_session_expiry_if_appropriate
227 | return if @session.expires_at.nil?
228 | return unless Authie.config.extend_session_expiry_on_touch
229 |
230 | # If enabled, sessions with an expiry time will automatiaclly be incremented
231 | # whenever a page is touched. The cookie will also be updated as appropriate.
232 | @session.expires_at = Authie.config.persistent_session_length.from_now
233 | set_cookie
234 | end
235 |
236 | class << self
237 | # Create a new session within the given controller for the
238 | #
239 | # @param controller [ActionController::Base]
240 | # @param user [ActiveRecord::Base] user
241 | # @param persistent [Boolean] create a persistent session
242 | # @return [Authie::Session]
243 | def start(controller, user:, persistent: false, see_password: false, **params)
244 | cookies = controller.send(:cookies)
245 | SessionModel.active.where(browser_id: cookies[:browser_id]).each(&:invalidate!)
246 |
247 | session = SessionModel.new(params)
248 | session.user = user
249 | session.browser_id = cookies[:browser_id]
250 | session.login_at = Time.now
251 | session.login_ip = controller.request.ip
252 | session.login_ip_country = Authie.config.lookup_ip_country(session.login_ip)
253 | session.host = controller.request.host
254 | session.user_agent = controller.request.user_agent
255 | session.expires_at = Time.now + Authie.config.persistent_session_length if persistent
256 | session.password_seen_at = Time.now if see_password
257 | session.save!
258 |
259 | new(controller, session).start
260 | end
261 |
262 | # Lookup a session for a given controller and return the session
263 | # object.
264 | #
265 | # @param controller [ActionController::Base]
266 | # @return [Authie::Session]
267 | def get_session(controller)
268 | cookies = controller.send(:cookies)
269 | return nil if cookies[:user_session].blank?
270 |
271 | session = SessionModel.find_session_by_token(cookies[:user_session])
272 | return nil if session.blank?
273 |
274 | session.temporary_token = cookies[:user_session]
275 | new(controller, session)
276 | end
277 |
278 | delegate :hash_token, to: SessionModel
279 | delegate :cleanup, to: SessionModel
280 | end
281 |
282 | # Backwards compatibility with Authie < 4.0. These methods were all available on sessions
283 | # in previous versions of Authie. They have been maintained for backwards-compatibility but
284 | # will be removed entirely in Authie 5.0.
285 | alias check_security! validate
286 | alias persist! persist
287 | alias invalidate! invalidate
288 | alias touch! touch
289 | alias set_cookie! set_cookie
290 | alias see_password! see_password
291 | alias mark_as_two_factored! mark_as_two_factored
292 |
293 | # Delegate key methods back to the underlying session model. Previous behaviour in Authie
294 | # exposed all methods on the session model. It is useful that these methods can be accessed
295 | # easily from this session proxy model so these are maintained as delegated methods.
296 | delegate :active?, to: :session
297 | delegate :browser_id, to: :session
298 | delegate :expired?, to: :session
299 | delegate :expires_at, to: :session
300 | delegate :first_session_for_browser?, to: :session
301 | delegate :first_session_for_ip?, to: :session
302 | delegate :get, to: :session
303 | delegate :inactive?, to: :session
304 | delegate :invalidate_others!, to: :session
305 | delegate :last_activity_at, to: :session
306 | delegate :last_activity_ip, to: :session
307 | delegate :last_activity_ip_country, to: :session
308 | delegate :last_activity_path, to: :session
309 | delegate :login_at, to: :session
310 | delegate :login_ip, to: :session
311 | delegate :login_ip_country, to: :session
312 | delegate :password_seen_at, to: :session
313 | delegate :persisted?, to: :session
314 | delegate :persistent?, to: :session
315 | delegate :recently_seen_password?, to: :session
316 | delegate :requests, to: :session
317 | delegate :set, to: :session
318 | delegate :temporary_token, to: :session
319 | delegate :token_hash, to: :session
320 | delegate :two_factored_at, to: :session
321 | delegate :two_factored_ip, to: :session
322 | delegate :two_factored_ip_country, to: :session
323 | delegate :two_factored?, to: :session
324 | delegate :skip_two_factor?, to: :session
325 | delegate :update, to: :session
326 | delegate :update!, to: :session
327 | delegate :user_agent, to: :session
328 | delegate :user, to: :session
329 | end
330 | end
331 |
--------------------------------------------------------------------------------
/lib/authie/session_model.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_record'
4 | require 'securerandom'
5 | require 'authie/config'
6 |
7 | module Authie
8 | class SessionModel < ActiveRecord::Base
9 | attr_accessor :temporary_token
10 |
11 | self.table_name = 'authie_sessions'
12 |
13 | belongs_to :parent, class_name: 'Authie::SessionModel', optional: true
14 |
15 | scope :active, -> { where(active: true) }
16 | scope :asc, -> { order(last_activity_at: :desc) }
17 | scope :for_user, ->(user) { where(user_type: user.class.name, user_id: user.id) }
18 |
19 | # Attributes
20 | serialize :data, type: Hash, coder: Authie.config.serialize_coder
21 |
22 | before_validation :shorten_strings
23 | before_create :set_new_token
24 |
25 | # Return the user that
26 | def user
27 | return unless user_id && user_type
28 | return @user if instance_variable_defined?('@user')
29 |
30 | @user = user_type.constantize.find_by(id: user_id)
31 | end
32 |
33 | # Set the user
34 | def user=(user)
35 | @user = user
36 | if user
37 | self.user_type = user.class.name
38 | self.user_id = user.id
39 | else
40 | self.user_type = nil
41 | self.user_id = nil
42 | end
43 | end
44 |
45 | def expired?
46 | expires_at.present? &&
47 | expires_at < Time.now
48 | end
49 |
50 | def inactive?
51 | expires_at.nil? &&
52 | last_activity_at.present? &&
53 | last_activity_at < Authie.config.session_inactivity_timeout.ago
54 | end
55 |
56 | def persistent?
57 | !!expires_at
58 | end
59 |
60 | def activate!
61 | self.active = true
62 | save!
63 | end
64 |
65 | def invalidate!
66 | active_now = active?
67 | self.active = false
68 | save!
69 | Authie.notify(:session_invalidate, session: self) if active_now
70 | true
71 | end
72 |
73 | def set(key, value)
74 | self.data ||= {}
75 | self.data[key.to_s] = value
76 | save!
77 | end
78 |
79 | def get(key)
80 | (self.data ||= {})[key.to_s]
81 | end
82 |
83 | def invalidate_others!
84 | self.class.where('id != ?', id).active.for_user(user).each(&:invalidate!)
85 | end
86 |
87 | # Have we seen the user's password recently in this sesion?
88 | def recently_seen_password?
89 | !!(password_seen_at && password_seen_at >= Authie.config.sudo_session_timeout.ago)
90 | end
91 |
92 | # Is two factor authentication required for this request?
93 | def two_factored?
94 | !!(two_factored_at || parent_id)
95 | end
96 |
97 | # Is this the first session for this session's browser?
98 | def first_session_for_browser?
99 | self.class.where('id < ?', id).for_user(user).where(browser_id: browser_id).empty?
100 | end
101 |
102 | # Is this the first session for the IP?
103 | def first_session_for_ip?
104 | self.class.where('id < ?', id).for_user(user).where(login_ip: login_ip).empty?
105 | end
106 |
107 | # Reset a new token for the session and return the new token
108 | #
109 | # @return [String]
110 | def reset_token
111 | set_new_token
112 | save!
113 | temporary_token
114 | end
115 |
116 | private
117 |
118 | def shorten_strings
119 | self.user_agent = user_agent[0, 255] if user_agent.is_a?(String)
120 | self.last_activity_path = last_activity_path[0, 255] if last_activity_path.is_a?(String)
121 | end
122 |
123 | def set_new_token
124 | self.temporary_token = SecureRandom.alphanumeric(Authie.config.session_token_length)
125 | self.token_hash = self.class.hash_token(temporary_token)
126 | end
127 |
128 | class << self
129 | # Find a session from the database for the given controller instance.
130 | # Returns a session object or :none if no session is found.
131 |
132 | # Find a session by a token (either from a hash or from the raw token)
133 | def find_session_by_token(token)
134 | return nil if token.blank?
135 |
136 | active.where(token_hash: hash_token(token)).first
137 | end
138 |
139 | # Cleanup any old sessions.
140 | def cleanup
141 | Authie.notify(:cleanup) do
142 | # Invalidate transient sessions that haven't been used
143 | active.where('expires_at IS NULL AND last_activity_at < ?',
144 | Authie.config.session_inactivity_timeout.ago).each(&:invalidate!)
145 | # Invalidate persistent sessions that have expired
146 | active.where('expires_at IS NOT NULL AND expires_at < ?', Time.now).each(&:invalidate!)
147 | end
148 | true
149 | end
150 |
151 | # Return a hash of a given token
152 | def hash_token(token)
153 | Digest::SHA256.hexdigest(token)
154 | end
155 | end
156 | end
157 | end
158 |
--------------------------------------------------------------------------------
/lib/authie/user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Authie
4 | module User
5 | def self.included(base)
6 | base.has_many :user_sessions, class_name: 'Authie::SessionModel', as: :user, dependent: :delete_all
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/authie/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Authie
4 | VERSION_FILE_ROOT = File.expand_path('../../VERSION', __dir__)
5 | VERSION = if File.file?(VERSION_FILE_ROOT)
6 | File.read(VERSION_FILE_ROOT).strip.sub(/\Av/, '')
7 | else
8 | '0.0.0.dev'
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "bootstrap-sha": "23c79175d75dc85cf0b059d9b33d8da8928e0384",
3 | "packages": {
4 | ".": {
5 | "release-type": "ruby",
6 | "changelog-path": "CHANGELOG.md",
7 | "bump-minor-pre-major": true,
8 | "bump-patch-for-minor-pre-major": true,
9 | "draft": false,
10 | "prerelease": false,
11 | "changelog-sections": [
12 | {
13 | "type": "feat",
14 | "section": "Features"
15 | },
16 | {
17 | "type": "feature",
18 | "section": "Features"
19 | },
20 | {
21 | "type": "fix",
22 | "section": "Bug Fixes"
23 | },
24 | {
25 | "type": "perf",
26 | "section": "Performance Improvements"
27 | },
28 | {
29 | "type": "revert",
30 | "section": "Reverts"
31 | },
32 | {
33 | "type": "docs",
34 | "section": "Documentation"
35 | },
36 | {
37 | "type": "style",
38 | "section": "Styles"
39 | },
40 | {
41 | "type": "chore",
42 | "section": "Miscellaneous Chores"
43 | },
44 | {
45 | "type": "refactor",
46 | "section": "Code Refactoring"
47 | },
48 | {
49 | "type": "test",
50 | "section": "Tests"
51 | },
52 | {
53 | "type": "build",
54 | "section": "Build System"
55 | },
56 | {
57 | "type": "ci",
58 | "section": "Continuous Integration"
59 | }
60 | ]
61 | }
62 | },
63 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
64 | }
65 |
--------------------------------------------------------------------------------
/spec/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Add your own tasks in files placed in lib/tasks ending in .rake,
4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
5 |
6 | require_relative 'config/application'
7 |
8 | Rails.application.load_tasks
9 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationController < ActionController::Base
4 | end
5 |
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/pages_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class PagesController < ApplicationController
4 | def index
5 | render plain: 'Hello world!'
6 | end
7 |
8 | def authenticated
9 | render plain: "Hello #{current_user.username}!"
10 | end
11 |
12 | def request_count
13 | render plain: "Count: #{auth_session.requests}"
14 | end
15 |
16 | def logged_in
17 | if logged_in?
18 | render plain: 'Logged in'
19 | else
20 | render plain: 'Not logged in'
21 | end
22 | end
23 |
24 | def error
25 | 1 / 0
26 | end
27 |
28 | def no_touching
29 | skip_touch_auth_session!
30 | render plain: 'Blah'
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | APP_PATH = File.expand_path('../config/application', __dir__)
5 | require_relative '../config/boot'
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/spec/dummy/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require_relative '../config/boot'
5 | require 'rake'
6 | Rake.application.run
7 |
--------------------------------------------------------------------------------
/spec/dummy/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require 'fileutils'
5 |
6 | # path to your application root.
7 | APP_ROOT = File.expand_path('..', __dir__)
8 |
9 | def system!(*args)
10 | system(*args) || abort("\n== Command #{args} failed ==")
11 | end
12 |
13 | FileUtils.chdir APP_ROOT do
14 | # This script is a way to set up or update your development environment automatically.
15 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
16 | # Add necessary setup steps to this file.
17 |
18 | puts '== Installing dependencies =='
19 | system! 'gem install bundler --conservative'
20 | system('bundle check') || system!('bundle install')
21 |
22 | # puts "\n== Copying sample files =="
23 | # unless File.exist?("config/database.yml")
24 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
25 | # end
26 |
27 | puts "\n== Preparing database =="
28 | system! 'bin/rails db:prepare'
29 |
30 | puts "\n== Removing old logs and tempfiles =="
31 | system! 'bin/rails log:clear tmp:clear'
32 |
33 | puts "\n== Restarting application server =="
34 | system! 'bin/rails restart'
35 | end
36 |
--------------------------------------------------------------------------------
/spec/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is used by Rack-based servers to start the application.
4 |
5 | require_relative 'config/environment'
6 |
7 | run Rails.application
8 | Rails.application.load_server
9 |
--------------------------------------------------------------------------------
/spec/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'boot'
4 |
5 | require 'action_controller/railtie'
6 |
7 | # Require the gems listed in Gemfile, including any gems
8 | # you've limited to :test, :development, or :production.
9 | Bundler.require(*Rails.groups)
10 | require 'authie/engine'
11 |
12 | module Dummy
13 | class Application < Rails::Application
14 | config.load_defaults Rails::VERSION::STRING.to_f
15 |
16 | # For compatibility with applications that use this config
17 | config.action_controller.include_all_helpers = false
18 |
19 | # Configuration for the application, engines, and railties goes here.
20 | #
21 | # These settings can be overridden in specific environments using the files
22 | # in config/environments, which are processed later.
23 | #
24 | # config.time_zone = "Central Time (US & Canada)"
25 | # config.eager_load_paths << Rails.root.join("extras")
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
5 |
6 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
7 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
8 |
--------------------------------------------------------------------------------
/spec/dummy/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
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: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: ':memory:'
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: ':memory:'
22 |
23 | production:
24 | <<: *default
25 | database: ':memory:'
26 |
--------------------------------------------------------------------------------
/spec/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Load the Rails application.
4 | require_relative 'application'
5 |
6 | # Initialize the Rails application.
7 | Rails.application.initialize!
8 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'active_support/core_ext/integer/time'
4 |
5 | # The test environment is used exclusively to run your application's
6 | # test suite. You never need to work with it otherwise. Remember that
7 | # your test database is "scratch space" for the test suite and is wiped
8 | # and recreated between test runs. Don't rely on the data there!
9 |
10 | Rails.application.configure do
11 | # Settings specified here will take precedence over those in config/application.rb.
12 |
13 | # Turn false under Spring and add config.action_view.cache_template_loading = true.
14 | config.cache_classes = true
15 |
16 | # Eager loading loads your whole application. When running a single test locally,
17 | # this probably isn't necessary. It's a good idea to do in a continuous integration
18 | # system, or in some way before deploying your code.
19 | config.eager_load = true
20 |
21 | # Configure public file server for tests with Cache-Control for performance.
22 | config.public_file_server.enabled = true
23 | config.public_file_server.headers = {
24 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
25 | }
26 |
27 | # Show full error reports and disable caching.
28 | config.consider_all_requests_local = true
29 | config.action_controller.perform_caching = false
30 | config.cache_store = :null_store
31 |
32 | # Raise exceptions instead of rendering exception templates.
33 | config.action_dispatch.show_exceptions = false
34 |
35 | # Disable request forgery protection in test environment.
36 | config.action_controller.allow_forgery_protection = false
37 |
38 | # Store uploaded files on the local file system in a temporary directory.
39 | # config.active_storage.service = :test
40 |
41 | # config.action_mailer.perform_caching = false
42 |
43 | # Tell Action Mailer not to deliver emails to the real world.
44 | # The :test delivery method accumulates sent emails in the
45 | # ActionMailer::Base.deliveries array.
46 | # config.action_mailer.delivery_method = :test
47 |
48 | # Print deprecation notices to the stderr.
49 | config.active_support.deprecation = :stderr
50 |
51 | # Raise exceptions for disallowed deprecations.
52 | config.active_support.disallowed_deprecation = :raise
53 |
54 | # Tell Active Support which deprecation messages to disallow.
55 | config.active_support.disallowed_deprecation_warnings = []
56 |
57 | # Raises error for missing translations.
58 | # config.i18n.raise_on_missing_translations = true
59 |
60 | # Annotate rendered view with file names.
61 | # config.action_view.annotate_rendered_view_with_filenames = true
62 | end
63 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Be sure to restart your server when you modify this file.
4 |
5 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of
6 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
7 | # notations and behaviors.
8 | Rails.application.config.filter_parameters += %i[
9 | passw secret token _key crypt salt certificate otp ssn
10 | ]
11 |
--------------------------------------------------------------------------------
/spec/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.routes.draw do
4 | get '/', to: 'pages#index'
5 | get '/authenticated', to: 'pages#authenticated'
6 | get '/logged_in', to: 'pages#logged_in'
7 | get '/request_count', to: 'pages#request_count'
8 | get '/error', to: 'pages#error'
9 | get '/no_touching', to: 'pages#no_touching'
10 | end
11 |
--------------------------------------------------------------------------------
/spec/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |If you are the application owner check the logs for more information.
64 |