crypt_ident 0.2.1
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 +7 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +29 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/.yardopts +16 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +263 -0
- data/Guardfile +26 -0
- data/HISTORY.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +548 -0
- data/Rakefile +93 -0
- data/bin/_guard-core +29 -0
- data/bin/bundle +105 -0
- data/bin/byebug +29 -0
- data/bin/code_climate_reek +29 -0
- data/bin/coderay +29 -0
- data/bin/commonmarker +29 -0
- data/bin/console +14 -0
- data/bin/erubis +29 -0
- data/bin/flay +29 -0
- data/bin/flog +29 -0
- data/bin/github-markup +29 -0
- data/bin/guard +29 -0
- data/bin/inch +29 -0
- data/bin/kwalify +29 -0
- data/bin/listen +29 -0
- data/bin/pry +29 -0
- data/bin/rackup +29 -0
- data/bin/rake +29 -0
- data/bin/redcarpet +29 -0
- data/bin/reek +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/ruby_parse +29 -0
- data/bin/ruby_parse_extract_error +29 -0
- data/bin/sequel +29 -0
- data/bin/setup +34 -0
- data/bin/sparkr +29 -0
- data/bin/term_cdiff +29 -0
- data/bin/term_colortab +29 -0
- data/bin/term_decolor +29 -0
- data/bin/term_display +29 -0
- data/bin/term_mandel +29 -0
- data/bin/term_snow +29 -0
- data/bin/thor +29 -0
- data/bin/yard +29 -0
- data/bin/yardoc +29 -0
- data/bin/yri +29 -0
- data/config.reek +19 -0
- data/crypt_ident.gemspec +80 -0
- data/docs/CryptIdent.html +2276 -0
- data/docs/_index.html +116 -0
- data/docs/class_list.html +51 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +496 -0
- data/docs/file.CODE_OF_CONDUCT.html +145 -0
- data/docs/file.HISTORY.html +91 -0
- data/docs/file.LICENSE.html +70 -0
- data/docs/file.README.html +692 -0
- data/docs/file_list.html +71 -0
- data/docs/frames.html +17 -0
- data/docs/index.html +692 -0
- data/docs/js/app.js +292 -0
- data/docs/js/full_list.js +216 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +115 -0
- data/docs/top-level-namespace.html +110 -0
- data/lib/crypt_ident.rb +13 -0
- data/lib/crypt_ident/change_password.rb +184 -0
- data/lib/crypt_ident/config.rb +47 -0
- data/lib/crypt_ident/generate_reset_token.rb +212 -0
- data/lib/crypt_ident/reset_password.rb +207 -0
- data/lib/crypt_ident/session_expired.rb +91 -0
- data/lib/crypt_ident/sign_in.rb +189 -0
- data/lib/crypt_ident/sign_out.rb +96 -0
- data/lib/crypt_ident/sign_up.rb +160 -0
- data/lib/crypt_ident/update_session_expiry.rb +125 -0
- data/lib/crypt_ident/version.rb +6 -0
- data/scripts/build-gem-list.rb +91 -0
- metadata +547 -0
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
require 'dry/monads/result'
|
6
|
+
require 'dry/matcher/result_matcher'
|
7
|
+
|
8
|
+
require_relative './config'
|
9
|
+
|
10
|
+
# Include and interact with `CryptIdent` to add authentication to a
|
11
|
+
# Hanami controller action.
|
12
|
+
#
|
13
|
+
# Note the emphasis on *controller action*; this module interacts with session
|
14
|
+
# data, which is quite theoretically possible in an Interactor but practically
|
15
|
+
# *quite* the PITA. YHBW.
|
16
|
+
#
|
17
|
+
# @author Jeff Dickey
|
18
|
+
# @version 0.2.0
|
19
|
+
module CryptIdent
|
20
|
+
# Generate a Password Reset Token
|
21
|
+
#
|
22
|
+
# Password Reset Tokens are useful for verifying that the person requesting a
|
23
|
+
# Password Reset for an existing User is sufficiently likely to be the person
|
24
|
+
# who Registered that User or, if not, that no compromise or other harm is
|
25
|
+
# done.
|
26
|
+
#
|
27
|
+
# Typically, this is done by sending a link through email or other such medium
|
28
|
+
# to the address previously associated with the User purportedly requesting
|
29
|
+
# the Password Reset. `CryptIdent` *does not* automate generation or sending
|
30
|
+
# of the email message. What it *does* provide is a method to generate a new
|
31
|
+
# Password Reset Token to be embedded into an HTML anchor link within an email
|
32
|
+
# that you construct, and then another method (`#reset_password`) to actually
|
33
|
+
# change the password given a valid, correct token.
|
34
|
+
#
|
35
|
+
# It also implements an expiry system, such that if the confirmation of the
|
36
|
+
# Password Reset request is not completed within a configurable time, that the
|
37
|
+
# token is no longer valid (and so cannot be later reused by unauthorised
|
38
|
+
# persons).
|
39
|
+
#
|
40
|
+
# This method *requires* a block, to which a `result` indicating success or
|
41
|
+
# failure is yielded. That block **must** in turn call **both**
|
42
|
+
# `result.success` and `result.failure` to handle success and failure results,
|
43
|
+
# respectively. On success, the block yielded to by `result.success` is called
|
44
|
+
# and passed a `user:` parameter, which is identical to the `user` parameter
|
45
|
+
# passed in to `#generate_reset_token` *except* that the `:token` and
|
46
|
+
# `:password_reset_expires_at` attributes have been updated to reflect the
|
47
|
+
# token request. An updated record matching that `:user` Entity will also have
|
48
|
+
# been saved to the Repository.
|
49
|
+
#
|
50
|
+
# On failure, the `result.failure` call will yield three parameters: `:code`,
|
51
|
+
# `:current_user`, and `:name`, and will be set as follows:
|
52
|
+
#
|
53
|
+
# If the `:code` value is `:user_logged_in`, that indicates that the
|
54
|
+
# `current_user` parameter to this method represented a Registered User. In
|
55
|
+
# this event, the `:current_user` value passed in to the `result.failure` call
|
56
|
+
# will be the same User Entity passed into the method, and the `:name` value
|
57
|
+
# will be `:unassigned`.
|
58
|
+
#
|
59
|
+
# If the `:code` value is `:user_not_found`, the named User was not found in
|
60
|
+
# the Repository. The `:current_user` parameter will be the Guest User Entity,
|
61
|
+
# and the `:name` parameter to the `result.failure` block will be the
|
62
|
+
# `user_name` value passed into the method.
|
63
|
+
# @yieldparam result [Dry::Matcher::Evaluator] Indicates whether the attempt
|
64
|
+
# to generate a new Reset Token succeeded or failed. The lock
|
65
|
+
# **must** call **both** `result.success` and `result.failure`
|
66
|
+
# methods, where the block passed to `result.success` accepts a
|
67
|
+
# parameter for `user:`, which is a User Entity with the
|
68
|
+
# specified `name` value as well as non-`nil` values for its
|
69
|
+
# `:token` and `:password_reset_expires_at` attributes. The
|
70
|
+
# block passed to `result.failure` accepts parameters for
|
71
|
+
# `code:`, `current_user:`, and `name` as described above.
|
72
|
+
# @yieldreturn (void) Use the `result.success` and `result.failure`
|
73
|
+
# method-call blocks to retrieve data from the method.
|
74
|
+
#
|
75
|
+
# @since 0.1.0
|
76
|
+
# @authenticated Must not be Authenticated.
|
77
|
+
# @param [String] user_name The name of the User for whom a Password Reset
|
78
|
+
# Token is to be generated.
|
79
|
+
# @param [User, Hash] current_user Entity representing the currently
|
80
|
+
# Authenticated User Entity. This **must** be a Registered
|
81
|
+
# User, either as an Entity or as a Hash of attributes.
|
82
|
+
# @return (void)
|
83
|
+
# @example Demonstrating a (refactorable) Controller Action Class #call method
|
84
|
+
#
|
85
|
+
# def call(params)
|
86
|
+
# config = CryptIdent.config
|
87
|
+
# # Remember that reading an Entity stored in session data will in fact
|
88
|
+
# # return a *Hash of its attribute values*. This is acceptable.
|
89
|
+
# other_params = { current_user: session[:current_user] }
|
90
|
+
# generate_reset_token(params[:name], other_params) do |result|
|
91
|
+
# result.success do |user:|
|
92
|
+
# @user = user
|
93
|
+
# flash[config.success_key] = 'Request for #{user.name} sent'
|
94
|
+
# end
|
95
|
+
# result.failure do |code:, current_user:, name:| do
|
96
|
+
# respond_to_error(code, current_user, name)
|
97
|
+
# end
|
98
|
+
# end
|
99
|
+
# end
|
100
|
+
#
|
101
|
+
# private
|
102
|
+
#
|
103
|
+
# def respond_to_error(code, current_user, name)
|
104
|
+
# # ...
|
105
|
+
# end
|
106
|
+
# @session_data
|
107
|
+
# `:current_user` **must not** be a Registered User.
|
108
|
+
# @ubiq_lang
|
109
|
+
# - Authentication
|
110
|
+
# - Guest User
|
111
|
+
# - Password Reset Token
|
112
|
+
# - Registered User
|
113
|
+
def generate_reset_token(user_name, current_user: nil)
|
114
|
+
other_params = { current_user: current_user }
|
115
|
+
GenerateResetToken.new.call(user_name, other_params) do |result|
|
116
|
+
yield result
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Generate Reset Token for non-Authenticated User
|
121
|
+
#
|
122
|
+
# This class *is not* part of the published API.
|
123
|
+
# @private
|
124
|
+
class GenerateResetToken
|
125
|
+
include Dry::Monads::Result::Mixin
|
126
|
+
include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
|
127
|
+
|
128
|
+
LogicError = Class.new(RuntimeError)
|
129
|
+
|
130
|
+
def initialize
|
131
|
+
@current_user = nil
|
132
|
+
@user_name = :unassigned
|
133
|
+
end
|
134
|
+
|
135
|
+
def call(user_name, current_user: nil)
|
136
|
+
init_ivars(user_name, current_user)
|
137
|
+
Success(user: updated_user)
|
138
|
+
rescue LogicError => error
|
139
|
+
# rubocop:disable Security/MarshalLoad
|
140
|
+
error_data = Marshal.load(error.message)
|
141
|
+
# rubocop:enable Security/MarshalLoad
|
142
|
+
Failure(error_data)
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
attr_reader :current_user, :user_name
|
148
|
+
|
149
|
+
def current_user_or_guest
|
150
|
+
guest_user = CryptIdent.config.repository.guest_user
|
151
|
+
current_user = @current_user || guest_user
|
152
|
+
# This will convert a Hash of attributes to an Entity instance. It leaves
|
153
|
+
# an actual Entity value unmolested.
|
154
|
+
@current_user = guest_user.class.new(current_user)
|
155
|
+
end
|
156
|
+
|
157
|
+
def init_ivars(user_name, current_user)
|
158
|
+
@current_user = current_user
|
159
|
+
@user_name = user_name
|
160
|
+
end
|
161
|
+
|
162
|
+
def new_token
|
163
|
+
token_length = CryptIdent.config.token_bytes
|
164
|
+
clear_text_token = SecureRandom.alphanumeric(token_length)
|
165
|
+
Base64.strict_encode64(clear_text_token)
|
166
|
+
end
|
167
|
+
|
168
|
+
def update_repo(user)
|
169
|
+
CryptIdent.config.repository.update(user.id, updated_attribs)
|
170
|
+
end
|
171
|
+
|
172
|
+
def updated_attribs
|
173
|
+
prea = Time.now + CryptIdent.config.reset_expiry
|
174
|
+
{ token: new_token, password_reset_expires_at: prea }
|
175
|
+
end
|
176
|
+
|
177
|
+
def updated_user
|
178
|
+
validate_current_user
|
179
|
+
update_repo(user_by_name)
|
180
|
+
end
|
181
|
+
|
182
|
+
def find_user_by_name
|
183
|
+
# will be `nil` if no match found
|
184
|
+
CryptIdent.config.repository.find_by_name(user_name)
|
185
|
+
end
|
186
|
+
|
187
|
+
def user_by_name
|
188
|
+
found_user = find_user_by_name
|
189
|
+
raise LogicError, user_not_found_error unless found_user
|
190
|
+
|
191
|
+
found_user
|
192
|
+
end
|
193
|
+
|
194
|
+
def user_logged_in_error
|
195
|
+
Marshal.dump(code: :user_logged_in, current_user: current_user,
|
196
|
+
name: :unassigned)
|
197
|
+
end
|
198
|
+
|
199
|
+
def user_not_found_error
|
200
|
+
Marshal.dump(code: :user_not_found, current_user: current_user,
|
201
|
+
name: user_name)
|
202
|
+
end
|
203
|
+
|
204
|
+
def validate_current_user
|
205
|
+
return current_user if current_user_or_guest.guest?
|
206
|
+
|
207
|
+
raise LogicError, user_logged_in_error
|
208
|
+
end
|
209
|
+
end
|
210
|
+
# Leave the class visible durinig Gem development and testing; hide in an app
|
211
|
+
private_constant :GenerateResetToken if Hanami.respond_to?(:env?)
|
212
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bcrypt'
|
4
|
+
|
5
|
+
require 'dry/monads/result'
|
6
|
+
require 'dry/matcher/result_matcher'
|
7
|
+
require 'hanami/utils/kernel'
|
8
|
+
|
9
|
+
require_relative './config'
|
10
|
+
|
11
|
+
# Include and interact with `CryptIdent` to add authentication to a
|
12
|
+
# Hanami controller action.
|
13
|
+
#
|
14
|
+
# Note the emphasis on *controller action*; this module interacts with session
|
15
|
+
# data, which is quite theoretically possible in an Interactor but practically
|
16
|
+
# *quite* the PITA. YHBW.
|
17
|
+
#
|
18
|
+
# @author Jeff Dickey
|
19
|
+
# @version 0.2.0
|
20
|
+
module CryptIdent
|
21
|
+
# Reset the password for the User associated with a Password Reset Token.
|
22
|
+
#
|
23
|
+
# After a Password Reset Token has been
|
24
|
+
# [generated](#generate_reset_token-instance_method) and sent to a User, that
|
25
|
+
# User would then exercise the Client system and perform a Password Reset.
|
26
|
+
#
|
27
|
+
# Calling `#reset_password` is different than calling `#change_password` in
|
28
|
+
# one vital respect: with `#change_password`, the User involved **must** be
|
29
|
+
# the Current User (as presumed by passing the appropriate User Entity in as
|
30
|
+
# the `current_user:` parameter), whereas `#reset_password` **must not** be
|
31
|
+
# called with *any* User other than the Guest User as the `current_user:`
|
32
|
+
# parameter (and, again presumably, the Current User for the session). How can
|
33
|
+
# we assure ourselves that the request is legitimate for a specific User? By
|
34
|
+
# use of the Token generated by a previous call to `#generate_reset_token`,
|
35
|
+
# which is used _in place of_ a User Name for this request.
|
36
|
+
#
|
37
|
+
# Given a valid set of parameters, and given that the updated User is
|
38
|
+
# successfully persisted, the method calls the **required** block with a
|
39
|
+
# `result` whose `result.success` matcher is yielded a `user:` parameter with
|
40
|
+
# the updated User as its value.
|
41
|
+
#
|
42
|
+
# NOTE: Each of the error returns documented below calls the **required**
|
43
|
+
# block with a `result` whose `result.failure` matcher is yielded a `code:`
|
44
|
+
# parameter as described, and a `token:` parameter that has the same value
|
45
|
+
# as the passed-in `token` parameter.
|
46
|
+
#
|
47
|
+
# If the passed-in `token` parameter matches the `token` field of a record in
|
48
|
+
# the Repository *and* that Token is determined to have Expired, then the
|
49
|
+
# `code:` parameter mentioned earlier will have the value `:expired_token`.
|
50
|
+
#
|
51
|
+
# If the passed-in `token` parameter *does not* match the `token` field of any
|
52
|
+
# record in the Repository, then the `code:` parameter will have the value
|
53
|
+
# `:token_not_found`.
|
54
|
+
#
|
55
|
+
# If the passed-in `current_user:` parameter is a Registered User, then the
|
56
|
+
# `code:` parameter will have the value `:invalid_current_user`.
|
57
|
+
#
|
58
|
+
# In no event are session values, including the Current User, changed. After a
|
59
|
+
# successful Password Reset, the User must Authenticate as usual.
|
60
|
+
#
|
61
|
+
# @yieldparam result [Dry::Matcher::Evaluator] Indicates whether the attempt
|
62
|
+
# to generate a new Reset Token succeeded or failed. The lock
|
63
|
+
# **must** call **both** `result.success` and `result.failure`
|
64
|
+
# methods, where the block passed to `result.success` accepts a
|
65
|
+
# parameter for `user:`, which is a User Entity with the
|
66
|
+
# specified `name` value as well as non-`nil` values for its
|
67
|
+
# `:token` and `:password_reset_expires_at` attributes. The
|
68
|
+
# block passed to `result.failure` accepts parameters for
|
69
|
+
# `code:`, `current_user:`, and `name` as described above.
|
70
|
+
# @yieldreturn (void) Use the `result.success` and `result.failure`
|
71
|
+
# method-call blocks to retrieve data from the method.
|
72
|
+
#
|
73
|
+
# @since 0.1.0
|
74
|
+
# @authenticated Must not be Authenticated.
|
75
|
+
# @param [String] token The Password Reset Token previously communicated to
|
76
|
+
# the User.
|
77
|
+
# @param [String] new_password New Clear-Text Password to encrypt and add to
|
78
|
+
# return value
|
79
|
+
# @return (void)
|
80
|
+
# @example
|
81
|
+
# def call(params)
|
82
|
+
# reset_password(params[:token], params[:new_password],
|
83
|
+
# current_user: session[:current_user]) do |result
|
84
|
+
# result.success do |user:|
|
85
|
+
# @user = user
|
86
|
+
# message = "Password for #{user.name} successfully reset."
|
87
|
+
# flash[CryptIdent.config.success_key] = message
|
88
|
+
# redirect_to routes.root_path
|
89
|
+
# end
|
90
|
+
# result.failure do |code:, token:|
|
91
|
+
# failure_key = CryptIdent.config.failure_key
|
92
|
+
# flash[failure_key] = failure_message_for(code, token)
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# private
|
98
|
+
#
|
99
|
+
# def failure_message_for(code, token)
|
100
|
+
# # ...
|
101
|
+
# end
|
102
|
+
# @session_data
|
103
|
+
# `:current_user` **must not** be a Registered User.
|
104
|
+
# @ubiq_lang
|
105
|
+
# - Authentication
|
106
|
+
# - Clear-Text Password
|
107
|
+
# - Encrypted Password
|
108
|
+
# - Password Reset Token
|
109
|
+
# - Registered User
|
110
|
+
#
|
111
|
+
def reset_password(token, new_password, current_user: nil)
|
112
|
+
other_params = { current_user: current_user }
|
113
|
+
ResetPassword.new.call(token, new_password, other_params) do |result|
|
114
|
+
yield result
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Reset Password using previously-sent Reset Token for non-Authenticated User
|
119
|
+
#
|
120
|
+
# This class *is not* part of the published API.
|
121
|
+
# @private
|
122
|
+
class ResetPassword
|
123
|
+
include Dry::Monads::Result::Mixin
|
124
|
+
include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
|
125
|
+
|
126
|
+
LogicError = Class.new(RuntimeError)
|
127
|
+
|
128
|
+
def initialize
|
129
|
+
@current_user = :unassigned
|
130
|
+
end
|
131
|
+
|
132
|
+
def call(token, new_password, current_user: nil)
|
133
|
+
init_ivars(current_user)
|
134
|
+
verify_no_current_user(token)
|
135
|
+
user = verify_token(token)
|
136
|
+
Success(user: update(user, new_password))
|
137
|
+
rescue LogicError => error
|
138
|
+
report_failure(error)
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
attr_reader :current_user
|
144
|
+
|
145
|
+
def encrypted(password)
|
146
|
+
BCrypt::Password.create(password)
|
147
|
+
end
|
148
|
+
|
149
|
+
def expired_token?(entity)
|
150
|
+
prea = entity.password_reset_expires_at
|
151
|
+
# Calling this on a non-reset Entity is treated as expiring at the epoch
|
152
|
+
Time.now > Hanami::Utils::Kernel.Time(prea.to_i)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Reek sees a :reek:ControlParameter. Yep.
|
156
|
+
def init_ivars(current_user)
|
157
|
+
guest_user = CryptIdent.config.guest_user
|
158
|
+
current_user ||= guest_user
|
159
|
+
@current_user = guest_user.class.new(current_user)
|
160
|
+
end
|
161
|
+
|
162
|
+
def matching_record_for(token)
|
163
|
+
Array(CryptIdent.config.repository.find_by_token(token)).first
|
164
|
+
end
|
165
|
+
|
166
|
+
def new_attribs(password)
|
167
|
+
{ password_hash: encrypted(password), password_reset_expires_at: nil,
|
168
|
+
token: nil }
|
169
|
+
end
|
170
|
+
|
171
|
+
def raise_logic_error(code, token)
|
172
|
+
payload = { code: code, token: token }
|
173
|
+
raise LogicError, Marshal.dump(payload)
|
174
|
+
end
|
175
|
+
|
176
|
+
def report_failure(error)
|
177
|
+
# rubocop:disable Security/MarshalLoad
|
178
|
+
error_data = Marshal.load(error.message)
|
179
|
+
# rubocop:enable Security/MarshalLoad
|
180
|
+
Failure(error_data)
|
181
|
+
end
|
182
|
+
|
183
|
+
def update(user, password)
|
184
|
+
CryptIdent.config.repository.update(user.id, new_attribs(password))
|
185
|
+
end
|
186
|
+
|
187
|
+
def validate_match_and_token(match, token)
|
188
|
+
raise_logic_error(:token_not_found, token) unless match
|
189
|
+
raise_logic_error(:expired_token, token) if expired_token?(match)
|
190
|
+
match
|
191
|
+
end
|
192
|
+
|
193
|
+
def verify_no_current_user(token)
|
194
|
+
return if current_user.guest?
|
195
|
+
|
196
|
+
payload = { code: :invalid_current_user, token: token }
|
197
|
+
raise LogicError, Marshal.dump(payload)
|
198
|
+
end
|
199
|
+
|
200
|
+
def verify_token(token)
|
201
|
+
match = matching_record_for(token)
|
202
|
+
validate_match_and_token(match, token)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
# Leave the class visible durinig Gem development and testing; hide in an app
|
206
|
+
private_constant :ResetPassword if Hanami.respond_to?(:env?)
|
207
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Include and interact with `CryptIdent` to add authentication to a
|
4
|
+
# Hanami controller action.
|
5
|
+
#
|
6
|
+
# Note the emphasis on *controller action*; this module interacts with session
|
7
|
+
# data, which is quite theoretically possible in an Interactor but practically
|
8
|
+
# *quite* the PITA. YHBW.
|
9
|
+
#
|
10
|
+
# @author Jeff Dickey
|
11
|
+
# @version 0.2.0
|
12
|
+
module CryptIdent
|
13
|
+
# Determine whether the Session has Expired due to User inactivity.
|
14
|
+
#
|
15
|
+
# This is one of two methods in `CryptIdent` (the other being
|
16
|
+
# [`#update_session_expiry?`](#update_session_expiry)) which *does not* follow
|
17
|
+
# the `result`/success/failure [monad workflow](#interfaces). This is because
|
18
|
+
# there is no success/failure division in the workflow. Calling the method
|
19
|
+
# determines if the Current User session has Expired. If the passed-in
|
20
|
+
# `:current_user` is a Registered User, then this will return `true` if the
|
21
|
+
# current time is *later than* the passed-in `:expires_at` value; for the
|
22
|
+
# Guest User, it should always return `false`. (Guest User sessions never
|
23
|
+
# expire; after all, what would you change the session state to?).
|
24
|
+
#
|
25
|
+
# The client code is responsible for applying these values to its own actual
|
26
|
+
# session data, as described by the sample session-management code shown in
|
27
|
+
# the README.
|
28
|
+
#
|
29
|
+
# @param [Hash] session_data The Rack session data of interest to the method.
|
30
|
+
# If the `:current_user` entry is defined, it **must** be either
|
31
|
+
# a User Entity or `nil`, signifying the Guest User. If the
|
32
|
+
# `:expires_at` entry is defined, its value in the returned Hash
|
33
|
+
# *will* be different.
|
34
|
+
#
|
35
|
+
# @since 0.1.0
|
36
|
+
# @authenticated Must be Authenticated.
|
37
|
+
# @return [Boolean]
|
38
|
+
# @example As used in module included by Controller Action Class (see README)
|
39
|
+
# def validate_session
|
40
|
+
# updates = update_session_expiry(session)
|
41
|
+
# if !session_expired?(session)
|
42
|
+
# session[:expires_at] = updates[:expires_at]
|
43
|
+
# return
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# # ... sign out and redirect appropriately ...
|
47
|
+
# end
|
48
|
+
# @session_data
|
49
|
+
# `:current_user` **must** be an Entity for a Registered User on entry
|
50
|
+
# `:expires_at` read during determination of expiry status
|
51
|
+
# @ubiq_lang
|
52
|
+
# - Authentication
|
53
|
+
# - Current User
|
54
|
+
# - Guest User
|
55
|
+
# - Registered User
|
56
|
+
# - Session Expiration
|
57
|
+
#
|
58
|
+
def session_expired?(session_data = {})
|
59
|
+
SessionExpired.new.call(session_data)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Determine whether the Session has Expired due to User inactivity.
|
63
|
+
#
|
64
|
+
# This class *is not* part of the published API.
|
65
|
+
# @private
|
66
|
+
class SessionExpired
|
67
|
+
def call(session_data)
|
68
|
+
# Guest sessions never expire.
|
69
|
+
return false if guest_user_from?(session_data)
|
70
|
+
|
71
|
+
expiry_from(session_data) <= Time.now
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def guest_user_from?(session_data)
|
77
|
+
user = session_data[:current_user] || UserRepository.guest_user
|
78
|
+
# If the `session_data` in fact came from Rack session data, then any
|
79
|
+
# objects (such as a `User` Entity) have been converted to JSON-compatible
|
80
|
+
# types. Hanami Entities can be implicitly converted to and from Hashes of
|
81
|
+
# their attributes, so this part's easy...
|
82
|
+
User.new(user).guest?
|
83
|
+
end
|
84
|
+
|
85
|
+
def expiry_from(session_data)
|
86
|
+
Hanami::Utils::Kernel.Time(session_data[:expires_at].to_i)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
# Leave the class visible durinig Gem development and testing; hide in an app
|
90
|
+
private_constant :SessionExpired if Hanami.respond_to?(:env?)
|
91
|
+
end
|