rodauth-rails 0.9.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/README.md +417 -250
- data/lib/generators/rodauth/install_generator.rb +17 -0
- data/lib/generators/rodauth/migration/base.erb +2 -2
- data/lib/generators/rodauth/templates/app/lib/rodauth_app.rb +31 -29
- data/lib/generators/rodauth/templates/app/mailers/rodauth_mailer.rb +3 -3
- data/lib/generators/rodauth/templates/app/views/rodauth/_global_logout_field.html.erb +1 -1
- data/lib/generators/rodauth/templates/app/views/rodauth/_login_confirm_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_login_display.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_login_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_new_password_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_otp_auth_code_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_password_confirm_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_password_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_recovery_code_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_sms_code_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_sms_phone_field.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/_submit.html.erb +1 -1
- data/lib/generators/rodauth/templates/app/views/rodauth/otp_setup.html.erb +2 -2
- data/lib/generators/rodauth/templates/app/views/rodauth/remember.html.erb +1 -1
- data/lib/generators/rodauth/templates/app/views/rodauth/webauthn_remove.html.erb +1 -1
- data/lib/generators/rodauth/templates/app/views/rodauth_mailer/unlock_account.text.erb +1 -1
- data/lib/rodauth/rails.rb +32 -4
- data/lib/rodauth/rails/app.rb +20 -22
- data/lib/rodauth/rails/app/flash.rb +2 -8
- data/lib/rodauth/rails/app/middleware.rb +20 -10
- data/lib/rodauth/rails/auth.rb +40 -0
- data/lib/rodauth/rails/controller_methods.rb +1 -5
- data/lib/rodauth/rails/feature.rb +17 -210
- data/lib/rodauth/rails/feature/base.rb +62 -0
- data/lib/rodauth/rails/feature/callbacks.rb +61 -0
- data/lib/rodauth/rails/feature/csrf.rb +65 -0
- data/lib/rodauth/rails/feature/email.rb +30 -0
- data/lib/rodauth/rails/feature/instrumentation.rb +71 -0
- data/lib/rodauth/rails/feature/render.rb +41 -0
- data/lib/rodauth/rails/version.rb +1 -1
- data/rodauth-rails.gemspec +1 -1
- metadata +12 -6
- data/lib/generators/rodauth/mailer_generator.rb +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc8ee44d094627dcacd9d9b7f5da1eb165cff1af209f079b667e0f04e9540b30
|
4
|
+
data.tar.gz: f179e4eaea99d04ff6ff71c6357cdf75a19991645c9904ab6373c03b5dcd1a16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 78c28c13751abb439179813948bf665cd040444171998e42ecdb4cb42f698097731f4c073b7595d083ba5825a9989940deee052771fb5f76f93bd333e94af500
|
7
|
+
data.tar.gz: eb3a04ae6333dc471fd7fbdb264527a359893fb100d7833bab3545f7d91e213bfc8a2daa562ffc22f531073f39f4bee3de893bffd87201b3e57d1dce99c97320
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,47 @@
|
|
1
|
+
## 0.13.0 (2021-06-10)
|
2
|
+
|
3
|
+
* Add `:query`, `:form`, `:session`, `:account`, and `:env` options to `Rodauth::Rails.rodauth` (@janko)
|
4
|
+
|
5
|
+
## 0.12.0 (2021-05-15)
|
6
|
+
|
7
|
+
* Include total view render time in logs for Rodauth requests (@janko)
|
8
|
+
|
9
|
+
* Instrument redirects (@janko)
|
10
|
+
|
11
|
+
* Instrument Rodauth requests on `action_controller` namespace (@janko)
|
12
|
+
|
13
|
+
* Update templates for Boostrap 5 compatibility (@janko)
|
14
|
+
|
15
|
+
* Log request parameters for Rodauth requests (@janko)
|
16
|
+
|
17
|
+
## 0.11.0 (2021-05-06)
|
18
|
+
|
19
|
+
* Add controller-like logging for requests to Rodauth endpoints (@janko)
|
20
|
+
|
21
|
+
* Add `#rails_routes` to Roda and Rodauth instance for accessing Rails route helpers (@janko)
|
22
|
+
|
23
|
+
* Add `#rails_request` to Roda and Rodauth instance for retrieving an `ActionDispatch::Request` instance (@janko)
|
24
|
+
|
25
|
+
## 0.10.0 (2021-03-23)
|
26
|
+
|
27
|
+
* Add `Rodauth::Rails::Auth` superclass for moving configurations into separate files (@janko)
|
28
|
+
|
29
|
+
* Load the `pass` Roda plugin and recommend calling `r.pass` on prefixed routes (@janko)
|
30
|
+
|
31
|
+
* Improve Roda middleware inspect output (@janko)
|
32
|
+
|
33
|
+
* Create `RodauthMailer` and email templates in `rodauth:install`, and remove `rodauth:mailer` (@janko)
|
34
|
+
|
35
|
+
* Raise `KeyError` in `#rodauth` method when the Rodauth instance doesn't exist (@janko)
|
36
|
+
|
37
|
+
* Add `Rodauth::Rails.authenticated` routing constraint for requiring authentication (@janko)
|
38
|
+
|
39
|
+
## 0.9.1 (2021-02-10)
|
40
|
+
|
41
|
+
* Fix flash integration being loaded for API-only apps and causing an error (@dmitryzuev)
|
42
|
+
|
43
|
+
* Change account status column default to `unverified` in migration to match Rodauth's default (@basabin54)
|
44
|
+
|
1
45
|
## 0.9.0 (2021-02-07)
|
2
46
|
|
3
47
|
* Load Roda's JSON support by default, so that enabling `json`/`jwt` feature is all that's needed (@janko)
|
data/README.md
CHANGED
@@ -2,35 +2,6 @@
|
|
2
2
|
|
3
3
|
Provides Rails integration for the [Rodauth] authentication framework.
|
4
4
|
|
5
|
-
## Table of contents
|
6
|
-
|
7
|
-
* [Resources](#resources)
|
8
|
-
* [Why Rodauth?](#why-rodauth)
|
9
|
-
* [Upgrading](#upgrading)
|
10
|
-
* [Installation](#installation)
|
11
|
-
* [Usage](#usage)
|
12
|
-
- [Routes](#routes)
|
13
|
-
- [Current account](#current-account)
|
14
|
-
- [Requiring authentication](#requiring-authentication)
|
15
|
-
- [Views](#views)
|
16
|
-
- [Mailer](#mailer)
|
17
|
-
- [Migrations](#migrations)
|
18
|
-
- [Multiple configurations](#multiple-configurations)
|
19
|
-
- [Calling controller methods](#calling-controller-methods)
|
20
|
-
- [Rodauth instance](#rodauth-instance)
|
21
|
-
* [How it works](#how-it-works)
|
22
|
-
- [Middleware](#middleware)
|
23
|
-
- [App](#app)
|
24
|
-
- [Sequel](#sequel)
|
25
|
-
* [JSON API](#json-api)
|
26
|
-
* [OmniAuth](#omniauth)
|
27
|
-
* [Configuring](#configuring)
|
28
|
-
* [Custom extensions](#custom-extensions)
|
29
|
-
* [Testing](#testing)
|
30
|
-
* [Rodauth defaults](#rodauth-defaults)
|
31
|
-
- [Database functions](#database-functions)
|
32
|
-
- [Account statuses](#account-statuses)
|
33
|
-
|
34
5
|
## Resources
|
35
6
|
|
36
7
|
Useful links:
|
@@ -43,6 +14,7 @@ Articles:
|
|
43
14
|
* [Rodauth: A Refreshing Authentication Solution for Ruby](https://janko.io/rodauth-a-refreshing-authentication-solution-for-ruby/)
|
44
15
|
* [Adding Authentication in Rails with Rodauth](https://janko.io/adding-authentication-in-rails-with-rodauth/)
|
45
16
|
* [Adding Multifactor Authentication in Rails with Rodauth](https://janko.io/adding-multifactor-authentication-in-rails-with-rodauth/)
|
17
|
+
* [How to build an OIDC provider using rodauth-oauth on Rails](https://honeyryderchuck.gitlab.io/httpx/2021/03/15/oidc-provider-on-rails-using-rodauth-oauth.html)
|
46
18
|
|
47
19
|
## Why Rodauth?
|
48
20
|
|
@@ -61,29 +33,23 @@ of the advantages that stand out for me:
|
|
61
33
|
* consistent before/after hooks around everything
|
62
34
|
* dedicated object encapsulating all authentication logic
|
63
35
|
|
64
|
-
|
65
|
-
|
66
|
-
|
36
|
+
One commmon concern is the fact that, unlike most other authentication
|
37
|
+
frameworks for Rails, Rodauth uses [Sequel] for database interaction instead of
|
38
|
+
Active Record. There are good reasons for this, and to make Rodauth work
|
39
|
+
smoothly alongside Active Record, rodauth-rails configures Sequel to [reuse
|
40
|
+
Active Record's database connection][sequel-activerecord_connection].
|
67
41
|
|
68
|
-
|
69
|
-
application's `secret_key_base` when setting default `hmac_secret`, including
|
70
|
-
when it's set via credentials or `$SECRET_KEY_BASE` environment variable. This
|
71
|
-
means that your authentication will now be more secure by default, and Rodauth
|
72
|
-
features that require `hmac_secret` should now work automatically as well.
|
42
|
+
## Upgrading
|
73
43
|
|
74
|
-
|
75
|
-
|
76
|
-
was not explicitly set, the fact that your authentication will now start using
|
77
|
-
HMACs has backwards compatibility considerations. See the [Rodauth
|
78
|
-
documentation][hmac] for instructions on how to safely transition, or just set
|
79
|
-
`hmac_secret nil` in your Rodauth configuration.
|
44
|
+
For instructions on upgrading from previous rodauth-rails versions, see
|
45
|
+
[UPGRADING.md](/UPGRADING.md).
|
80
46
|
|
81
47
|
## Installation
|
82
48
|
|
83
49
|
Add the gem to your Gemfile:
|
84
50
|
|
85
51
|
```rb
|
86
|
-
gem "rodauth-rails", "~> 0.
|
52
|
+
gem "rodauth-rails", "~> 0.13"
|
87
53
|
|
88
54
|
# gem "jwt", require: false # for JWT feature
|
89
55
|
# gem "rotp", require: false # for OTP feature
|
@@ -108,114 +74,22 @@ $ rails generate rodauth:install --jwt # token authentication via the "Authoriza
|
|
108
74
|
$ bundle add jwt
|
109
75
|
```
|
110
76
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
* Rodauth initializer at `config/initializers/rodauth.rb`
|
115
|
-
* Sequel initializer at `config/initializers/sequel.rb` for ActiveRecord integration
|
116
|
-
* Rodauth app at `app/lib/rodauth_app.rb`
|
117
|
-
* Rodauth controller at `app/controllers/rodauth_controller.rb`
|
118
|
-
* Account model at `app/models/account.rb`
|
119
|
-
|
120
|
-
### Migration
|
121
|
-
|
122
|
-
The migration file creates tables required by Rodauth. You're encouraged to
|
123
|
-
review the migration, and modify it to only create tables for features you
|
124
|
-
intend to use.
|
125
|
-
|
126
|
-
```rb
|
127
|
-
# db/migrate/*_create_rodauth.rb
|
128
|
-
class CreateRodauth < ActiveRecord::Migration
|
129
|
-
def change
|
130
|
-
create_table :accounts do |t| ... end
|
131
|
-
create_table :account_password_hashes do |t| ... end
|
132
|
-
create_table :account_password_reset_keys do |t| ... end
|
133
|
-
create_table :account_verification_keys do |t| ... end
|
134
|
-
create_table :account_login_change_keys do |t| ... end
|
135
|
-
create_table :account_remember_keys do |t| ... end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
```
|
77
|
+
This generator will create a Rodauth app with common authentication features
|
78
|
+
enabled, a database migration with tables required by those features, a mailer
|
79
|
+
with default templates, and a few other files.
|
139
80
|
|
140
|
-
|
81
|
+
Feel free to remove any features you don't need, along with their corresponding
|
82
|
+
tables. Afterwards, run the migration:
|
141
83
|
|
142
|
-
```
|
84
|
+
```sh
|
143
85
|
$ rails db:migrate
|
144
86
|
```
|
145
87
|
|
146
|
-
### Rodauth initializer
|
147
|
-
|
148
|
-
The Rodauth initializer assigns the constant for your Rodauth app, which will
|
149
|
-
be called by the Rack middleware that's added in front of your Rails router.
|
150
|
-
|
151
|
-
```rb
|
152
|
-
# config/initializers/rodauth.rb
|
153
|
-
Rodauth::Rails.configure do |config|
|
154
|
-
config.app = "RodauthApp"
|
155
|
-
end
|
156
|
-
```
|
157
|
-
|
158
|
-
### Sequel initializer
|
159
|
-
|
160
|
-
Rodauth uses [Sequel] for database interaction. If you're using ActiveRecord,
|
161
|
-
an additional initializer will be created which configures Sequel to use the
|
162
|
-
ActiveRecord connection.
|
163
|
-
|
164
|
-
```rb
|
165
|
-
# config/initializers/sequel.rb
|
166
|
-
require "sequel/core"
|
167
|
-
|
168
|
-
# initialize Sequel and have it reuse Active Record's database connection
|
169
|
-
DB = Sequel.connect("postgresql://", extensions: :activerecord_connection)
|
170
|
-
```
|
171
|
-
|
172
|
-
### Rodauth app
|
173
|
-
|
174
|
-
Your Rodauth app is created in the `app/lib/` directory, and comes with a
|
175
|
-
default set of authentication features enabled, as well as extensive examples
|
176
|
-
on ways you can configure authentication behaviour.
|
177
|
-
|
178
|
-
```rb
|
179
|
-
# app/lib/rodauth_app.rb
|
180
|
-
class RodauthApp < Rodauth::Rails::App
|
181
|
-
configure do
|
182
|
-
# authentication configuration
|
183
|
-
end
|
184
|
-
|
185
|
-
route do |r|
|
186
|
-
# request handling
|
187
|
-
end
|
188
|
-
end
|
189
|
-
```
|
190
|
-
|
191
|
-
### Controller
|
192
|
-
|
193
|
-
Your Rodauth app will by default use `RodauthController` for view rendering,
|
194
|
-
CSRF protection, and running controller callbacks and rescue handlers around
|
195
|
-
Rodauth actions.
|
196
|
-
|
197
|
-
```rb
|
198
|
-
# app/controllers/rodauth_controller.rb
|
199
|
-
class RodauthController < ApplicationController
|
200
|
-
end
|
201
|
-
```
|
202
|
-
|
203
|
-
### Account model
|
204
|
-
|
205
|
-
Rodauth stores user accounts in the `accounts` table, so the generator will
|
206
|
-
also create an `Account` model for custom use.
|
207
|
-
|
208
|
-
```rb
|
209
|
-
# app/models/account.rb
|
210
|
-
class Account < ApplicationRecord
|
211
|
-
end
|
212
|
-
```
|
213
|
-
|
214
88
|
## Usage
|
215
89
|
|
216
90
|
### Routes
|
217
91
|
|
218
|
-
|
92
|
+
You can see the list of routes our Rodauth middleware handles:
|
219
93
|
|
220
94
|
```sh
|
221
95
|
$ rails rodauth:routes
|
@@ -237,7 +111,7 @@ Routes handled by RodauthApp:
|
|
237
111
|
/close-account rodauth.close_account_path
|
238
112
|
```
|
239
113
|
|
240
|
-
Using this information,
|
114
|
+
Using this information, you can add some basic authentication links to your
|
241
115
|
navigation header:
|
242
116
|
|
243
117
|
```erb
|
@@ -253,9 +127,22 @@ These routes are fully functional, feel free to visit them and interact with the
|
|
253
127
|
pages. The templates that ship with Rodauth aim to provide a complete
|
254
128
|
authentication experience, and the forms use [Bootstrap] markup.
|
255
129
|
|
130
|
+
Inside Rodauth configuration and the `route` block you can access Rails route
|
131
|
+
helpers through `#rails_routes`:
|
132
|
+
|
133
|
+
```rb
|
134
|
+
class RodauthApp < Rodauth::Rails::App
|
135
|
+
configure do
|
136
|
+
# ...
|
137
|
+
login_redirect { rails_routes.activity_path }
|
138
|
+
# ...
|
139
|
+
end
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
256
143
|
### Current account
|
257
144
|
|
258
|
-
To be able to fetch currently authenticated account,
|
145
|
+
To be able to fetch currently authenticated account, you can define a
|
259
146
|
`#current_account` method that fetches the account id from session and
|
260
147
|
retrieves the corresponding account record:
|
261
148
|
|
@@ -272,11 +159,11 @@ class ApplicationController < ActionController::Base
|
|
272
159
|
rodauth.logout
|
273
160
|
rodauth.login_required
|
274
161
|
end
|
275
|
-
helper_method :current_account
|
162
|
+
helper_method :current_account # skip if inheriting from ActionController::API
|
276
163
|
end
|
277
164
|
```
|
278
165
|
|
279
|
-
This allows
|
166
|
+
This allows you to access the current account in controllers and views:
|
280
167
|
|
281
168
|
```erb
|
282
169
|
<p>Authenticated as: <%= current_account.email %></p>
|
@@ -284,9 +171,9 @@ This allows us to access the current account in controllers and views:
|
|
284
171
|
|
285
172
|
### Requiring authentication
|
286
173
|
|
287
|
-
|
288
|
-
redirecting the user to the login page if they're not logged in.
|
289
|
-
in
|
174
|
+
You'll likely want to require authentication for certain parts of your app,
|
175
|
+
redirecting the user to the login page if they're not logged in. You can do this
|
176
|
+
in your Rodauth app's routing block, which helps keep the authentication logic
|
290
177
|
encapsulated:
|
291
178
|
|
292
179
|
```rb
|
@@ -305,7 +192,7 @@ class RodauthApp < Rodauth::Rails::App
|
|
305
192
|
end
|
306
193
|
```
|
307
194
|
|
308
|
-
|
195
|
+
You can also require authentication at the controller layer:
|
309
196
|
|
310
197
|
```rb
|
311
198
|
# app/controllers/application_controller.rb
|
@@ -330,15 +217,52 @@ class PostsController < ApplicationController
|
|
330
217
|
end
|
331
218
|
```
|
332
219
|
|
333
|
-
|
220
|
+
#### Routing constraints
|
221
|
+
|
222
|
+
In some cases it makes sense to require authentication at the Rails router
|
223
|
+
level. You can do this via the built-in `authenticated` routing constraint:
|
334
224
|
|
335
225
|
```rb
|
336
226
|
# config/routes.rb
|
337
227
|
Rails.application.routes.draw do
|
338
|
-
constraints
|
339
|
-
|
340
|
-
|
341
|
-
|
228
|
+
constraints Rodauth::Rails.authenticated do
|
229
|
+
# ... authenticated routes ...
|
230
|
+
end
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
If you want additional conditions, you can pass in a block, which is
|
235
|
+
called with the Rodauth instance:
|
236
|
+
|
237
|
+
```rb
|
238
|
+
# config/routes.rb
|
239
|
+
Rails.application.routes.draw do
|
240
|
+
# require multifactor authentication to be setup
|
241
|
+
constraints Rodauth::Rails.authenticated { |rodauth| rodauth.uses_two_factor_authentication? } do
|
242
|
+
# ...
|
243
|
+
end
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
You can specify the Rodauth configuration by passing the configuration name:
|
248
|
+
|
249
|
+
```rb
|
250
|
+
# config/routes.rb
|
251
|
+
Rails.application.routes.draw do
|
252
|
+
constraints Rodauth::Rails.authenticated(:admin) do
|
253
|
+
# ...
|
254
|
+
end
|
255
|
+
end
|
256
|
+
```
|
257
|
+
|
258
|
+
If you need something more custom, you can always create the routing constraint
|
259
|
+
manually:
|
260
|
+
|
261
|
+
```rb
|
262
|
+
# config/routes.rb
|
263
|
+
Rails.application.routes.draw do
|
264
|
+
constraints -> (r) { !r.env["rodauth"].logged_in? } do # or "rodauth.admin"
|
265
|
+
# routes when the user is not logged in
|
342
266
|
end
|
343
267
|
end
|
344
268
|
```
|
@@ -358,7 +282,7 @@ This will generate views for the default set of Rodauth features into the
|
|
358
282
|
`RodauthController`.
|
359
283
|
|
360
284
|
You can pass a list of Rodauth features to the generator to create views for
|
361
|
-
these features (this will not remove any existing views):
|
285
|
+
these features (this will not remove or overwrite any existing views):
|
362
286
|
|
363
287
|
```sh
|
364
288
|
$ rails generate rodauth:views login create_account lockout otp
|
@@ -406,58 +330,36 @@ end
|
|
406
330
|
|
407
331
|
### Mailer
|
408
332
|
|
409
|
-
|
410
|
-
|
333
|
+
The install generator will create `RodauthMailer` with default email templates,
|
334
|
+
and configure Rodauth features that send emails as part of the authentication
|
335
|
+
flow to use it.
|
411
336
|
|
412
337
|
```rb
|
413
|
-
# app/
|
414
|
-
class
|
415
|
-
|
416
|
-
|
338
|
+
# app/mailers/rodauth_mailer.rb
|
339
|
+
class RodauthMailer < ApplicationMailer
|
340
|
+
def verify_account(recipient, email_link)
|
341
|
+
# ...
|
342
|
+
end
|
343
|
+
def reset_password(recipient, email_link)
|
417
344
|
# ...
|
418
|
-
|
419
|
-
|
420
|
-
email_subject_prefix "[MyApp] "
|
421
|
-
send_email(&:deliver_later)
|
345
|
+
end
|
346
|
+
def verify_login_change(recipient, old_login, new_login, email_link)
|
422
347
|
# ...
|
423
|
-
|
424
|
-
|
425
|
-
verify_account_email_body { "Verify your account by visting this link: #{verify_account_email_link}" }
|
348
|
+
end
|
349
|
+
def password_changed(recipient)
|
426
350
|
# ...
|
427
351
|
end
|
352
|
+
# def email_auth(recipient, email_link)
|
353
|
+
# ...
|
354
|
+
# end
|
355
|
+
# def unlock_account(recipient, email_link)
|
356
|
+
# ...
|
357
|
+
# end
|
428
358
|
end
|
429
359
|
```
|
430
|
-
|
431
|
-
This is convenient when starting out, but eventually you might want to use your
|
432
|
-
own mailer. You can start by running the following command:
|
433
|
-
|
434
|
-
```sh
|
435
|
-
$ rails generate rodauth:mailer
|
436
|
-
```
|
437
|
-
|
438
|
-
This will create a `RodauthMailer` with the associated mailer views in
|
439
|
-
`app/views/rodauth_mailer` directory:
|
440
|
-
|
441
|
-
```rb
|
442
|
-
# app/mailers/rodauth_mailer.rb
|
443
|
-
class RodauthMailer < ApplicationMailer
|
444
|
-
def verify_account(recipient, email_link) ... end
|
445
|
-
def reset_password(recipient, email_link) ... end
|
446
|
-
def verify_login_change(recipient, old_login, new_login, email_link) ... end
|
447
|
-
def password_changed(recipient) ... end
|
448
|
-
# def email_auth(recipient, email_link) ... end
|
449
|
-
# def unlock_account(recipient, email_link) ... end
|
450
|
-
end
|
451
|
-
```
|
452
|
-
|
453
|
-
You can then uncomment the lines in your Rodauth configuration to have it call
|
454
|
-
your mailer. If you've enabled additional authentication features that send
|
455
|
-
emails, make sure to override their `create_*_email` methods as well.
|
456
|
-
|
457
360
|
```rb
|
458
361
|
# app/lib/rodauth_app.rb
|
459
362
|
class RodauthApp < Rodauth::Rails::App
|
460
|
-
# ...
|
461
363
|
configure do
|
462
364
|
# ...
|
463
365
|
create_reset_password_email do
|
@@ -487,10 +389,17 @@ class RodauthApp < Rodauth::Rails::App
|
|
487
389
|
end
|
488
390
|
```
|
489
391
|
|
490
|
-
This
|
491
|
-
|
492
|
-
|
493
|
-
|
392
|
+
This configuration calls `#deliver_later`, which uses Active Job to deliver
|
393
|
+
emails in a background job. It's generally recommended to send emails
|
394
|
+
asynchronously for better request throughput and the ability to retry
|
395
|
+
deliveries. However, if you want to send emails synchronously, modify the
|
396
|
+
configuration to call `#deliver_now` instead.
|
397
|
+
|
398
|
+
If you're using a background processing library without an Active Job adapter,
|
399
|
+
or a 3rd-party service for sending transactional emails, this two-phase API
|
400
|
+
might not be suitable. In this case, instead of overriding `#create_*_email`
|
401
|
+
and `#send_email`, override the `#send_*_email` methods instead, which are
|
402
|
+
required to send the email immediately.
|
494
403
|
|
495
404
|
### Migrations
|
496
405
|
|
@@ -515,7 +424,7 @@ end
|
|
515
424
|
### Multiple configurations
|
516
425
|
|
517
426
|
If you need to handle multiple types of accounts that require different
|
518
|
-
authentication logic, you can create
|
427
|
+
authentication logic, you can create additional configurations for them:
|
519
428
|
|
520
429
|
```rb
|
521
430
|
# app/lib/rodauth_app.rb
|
@@ -531,12 +440,21 @@ class RodauthApp < Rodauth::Rails::App
|
|
531
440
|
prefix "/admin"
|
532
441
|
session_key_prefix "admin_"
|
533
442
|
remember_cookie_key "_admin_remember" # if using remember feature
|
443
|
+
|
444
|
+
# if you want separate tables
|
445
|
+
accounts_table :admin_accounts
|
446
|
+
password_hash_table :admin_account_password_hashes
|
534
447
|
# ...
|
535
448
|
end
|
536
449
|
|
537
450
|
route do |r|
|
538
451
|
r.rodauth
|
539
|
-
|
452
|
+
|
453
|
+
r.on "admin" do
|
454
|
+
r.rodauth(:admin)
|
455
|
+
break # allow routing of other /admin/* requests to continue to Rails
|
456
|
+
end
|
457
|
+
|
540
458
|
# ...
|
541
459
|
end
|
542
460
|
end
|
@@ -548,6 +466,98 @@ Then in your application you can reference the secondary Rodauth instance:
|
|
548
466
|
rodauth(:admin).login_path #=> "/admin/login"
|
549
467
|
```
|
550
468
|
|
469
|
+
#### Named auth classes
|
470
|
+
|
471
|
+
A `configure` block inside `Rodauth::Rails::App` will internally create an
|
472
|
+
anonymous `Rodauth::Auth` subclass, and register it under the given name.
|
473
|
+
However, you can also define the auth classes explicitly, by creating
|
474
|
+
subclasses of `Rodauth::Rails::Auth`:
|
475
|
+
|
476
|
+
```rb
|
477
|
+
# app/lib/rodauth_main.rb
|
478
|
+
class RodauthMain < Rodauth::Rails::Auth
|
479
|
+
configure do
|
480
|
+
# ... main configuration ...
|
481
|
+
end
|
482
|
+
end
|
483
|
+
```
|
484
|
+
```rb
|
485
|
+
# app/lib/rodauth_admin.rb
|
486
|
+
class RodauthAdmin < Rodauth::Rails::Auth
|
487
|
+
configure do
|
488
|
+
# ...
|
489
|
+
prefix "/admin"
|
490
|
+
session_key_prefix "admin_"
|
491
|
+
# ...
|
492
|
+
end
|
493
|
+
end
|
494
|
+
```
|
495
|
+
```rb
|
496
|
+
# app/lib/rodauth_app.rb
|
497
|
+
class RodauthApp < Rodauth::Rails::App
|
498
|
+
configure RodauthMain
|
499
|
+
configure RodauthAdmin, :admin
|
500
|
+
# ...
|
501
|
+
end
|
502
|
+
```
|
503
|
+
|
504
|
+
This allows having each configuration in a dedicated file, and named constants
|
505
|
+
improve introspection and error messages. You can also use inheritance to share
|
506
|
+
common settings:
|
507
|
+
|
508
|
+
```rb
|
509
|
+
# app/lib/rodauth_base.rb
|
510
|
+
class RodauthBase < Rodauth::Rails::Auth
|
511
|
+
# common settings that can be shared between multiple configurations
|
512
|
+
configure do
|
513
|
+
enable :login, :logout
|
514
|
+
login_return_to_requested_location? true
|
515
|
+
logout_redirect "/"
|
516
|
+
# ...
|
517
|
+
end
|
518
|
+
end
|
519
|
+
```
|
520
|
+
```rb
|
521
|
+
# app/lib/rodauth_main.rb
|
522
|
+
class RodauthMain < RodauthBase # inherit common settings
|
523
|
+
configure do
|
524
|
+
# ... customize main ...
|
525
|
+
end
|
526
|
+
end
|
527
|
+
```
|
528
|
+
```rb
|
529
|
+
# app/lib/rodauth_admin.rb
|
530
|
+
class RodauthAdmin < RodauthBase # inherit common settings
|
531
|
+
configure do
|
532
|
+
# ... customize admin ...
|
533
|
+
end
|
534
|
+
end
|
535
|
+
```
|
536
|
+
|
537
|
+
Another benefit of explicit classes is that you can define custom methods
|
538
|
+
directly at the class level instead of inside an `auth_class_eval`:
|
539
|
+
|
540
|
+
```rb
|
541
|
+
# app/lib/rodauth_admin.rb
|
542
|
+
class RodauthAdmin < Rodauth::Rails::Auth
|
543
|
+
configure do
|
544
|
+
# ...
|
545
|
+
end
|
546
|
+
|
547
|
+
def superadmin?
|
548
|
+
Role.where(account_id: session_id, type: "superadmin").any?
|
549
|
+
end
|
550
|
+
end
|
551
|
+
```
|
552
|
+
```rb
|
553
|
+
# config/routes.rb
|
554
|
+
Rails.application.routes.draw do
|
555
|
+
constraints Rodauth::Rails.authenticated(:admin) { |rodauth| rodauth.superadmin? } do
|
556
|
+
mount Sidekiq::Web => "sidekiq"
|
557
|
+
end
|
558
|
+
end
|
559
|
+
```
|
560
|
+
|
551
561
|
### Calling controller methods
|
552
562
|
|
553
563
|
When using Rodauth before/after hooks or generally overriding your Rodauth
|
@@ -577,8 +587,8 @@ end
|
|
577
587
|
### Rodauth instance
|
578
588
|
|
579
589
|
In some cases you might need to use Rodauth more programmatically, and perform
|
580
|
-
Rodauth operations outside of the request context. rodauth-rails gives you
|
581
|
-
|
590
|
+
Rodauth operations outside of the request context. rodauth-rails gives you a
|
591
|
+
helper method for building a Rodauth instance:
|
582
592
|
|
583
593
|
```rb
|
584
594
|
rodauth = Rodauth::Rails.rodauth # or Rodauth::Rails.rodauth(:admin)
|
@@ -590,8 +600,22 @@ rodauth.setup_account_verification
|
|
590
600
|
rodauth.close_account
|
591
601
|
```
|
592
602
|
|
593
|
-
|
594
|
-
|
603
|
+
The base URL is taken from Action Mailer's `default_url_options` setting if
|
604
|
+
configured. The `Rodauth::Rails.rodauth` method accepts additional keyword
|
605
|
+
arguments:
|
606
|
+
|
607
|
+
* `:account` – Active Record model instance from which to set `account` and `session[:account_id]`
|
608
|
+
* `:query` & `:form` – set specific query/form parameters
|
609
|
+
* `:session` – set any session values
|
610
|
+
* `:env` – set any additional Rack env values
|
611
|
+
|
612
|
+
```rb
|
613
|
+
Rodauth::Rails.rodauth(account: Account.find(account_id))
|
614
|
+
Rodauth::Rails.rodauth(query: { "param" => "value" })
|
615
|
+
Rodauth::Rails.rodauth(form: { "param" => "value" })
|
616
|
+
Rodauth::Rails.rodauth(session: { two_factor_auth_setup: true })
|
617
|
+
Rodauth::Rails.rodauth(env: { "HTTP_USER_AGENT" => "programmatic" })
|
618
|
+
```
|
595
619
|
|
596
620
|
## How it works
|
597
621
|
|
@@ -702,7 +726,7 @@ class RodauthApp < Rodauth::Rails::App
|
|
702
726
|
configure do
|
703
727
|
# ...
|
704
728
|
enable :json
|
705
|
-
only_json? true # accept only JSON requests
|
729
|
+
only_json? true # accept only JSON requests (optional)
|
706
730
|
# ...
|
707
731
|
end
|
708
732
|
end
|
@@ -723,7 +747,25 @@ class RodauthApp < Rodauth::Rails::App
|
|
723
747
|
# ...
|
724
748
|
enable :jwt
|
725
749
|
jwt_secret "<YOUR_SECRET_KEY>" # store the JWT secret in a safe place
|
726
|
-
only_json? true # accept only JSON requests
|
750
|
+
only_json? true # accept only JSON requests (optional)
|
751
|
+
# ...
|
752
|
+
end
|
753
|
+
end
|
754
|
+
```
|
755
|
+
|
756
|
+
If you need Cross-Origin Resource Sharing and/or JWT refresh tokens, enable the
|
757
|
+
corresponding Rodauth features and create the necessary tables:
|
758
|
+
|
759
|
+
```sh
|
760
|
+
$ rails generate rodauth:migration jwt_refresh
|
761
|
+
$ rails db:migrate
|
762
|
+
```
|
763
|
+
```rb
|
764
|
+
# app/lib/rodauth_app.rb
|
765
|
+
class RodauthApp < Rodauth::Rails::App
|
766
|
+
configure do
|
767
|
+
# ...
|
768
|
+
enable :jwt, :jwt_cors, :jwt_refresh
|
727
769
|
# ...
|
728
770
|
end
|
729
771
|
end
|
@@ -785,7 +827,8 @@ end
|
|
785
827
|
<%= link_to "Login via Facebook", "/auth/facebook" %>
|
786
828
|
```
|
787
829
|
|
788
|
-
|
830
|
+
Finally, let's implement the OmniAuth callback endpoint on our Rodauth
|
831
|
+
controller:
|
789
832
|
|
790
833
|
```rb
|
791
834
|
# config/routes.rb
|
@@ -821,7 +864,7 @@ class RodauthController < ApplicationController
|
|
821
864
|
|
822
865
|
# create new account if it doesn't exist
|
823
866
|
unless account
|
824
|
-
account = Account.create!(email: auth["info"]["email"])
|
867
|
+
account = Account.create!(email: auth["info"]["email"], status: rodauth.account_open_status_value)
|
825
868
|
end
|
826
869
|
|
827
870
|
# create new identity if it doesn't exist
|
@@ -838,11 +881,8 @@ end
|
|
838
881
|
|
839
882
|
## Configuring
|
840
883
|
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
The `rails` feature rodauth-rails loads is customizable as well, here is the
|
845
|
-
list of its configuration methods:
|
884
|
+
The `rails` feature rodauth-rails loads provides the following configuration
|
885
|
+
methods:
|
846
886
|
|
847
887
|
| Name | Description |
|
848
888
|
| :---- | :---------- |
|
@@ -869,21 +909,27 @@ Rodauth::Rails.configure do |config|
|
|
869
909
|
end
|
870
910
|
```
|
871
911
|
|
912
|
+
For the list of configuration methods provided by Rodauth, see the [feature
|
913
|
+
documentation].
|
914
|
+
|
872
915
|
## Custom extensions
|
873
916
|
|
874
917
|
When developing custom extensions for Rodauth inside your Rails project, it's
|
875
|
-
better to use plain modules
|
876
|
-
feature
|
918
|
+
probably better to use plain modules, at least in the beginning, as Rodauth
|
919
|
+
feature design doesn't yet work well with Zeitwerk reloading.
|
920
|
+
|
921
|
+
Here is an example of an LDAP authentication extension that uses the
|
922
|
+
[simple_ldap_authenticator] gem.
|
877
923
|
|
878
924
|
```rb
|
879
|
-
# app/lib/
|
880
|
-
module
|
881
|
-
def
|
882
|
-
|
925
|
+
# app/lib/rodauth_ldap.rb
|
926
|
+
module RodauthLdap
|
927
|
+
def require_bcrypt?
|
928
|
+
false
|
883
929
|
end
|
884
930
|
|
885
|
-
def
|
886
|
-
|
931
|
+
def password_match?(password)
|
932
|
+
SimpleLdapAuthenticator.valid?(account[:email], password)
|
887
933
|
end
|
888
934
|
end
|
889
935
|
```
|
@@ -893,7 +939,7 @@ class RodauthApp < Rodauth::Rails::App
|
|
893
939
|
configure do
|
894
940
|
# ...
|
895
941
|
auth_class_eval do
|
896
|
-
include
|
942
|
+
include RodauthLdap
|
897
943
|
end
|
898
944
|
# ...
|
899
945
|
end
|
@@ -902,48 +948,156 @@ end
|
|
902
948
|
|
903
949
|
## Testing
|
904
950
|
|
905
|
-
|
906
|
-
authentication flow with tools like Capybara, and to not use any stubbing.
|
907
|
-
|
908
|
-
In functional and integration tests you can just make requests to Rodauth
|
909
|
-
routes:
|
951
|
+
System (browser) tests for Rodauth actions could look something like this:
|
910
952
|
|
911
953
|
```rb
|
912
|
-
# test/
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
954
|
+
# test/system/authentication_test.rb
|
955
|
+
require "test_helper"
|
956
|
+
|
957
|
+
class AuthenticationTest < ActionDispatch::SystemTestCase
|
958
|
+
include ActiveJob::TestHelper
|
959
|
+
driven_by :rack_test
|
960
|
+
|
961
|
+
test "creating and verifying an account" do
|
962
|
+
create_account
|
963
|
+
assert_match "An email has been sent to you with a link to verify your account", page.text
|
964
|
+
|
965
|
+
verify_account
|
966
|
+
assert_match "Your account has been verified", page.text
|
967
|
+
end
|
968
|
+
|
969
|
+
test "logging in and logging out" do
|
970
|
+
create_account(verify: true)
|
971
|
+
|
972
|
+
logout
|
973
|
+
assert_match "You have been logged out", page.text
|
917
974
|
|
918
975
|
login
|
919
|
-
|
976
|
+
assert_match "You have been logged in", page.text
|
977
|
+
end
|
978
|
+
|
979
|
+
private
|
980
|
+
|
981
|
+
def create_account(email: "user@example.com", password: "secret", verify: false)
|
982
|
+
visit "/create-account"
|
983
|
+
fill_in "Login", with: email
|
984
|
+
fill_in "Password", with: password
|
985
|
+
fill_in "Confirm Password", with: password
|
986
|
+
click_on "Create Account"
|
987
|
+
verify_account if verify
|
988
|
+
end
|
989
|
+
|
990
|
+
def verify_account
|
991
|
+
perform_enqueued_jobs # run enqueued email deliveries
|
992
|
+
email = ActionMailer::Base.deliveries.last
|
993
|
+
verify_account_link = email.body.to_s[/\S+verify-account\S+/]
|
994
|
+
visit verify_account_link
|
995
|
+
click_on "Verify Account"
|
996
|
+
end
|
997
|
+
|
998
|
+
def login(email: "user@example.com", password: "secret")
|
999
|
+
visit "/login"
|
1000
|
+
fill_in "Login", with: email
|
1001
|
+
fill_in "Password", with: password
|
1002
|
+
click_on "Login"
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
def logout
|
1006
|
+
visit "/logout"
|
1007
|
+
click_on "Logout"
|
1008
|
+
end
|
1009
|
+
end
|
1010
|
+
```
|
1011
|
+
|
1012
|
+
While request tests in JSON API mode with JWT tokens could look something like
|
1013
|
+
this:
|
1014
|
+
|
1015
|
+
```rb
|
1016
|
+
# test/integration/authentication_test.rb
|
1017
|
+
require "test_helper"
|
1018
|
+
|
1019
|
+
class AuthenticationTest < ActionDispatch::IntegrationTest
|
1020
|
+
test "creating and verifying an account" do
|
1021
|
+
create_account
|
920
1022
|
assert_response :success
|
1023
|
+
assert_match "An email has been sent to you with a link to verify your account", JSON.parse(body)["success"]
|
1024
|
+
|
1025
|
+
verify_account
|
1026
|
+
assert_response :success
|
1027
|
+
assert_match "Your account has been verified", JSON.parse(body)["success"]
|
1028
|
+
end
|
1029
|
+
|
1030
|
+
test "logging in and logging out" do
|
1031
|
+
create_account(verify: true)
|
921
1032
|
|
922
1033
|
logout
|
923
|
-
|
1034
|
+
assert_response :success
|
1035
|
+
assert_match "You have been logged out", JSON.parse(body)["success"]
|
1036
|
+
|
1037
|
+
login
|
1038
|
+
assert_response :success
|
1039
|
+
assert_match "You have been logged in", JSON.parse(body)["success"]
|
924
1040
|
end
|
925
1041
|
|
926
1042
|
private
|
927
1043
|
|
928
|
-
def
|
929
|
-
post "/create-account", params: {
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
1044
|
+
def create_account(email: "user@example.com", password: "secret", verify: false)
|
1045
|
+
post "/create-account", as: :json, params: { login: email, password: password, "password-confirm": password }
|
1046
|
+
verify_account if verify
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
def verify_account
|
1050
|
+
perform_enqueued_jobs # run enqueued email deliveries
|
1051
|
+
email = ActionMailer::Base.deliveries.last
|
1052
|
+
verify_account_key = email.body.to_s[/verify-account\?key=(\S+)/, 1]
|
1053
|
+
post "/verify-account", as: :json, params: { key: verify_account_key }
|
1054
|
+
end
|
1055
|
+
|
1056
|
+
def login(email: "user@example.com", password: "secret")
|
1057
|
+
post "/login", as: :json, params: { login: email, password: password }
|
939
1058
|
end
|
940
1059
|
|
941
1060
|
def logout
|
942
|
-
post "/logout"
|
1061
|
+
post "/logout", as: :json, headers: { "Authorization" => headers["Authorization"] }
|
943
1062
|
end
|
944
1063
|
end
|
945
1064
|
```
|
946
1065
|
|
1066
|
+
If you're delivering emails in the background, make sure to set Active Job
|
1067
|
+
queue adapter to `:test` or `:inline`:
|
1068
|
+
|
1069
|
+
```rb
|
1070
|
+
# config/environments/test.rb
|
1071
|
+
Rails.application.configure do |config|
|
1072
|
+
# ...
|
1073
|
+
config.active_job.queue_adapter = :test # or :inline
|
1074
|
+
# ...
|
1075
|
+
end
|
1076
|
+
```
|
1077
|
+
|
1078
|
+
If you need to create an account record with a password directly, you can do it
|
1079
|
+
as follows:
|
1080
|
+
|
1081
|
+
```rb
|
1082
|
+
# app/models/account.rb
|
1083
|
+
class Account < ApplicationRecord
|
1084
|
+
has_one :password_hash, foreign_key: :id
|
1085
|
+
end
|
1086
|
+
```
|
1087
|
+
```rb
|
1088
|
+
# app/models/account/password_hash.rb
|
1089
|
+
class Account::PasswordHash < ApplicationRecord
|
1090
|
+
belongs_to :account, foreign_key: :id
|
1091
|
+
end
|
1092
|
+
```
|
1093
|
+
```rb
|
1094
|
+
require "bcrypt"
|
1095
|
+
|
1096
|
+
account = Account.create!(email: "user@example.com", status: "verified")
|
1097
|
+
password_hash = BCrypt::Password.create("secret", cost: BCrypt::Engine::MIN_COST)
|
1098
|
+
account.create_password_hash!(id: account.id, password_hash: password_hash)
|
1099
|
+
```
|
1100
|
+
|
947
1101
|
## Rodauth defaults
|
948
1102
|
|
949
1103
|
rodauth-rails changes some of the default Rodauth settings for easier setup:
|
@@ -1024,6 +1178,18 @@ configure do
|
|
1024
1178
|
end
|
1025
1179
|
```
|
1026
1180
|
|
1181
|
+
### Deadline values
|
1182
|
+
|
1183
|
+
To simplify changes to the database schema, rodauth-rails configures Rodauth
|
1184
|
+
to set deadline values for various features in Ruby, instead of relying on
|
1185
|
+
the database to set default column values.
|
1186
|
+
|
1187
|
+
You can easily change this back:
|
1188
|
+
|
1189
|
+
```rb
|
1190
|
+
set_deadline_values? false
|
1191
|
+
```
|
1192
|
+
|
1027
1193
|
## License
|
1028
1194
|
|
1029
1195
|
The gem is available as open source under the terms of the [MIT
|
@@ -1064,3 +1230,4 @@ conduct](https://github.com/janko/rodauth-rails/blob/master/CODE_OF_CONDUCT.md).
|
|
1064
1230
|
[session_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/session_expiration_rdoc.html
|
1065
1231
|
[single_session]: http://rodauth.jeremyevans.net/rdoc/files/doc/single_session_rdoc.html
|
1066
1232
|
[account_expiration]: http://rodauth.jeremyevans.net/rdoc/files/doc/account_expiration_rdoc.html
|
1233
|
+
[simple_ldap_authenticator]: https://github.com/jeremyevans/simple_ldap_authenticator
|