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,189 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bcrypt'
|
4
|
+
|
5
|
+
require 'dry/monads/result'
|
6
|
+
require 'dry/matcher/result_matcher'
|
7
|
+
|
8
|
+
# Include and interact with `CryptIdent` to add authentication to a
|
9
|
+
# Hanami controller action.
|
10
|
+
#
|
11
|
+
# Note the emphasis on *controller action*; this module interacts with session
|
12
|
+
# data, which is quite theoretically possible in an Interactor but practically
|
13
|
+
# *quite* the PITA. YHBW.
|
14
|
+
#
|
15
|
+
# @author Jeff Dickey
|
16
|
+
# @version 0.2.0
|
17
|
+
module CryptIdent
|
18
|
+
# Attempt to Authenticate a User, passing in an Entity for that User (which
|
19
|
+
# **must** contain a `password_hash` attribute), and a Clear-Text Password.
|
20
|
+
# It also passes in the Current User.
|
21
|
+
#
|
22
|
+
# If the Current User is not a Registered User, then Authentication of the
|
23
|
+
# specified User Entity against the specified Password is accomplished by
|
24
|
+
# comparing the User Entity's `password_hash` attribute to the passed-in
|
25
|
+
# Clear-Text Password.
|
26
|
+
#
|
27
|
+
# The method *requires* a block, to which a `result` indicating success or
|
28
|
+
# failure is yielded. That block **must** in turn call **both**
|
29
|
+
# `result.success` and `result.failure` to handle success and failure results,
|
30
|
+
# respectively. On success, the block yielded to by `result.success` is called
|
31
|
+
# and passed a `user:` parameter, which is the Authenticated User (and is the
|
32
|
+
# same Entity as the `user` parameter passed in to `#sign_in`).
|
33
|
+
#
|
34
|
+
# On failure, the `result.failure` call will yield a `code:` parameter to its
|
35
|
+
# block, which indicates the cause of failure as follows:
|
36
|
+
#
|
37
|
+
# If the specified password *did not* match the passed-in `user` Entity, then
|
38
|
+
# the `code:` for failure will be `:invalid_password`.
|
39
|
+
#
|
40
|
+
# If the specified `user` was not a Registered User, then the `code:` for
|
41
|
+
# failure will be `:user_is_guest`.
|
42
|
+
#
|
43
|
+
# If the specified `current_user` is *neither* the Guest User *nor* the `user`
|
44
|
+
# passed in as a parameter to `#sign_in`, then the `code:` for failure will be
|
45
|
+
# `:illegal_current_user`.
|
46
|
+
#
|
47
|
+
# On *success,* the Controller-level client code **must** set:
|
48
|
+
#
|
49
|
+
# * `session[:expires_at]` to the expiration time for the session. This is
|
50
|
+
# ordinarily computed by adding the current time as returned by `Time.now`
|
51
|
+
# to the `:session_expiry` value in the current configuration.
|
52
|
+
# * `session[:current_user]` to tne returned *Entity* for the successfully
|
53
|
+
# Authenticated User. This is to eliminate possible repeated reads of the
|
54
|
+
# Repository.
|
55
|
+
#
|
56
|
+
# On *failure,* the Controller-level client code **should** set:
|
57
|
+
#
|
58
|
+
# * `session[:expires_at]` to some sufficiently-past time to *always* trigger
|
59
|
+
# `#session_expired?`; `Hanami::Utils::Kernel.Time(0)` does this quite well
|
60
|
+
# (returning midnight GMT on 1 January 1970, converted to local time).
|
61
|
+
# * `session[:current_user]` to either `nil` or the Guest User.
|
62
|
+
#
|
63
|
+
# @since 0.1.0
|
64
|
+
# @authenticated Must not be Authenticated as a different User.
|
65
|
+
# @param [User] user_in Entity representing a User to be Authenticated.
|
66
|
+
# @param [String] password Claimed Clear-Text Password for the specified User.
|
67
|
+
# @param [User, nil] current_user Entity representing the currently
|
68
|
+
# Authenticated User Entity; either `nil` or the Guest User if
|
69
|
+
# none.
|
70
|
+
# @return (void) Use the `result` yield parameter to determine results.
|
71
|
+
# @yieldparam result [Dry::Matcher::Evaluator] Indicates whether the attempt
|
72
|
+
# to Authenticate a User succeeded or failed. Block **must**
|
73
|
+
# call **both** `result.success` and `result.failure` methods,
|
74
|
+
# where the block passed to `result.success` accepts a parameter
|
75
|
+
# for `user:` (which is the newly-created User Entity). The
|
76
|
+
# block passed to `result.failure` accepts a parameter for
|
77
|
+
# `code:`, which is a Symbol reporting the reason for the
|
78
|
+
# failure (as described above).
|
79
|
+
# @yieldreturn [void]
|
80
|
+
# @example As in a Controller Action Class (which you'd refactor somewhat):
|
81
|
+
# def call(params)
|
82
|
+
# user = UserRepository.new.find_by_email(params[:email])
|
83
|
+
# guest_user = CryptIdent.config.guest_user
|
84
|
+
# return update_session_data(guest_user, 0) unless user
|
85
|
+
#
|
86
|
+
# current_user = session[:current_user]
|
87
|
+
# config = CryptIdent.config
|
88
|
+
# sign_in(user, params[:password], current_user: current_user) do |result|
|
89
|
+
# result.success do |user:|
|
90
|
+
# @user = user
|
91
|
+
# update_session_data(user, Time.now)
|
92
|
+
# flash[config.success_key] = "User #{user.name} signed in."
|
93
|
+
# redirect_to routes.root_path
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# result.failure do |code:|
|
97
|
+
# update_session_data(guest_user, config, 0)
|
98
|
+
# flash[config.error_key] = error_message_for(code)
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# private
|
103
|
+
#
|
104
|
+
# def error_message_for(code)
|
105
|
+
# # ...
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# def update_session_data(user, time)
|
109
|
+
# session[:current_user] = user
|
110
|
+
# expiry = Time.now + CryptIdent.config.session_expiry
|
111
|
+
# session[:expires_at] == Hanami::Utils::Kernel.Time(expiry)
|
112
|
+
# end
|
113
|
+
# @session_data
|
114
|
+
# `:current_user` **must not** be a Registered User
|
115
|
+
# @ubiq_lang
|
116
|
+
# - Authenticated User
|
117
|
+
# - Authentication
|
118
|
+
# - Clear-Text Password
|
119
|
+
# - Entity
|
120
|
+
# - Guest User
|
121
|
+
# - Registered User
|
122
|
+
#
|
123
|
+
def sign_in(user_in, password, current_user: nil)
|
124
|
+
params = { user: user_in, password: password, current_user: current_user }
|
125
|
+
SignIn.new.call(params) { |result| yield result }
|
126
|
+
end
|
127
|
+
|
128
|
+
# Reworked sign-in logic for `CryptIdent`, per Issue #9.
|
129
|
+
#
|
130
|
+
# This class *is not* part of the published API.
|
131
|
+
# @private
|
132
|
+
class SignIn
|
133
|
+
include Dry::Monads::Result::Mixin
|
134
|
+
include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
|
135
|
+
|
136
|
+
# As a reminder, calling `Failure` *does not* interrupt control flow *or*
|
137
|
+
# prevent a future `Success` call from overriding the result. This is one
|
138
|
+
# case where raising *and catching* an exception is Useful
|
139
|
+
def call(user:, password:, current_user: nil)
|
140
|
+
set_ivars(user, password, current_user)
|
141
|
+
validate_call_params
|
142
|
+
Success(user: user)
|
143
|
+
rescue LogicError => error
|
144
|
+
Failure(code: error.message.to_sym)
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
attr_reader :current_user, :password, :user
|
150
|
+
|
151
|
+
LogicError = Class.new(RuntimeError)
|
152
|
+
private_constant :LogicError
|
153
|
+
|
154
|
+
def illegal_current_user?
|
155
|
+
!current_user.guest? && !same_user?
|
156
|
+
end
|
157
|
+
|
158
|
+
def password_comparator
|
159
|
+
BCrypt::Password.new(user.password_hash)
|
160
|
+
end
|
161
|
+
|
162
|
+
def same_user?
|
163
|
+
current_user.name == user.name
|
164
|
+
end
|
165
|
+
|
166
|
+
# Reek complains about a :reek:ControlParameter for `current`. Never mind.
|
167
|
+
def set_ivars(user, password, current)
|
168
|
+
@user = user
|
169
|
+
@password = password
|
170
|
+
guest_user = CryptIdent.config.guest_user
|
171
|
+
current ||= guest_user
|
172
|
+
@current_user = guest_user.class.new(current)
|
173
|
+
end
|
174
|
+
|
175
|
+
def validate_call_params
|
176
|
+
raise LogicError, 'user_is_guest' if user.guest?
|
177
|
+
raise LogicError, 'illegal_current_user' if illegal_current_user?
|
178
|
+
|
179
|
+
verify_matching_password
|
180
|
+
end
|
181
|
+
|
182
|
+
def verify_matching_password
|
183
|
+
match = password_comparator == password
|
184
|
+
raise LogicError, 'invalid_password' unless match
|
185
|
+
end
|
186
|
+
end
|
187
|
+
# Leave the class visible durinig Gem development and testing; hide in an app
|
188
|
+
private_constant :SignIn if Hanami.respond_to?(:env?)
|
189
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/monads/result'
|
4
|
+
require 'dry/matcher/result_matcher'
|
5
|
+
|
6
|
+
# Include and interact with `CryptIdent` to add authentication to a
|
7
|
+
# Hanami controller action.
|
8
|
+
#
|
9
|
+
# Note the emphasis on *controller action*; this module interacts with session
|
10
|
+
# data, which is quite theoretically possible in an Interactor but practically
|
11
|
+
# *quite* the PITA. YHBW.
|
12
|
+
#
|
13
|
+
# @author Jeff Dickey
|
14
|
+
# @version 0.2.0
|
15
|
+
module CryptIdent
|
16
|
+
# Sign out a previously Authenticated User.
|
17
|
+
#
|
18
|
+
# The method *requires* a block, to which a `result` indicating success or
|
19
|
+
# failure is yielded. (Presently, any call to `#sign_out` results in success.)
|
20
|
+
# That block **must** in turn call **both** `result.success` and
|
21
|
+
# `result.failure` (even though no failure is implemented) to handle success
|
22
|
+
# and failure results, respectively. On success, the block yielded to by
|
23
|
+
# `result.success` is called without parameters.
|
24
|
+
#
|
25
|
+
# @since 0.1.0
|
26
|
+
# @authenticated Should be Authenticated.
|
27
|
+
# @param [User, `nil`] current_user Entity representing the currently
|
28
|
+
# Authenticated User Entity. This **should** be a Registered
|
29
|
+
# User.
|
30
|
+
# @return (void)
|
31
|
+
# @yieldparam result [Dry::Matcher::Evaluator] Normally, used to report
|
32
|
+
# whether a method succeeded or failed. The block **must**
|
33
|
+
# call **both** `result.success` and `result.failure` methods.
|
34
|
+
# In practice, parameters to both may presently be safely
|
35
|
+
# ignored.
|
36
|
+
# @yieldreturn [void]
|
37
|
+
#
|
38
|
+
# @example Controller Action Class method example resetting values
|
39
|
+
# def call(_params)
|
40
|
+
# sign_out(session[:current_user]) do |result|
|
41
|
+
# result.success do
|
42
|
+
# session[:current_user] = CryptIdent.config.guest_user
|
43
|
+
# session[:expires_at] = Hanami::Utils::Kernel.Time(0)
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# result.failure { next }
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# @example Controller Action Class method example deleting values
|
51
|
+
# def call(_params)
|
52
|
+
# sign_out(session[:current_user]) do |result|
|
53
|
+
# result.success do
|
54
|
+
# session[:current_user] = nil
|
55
|
+
# session[:expires_at] = nil
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# result.failure { next }
|
59
|
+
# end
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# @session_data
|
63
|
+
# See method description above.
|
64
|
+
#
|
65
|
+
# @ubiq_lang
|
66
|
+
# - Authenticated User
|
67
|
+
# - Authentication
|
68
|
+
# - Controller Action Class
|
69
|
+
# - Entity
|
70
|
+
# - Guest User
|
71
|
+
# - Interactor
|
72
|
+
# - Repository
|
73
|
+
#
|
74
|
+
def sign_out(current_user:)
|
75
|
+
SignOut.new.call(current_user: current_user) { |result| yield result }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Sign-out logic for `CryptIdent`, per Issue #9.
|
79
|
+
#
|
80
|
+
# This class *is not* part of the published API.
|
81
|
+
# @private
|
82
|
+
class SignOut
|
83
|
+
include Dry::Monads::Result::Mixin
|
84
|
+
include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
|
85
|
+
|
86
|
+
# This method exists, despite YAGNI, to provide for future expansion of
|
87
|
+
# features like analytics. More importantly, it provides an API congruent
|
88
|
+
# with that of the (reworked) `#sign_up` and `#sign_in` methods.
|
89
|
+
def call(current_user:)
|
90
|
+
_ = current_user # presently ignored
|
91
|
+
Success()
|
92
|
+
end
|
93
|
+
end
|
94
|
+
# Leave the class visible durinig Gem development and testing; hide in an app
|
95
|
+
private_constant :SignOut if Hanami.respond_to?(:env?)
|
96
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
require 'bcrypt'
|
6
|
+
|
7
|
+
require 'dry/monads/result'
|
8
|
+
require 'dry/matcher/result_matcher'
|
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
|
+
# Persist a new User to a Repository based on passed-in attributes, where the
|
21
|
+
# resulting Entity (on success) contains a `:password_hash` attribute
|
22
|
+
# containing the encrypted value of a **random** Clear-Text Password; any
|
23
|
+
# `password` value within `attribs` is ignored.
|
24
|
+
#
|
25
|
+
# The method *requires* a block, to which a `result` indicating success or
|
26
|
+
# failure is yielded. That block **must** in turn call **both**
|
27
|
+
# `result.success` and `result.failure` to handle success and failure results,
|
28
|
+
# respectively. On success, the block yielded to by `result.success` is called
|
29
|
+
# and passed a `user:` parameter, which is the newly-created User Entity.
|
30
|
+
#
|
31
|
+
# If the call fails, the `result.success` block is yielded to, and passed a
|
32
|
+
# `code:` parameter, which will contain one of the following symbols:
|
33
|
+
#
|
34
|
+
# * `:current_user_exists` indicates that the method was called with a
|
35
|
+
# Registered User as the `current_user` parameter.
|
36
|
+
# * `:user_already_created` indicates that the specified `name` attribute
|
37
|
+
# matches a record that already exists in the underlying Repository.
|
38
|
+
# * `:user_creation_failed` indicates that the Repository was unable to create
|
39
|
+
# the new User for some other reason, such as an internal error.
|
40
|
+
#
|
41
|
+
# **NOTE** that the incoming `params` are expected to have been whitelisted at
|
42
|
+
# the Controller Action Class level.
|
43
|
+
#
|
44
|
+
# @since 0.1.0
|
45
|
+
# @authenticated Must not be Authenticated.
|
46
|
+
# @param [Hash] attribs Hash-like object of attributes for new User Entity and
|
47
|
+
# record. **Must** include `name` and any other attributes
|
48
|
+
# required by the underlying database schema. Any `password`
|
49
|
+
# attribute will be ignored.
|
50
|
+
# @param [User, nil] current_user Entity representing the current
|
51
|
+
# Authenticated User, or the Guest User. A value of `nil` is
|
52
|
+
# treated as though the Guest User had been specified.
|
53
|
+
# @return (void) Use the `result` yield parameter to determine results.
|
54
|
+
# @yieldparam result [Dry::Matcher::Evaluator] Indicates whether the attempt
|
55
|
+
# to create a new User succeeded or failed. Block **must**
|
56
|
+
# call **both** `result.success` and `result.failure` methods,
|
57
|
+
# where the block passed to `result.success` accepts a parameter
|
58
|
+
# for `user:` (which is the newly-created User Entity). The
|
59
|
+
# block passed to `result.failure` accepts a parameter for
|
60
|
+
# `code:`, which is a Symbol reporting the reason for the
|
61
|
+
# failure (as described above).
|
62
|
+
# @example in a Controller Action Class
|
63
|
+
# def call(_params)
|
64
|
+
# sign_up(params, current_user: session[:current_user]) do |result|
|
65
|
+
# result.success do |user:|
|
66
|
+
# @user = user
|
67
|
+
# message = "#{user.name} successfully created. You may sign in now."
|
68
|
+
# flash[CryptIdent.config.success_key] = message
|
69
|
+
# redirect_to routes.root_path
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# result.failure do |code:|
|
73
|
+
# # `#error_message_for` is a method on the same class, not shown
|
74
|
+
# failure_key = CryptIdent.config.failure_key
|
75
|
+
# flash[failure_key] = error_message_for(code, params)
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
# @session_data
|
80
|
+
# `:current_user` **must not** be a Registered User.
|
81
|
+
# @ubiq_lang
|
82
|
+
# - Authentication
|
83
|
+
# - Clear-Text Password
|
84
|
+
# - Entity
|
85
|
+
# - Guest User
|
86
|
+
# - Registered User
|
87
|
+
def sign_up(attribs, current_user:)
|
88
|
+
SignUp.new.call(attribs, current_user: current_user) do |result|
|
89
|
+
yield result
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Reworked sign-up logic for `CryptIdent`, per Issue #9
|
94
|
+
#
|
95
|
+
# This class *is not* part of the published API.
|
96
|
+
# @private
|
97
|
+
class SignUp
|
98
|
+
include Dry::Monads::Result::Mixin
|
99
|
+
include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
|
100
|
+
|
101
|
+
def call(attribs, current_user:)
|
102
|
+
return failure_for(:current_user_exists) if current_user?(current_user)
|
103
|
+
|
104
|
+
create_result(all_attribs(attribs))
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def all_attribs(attribs)
|
110
|
+
new_attribs.merge(attribs)
|
111
|
+
end
|
112
|
+
|
113
|
+
# XXX: This has a Flog score of 9.8. Truly simplifying PRs welcome.
|
114
|
+
def create_result(attribs_in)
|
115
|
+
user = CryptIdent.config.repository.create(attribs_in)
|
116
|
+
success_for(user)
|
117
|
+
rescue Hanami::Model::UniqueConstraintViolationError
|
118
|
+
failure_for(:user_already_created)
|
119
|
+
rescue Hanami::Model::Error
|
120
|
+
failure_for(:user_creation_failed)
|
121
|
+
end
|
122
|
+
|
123
|
+
def current_user?(user)
|
124
|
+
guest_user = CryptIdent.config.guest_user
|
125
|
+
user ||= guest_user
|
126
|
+
!guest_user.class.new(user).guest?
|
127
|
+
end
|
128
|
+
|
129
|
+
def failure_for(code)
|
130
|
+
Failure(code: code)
|
131
|
+
end
|
132
|
+
|
133
|
+
def hashed_password(password_in)
|
134
|
+
password = password_in.to_s.strip
|
135
|
+
password = SecureRandom.alphanumeric(64) if password.empty?
|
136
|
+
::BCrypt::Password.create(password)
|
137
|
+
end
|
138
|
+
|
139
|
+
def new_attribs
|
140
|
+
prea = Time.now + CryptIdent.config.reset_expiry
|
141
|
+
{
|
142
|
+
password_hash: hashed_password(nil),
|
143
|
+
password_reset_expires_at: prea,
|
144
|
+
token: new_token
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
def new_token
|
149
|
+
token_length = CryptIdent.config.token_bytes
|
150
|
+
clear_text_token = SecureRandom.alphanumeric(token_length)
|
151
|
+
Base64.strict_encode64(clear_text_token)
|
152
|
+
end
|
153
|
+
|
154
|
+
def success_for(user)
|
155
|
+
Success(user: user)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
# Leave the class visible durinig Gem development and testing; hide in an app
|
159
|
+
private_constant :SignUp if Hanami.respond_to?(:env?)
|
160
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './config'
|
4
|
+
|
5
|
+
# Include and interact with `CryptIdent` to add authentication to a
|
6
|
+
# Hanami controller action.
|
7
|
+
#
|
8
|
+
# Note the emphasis on *controller action*; this module interacts with session
|
9
|
+
# data, which is quite theoretically possible in an Interactor but practically
|
10
|
+
# *quite* the PITA. YHBW.
|
11
|
+
#
|
12
|
+
# @author Jeff Dickey
|
13
|
+
# @version 0.2.0
|
14
|
+
module CryptIdent
|
15
|
+
# Generate a Hash containing an updated Session Expiration timestamp, which
|
16
|
+
# can then be used for session management.
|
17
|
+
#
|
18
|
+
# This is one of two methods in `CryptIdent` (the other being
|
19
|
+
# [`#session_expired?`](#session-expired)) which *does not* follow the
|
20
|
+
# `result`/success/failure [monad workflow](#interfaces). This is because
|
21
|
+
# there is no success/failure division in the workflow. Calling the method
|
22
|
+
# only makes sense if there is a Registered User as the Current User, but *all
|
23
|
+
# this method does* is build a Hash with `:current_user` and `:expires_at`
|
24
|
+
# entries. The returned `:current_user` is the passed-in `:current_user` if a
|
25
|
+
# Registered User, or the Guest User if not. The returned `:updated_at` value,
|
26
|
+
# for a Registered User, is the configured Session Expiry added to the current
|
27
|
+
# time, and for the Guest User, a time far enough in the future that any call
|
28
|
+
# to `#session_expired?` will be highly unlikely to ever return `true`.
|
29
|
+
#
|
30
|
+
# The client code is responsible for applying these values to its own actual
|
31
|
+
# session data, as described by the sample session-management code shown in
|
32
|
+
# the README.
|
33
|
+
#
|
34
|
+
# @param [Hash] session_data The Rack session data of interest to the method.
|
35
|
+
# If the `:current_user` entry is defined, it **must** be either
|
36
|
+
# a User Entity or `nil`, signifying the Guest User. If the
|
37
|
+
# `:expires_at` entry is defined, its value in the returned Hash
|
38
|
+
# *will* be different.
|
39
|
+
# @since 0.1.0
|
40
|
+
# @authenticated Must be Authenticated.
|
41
|
+
# @return [Hash] A `Hash` with entries to be used to update session data.
|
42
|
+
# `expires_at` will have a value of the current time plus the
|
43
|
+
# configuration-specified `session_expiry` offset *if* the
|
44
|
+
# supplied `:current_user` value is a Registered User;
|
45
|
+
# otherwise it will have a value far enough in advance of the
|
46
|
+
# current time (e.g., by 100 years) that the
|
47
|
+
# `#session_expired?` method is highly unlikely to ever return
|
48
|
+
# `true`. The `:current_user` value will be the passed-in
|
49
|
+
# `session_data[:current_user]` value if that represents a
|
50
|
+
# Registered User, or the Guest User otherwise.
|
51
|
+
#
|
52
|
+
# @example As used in module included by Controller Action Class (see README)
|
53
|
+
# def validate_session
|
54
|
+
# if !session_expired?(session)
|
55
|
+
# updates = update_session_expiry(session)
|
56
|
+
# session[:expires_at] = updates[:expires_at]
|
57
|
+
# return
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# # ... sign out and redirect appropriately ...
|
61
|
+
# end
|
62
|
+
# @session_data
|
63
|
+
# `:current_user` **must** be a User Entity. `nil` is accepted to indicate
|
64
|
+
# the Guest User
|
65
|
+
# `:expires_at` set to the session-expiration time on exit, which will be
|
66
|
+
# arbitrarily far in the future for the Guest User.
|
67
|
+
# @ubiq_lang
|
68
|
+
# - Authentication
|
69
|
+
# - Guest User
|
70
|
+
# - Registered User
|
71
|
+
# - Session Expiration
|
72
|
+
# - User
|
73
|
+
#
|
74
|
+
def update_session_expiry(session_data = {})
|
75
|
+
UpdateSessionExpiry.new.call(session_data)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Produce an updated Session Expiration timestamp, to support session
|
79
|
+
# management and prevent prematurely Signing Out a User.
|
80
|
+
#
|
81
|
+
# This class *is not* part of the published API.
|
82
|
+
# @private
|
83
|
+
class UpdateSessionExpiry
|
84
|
+
def initialize
|
85
|
+
config = CryptIdent.config
|
86
|
+
@guest_user = config.guest_user
|
87
|
+
@session_expiry = config.session_expiry
|
88
|
+
end
|
89
|
+
|
90
|
+
def call(session_data = {})
|
91
|
+
if guest_user?(session_data)
|
92
|
+
session_data.merge(guest_data)
|
93
|
+
else
|
94
|
+
session_data.merge(updated_expiry)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
attr_reader :guest_user, :session_expiry
|
101
|
+
|
102
|
+
GUEST_YEARS = 100
|
103
|
+
SECONDS_PER_YEAR = 31_536_000
|
104
|
+
private_constant :GUEST_YEARS, :SECONDS_PER_YEAR
|
105
|
+
|
106
|
+
def guest_data
|
107
|
+
{ expires_at: Time.now + GUEST_YEARS * SECONDS_PER_YEAR }
|
108
|
+
end
|
109
|
+
|
110
|
+
def guest_user?(session_data)
|
111
|
+
user = session_data[:current_user] || guest_user
|
112
|
+
# If the `session_data` in fact came from Rack session data, then any
|
113
|
+
# objects (such as a `User` Entity) have been converted to JSON-compatible
|
114
|
+
# types. Hanami Entities can be implicitly converted to and from Hashes of
|
115
|
+
# their attributes, so this part's easy...
|
116
|
+
User.new(user).guest?
|
117
|
+
end
|
118
|
+
|
119
|
+
def updated_expiry
|
120
|
+
{ expires_at: Time.now + session_expiry }
|
121
|
+
end
|
122
|
+
end
|
123
|
+
# Leave the class visible durinig Gem development and testing; hide in an app
|
124
|
+
private_constant :UpdateSessionExpiry if Hanami.respond_to?(:env?)
|
125
|
+
end
|