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,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
|