crypt_ident 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|