tachiban 1.0.0 → 2.1.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/.gitignore +2 -1
- data/README.md +330 -76
- data/Rakefile +0 -2
- data/lib/tachiban/version.rb +1 -1
- data/lib/tachiban.rb +249 -99
- data/tachiban.gemspec +5 -10
- metadata +14 -104
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7608a00958bd0996ebd4d9f0aee635a3d99a2f997bce0be1fcf25672aeba9d59
|
|
4
|
+
data.tar.gz: 9ba708be4e0d7b9686ef2b00483d695151ccdffa4e39f6ceaabcc6c341bcf9b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05f96fc3be6acf310936d127f258c59a4adf152828c9269a26b6429e1e5a2db7f2fa91de9981ad9e1959bcfe68eab7328e769330a88a7204a9ab62cc1656b396
|
|
7
|
+
data.tar.gz: dc769a77a390224cf2f76abc28a3a9e1fa132908a049165e3f164fdb9320433e1bddcd6340780d7f42568fb6bdc9890e398e53ba4361fa971de29b1eef2b0dab
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Tachiban
|
|
2
2
|
|
|
3
|
-
[](https://gitter.im/sebastjan-hribar/tachiban?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://badge.fury.io/rb/tachiban)
|
|
3
|
+
[](https://gitter.im/sebastjan-hribar/tachiban?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [](https://badge.fury.io/rb/tachiban)
|
|
4
4
|
|
|
5
|
-
Tachiban (立ち番 - standing watch) provides simple authentication system for [Hanami web applications](http://hanamirb.org/) by using Argon2 for password hashing and
|
|
5
|
+
Tachiban (立ち番 - standing watch) provides simple authentication system for [Hanami 2.x web applications](http://hanamirb.org/) by using Argon2 for password hashing and
|
|
6
6
|
offers the following functionalities (with methods listed below
|
|
7
7
|
under Methods by features):
|
|
8
8
|
- Signup
|
|
@@ -12,7 +12,10 @@ offers the following functionalities (with methods listed below
|
|
|
12
12
|
- Password reset
|
|
13
13
|
- Authorization has been moved to [Rokku](https://github.com/sebastjan-hribar/rokku)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**Note:** For Hanami 1.3 support, see the [1.0.0 branch](https://github.com/sebastjan-hribar/tachiban/tree/1.0.0) or install Tachiban 1.0.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## 1. Installation
|
|
16
19
|
|
|
17
20
|
Add this line to your application's Gemfile:
|
|
18
21
|
|
|
@@ -28,21 +31,43 @@ Or install it yourself as:
|
|
|
28
31
|
|
|
29
32
|
$ gem install tachiban
|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
|
|
35
|
+
Tachiban needs to be included in the action:
|
|
32
36
|
|
|
33
37
|
```ruby
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
# app/action.rb
|
|
39
|
+
# auto_register: false
|
|
40
|
+
# frozen_string_literal: true
|
|
41
|
+
|
|
42
|
+
require "hanami/action"
|
|
43
|
+
require "dry/monads"
|
|
44
|
+
require "tachiban"
|
|
45
|
+
|
|
46
|
+
module MyApplication
|
|
47
|
+
class Action < Hanami::Action
|
|
48
|
+
# Provide `Success` and `Failure` for pattern matching on operation results
|
|
49
|
+
include Dry::Monads[:result]
|
|
36
50
|
include Hanami::Tachiban
|
|
51
|
+
|
|
52
|
+
handle_exception "ROM::TupleCountMismatchError" => :handle_not_found
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def handle_not_found(request, response, exception)
|
|
57
|
+
response.status = 404
|
|
58
|
+
response.format = :html
|
|
59
|
+
response.body = "Not found"
|
|
60
|
+
end
|
|
37
61
|
end
|
|
38
62
|
end
|
|
39
63
|
```
|
|
40
64
|
|
|
41
|
-
## Usage
|
|
65
|
+
## 2. Usage
|
|
42
66
|
|
|
43
|
-
### Prerequisites
|
|
67
|
+
### 2.1 Prerequisites
|
|
44
68
|
Prior to logging in or authenticating the user, retrieve the entity from the
|
|
45
|
-
database and assign it to
|
|
69
|
+
database and assign it to a variable (e.g. `user`), which you then pass to
|
|
70
|
+
the methods as required.
|
|
46
71
|
|
|
47
72
|
In addition to that, the user entity must have the following attributes:
|
|
48
73
|
|
|
@@ -51,136 +76,365 @@ In addition to that, the user entity must have the following attributes:
|
|
|
51
76
|
* **hashed_pass** (to hold the generated hashed password)
|
|
52
77
|
|
|
53
78
|
|
|
54
|
-
### Usage
|
|
79
|
+
### 2.2 Usage
|
|
55
80
|
|
|
56
|
-
#### Signup
|
|
81
|
+
#### 2.2.3 Signup
|
|
57
82
|
To create a user with a hashed password use the `hashed_password(password)`
|
|
58
83
|
method for the password and store it as the user's attribute `hashed_pass`.
|
|
59
84
|
|
|
60
85
|
*Example*
|
|
61
86
|
|
|
62
87
|
```ruby
|
|
63
|
-
# Create action for
|
|
64
|
-
def
|
|
65
|
-
password = params[:newuser][:password]
|
|
88
|
+
# Create action for the user
|
|
89
|
+
def handle(request, response)
|
|
90
|
+
password = request.params[:newuser][:password]
|
|
66
91
|
hashed_pass = hashed_password(password)
|
|
67
|
-
repository = UserRepository.new
|
|
68
92
|
|
|
69
|
-
|
|
70
|
-
hashed_pass: hashed_pass)
|
|
93
|
+
user = user_repo.create(name: name, surname: surname, email: email,
|
|
94
|
+
hashed_pass: hashed_pass)
|
|
71
95
|
end
|
|
72
96
|
```
|
|
73
97
|
|
|
74
|
-
#### Authentication and login
|
|
75
|
-
To authenticate a user use the `authenticated?(input_password)` method and log
|
|
76
|
-
them in with the `login
|
|
98
|
+
#### 2.2.4 Authentication and login
|
|
99
|
+
To authenticate a user use the `authenticated?(input_password, user)` method and log
|
|
100
|
+
them in with the `login(request, response, user_id, flash_message: nil, login_redirect_url: nil)` method.
|
|
101
|
+
|
|
102
|
+
Authentication is successful if the user exists and passwords match. It's possible to provide your own flash message and / or redirect url. Otherwise, the **default values** will be used (see the table below).
|
|
77
103
|
|
|
78
|
-
The user is logged in by setting the user object ID as the `session[:current_user]`.
|
|
79
|
-
After the user is logged in the session start time is defined as
|
|
80
|
-
`session[:session_start_time] = Time.now`. A default flash message is also
|
|
104
|
+
The user is logged in by setting the user object ID as the `request.session[:current_user]`.
|
|
105
|
+
After the user is logged in, the session start time is defined as
|
|
106
|
+
`request.session[:session_start_time] = Time.now`. A default flash message is also
|
|
81
107
|
assigned as 'You have been successfully logged in.'.
|
|
82
108
|
|
|
83
|
-
The `session[:session_start_time]` is then used by the `session_expired
|
|
84
|
-
method to determine whether the session has expired or not.
|
|
109
|
+
The `request.session[:session_start_time]` is then used by the `session_expired?(request, response)` method to determine whether the session has expired or not.
|
|
85
110
|
|
|
86
|
-
|
|
111
|
+
**_Example of session creation for an entity_**
|
|
87
112
|
|
|
88
113
|
```ruby
|
|
89
|
-
# Create action for
|
|
90
|
-
email = params[:entity_session][:email]
|
|
91
|
-
password = params[:entity_session][:password]
|
|
114
|
+
# Create action for the user session
|
|
115
|
+
email = request.params[:entity_session][:email]
|
|
116
|
+
password = request.params[:entity_session][:password]
|
|
92
117
|
|
|
93
|
-
|
|
94
|
-
login if authenticated?(password)
|
|
118
|
+
user = user_repo.find_by_email(email) #required by login
|
|
119
|
+
login(request, response, user.id) if authenticated?(password, user)
|
|
95
120
|
```
|
|
96
121
|
|
|
97
|
-
To check whether
|
|
98
|
-
If the user is not logged in the `logout` method takes over.
|
|
122
|
+
To check whether a user is logged in, use the `check_for_logged_in_user(request, response)` method. If the user is not logged in, the `logout(request, response, logout_redirect_url: nil)` method takes over.
|
|
99
123
|
|
|
124
|
+
The `authenticated?` method now performs the same amount of work whether or not the email matches a
|
|
125
|
+
user, so response time does not reveal whether an account exists. It returns false for
|
|
126
|
+
a nil password or a user record with no stored hash.
|
|
100
127
|
|
|
101
|
-
|
|
128
|
+
|
|
129
|
+
#### 2.2.5 Session handling
|
|
102
130
|
Tachiban handles session expiration by checking if a session has
|
|
103
131
|
expired and then restarts the session start time if the session
|
|
104
132
|
is still valid or proceeds with the following if the session
|
|
105
133
|
has expired:
|
|
106
134
|
|
|
107
|
-
- setting the `session[:current_user]` to `nil`,
|
|
108
|
-
- a flash message is set: `flash[:failed_notice] = "Your session has expired"`,
|
|
109
|
-
- redirects to the
|
|
110
|
-
|
|
135
|
+
- setting the `request.session[:current_user]` to `nil`,
|
|
136
|
+
- a flash message is set: `response.flash[:failed_notice] = "Your session has expired"`,
|
|
137
|
+
- redirects to the root path `/`, which can be overridden.
|
|
138
|
+
|
|
111
139
|
|
|
140
|
+
The `session_expired?(request, validity_time: nil)` method compares the session start time
|
|
141
|
+
increased for the defined `validity_time` (set to 10 minutes
|
|
142
|
+
by default, but can be overridden) with the current time.
|
|
112
143
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
144
|
+
On expiry, `handle_session` clears the current user, sets
|
|
145
|
+
`response.flash[:failed_notice]` and redirects. The flash and the redirect url
|
|
146
|
+
are configurable — see section 3.
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
#### 2.2.6 Session handling in a share code module
|
|
150
|
+
It is possible to enable session handling in a share code module as provided by Hanami.
|
|
151
|
+
To do this, create an authentication module in **app/actions/authentication.rb**.
|
|
152
|
+
The example below shows also how to custom values to replace default values in
|
|
153
|
+
actions.
|
|
116
154
|
|
|
117
|
-
`handle_session` method:
|
|
118
155
|
```ruby
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
156
|
+
module MyApplication
|
|
157
|
+
module Actions
|
|
158
|
+
module Authentication
|
|
159
|
+
def self.included(action_class)
|
|
160
|
+
action_class.class_eval do
|
|
161
|
+
before :check_for_logged_in_user
|
|
162
|
+
before :handle_session
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def custom_handle_session_redirect_url
|
|
169
|
+
'/login'
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def custom_logout_redirect_url
|
|
173
|
+
'/login'
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def custom_login_redirect_url
|
|
177
|
+
'/'
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def custom_session_validity_time
|
|
181
|
+
if ENV['HANAMI_ENV'] == 'test'
|
|
182
|
+
600
|
|
183
|
+
else
|
|
184
|
+
1800
|
|
185
|
+
end
|
|
186
|
+
end
|
|
127
187
|
end
|
|
128
188
|
end
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
We can then simply include the `Authentication` module in actions, where required.
|
|
192
|
+
|
|
193
|
+
However, if we include this in the base action class, it will be available in all
|
|
194
|
+
actions and there is no need for separate includes in actions:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
#
|
|
198
|
+
#
|
|
199
|
+
module MyApplication
|
|
200
|
+
class Action < Hanami::Action
|
|
201
|
+
# Provide `Success` and `Failure` for pattern matching on operation results
|
|
202
|
+
include Dry::Monads[:result]
|
|
203
|
+
include Hanami::Tachiban
|
|
204
|
+
include MyApplication::Actions::Authentication
|
|
205
|
+
|
|
206
|
+
handle_exception "ROM::TupleCountMismatchError" => :handle_not_found
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
#
|
|
210
|
+
#
|
|
129
211
|
```
|
|
130
212
|
|
|
131
|
-
|
|
213
|
+
|
|
214
|
+
**_Disabling the authentication shared module methods in specific actions_**
|
|
215
|
+
|
|
216
|
+
Any action a logged-out user must be able to reach, should disable the
|
|
217
|
+
authentication. One such example would be the `login` action. If we check
|
|
218
|
+
for an authenticated user there, it will cause an infinite loop. So we
|
|
219
|
+
have to disable the authentication module methods by overriding
|
|
220
|
+
the desired methods in the action. Below is a concrete example for the
|
|
221
|
+
new action for `UserSessions`, which renders the login form when a user
|
|
222
|
+
visits the '/login' url:
|
|
132
223
|
|
|
133
224
|
```ruby
|
|
134
|
-
|
|
135
|
-
|
|
225
|
+
# frozen_string_literal: true
|
|
226
|
+
|
|
227
|
+
module Myapplication
|
|
228
|
+
module Actions
|
|
229
|
+
module UserSessions
|
|
230
|
+
class New < Myapplication::Action
|
|
231
|
+
|
|
232
|
+
def handle(request, response)
|
|
233
|
+
request.session[:current_user] = nil
|
|
234
|
+
end
|
|
136
235
|
|
|
137
|
-
|
|
138
|
-
action.class_eval do
|
|
139
|
-
before :handle_session
|
|
140
|
-
end
|
|
141
|
-
end
|
|
236
|
+
private
|
|
142
237
|
|
|
238
|
+
def check_for_logged_in_user; end
|
|
239
|
+
def handle_session; end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
143
242
|
end
|
|
144
243
|
end
|
|
145
244
|
```
|
|
146
245
|
|
|
246
|
+
#### 2.2.7 Password reset
|
|
247
|
+
The password reset feature provides methods to:
|
|
248
|
+
* generate a token, build email subject and body (text and html part),
|
|
249
|
+
* verify the reset link and
|
|
250
|
+
* invalidate a used token.
|
|
251
|
+
|
|
252
|
+
The link validity must me specified in seconds. The method compares the
|
|
253
|
+
current time with the time when the password reset link was sent increased
|
|
254
|
+
by the link validity: `Time.now > user.password_reset_sent_at + link_validity`.
|
|
255
|
+
|
|
147
256
|
|
|
148
|
-
|
|
149
|
-
The password reset feature provides a few simple methods to generate a
|
|
150
|
-
token, email subject and body. It is also possible to specify and
|
|
151
|
-
check the validity of the password reset url.
|
|
257
|
+
**Generating a reset link**
|
|
152
258
|
|
|
153
259
|
```ruby
|
|
154
|
-
token # =>
|
|
260
|
+
reset_token = token # => 43-character URL-safe string
|
|
261
|
+
user_repo.update(user.id, token: reset_token, password_reset_sent_at: Time.now)
|
|
155
262
|
```
|
|
156
263
|
|
|
157
264
|
```ruby
|
|
158
|
-
email_subject(SomeApp) # => "SomeApp -- password reset request"
|
|
265
|
+
email_subject("SomeApp") # => "SomeApp -- password reset request"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Provide the reset url, user's name, link validity, time unit and optionally the
|
|
269
|
+
application name when building the body:
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
html_body = email_body_html(
|
|
273
|
+
reset_url: reset_url,
|
|
274
|
+
user_name: "#{user.name} #{user.surname}",
|
|
275
|
+
link_validity: 2,
|
|
276
|
+
time_unit: "hour",
|
|
277
|
+
app_name: nil
|
|
278
|
+
)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`app_name` falls back to the `APP_NAME` environment variable, then to your Hanami
|
|
282
|
+
app's namespace, then to "Application".
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
**Verifying a reset link**
|
|
286
|
+
|
|
287
|
+
Use `verify_reset_token`. It takes the token from the params and a block that looks
|
|
288
|
+
the user up, and returns the user only if the token is present, a user matches, and
|
|
289
|
+
the link is still inside its validity window. Any failure returns nil, so one check
|
|
290
|
+
covers every case:
|
|
291
|
+
|
|
292
|
+
```ruby
|
|
293
|
+
def handle(request, response)
|
|
294
|
+
user = verify_reset_token(request.params[:token]) { |t| user_repo.find_by_token(t) }
|
|
295
|
+
|
|
296
|
+
unless user
|
|
297
|
+
response.flash[:failed_notice] = "This link is invalid or has expired."
|
|
298
|
+
response.redirect_to "/passwordreset/new"
|
|
299
|
+
return
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
end
|
|
159
303
|
```
|
|
160
304
|
|
|
305
|
+
The block must return nil when no user matches. With ROM, use `.one` rather than
|
|
306
|
+
`.one!` — `.one!` raises on no match and the exception escapes before
|
|
307
|
+
`verify_reset_token` can return nil.
|
|
308
|
+
|
|
309
|
+
Validity defaults to 3600 seconds. Override per call with
|
|
310
|
+
`link_validity_seconds:`, or globally with a `custom_link_validity_seconds` method.
|
|
311
|
+
|
|
312
|
+
`password_reset_url_valid?(link_validity, user)` remains available for direct use
|
|
313
|
+
to support backward compatibility, but `verify_reset_token` is preferred because
|
|
314
|
+
it performs the checks in the correct order.
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
**Invalidating the token after a reset — required**
|
|
161
318
|
|
|
162
|
-
|
|
163
|
-
|
|
319
|
+
Tachiban does not clear tokens after use. It's best you clear them in
|
|
320
|
+
the same update as the new password and you can use the `reset_token_attributes` for that.
|
|
321
|
+
Otherwise the token is valid until its window expires and can be reused.
|
|
164
322
|
|
|
165
323
|
```ruby
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
324
|
+
user_repo.update(
|
|
325
|
+
user.id,
|
|
326
|
+
hashed_pass: hashed_password(new_password),
|
|
327
|
+
**reset_token_attributes
|
|
328
|
+
)
|
|
169
329
|
```
|
|
170
330
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
331
|
+
|
|
332
|
+
## 3. Default values and custom overrides
|
|
333
|
+
There are a few default values set which can be overridden in two ways: by passing an argument at the call site,
|
|
334
|
+
or by defining a private custom method on your action. See the table below.
|
|
335
|
+
|
|
336
|
+
Precedence: **explicit argument → custom method → built-in default.**
|
|
337
|
+
|
|
338
|
+
|Method |Argument |Custom method |Default |
|
|
339
|
+
|--- |--- |--- |--- |
|
|
340
|
+
|`login` |`flash_message:` |`custom_login_flash_message` |'You have been successfully logged in.'|
|
|
341
|
+
|`login` |`login_redirect_url:`|`custom_login_redirect_url` |'/' |
|
|
342
|
+
|`logout` |`logout_redirect_url:`|`custom_logout_redirect_url` |'/login' |
|
|
343
|
+
|`session_expired?`|`validity_time:` |`custom_session_validity_time` |600 |
|
|
344
|
+
|`handle_session` |`redirect_url:` |`custom_handle_session_redirect_url`|'/' |
|
|
345
|
+
|`handle_session` |— |`custom_session_expired_message` |'Your session has expired.' |
|
|
346
|
+
|`verify_reset_token`|`link_validity_seconds:`|`custom_link_validity_seconds`|3600 |
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
**All time values are in seconds.** `validity_time` and `custom_session_validity_time`
|
|
350
|
+
are the same value under different names — 600 means ten minutes in both.
|
|
351
|
+
|
|
352
|
+
Custom methods can be private — Tachiban looks up private methods too, so you don't
|
|
353
|
+
need to expose them on your action's public interface.
|
|
354
|
+
|
|
355
|
+
Define them on your base action to apply everywhere, or on a single action to
|
|
356
|
+
override just that one:
|
|
174
357
|
|
|
175
358
|
```ruby
|
|
176
|
-
|
|
359
|
+
# app/action.rb — applies to every action
|
|
360
|
+
module MyApplication
|
|
361
|
+
class Action < Hanami::Action
|
|
362
|
+
include Hanami::Tachiban
|
|
363
|
+
|
|
364
|
+
private
|
|
365
|
+
|
|
366
|
+
def custom_session_validity_time
|
|
367
|
+
1800
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# app/actions/admin/dashboard.rb — tighter window for this action only
|
|
373
|
+
module MyApplication
|
|
374
|
+
module Actions
|
|
375
|
+
module Admin
|
|
376
|
+
class Dashboard < MyApplication::Action
|
|
377
|
+
private
|
|
378
|
+
|
|
379
|
+
def custom_session_validity_time
|
|
380
|
+
300
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
177
386
|
```
|
|
178
387
|
|
|
179
|
-
### Example of use in an application
|
|
180
|
-
[Using Tachiban with a Hanami app](https://sebastjan-hribar.github.io/programming/2021/09/03/tachiban-with-hanami.html)
|
|
181
388
|
|
|
182
389
|
|
|
183
|
-
|
|
390
|
+
|
|
391
|
+
## 4. Changelog
|
|
392
|
+
|
|
393
|
+
#### 2.1.0
|
|
394
|
+
Backward compatible with 2.0.0. All existing method signatures are unchanged.
|
|
395
|
+
|
|
396
|
+
**Security fixes:**
|
|
397
|
+
- `authenticated?` no longer returns early when no user is found. It verifies the
|
|
398
|
+
submitted password against a precomputed dummy hash instead, so existing and
|
|
399
|
+
non-existing accounts take the same time to respond. This closes an account
|
|
400
|
+
enumeration vulnerability where a valid email could be identified
|
|
401
|
+
by a slower response.
|
|
402
|
+
- `authenticated?` also handles a user record with a nil `hashed_pass`, and returns
|
|
403
|
+
false for a nil password instead of raising.
|
|
404
|
+
- `password_reset_url_valid?` now fails closed. It returns false for a nil user or a
|
|
405
|
+
user with no `password_reset_sent_at`, where it previously raised NoMethodError.
|
|
406
|
+
- `session_expired?` now returns true when `session_start_time` is missing, rather
|
|
407
|
+
than raising. An undateable session is treated as expired.
|
|
408
|
+
- Reset tokens are now 32 bytes (`SecureRandom.urlsafe_base64(32)`).
|
|
409
|
+
|
|
410
|
+
**New methods:**
|
|
411
|
+
- `verify_reset_token(token, link_validity_seconds: nil)` — performs the
|
|
412
|
+
blank-token check, the user lookup and the expiry check in the correct order and
|
|
413
|
+
returns the user or nil. Recommended over calling `password_reset_url_valid?`
|
|
414
|
+
directly.
|
|
415
|
+
- `reset_token_attributes` — returns `{token: nil, password_reset_sent_at: nil}` for
|
|
416
|
+
passing to the repo's update method after a successful reset. **Tachiban does not
|
|
417
|
+
invalidate tokens by itself.** Failing to clear them leaves reset links replayable
|
|
418
|
+
indefinitely.
|
|
419
|
+
|
|
420
|
+
**New feature — custom method hooks:**
|
|
421
|
+
Default values can now be overridden by defining private methods on your action
|
|
422
|
+
instead of passing arguments. This makes defaults configurable for `before` callbacks
|
|
423
|
+
like `handle_session`, which Hanami calls with a fixed argument list. See section 3.
|
|
424
|
+
|
|
425
|
+
**Other:**
|
|
426
|
+
- Requires for `hanami-controller` and `hanami-action` were dropped.
|
|
427
|
+
|
|
428
|
+
#### 2.0.0
|
|
429
|
+
|
|
430
|
+
**Breaking Changes:**
|
|
431
|
+
- Supports Hanami ~> 2.0 applications only.
|
|
432
|
+
- Method signatures updated: `login`, `logout`, `check_for_logged_in_user`, and `handle_session` now require `(request, response)` parameters.
|
|
433
|
+
- Methods `session_expired?` and `restart_session_counter` now require `(request)` parameter.
|
|
434
|
+
- Tachiban must be explicitly included in the base action: `include Hanami::Tachiban`.
|
|
435
|
+
- Tachiban 2.0.0 doesn't rely on instance variable like `@user` anymore. Instead, a `user` variable must be passed as an argument to a method.
|
|
436
|
+
|
|
437
|
+
For Hanami 1.3 support, use Tachiban 1.0.
|
|
184
438
|
|
|
185
439
|
#### 1.0.0
|
|
186
440
|
|
data/Rakefile
CHANGED
data/lib/tachiban/version.rb
CHANGED
data/lib/tachiban.rb
CHANGED
|
@@ -1,148 +1,298 @@
|
|
|
1
1
|
require 'tachiban/version'
|
|
2
|
-
require 'hanami/controller'
|
|
3
|
-
require 'hanami/action/session'
|
|
4
2
|
require 'argon2'
|
|
3
|
+
require 'securerandom'
|
|
5
4
|
|
|
6
5
|
module Hanami
|
|
7
6
|
module Tachiban
|
|
8
|
-
private
|
|
9
7
|
|
|
8
|
+
# ### Account Enumeration Vulnerability Mitigation ###
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
# To close the account enumeration vulnerability via response timing,
|
|
11
|
+
# a dummy hash is computed once when Tachiban loads. When no user is found,
|
|
12
|
+
# the provided password is verified against that dummy hash instead of
|
|
13
|
+
# returning immediately. Both the existing-account and missing-account paths
|
|
14
|
+
# then perform one Argon2 verification, so the response time no longer tells
|
|
15
|
+
# an attacker whether an account exists.
|
|
16
|
+
#
|
|
17
|
+
# The dummy hash is precomputed rather than generated per request on purpose:
|
|
18
|
+
# Argon2 hash creation is more expensive than verification, so generating one
|
|
19
|
+
# per miss would make the missing-account path measurably slower and reopen
|
|
20
|
+
# the leak in the other direction.
|
|
12
21
|
|
|
13
|
-
|
|
14
|
-
# password. Password hashing is provided by Argon2. Hashed password
|
|
15
|
-
# by default includes a salt and the default cost factorr.
|
|
16
|
-
#
|
|
17
|
-
# Hashed password should be stored in the database as an user's
|
|
18
|
-
# attribute so it can be retrieved during the login process.
|
|
22
|
+
DUMMY_HASH = Argon2::Password.create(SecureRandom.hex(32)).freeze
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# The `tachiban_custom` helper looks up an optional customization method
|
|
27
|
+
# on the including action. If the action defines `name`, calls it and
|
|
28
|
+
# returns its result; otherwise returns `fallback`.
|
|
29
|
+
# The second argument to `respond_to?` includes private methods, so actions
|
|
30
|
+
# can keep their overrides private.
|
|
31
|
+
#
|
|
32
|
+
# Custom methods can be used in the base action, a specific action or both.
|
|
33
|
+
#
|
|
34
|
+
# # app/action.rb
|
|
35
|
+
# module MyApp
|
|
36
|
+
# class Action < Hanami::Action
|
|
37
|
+
# include Hanami::Tachiban
|
|
38
|
+
#
|
|
39
|
+
# private
|
|
40
|
+
#
|
|
41
|
+
# def custom_session_validity_time
|
|
42
|
+
# 1800
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# def custom_handle_session_redirect_url
|
|
46
|
+
# "/login"
|
|
47
|
+
# end
|
|
48
|
+
# end
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# # app/actions/admin/dashboard.rb
|
|
52
|
+
# module MyApp
|
|
53
|
+
# module Actions
|
|
54
|
+
# module Admin
|
|
55
|
+
# class Dashboard < MyApp::Action
|
|
56
|
+
# private
|
|
57
|
+
#
|
|
58
|
+
# def custom_session_validity_time
|
|
59
|
+
# 300
|
|
60
|
+
# end
|
|
61
|
+
# end
|
|
62
|
+
# end
|
|
63
|
+
# end
|
|
64
|
+
# end
|
|
65
|
+
def tachiban_custom(name, fallback)
|
|
66
|
+
respond_to?(name, true) ? send(name) : fallback
|
|
22
67
|
end
|
|
23
68
|
|
|
24
|
-
# ### Login ###
|
|
25
69
|
|
|
26
|
-
|
|
27
|
-
# are true:
|
|
28
|
-
# - a user exists
|
|
29
|
-
# - a user's hashed password from the database matches the input password
|
|
70
|
+
# ### Signup ###
|
|
30
71
|
|
|
31
|
-
|
|
32
|
-
|
|
72
|
+
# The `hashed_password` method generates a hashed version of the user's
|
|
73
|
+
# password. Password hashing is provided by Argon2. Hashed password
|
|
74
|
+
# by default includes a salt and the default cost factor.
|
|
75
|
+
#
|
|
76
|
+
# Hashed password should be stored in the database as a user's
|
|
77
|
+
# attribute so it can be retrieved during the login process.
|
|
78
|
+
def hashed_password(password)
|
|
79
|
+
Argon2::Password.create(password)
|
|
33
80
|
end
|
|
34
81
|
|
|
35
|
-
|
|
36
|
-
# log the user in if the authenticated? method returns true. The user is
|
|
37
|
-
# logged in by setting the user object id as the session[:current_user].
|
|
38
|
-
# After the user is logged in the session start time is defined, which is then used
|
|
39
|
-
# by the session_expired? method to determine whether the session has
|
|
40
|
-
# expired or not.
|
|
41
|
-
|
|
42
|
-
# There are two defualt values set: one for flash message and
|
|
43
|
-
# the other for redirect url. Both can be overwritten by assigning
|
|
44
|
-
# new values for @flash_message and @login_redirect_url.
|
|
82
|
+
# ### Login ###
|
|
45
83
|
|
|
46
|
-
|
|
47
|
-
|
|
84
|
+
# The `authenticated?` method returns true if the following criteria
|
|
85
|
+
# are true:
|
|
86
|
+
# - a user exists
|
|
87
|
+
# - a user's hashed password from the database matches the input password
|
|
88
|
+
#
|
|
89
|
+
# The Account Enumeration Vulnerability Mitigation is in place.
|
|
90
|
+
def authenticated?(input_pass, user)
|
|
91
|
+
return false if input_pass.nil?
|
|
48
92
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
flash[:success_notice] = @flash_message
|
|
54
|
-
@login_redirect_url ||= routes.root_path
|
|
55
|
-
redirect_to @login_redirect_url
|
|
56
|
-
end
|
|
93
|
+
if user.nil? || user.hashed_pass.nil?
|
|
94
|
+
Argon2::Password.verify_password(input_pass, DUMMY_HASH)
|
|
95
|
+
return false
|
|
96
|
+
end
|
|
57
97
|
|
|
58
|
-
|
|
59
|
-
# and performs a redirect to the redirect_url which is set to
|
|
60
|
-
# /login, but can be overwritten as needed with a specific url
|
|
61
|
-
# by setting a new value for @logout_redirect_url.
|
|
98
|
+
Argon2::Password.verify_password(input_pass, user.hashed_pass)
|
|
62
99
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
session.clear
|
|
66
|
-
@logout_redirect_url ||= '/login'
|
|
67
|
-
redirect_to @logout_redirect_url
|
|
100
|
+
rescue Argon2::ArgonHashFail
|
|
101
|
+
false
|
|
68
102
|
end
|
|
69
103
|
|
|
70
|
-
|
|
104
|
+
# The `login` method can be used in combination with the `authenticated?` method to
|
|
105
|
+
# log the user in if the `authenticated?` method returns true. The user is
|
|
106
|
+
# logged in by setting the user object id as the `session[:current_user]`.
|
|
107
|
+
# After the user is logged in the session start time is defined, which is then used
|
|
108
|
+
# by the `session_expired?` method to determine whether the session has
|
|
109
|
+
# expired or not.
|
|
71
110
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
111
|
+
# There are two default values set: one for flash message and
|
|
112
|
+
# the other for redirect url. Both can be overwritten by using
|
|
113
|
+
# custom method helper.
|
|
75
114
|
|
|
76
|
-
|
|
77
|
-
|
|
115
|
+
# Example:
|
|
116
|
+
# login(request, response, user.id) if authenticated?(input_pass)
|
|
117
|
+
def login(request, response, user_id, flash_message: nil, login_redirect_url: nil)
|
|
118
|
+
request.session[:current_user] = user_id
|
|
119
|
+
request.session[:session_start_time] = Time.now
|
|
120
|
+
response.flash[:success_notice] = flash_message || tachiban_custom(:custom_login_flash_message,
|
|
121
|
+
'You have been successfully logged in.')
|
|
122
|
+
response.redirect_to(login_redirect_url || tachiban_custom(:custom_login_redirect_url, '/'))
|
|
78
123
|
end
|
|
79
124
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
125
|
+
# The `logout` method sets the current user in the session to nil
|
|
126
|
+
# and performs a redirect to the `logout_redirect_url` which is set to
|
|
127
|
+
# `'/login'`, but can be overwritten as needed with a specific url
|
|
128
|
+
# by using custom method helper.
|
|
129
|
+
def logout(request, response, logout_redirect_url: nil)
|
|
130
|
+
request.session[:current_user] = nil
|
|
131
|
+
request.session.clear
|
|
132
|
+
response.redirect_to(logout_redirect_url || tachiban_custom(:custom_logout_redirect_url, "/login"))
|
|
133
|
+
end
|
|
84
134
|
|
|
85
|
-
|
|
86
|
-
# increased for the defined validity time (set to 10 minutes
|
|
87
|
-
# by default and can be overwritten) with the current time.
|
|
135
|
+
# ### Authentication ###
|
|
88
136
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
137
|
+
# The `check_for_logged_in_user` method can be used to check for each
|
|
138
|
+
# request whether the user is logged in. If the user is not logged in
|
|
139
|
+
# the logout method takes over.
|
|
140
|
+
def check_for_logged_in_user(request, response)
|
|
141
|
+
logout(request, response) unless request.session[:current_user]
|
|
94
142
|
end
|
|
95
143
|
|
|
96
|
-
|
|
97
|
-
# Time.now. It's used in the handle session method.
|
|
144
|
+
# ### Session handling ###
|
|
98
145
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
# Session handling includes methods `session_expired?`,
|
|
147
|
+
# `restart_session_counter` and `handle_session`.
|
|
148
|
+
#
|
|
149
|
+
# The `session_expired?` method compares the session start time
|
|
150
|
+
# increased for the defined validity time in seconds with the current time.
|
|
151
|
+
# The default validity of 600 seconds (10 minutes) can be overwritten by using the
|
|
152
|
+
# custom method helper.
|
|
153
|
+
def session_expired?(request, validity_time: nil)
|
|
154
|
+
return false unless request.session[:current_user]
|
|
155
|
+
return true unless request.session[:session_start_time]
|
|
102
156
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# is provided and a redirect to a default url of routes.root_path
|
|
107
|
-
# is triggered.
|
|
157
|
+
validity_time ||= tachiban_custom(:custom_session_validity_time, 600)
|
|
158
|
+
request.session[:session_start_time] + validity_time.to_i < Time.now
|
|
159
|
+
end
|
|
108
160
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
161
|
+
# The `restart_session_counter` method resets the session start time to
|
|
162
|
+
# `Time.now`. It's used in the `handle_session` method.
|
|
163
|
+
def restart_session_counter(request)
|
|
164
|
+
request.session[:session_start_time] = Time.now
|
|
165
|
+
end
|
|
111
166
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
167
|
+
# The `handle_session` method is used to handle the incoming requests
|
|
168
|
+
# based on the session expiration. If the session has expired the
|
|
169
|
+
# session user is set to nil, a flash message of "Your session has expired"
|
|
170
|
+
# is provided and a redirect to a default url of "/" is triggered.
|
|
171
|
+
#
|
|
172
|
+
# Both default values can be overwritten by using the custom method helper.
|
|
173
|
+
#
|
|
174
|
+
# If the session hasn't expired the `restart_session_counter` method is
|
|
175
|
+
# called to reset the session start time.
|
|
176
|
+
def handle_session(request, response, redirect_url: nil)
|
|
177
|
+
if session_expired?(request)
|
|
178
|
+
redirect_url ||= tachiban_custom(:custom_handle_session_redirect_url, "/")
|
|
179
|
+
request.session[:current_user] = nil
|
|
180
|
+
response.flash[:failed_notice] = tachiban_custom(:custom_session_expired_message,
|
|
181
|
+
'Your session has expired.')
|
|
182
|
+
response.redirect_to redirect_url
|
|
118
183
|
else
|
|
119
|
-
restart_session_counter
|
|
184
|
+
restart_session_counter(request)
|
|
120
185
|
end
|
|
121
186
|
end
|
|
122
187
|
|
|
123
|
-
|
|
188
|
+
# ### Password reset ###
|
|
189
|
+
# The password reset functionalities include token generation, token invalidation,
|
|
190
|
+
# email subject, email body in the text as well as in the html format,
|
|
191
|
+
# checking the reset link validity and getting the app name.
|
|
124
192
|
def token
|
|
125
|
-
SecureRandom.urlsafe_base64
|
|
193
|
+
SecureRandom.urlsafe_base64(32)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# After a successful reset the token and the timestamp of the password reset link
|
|
197
|
+
# must be reset. The attributes are prepared to be passed to the update repo method.
|
|
198
|
+
#
|
|
199
|
+
# Usage:
|
|
200
|
+
# user_repo.update(user.id, hashed_pass: hashed_password(new_password), **reset_token_attributes)
|
|
201
|
+
def reset_token_attributes
|
|
202
|
+
{token: nil, password_reset_sent_at: nil}
|
|
126
203
|
end
|
|
127
204
|
|
|
128
205
|
def email_subject(app_name)
|
|
206
|
+
app_name ||= default_app_name
|
|
129
207
|
"#{app_name} -- password reset request"
|
|
130
208
|
end
|
|
131
209
|
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
-
|
|
210
|
+
def email_body_text(reset_url:, user_name:, link_validity:, time_unit:, app_name: nil)
|
|
211
|
+
app_name ||= default_app_name
|
|
212
|
+
|
|
213
|
+
<<~TEXT
|
|
214
|
+
Hello #{user_name},
|
|
215
|
+
|
|
216
|
+
Click the link below to reset your password:
|
|
217
|
+
|
|
218
|
+
#{reset_url}
|
|
219
|
+
|
|
220
|
+
This link will expire in #{link_validity} #{time_unit}(s).
|
|
221
|
+
|
|
222
|
+
Kind regards,
|
|
223
|
+
The #{app_name} Team
|
|
224
|
+
TEXT
|
|
135
225
|
end
|
|
136
226
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
227
|
+
def email_body_html(reset_url:, user_name:, link_validity:, time_unit:, app_name: nil)
|
|
228
|
+
app_name ||= default_app_name
|
|
229
|
+
|
|
230
|
+
<<~HTML
|
|
231
|
+
<!DOCTYPE html>
|
|
232
|
+
<html>
|
|
233
|
+
<body style="font-family: Arial, sans-serif;">
|
|
234
|
+
<h2>Password reset request</h2>
|
|
235
|
+
<br>
|
|
236
|
+
<p>Hello #{user_name},</p>
|
|
237
|
+
<br>
|
|
238
|
+
<p>Click the button below to reset your password:</p>
|
|
239
|
+
<p>
|
|
240
|
+
<a href="#{reset_url}" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;">
|
|
241
|
+
Reset Password
|
|
242
|
+
</a>
|
|
243
|
+
</p>
|
|
244
|
+
<br>
|
|
245
|
+
<p>Or copy this link: #{reset_url}</p>
|
|
246
|
+
<p style="color: #666; font-size: 12px;">This link expires in #{link_validity} #{time_unit}(s).</p>
|
|
247
|
+
</body>
|
|
248
|
+
</html>
|
|
249
|
+
HTML
|
|
140
250
|
end
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
251
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
252
|
+
# The `password_reset_url_valid?` method checks whether a user's password
|
|
253
|
+
# reset link is still within its validity window.
|
|
254
|
+
# State the link validity in seconds. The method returns false
|
|
255
|
+
# for a nil user or a user with no reset timestamp, so it fails closed.
|
|
256
|
+
#
|
|
257
|
+
# This method can still be called directly, but it is advised to use the
|
|
258
|
+
# `verify_reset_token` method to cover all link and token validity
|
|
259
|
+
# checks at the same time.
|
|
260
|
+
def password_reset_url_valid?(link_validity, user)
|
|
261
|
+
return false unless user
|
|
262
|
+
return false unless user.password_reset_sent_at
|
|
263
|
+
|
|
264
|
+
Time.now < user.password_reset_sent_at + link_validity.to_i
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Verifies the validity of a password reset token by checking:
|
|
268
|
+
# - presence of the token,
|
|
269
|
+
# - finding the matching user if token is present and
|
|
270
|
+
# - checking the link validity in seconds.
|
|
271
|
+
#
|
|
272
|
+
# If all checks pass, it returns a valid matching user, otherwise it returns nil.
|
|
273
|
+
# The token is passed to the block when the method is called.
|
|
274
|
+
#
|
|
275
|
+
# The block must return nil when no user matches — use ROM's `.one`
|
|
276
|
+
# rather than `.one!`, since `.one!` raises and the exception would
|
|
277
|
+
# escape before this method can return nil.
|
|
278
|
+
#
|
|
279
|
+
# In a handle method it can be used like:
|
|
280
|
+
#
|
|
281
|
+
# user = verify_reset_token(request.params[:token]) { |t| user_repo.find_by_token(t) }
|
|
282
|
+
def verify_reset_token(token, link_validity_seconds: nil)
|
|
283
|
+
return nil if token.nil? || token.to_s.strip.empty?
|
|
284
|
+
|
|
285
|
+
user = yield(token)
|
|
286
|
+
link_validity_seconds ||= tachiban_custom(:custom_link_validity_seconds, 3600)
|
|
287
|
+
password_reset_url_valid?(link_validity_seconds, user) ? user : nil
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def default_app_name
|
|
291
|
+
ENV.fetch("APP_NAME") do
|
|
292
|
+
Hanami.respond_to?(:app) ? Hanami.app.namespace.to_s : "Application"
|
|
293
|
+
end
|
|
294
|
+
rescue StandardError
|
|
295
|
+
"Application"
|
|
296
|
+
end
|
|
147
297
|
end
|
|
148
|
-
end
|
|
298
|
+
end
|
data/tachiban.gemspec
CHANGED
|
@@ -17,16 +17,11 @@ Gem::Specification.new do |spec|
|
|
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
18
18
|
spec.require_paths = ["lib"]
|
|
19
19
|
|
|
20
|
-
spec.add_development_dependency "
|
|
21
|
-
spec.add_development_dependency "rake", "~> 12.3", ">= 12.3.3"
|
|
20
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
22
21
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
23
|
-
spec.add_development_dependency "
|
|
24
|
-
spec.add_development_dependency
|
|
25
|
-
spec.add_development_dependency 'hanami-controller', "~> 1.0"
|
|
26
|
-
spec.add_development_dependency 'hanami-router', "~> 1.0"
|
|
27
|
-
spec.add_development_dependency 'pry', "~> 0"
|
|
22
|
+
spec.add_development_dependency "timecop", "~> 0.9.0"
|
|
23
|
+
spec.add_development_dependency 'pry', "~> 0.16.0"
|
|
28
24
|
|
|
29
25
|
spec.add_runtime_dependency "argon2", "~> 2.3"
|
|
30
|
-
spec.
|
|
31
|
-
|
|
32
|
-
end
|
|
26
|
+
spec.required_ruby_version = ">= 3.0.0"
|
|
27
|
+
end
|
metadata
CHANGED
|
@@ -1,49 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tachiban
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 2.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastjan Hribar
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-09-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: bundler
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - "~>"
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '2.0'
|
|
20
|
-
type: :development
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - "~>"
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '2.0'
|
|
27
13
|
- !ruby/object:Gem::Dependency
|
|
28
14
|
name: rake
|
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
|
30
16
|
requirements:
|
|
31
17
|
- - "~>"
|
|
32
18
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
34
|
-
- - ">="
|
|
35
|
-
- !ruby/object:Gem::Version
|
|
36
|
-
version: 12.3.3
|
|
19
|
+
version: '13.0'
|
|
37
20
|
type: :development
|
|
38
21
|
prerelease: false
|
|
39
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
40
23
|
requirements:
|
|
41
24
|
- - "~>"
|
|
42
25
|
- !ruby/object:Gem::Version
|
|
43
|
-
version: '
|
|
44
|
-
- - ">="
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: 12.3.3
|
|
26
|
+
version: '13.0'
|
|
47
27
|
- !ruby/object:Gem::Dependency
|
|
48
28
|
name: minitest
|
|
49
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -58,76 +38,34 @@ dependencies:
|
|
|
58
38
|
- - "~>"
|
|
59
39
|
- !ruby/object:Gem::Version
|
|
60
40
|
version: '5.0'
|
|
61
|
-
- !ruby/object:Gem::Dependency
|
|
62
|
-
name: hanami-model
|
|
63
|
-
requirement: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '1.0'
|
|
68
|
-
type: :development
|
|
69
|
-
prerelease: false
|
|
70
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - "~>"
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: '1.0'
|
|
75
41
|
- !ruby/object:Gem::Dependency
|
|
76
42
|
name: timecop
|
|
77
|
-
requirement: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - '='
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: 0.8.1
|
|
82
|
-
type: :development
|
|
83
|
-
prerelease: false
|
|
84
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
-
requirements:
|
|
86
|
-
- - '='
|
|
87
|
-
- !ruby/object:Gem::Version
|
|
88
|
-
version: 0.8.1
|
|
89
|
-
- !ruby/object:Gem::Dependency
|
|
90
|
-
name: hanami-controller
|
|
91
43
|
requirement: !ruby/object:Gem::Requirement
|
|
92
44
|
requirements:
|
|
93
45
|
- - "~>"
|
|
94
46
|
- !ruby/object:Gem::Version
|
|
95
|
-
version:
|
|
47
|
+
version: 0.9.0
|
|
96
48
|
type: :development
|
|
97
49
|
prerelease: false
|
|
98
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
99
51
|
requirements:
|
|
100
52
|
- - "~>"
|
|
101
53
|
- !ruby/object:Gem::Version
|
|
102
|
-
version:
|
|
103
|
-
- !ruby/object:Gem::Dependency
|
|
104
|
-
name: hanami-router
|
|
105
|
-
requirement: !ruby/object:Gem::Requirement
|
|
106
|
-
requirements:
|
|
107
|
-
- - "~>"
|
|
108
|
-
- !ruby/object:Gem::Version
|
|
109
|
-
version: '1.0'
|
|
110
|
-
type: :development
|
|
111
|
-
prerelease: false
|
|
112
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
113
|
-
requirements:
|
|
114
|
-
- - "~>"
|
|
115
|
-
- !ruby/object:Gem::Version
|
|
116
|
-
version: '1.0'
|
|
54
|
+
version: 0.9.0
|
|
117
55
|
- !ruby/object:Gem::Dependency
|
|
118
56
|
name: pry
|
|
119
57
|
requirement: !ruby/object:Gem::Requirement
|
|
120
58
|
requirements:
|
|
121
59
|
- - "~>"
|
|
122
60
|
- !ruby/object:Gem::Version
|
|
123
|
-
version:
|
|
61
|
+
version: 0.16.0
|
|
124
62
|
type: :development
|
|
125
63
|
prerelease: false
|
|
126
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
127
65
|
requirements:
|
|
128
66
|
- - "~>"
|
|
129
67
|
- !ruby/object:Gem::Version
|
|
130
|
-
version:
|
|
68
|
+
version: 0.16.0
|
|
131
69
|
- !ruby/object:Gem::Dependency
|
|
132
70
|
name: argon2
|
|
133
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -142,35 +80,7 @@ dependencies:
|
|
|
142
80
|
- - "~>"
|
|
143
81
|
- !ruby/object:Gem::Version
|
|
144
82
|
version: '2.3'
|
|
145
|
-
|
|
146
|
-
name: hanami-controller
|
|
147
|
-
requirement: !ruby/object:Gem::Requirement
|
|
148
|
-
requirements:
|
|
149
|
-
- - "~>"
|
|
150
|
-
- !ruby/object:Gem::Version
|
|
151
|
-
version: '1.0'
|
|
152
|
-
type: :runtime
|
|
153
|
-
prerelease: false
|
|
154
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
155
|
-
requirements:
|
|
156
|
-
- - "~>"
|
|
157
|
-
- !ruby/object:Gem::Version
|
|
158
|
-
version: '1.0'
|
|
159
|
-
- !ruby/object:Gem::Dependency
|
|
160
|
-
name: hanami-router
|
|
161
|
-
requirement: !ruby/object:Gem::Requirement
|
|
162
|
-
requirements:
|
|
163
|
-
- - "~>"
|
|
164
|
-
- !ruby/object:Gem::Version
|
|
165
|
-
version: '1.0'
|
|
166
|
-
type: :runtime
|
|
167
|
-
prerelease: false
|
|
168
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
169
|
-
requirements:
|
|
170
|
-
- - "~>"
|
|
171
|
-
- !ruby/object:Gem::Version
|
|
172
|
-
version: '1.0'
|
|
173
|
-
description:
|
|
83
|
+
description:
|
|
174
84
|
email:
|
|
175
85
|
- sebastjan.hribar@gmail.com
|
|
176
86
|
executables:
|
|
@@ -197,7 +107,7 @@ homepage: https://github.com/sebastjan-hribar/tachiban
|
|
|
197
107
|
licenses:
|
|
198
108
|
- MIT
|
|
199
109
|
metadata: {}
|
|
200
|
-
post_install_message:
|
|
110
|
+
post_install_message:
|
|
201
111
|
rdoc_options: []
|
|
202
112
|
require_paths:
|
|
203
113
|
- lib
|
|
@@ -205,15 +115,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
205
115
|
requirements:
|
|
206
116
|
- - ">="
|
|
207
117
|
- !ruby/object:Gem::Version
|
|
208
|
-
version:
|
|
118
|
+
version: 3.0.0
|
|
209
119
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
210
120
|
requirements:
|
|
211
121
|
- - ">="
|
|
212
122
|
- !ruby/object:Gem::Version
|
|
213
123
|
version: '0'
|
|
214
124
|
requirements: []
|
|
215
|
-
rubygems_version: 3.
|
|
216
|
-
signing_key:
|
|
125
|
+
rubygems_version: 3.5.9
|
|
126
|
+
signing_key:
|
|
217
127
|
specification_version: 4
|
|
218
128
|
summary: Tachiban provides simple password hashing for user authentication with Argon2
|
|
219
129
|
for Hanami web applications.
|