crypt_ident 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +29 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +7 -0
  6. data/.yardopts +16 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +263 -0
  10. data/Guardfile +26 -0
  11. data/HISTORY.md +22 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +548 -0
  14. data/Rakefile +93 -0
  15. data/bin/_guard-core +29 -0
  16. data/bin/bundle +105 -0
  17. data/bin/byebug +29 -0
  18. data/bin/code_climate_reek +29 -0
  19. data/bin/coderay +29 -0
  20. data/bin/commonmarker +29 -0
  21. data/bin/console +14 -0
  22. data/bin/erubis +29 -0
  23. data/bin/flay +29 -0
  24. data/bin/flog +29 -0
  25. data/bin/github-markup +29 -0
  26. data/bin/guard +29 -0
  27. data/bin/inch +29 -0
  28. data/bin/kwalify +29 -0
  29. data/bin/listen +29 -0
  30. data/bin/pry +29 -0
  31. data/bin/rackup +29 -0
  32. data/bin/rake +29 -0
  33. data/bin/redcarpet +29 -0
  34. data/bin/reek +29 -0
  35. data/bin/rubocop +29 -0
  36. data/bin/ruby-parse +29 -0
  37. data/bin/ruby-rewrite +29 -0
  38. data/bin/ruby_parse +29 -0
  39. data/bin/ruby_parse_extract_error +29 -0
  40. data/bin/sequel +29 -0
  41. data/bin/setup +34 -0
  42. data/bin/sparkr +29 -0
  43. data/bin/term_cdiff +29 -0
  44. data/bin/term_colortab +29 -0
  45. data/bin/term_decolor +29 -0
  46. data/bin/term_display +29 -0
  47. data/bin/term_mandel +29 -0
  48. data/bin/term_snow +29 -0
  49. data/bin/thor +29 -0
  50. data/bin/yard +29 -0
  51. data/bin/yardoc +29 -0
  52. data/bin/yri +29 -0
  53. data/config.reek +19 -0
  54. data/crypt_ident.gemspec +80 -0
  55. data/docs/CryptIdent.html +2276 -0
  56. data/docs/_index.html +116 -0
  57. data/docs/class_list.html +51 -0
  58. data/docs/css/common.css +1 -0
  59. data/docs/css/full_list.css +58 -0
  60. data/docs/css/style.css +496 -0
  61. data/docs/file.CODE_OF_CONDUCT.html +145 -0
  62. data/docs/file.HISTORY.html +91 -0
  63. data/docs/file.LICENSE.html +70 -0
  64. data/docs/file.README.html +692 -0
  65. data/docs/file_list.html +71 -0
  66. data/docs/frames.html +17 -0
  67. data/docs/index.html +692 -0
  68. data/docs/js/app.js +292 -0
  69. data/docs/js/full_list.js +216 -0
  70. data/docs/js/jquery.js +4 -0
  71. data/docs/method_list.html +115 -0
  72. data/docs/top-level-namespace.html +110 -0
  73. data/lib/crypt_ident.rb +13 -0
  74. data/lib/crypt_ident/change_password.rb +184 -0
  75. data/lib/crypt_ident/config.rb +47 -0
  76. data/lib/crypt_ident/generate_reset_token.rb +212 -0
  77. data/lib/crypt_ident/reset_password.rb +207 -0
  78. data/lib/crypt_ident/session_expired.rb +91 -0
  79. data/lib/crypt_ident/sign_in.rb +189 -0
  80. data/lib/crypt_ident/sign_out.rb +96 -0
  81. data/lib/crypt_ident/sign_up.rb +160 -0
  82. data/lib/crypt_ident/update_session_expiry.rb +125 -0
  83. data/lib/crypt_ident/version.rb +6 -0
  84. data/scripts/build-gem-list.rb +91 -0
  85. metadata +547 -0
@@ -0,0 +1,22 @@
1
+ # CryptIdent Version History
2
+
3
+ ## 0.2.1 (16 February 2018)
4
+
5
+ To commemorate The Valentine's Day Massacre, we've just re-discovered that Rack (the server protocol underlying all reasonably-modern Ruby Web frameworks) doesn't deal nicely with objects stored in `session` data (which is, by default, persisted in a cookie). If you assign, say, a `Hanami::Entity`-subclass instance to `session[:current_user]` (where `session` is Hanami's access to `Rack::Session`), when you later read from `session[:current_user]`, you'll be handed back a `Hash` of the Entity's attributes. Entity semantics specify that any two instances of the same Entity class with the sasme attribute values refer to _the same value_ of the Entity, not merely equal values, so converting back to an Entity is harmless. You just need to remember to do it and, as of 0.2.0, we didn't. That is fixed here, throughout the published API.
6
+
7
+ ## 0.2.0 (2 February 2018)
8
+
9
+ Initial *confident* public (pre-)release.
10
+
11
+ Many bug-fixes; vastly improved documentation; better configuration; and integration tests (`test/integration/*_test.rb`) as the authoritative reference for usage, largely in conjunction with the API Documentation.
12
+
13
+ Beat on it as you will; feel free to [open issues](https://github.com/jdickey/crypt_ident/issues/new) or join the discussion on [Gitter](https://gitter.im/crypt_ident).
14
+
15
+ This is *far* more likely to become 1.0.0.
16
+
17
+ ## 0.1.0 (18 December 2018)
18
+
19
+ Initial public (pre-)release.
20
+
21
+ "Why isn't this 1.0.0?", you ask. Because nobody, including me, has yet put code using `CryptIdent` into production (internal or otherwise). Therefore, any problems encountered in doing so can be resolved prior to an official 1.0.0 release. After 1.0.0, [semantic-versioning](https://semver.org/) rules apply.
22
+
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Jeff Dickey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,548 @@
1
+ # CryptIdent
2
+
3
+ Yet another fairly basic authentication Gem. (Authorisation, and batteries, sold separately.)
4
+
5
+ This is initially tied to Hanami 1.3.0+; specifically, it assumes that user entities have an API compatible with `Hanami::Entity` for accessing the field/attribute values listed below in [_Database/Repository Setup_](#databaserepository-setup) (which itself assumes a Repository API compatible with that of Hanami 1.3's Repository classes). The Gem is mostly a thin layer around [BCrypt](https://github.com/codahale/bcrypt-ruby) that, in conjunction with Hanami entities or work-alikes, supports the most common use cases for password-based authentication:
6
+
7
+ 1. [Registration](#registration);
8
+ 2. [Signing in](#signing-in);
9
+ 3. [Signing out](#signing-out);
10
+ 4. [Password change](#password-change);
11
+ 5. [Password reset](#password-reset); and
12
+ 6. [Session expiration management](#session-management-overview).
13
+
14
+ It *does not* implement features such as
15
+
16
+ 1. Password-strength testing;
17
+ 2. Password occurrence in a [list of most popular (and easily hacked) passwords](https://www.passwordrandom.com/most-popular-passwords); or
18
+ 3. Password ageing (requiring password changes after a period of time).
19
+
20
+ These either violate current best-practice recommendations from security leaders (e.g., NIST and others no longer recommend password ageing as a defence against cracking) or have other Gems that focus on the features in question (e.g., [`bdmac/strong_password`](https://github.com/bdmac/strong_password)).
21
+
22
+ **NOTE:** One feature of this Gem is that most of the [configuration](#configuration) *should* Work Just Fine for most use cases. However, you **must** explicitly initialise the `:repository` configuration item prior to using the configuration data for the `repository` or the `guest_user` entries. We **recommend** assigning this once, during application startup when other configuration setup is being completed.
23
+
24
+ # Installation
25
+
26
+ Add this line to your application's Gemfile:
27
+
28
+ ```ruby
29
+ gem 'crypt_ident'
30
+ ```
31
+
32
+ And then execute:
33
+
34
+ $ bundle
35
+
36
+ Or install it yourself as:
37
+
38
+ $ gem install crypt_ident
39
+
40
+ # Usage
41
+
42
+ ## Database/Repository Setup
43
+
44
+ In this README and in the API Documentation, the Repository used to read and update the underlying database table is named `UserRepository` and, per Hanami conventions, the Entity is named `User`. *The code itself enforces no such assumption;* so long as you assign your Repository class to `CryptIdent.config.repository` (see [_Configuration_](#configuration) below), and it follows Hanami conventions for Entity names, this *should* Just Work. (Please [open an issue](https://github.com/jdickey/crypt_ident/issues/new) if you prove otherwise.)
45
+
46
+ We assume that the Repository object
47
+
48
+ 1. Has a _class method_ named `.entity` that returns the _class_ of the Entity used by that Repository (e.g., `User` for a `UserRepository`). (This is to match `Hanami::Repository`.)
49
+ 2. Has a class method named `.guest_user` that returns an Entity with a descriptive name (e.g., "Guest User") and is otherwise invalid for persistence (e.g., it has an invalid `id` attribute); and
50
+ 3. Implements the "usual" common methods (`#create`, `#update`, `#delete`, etc) conforming to an interface compatible with [`Hanami::Repository`](https://github.com/hanami/model/blob/master/lib/hanami/repository.rb). This interface is suitably generic and sufficiently widely implemented, even in ORMs such as ActiveRecord that make no claim to implementing the [Repository Pattern](https://8thlight.com/blog/mike-ebert/2013/03/23/the-repository-pattern.html).
51
+
52
+ The database table for that Repository **must** have the following fields, in any order within the schema:
53
+
54
+ | Field | Type | Description |
55
+ |:----- | ---- | ----------- |
56
+ | `name` | string | The name of an individual User to be Authenticated |
57
+ | `email` | string | The Email Address for that User, to be used for Password Recovery, for example. |
58
+ | `password_hash` | text | The *encrypted* Password associated with that User. |
59
+ | `password_reset_expires_at` | timestamp without time zone | Defaults to `nil`; set this to the Expiry Time (`Time.now + config.reset_expiry`) when responding to a Password Reset request (e.g., by email). The `token` (below) will expire at this time (see _Configuration_, below). |
60
+ | `token` | text | Defaults to `nil`. A Password Reset Token; a URL-safe secure random number (see [standard-library documentation](https://ruby-doc.org/stdlib-2.5.1/libdoc/securerandom/rdoc/Random/Formatter.html#method-i-urlsafe_base64)) used to uniquely identify a Password Reset request. |
61
+
62
+ For examples of this, examine the `test/support/fake_repository.rb` and `test/support/unit_test_model_and_repo_classes.rb` files, and the unit (`test/crypt_ident/*`) and integration (`test/integration/*`) tests.
63
+
64
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
65
+
66
+ ## User Entity
67
+
68
+ As mentioned in the _Database/Repository Setup_ section above, `CryptIdent` makes no assumption about the class constant/name of the Entity persisted to and retrieved from the Repository, other than it follow Hanami conventions. (In this and related documents, we refer to that Entity class as `User`.) In addition to attributes matching the fields specified in the previous section, (which `Hanami::Entity` and most analogous ORM Entities expose by default), the Entity **must** respond to the `#guest?` message, returning `true` if it is the Guest User (as returned by `UserRepository#guest_user`), or `false` otherwise. It *may* have other methods as appropriate to the client code.
69
+
70
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
71
+
72
+ ## Configuration
73
+
74
+ The currently-configurable details for `CryptIdent` are as follows:
75
+
76
+ | Key | Default | Description |
77
+ |:--- | ------- | ----------- |
78
+ | `:error_key` | `:error` | Modify this setting if you want to use a different key for flash messages reporting unsuccessful actions. |
79
+ | `:guest_user` | Return value from `repository` `.guest_user` method | This value is used for the session variable `session[:current_user]` when no User has [signed in](#signing-in), or after a previously Authenticated User has [signed out](#signing-out). If your application *does not* make use of the [Null Object pattern](https://en.wikipedia.org/wiki/Null_object_pattern), you would assign `nil` to this configuration setting. (See [this Thoughtbot post](https://robots.thoughtbot.com/rails-refactoring-example-introduce-null-object) for a good discussion of Null Objects in Ruby.) |
80
+ | `:hashing_cost` | 8 | This is the [hashing cost](https://github.com/codahale/bcrypt-ruby#cost-factors) used to *encrypt a password* and is applied at the hashed-password-creation step; it **does not** modify the default cost for the encryption engine. **Note that** any change to this value **will** invalidate and make useless all existing Encrypted Password stored values. |
81
+ | `:repository` | `UserRepository.new` | Modify this if your user records are in a different (or namespaced) class. |
82
+ | `:reset_expiry` | 86400 (24 hours in seconds) | Number of seconds from the time a password-reset request token is stored before it becomes invalid. |
83
+ | `:session_expiry` | 900 (15 minutes, in seconds) | Number of seconds *from either* the time that a User is successfully Authenticated *or* the `update_session_expiry` method is called *before* a call to `session_expired?` will return `true`. |
84
+ | `:success_key` | `:success` | Modify this setting if you want to use a different key for flash messages reporting successful actions. |
85
+ | `:token_bytes` | 24 | Number of bytes of random data to generate when building a password-reset token. See `token` in the [_Database/Repository Setup_](#databaserepository-setup) section, above.
86
+
87
+ For example
88
+
89
+ ```ruby
90
+ include CryptIdent
91
+
92
+ CryptIdent.configure do |config|
93
+ config.repository = MainApp::Repositories::User.new # note: *not* a Hanami recommended practice!
94
+ config.error_key = :alert
95
+ config.hashing_cost = 6 # less secure and less resource-intensive
96
+ config.token_bytes = 20
97
+ config.reset_expiry = 7200 # two hours; "we run a tight ship here"
98
+ config.guest_user = MainApp::Repositories::User.new.guest_user
99
+ end
100
+ ```
101
+
102
+ would change the configuration as you would expect whenever that code was run. (We **recommend** that this be done inside the `controller.prepare` block of your Hanami `web` (or equivalent) app's `application.rb` file.)
103
+
104
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
105
+
106
+ ## Introductory Notes on Workflows
107
+
108
+ ### Interfaces
109
+
110
+ The methods employed directly by these use cases use [Result matchers](https://dry-rb.org/gems/dry-matcher/result-matcher/) and [`Result` monads](https://dry-rb.org/gems/dry-monads/1.0/result/) to provide a *consistent, fluent, explicit, and understandable* mechanism for detecting and handling success and failure.
111
+
112
+ Each method (with two exceptions, noted in their documentation) *requires* a block, to which a `result` indicating success or failure is yielded. That block **must** in turn define blocks for **both** `result.success` and `result.failure` to handle success and failure results, respectively. Each of the two blocks takes parameters which the method uses to communicate either the successful result (and possible supporting information), or the reason for failure, along with supporting information. Not all failure cases use all parameters to the `result.failure` block. Any that are not relevant may be safely ignored (and **should** by convention have a value of `:unassigned` yielded to the `result.failure` block).
113
+
114
+ The active configuration **is not** passed as a parameter to either the `success` or `failure` blocks; it is always accessible as `CryptIdent.config`, and is based on the [`dry-configurable`](https://dry-rb.org/gems/dry-configurable/) Gem.
115
+
116
+ For further discussion of this, see the documentation of the individual methods in the [API Reference](docs/CryptIdent.html).
117
+
118
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
119
+
120
+ ### Session Handling Not Automatic
121
+
122
+ If you've set up your `controller.prepare` block as **recommended** in the preceding section, `CryptIdent` is loaded and configured but *does not* implement session-handling "out of the box"; as with [other libraries](https://github.com/sebastjan-hribar/tachiban#session-handling), it must be implemented *by you* as described in the [*Session Expiration*](#session-expiration) description below.
123
+
124
+ ### Code Samples in Integration Tests are Authoritative
125
+
126
+ Integration tests, in `test/integration/*_test.rb`, are the authoritative documentation-through-working-code of each method and workflow supported by this Gem. Only minimal code snippets are included here to help explain use cases. However, the [API Reference](docs/CryptIdent.html) provides a more conventionally-documented reference to each `CryptIdent` method; any discrepancies between the integration tests, documented API, and/or code snippets there and here should be regarded as a bug (and a [report](https://github.com/jdickey/crypt_ident/issues/) filed if not already filed.
127
+
128
+ ### Terminology and the project Ubiquitous Language
129
+
130
+ Finally, a note on terminology. Terms that have meaning (e.g., _Guest User_) within this module's domain language, or [Ubiquitous Language](https://www.martinfowler.com/bliki/UbiquitousLanguage.html), **must** be capitalised, at least on first use within a paragraph. This is to stress to the reader that, while these terms may have "obvious" meanings, their use within this module and its related documents (including this one) **must** be consistent, specific, and rigorous in their meaning. In the [API Documentation](docs/CryptIdent.html), each of these terms **must** be listed in the _Ubiquitous Language Terms_ section under each method description in which they are used. (If you find any omissions, inconsistencies, or other errors, please open a [new issue](https://github.com/jdickey/conversagence-hanami/issues/new) if it has not already been [reported](https://github.com/jdickey/conversagence-hanami/issues).)
131
+
132
+ After the first usage in a paragraph, the term **may** be used less strictly; e.g., by referring to a _Clear-Text Password_ simply as a _password_ *if* doing so does not introduce ambiguity or confusion. The reader should feel free to [open an issue report](https://github.com/jdickey/crypt_ident/issues) for any lapses of consistency or clarity. (Thank you!)
133
+
134
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
135
+
136
+ ## Use-Case Workflows
137
+
138
+ ### Registration
139
+
140
+ #### Overview
141
+
142
+ Method involved:
143
+
144
+ ```ruby
145
+ module CryptIdent
146
+ def sign_up(attribs, current_user:)
147
+ # ...
148
+ end
149
+ end
150
+ ```
151
+
152
+ This is the first of our use cases that involves calling a function which expects a block to be supplied. If one isn't, then a `LocalJumpError` will be raised. If either the `success` or `failure` blocks are omitted within that block, then a `Dry::Matcher::NonExhaustiveMatchError` will be raised. (It *is* permissible to completely omit the parameters to a `success` or `failure` block; e.g., for the `#sign_out` method which does not support reporting a failure.)
153
+
154
+ The `attribs` parameter is a Hash-like object such as a `Hanami::Action::Params` instance. It **must** have a `:name` entry, as well as any other keys and matching field values required by the Entity which will be created from the `params` values, *other than* a `:password_hash` key. It also **must not** have a `:password` entry; if one is supplied, it will be *ignored*. This is to support our standard workflow of having newly-Registered Users be initially assigned a Clear-Text Password of random text, then immediately starting the [Password Reset](#password-reset) workflow to further validate their supplied email address.
155
+
156
+ Pass in the value of the `session[:current_user]` session variable as the `:current_user` parameter. This **must** be an Entity value rather than an `id` value. Supplying a value of `nil` is permitted, and is equivalent to specifying the Guest User (see [_Database/Repository Setup_](#database-repository-setup)).
157
+
158
+ As described [earlier](#interfaces), this method **requires** a block which accepts a `result` parameter. The block **must** define *both* `result.success` and `result.failure` blocks, passing each a block which itself takes appropriate parameters. These will be further described below.
159
+
160
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
161
+
162
+ #### Success, aka Golden Path
163
+
164
+ If the `params` include all values required by the underlying schema, including a valid `name` attribute that does not exist in the underlying data store, then it (with a `password_hash` attribute created from a random-text Clear-Text Password) will be persisted to the Repository specified by `repo:` (or to the Repository specified by the [_Configuration_](#configuratino) if the `repo:` value is `nil`). That User Entity will be passed to the `result.success` block as the `user:` parameter.
165
+
166
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
167
+
168
+ #### Error Conditions
169
+
170
+ ##### Authenticated User as `current_user:` Parameter
171
+
172
+ If the specified `current_user:` parameter is a valid User Entity other than the Guest User, then that is presumed to be the Current User of the application. Authenticated Users are prohibited from creating other Users, and so the `result.failure` block will be called with a `code:` of `:current_user_exists`.
173
+
174
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
175
+
176
+ ##### Specified `:name` Attribute Already Used for an Existing User
177
+
178
+ If there is no improper value for the `current_user:` parameter, and if the specified `:name` attribute exists in a record within the Repository, then the `result.failure` block will be called with a `:code` of `:user_already_created`.
179
+
180
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
181
+
182
+ ##### Record Could Not be Created Within Repository
183
+
184
+ If neither of the earlier conditions apply, but the Repository method `#create` returned an error, then the `result.failure` block will be called with a `:code` of `:user_creation_failed `.
185
+
186
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
187
+
188
+ ### Signing In
189
+
190
+ #### Overview
191
+
192
+ Method involved:
193
+
194
+ ```ruby
195
+ module CryptIdent
196
+ def sign_in(user, password, current_user: nil)
197
+ # ...
198
+ end
199
+ end
200
+ ```
201
+
202
+ Once a User has been [Registered](#registration) and [Reset their Password](#password-reset), Signing In is a matter of retrieving that user's Entity (containing a `password_hash` attribute) and calling `#sign_in` passing in that Entity, the purported Clear-Text Password, and the currently Authenticated User (if any), then using the `result` passed to the yielded block to determine and respond to the success or failure of the call.
203
+
204
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
205
+
206
+ #### Successfully Signing In
207
+
208
+ So long as no User is currently Authenticated in the Session (as shown by the `session[:current_user]` having a value of either `nil` or the Guest User), supplying a User Entity and the correct Clear-Text Password for that User to a call to `#sign_in` will cause the block for the `#sign_in` method call to yield the *same* User Entity to the `result.success` block, indicating success.
209
+
210
+ Note that this process is unchanged if the passed-in `current_user` is *the same as* the User Entity attempting Authentication. It is up to client code to determine how to proceed if Authentication fails in this case.
211
+
212
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
213
+
214
+ #### Error Conditions
215
+
216
+ ##### Incorrect Password Supplied
217
+
218
+ While no Authenticated Member currently exists (as shown by the `session[:current_user]` having a value of either `nil` or the Guest User), supplying a User Entity and an *incorrect* Clear-Text Password for that User to a call to `#sign_in` will yield a call to the block's `result.failure` block with a `code:` of `:invalid_password`.
219
+
220
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
221
+
222
+ ##### Authenticated User Exists
223
+
224
+ If the passed-in `current_user` is a User Entity *other than* the specified `user` Entity *or* the Guest User, no match will be attempted, and the method will yield a call to the block's `result.failure` block with a `code:` value of `:illegal_current_user`.
225
+
226
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
227
+
228
+ ##### Guest User Attempts Authentication
229
+
230
+ While no Authenticated Member currently exists (as shown by the `session[:current_user]` having a value of either `nil` or the Guest User), supplying *the Guest User* as the User Entity to be Authenticated will yield a call to the block's `result.failure` block with a `code:` value of `:user_is_guest`.
231
+
232
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
233
+
234
+ #### Other Notes
235
+
236
+ This method **does not** interact with a Repository, and therefore doesn't need to account for an invalid User Name parameter, for instance. Nor does it directly modify session data, although the associated Controller Action Class code **must** set `session[:current_user]` and `session[:start_time]` as below. This is to support extraction of this code (along with anything else not using `Hanami::Controller`-dependent input validation, redirects, flash messages, etc) to an Interactor, into which would be explicitly passed `session[:current_user]`.
237
+
238
+ On *success*, the Controller Action Class calling code **must** set:
239
+
240
+ * `session[:start_time]` to the current time as returned by `Time.now`; and
241
+ * `session[:current_user]` to the *Entity* (not the ID value from the Repository) for the newly-Authenticated User. This is to eliminate repeated reads of the Repository.
242
+
243
+ On *failure*, the Controller Action Class calling code **must** set:
244
+
245
+ * `session[:start_time]` to some sufficiently-past time to *always* trigger `#session_expired?`; `Hanami::Utils::Kernel.Time(0)` does this quite well, returning midnight GMT on 1 January 1970, converted to local time.
246
+ * `session[:current_user]` to `nil` or to the Guest User (see [_Configuration_](#configuration)).
247
+
248
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
249
+
250
+ ### Signing Out
251
+
252
+ #### Overview
253
+
254
+ Method involved:
255
+
256
+ ```
257
+ module CryptIdent
258
+ def sign_out(current_user:)
259
+ end
260
+ end
261
+ ```
262
+
263
+ Signing out any previously Authenticated User is straightforward: call the `sign_out` method, passing in that User as the `current_user:` parameter. As with the earlier methods, this method also **requires** a block which accepts a `result` parameter and has `result.success` and `result.failure` calls/blocks. No parameters are yielded to either block.
264
+
265
+ Note that, as of Release 0.2.0, the method simply passes control to the (required) block, in whose `result.success` call block you can delete or reset `session[:current_user]` and `session[:start_time]`. We **recommend** reset values of:
266
+
267
+ * `CryptIdent.config.guest_user` for `session[:current_user]` and
268
+ * `Hanami::Utils::Kernel.Time(0)` for `session[:start_time]`, which will set the timestamp to 1 January 1970 at midnight &mdash; a value which should *far* exceed your session-expiry limit if you decide not to simply delete the previous values by assigning `nil` to them.
269
+
270
+ The required `result.failure` block can simply be skipped, as
271
+
272
+ ```
273
+ result.failure { next }
274
+ ```
275
+
276
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
277
+
278
+ ### Password Change
279
+
280
+ #### Overview
281
+
282
+ Method involved:
283
+
284
+ ```ruby
285
+ module CryptIdent
286
+ def change_password(user, current_password, new_password)
287
+ # ...
288
+ end
289
+ end
290
+ ```
291
+
292
+ To change an Authenticated User's password, an Entity for that User, the current Clear-Text Password, and the new Clear-Text Password are required.
293
+
294
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
295
+
296
+ #### Successfully Changing the Password
297
+
298
+ If all parameters are valid and the updated User is successfully persisted, the method calls the **required** block with a `result` whose `result.success` matcher is yielded a `user:` parameter with the updated User as its value. From that point, the User is able to Sign In using the User Name and updated Clear-Text Password.
299
+
300
+ Client code **must** take care not to try to Authenticate using the Encrypted Password in the Entity passed in to this method, as it is no longer current. Either retain the returned User Entity from the method, or read it again from the Repository.
301
+
302
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
303
+
304
+ #### Error Conditions
305
+
306
+ ##### Specified User is Guest User
307
+
308
+ If the passed-in `user` is the Guest User (or `nil`), the method calls the **required** block with a `result` whose `result.failure` matcher is yielded a `code:` of `:invalid_user`. No new Entity with updated values is created; no changes are made to the Repository.
309
+
310
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
311
+
312
+ ##### Invalid Current Clear-Text Password
313
+
314
+ If the specified Current Clear-Text Password cannot Authenticate against the encrypted value within the `user` Entity, the method calls the **required** block with a `result` whose `result.failure` matcher is yielded a `code:` of `:bad_password`. No new Entity with updated values is created; no changes are made to the Repository.
315
+
316
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
317
+
318
+ ### Generate Password Reset Token and Password Reset: Introduction
319
+
320
+ Password Reset Tokens are useful for verifying that the person requesting a Password Reset for an existing User is sufficiently likely to be the person who Registered that User or, if not, that no compromise or other harm is done.
321
+
322
+ Typically, this is done by sending a link through email or other such medium to the address previously associated with the User purportedly requesting the Password Reset. `CryptIdent` *does not* automate generation or sending of the email message. What it *does* provide is a method to generate a new Password Reset Token to be embedded into such a message, often in the form of an HTML anchor link within an email that you construct. It also provides another method (`#reset_password`) to actually change the password given a valid, correct token.
323
+
324
+ It also implements an expiry system, such that if the confirmation of the Password Reset request is not completed within a [configurable](#Configuration) time, that the Token is no longer valid (and cannot be later reused by unauthorised persons).
325
+
326
+ **Note that** multiple successful calls to generate a new Password Reset Token for a single User overwrite the data generated by previous calls, invalidating the previously-generated Tokens and resetting the expiry.
327
+
328
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
329
+
330
+ ### Generate Password Reset Token
331
+
332
+ Method involved:
333
+
334
+ ```ruby
335
+ module CryptIdent
336
+ def generate_reset_token(user_name, current_user: nil)
337
+ # ...
338
+ end
339
+ end
340
+ ```
341
+
342
+ #### Successfully Generating a Token
343
+
344
+ Given a `user_name` parameter that specifies an existing User Name, and a `current_user:` parameter that is either `nil` or the Guest User, the method calls the **required** block with a `result` whose `result.success` matcher is yielded a `user:` parameter with a User Entity as its value. That User will be an Entity whose `name` matches the specified `user_name` parameter, with (new) values for the `token` and `password_reset_expires_at` attributes. The `token` attribute uniquely identifies the Password Reset request, and the `password_reset_expires_at` attribute is based on both the current (server-local) time when the updated User Entity was persisted to the Repository, and the `:reset_expiry` attribute of the configuration.
345
+
346
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
347
+
348
+ #### Error Conditions
349
+
350
+ ##### Authenticated User Exists
351
+
352
+ If the specified `current_user:` parameter is a valid User Entity other than the Guest User, then that is presumed to be the Current User of the Application. Authenticated Users are prohibited from requesting Password Resets for other Users; if they wish to change their *own* Clear-Text Password, there's a [method](#password-change) for that.
353
+
354
+ In this case, the **required** block will be passed a `result` whose `result.failure` matcher is yielded a `code:` parameter of `:user_logged_in`, a `current_user:` parameter matching the passed-in User Entity, and a `name:` parameter of `:unassigned` (which must be included in the block parameters but can be ignored thereafter).
355
+
356
+ ##### Named User Not Found in Repository
357
+
358
+ If the specified `user_name` parameter value does not match the `name` of any User in the Repository, then the **required** block will be passed a `result` whose `result.failure` matcher is yielded a `code:` parameter of `:user_not_found`, a `current_user:` parameter of the Guest User, and a `name:` parameter whose value is the passed-in `user_name` value.
359
+
360
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
361
+
362
+ ### Password Reset
363
+
364
+ #### Overview
365
+
366
+ Method involved:
367
+
368
+ ```ruby
369
+ module CryptIdent
370
+ def reset_password(token, new_password, current_user: nil)
371
+ # ...
372
+ end
373
+ end
374
+ ```
375
+
376
+ Calling `#reset_password` is different than calling `#change_password` in one vital respect: with `#change_password`, the User involved **must** be the Current User (as presumed by passing the appropriate User Entity in as the `current_user:` parameter), whereas `#reset_password` **must not** be called with *any* User other than the Guest User as the `current_user:` parameter (and, again presumably, the Current User for the session). How can we assure ourselves that the request is legitimate for a specific User? By use of the Token generated by a previous call to `#generate_reset_token`, which is used _in place of_ a User Name for this request.
377
+
378
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
379
+
380
+ #### Successfully Resetting a Password
381
+
382
+ To successfully perform a Password Reset, supply a valid, non-expired Token along with a new Clear-Text Password to the `#reset_password` method. Once the Token is found in the configuration-default Repository, and is verified not to have Expired, then the Repository will be updated with a record for that User where the `password_hash` field has been updated to reflect the new Clear-Text Password, and the `token` and `password_reset_expires_at` fields will be set to `nil`.
383
+
384
+ If all the preceding is successful and the updated User is successfully persisted, the method calls the **required** block with a `result` whose `result.success` matcher is yielded a `user:` parameter with the updated User as its value. From that point, the User is able to Sign In using the User Name and updated Clear-Text Password.
385
+
386
+ Client code **must** take care not to try to Authenticate using the Encrypted Password in the Entity passed in to this method, as it is no longer current. Either retain the returned User Entity from the method, or read it again from the Repository.
387
+
388
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
389
+
390
+ #### Error Conditions
391
+
392
+ ##### Expired Token
393
+
394
+ If the passed-in `token` parameter matches the `token` field of a record in the Repository *and* that Token is determined to have Expired, then this method calls the **required** block with a `result` whose `result.failure` matcher is yielded a `code:` parameter of `:expired_token` and a `token:` parameter that has the same value as the passed-in `token` parameter.
395
+
396
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
397
+
398
+ ##### Token Not Found
399
+
400
+ If the passed-in `token` parameter *does not* match the `token` field of any record in the Repository, then this method calls the **required** block with a `result` whose `result.failure` matcher is yielded a `code:` parameter of `:token_not_found` and a `token:` parameter that has the same value as the passed-in `token` parameter.
401
+
402
+ ##### Invalid Current User
403
+
404
+ If the passed-in `current_user:` parameter *is not* either the default `nil` or the Guest User, then this method calls the **required** block with a `result` whose `result.failure` matcher is yielded a `code:` parameter of `:invalid_current_user` and a `token:` parameter that has the same value as the passed-in `token` parameter.
405
+
406
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
407
+
408
+ ### Session Management Overview
409
+
410
+ Session management is a necessary part of implementing authentication (and authorisation) within an app. However, it's not something that an authentication *library* can fully implement without making the client application excessively inflexible and brittle.
411
+
412
+ `CryptIdent` has two convenience methods which *help in* implementing session-expiration logic; these make use of the `session_expiry` [configuration value](#configuration).
413
+
414
+ * `CryptIdent#update_session_expiry` returns a `Hash` whose `:expires_at` value the current time *plus* the number of seconds specified by the `session_expiry` configuration value. This can be used to update the corresponding `session` data which defines the session-expiry time;
415
+ * `CryptIdent#session_expired?` returns `true` if the current time is not less than the session-expiry time; it returns `false` otherwise.
416
+
417
+ Example code which uses these methods is illustrated below, as a shared-code module that may be included in your controllers' action classes:
418
+
419
+ ```ruby
420
+ # apps/web/controllers/handle_session.rb
421
+
422
+ module Web
423
+ module HandleSession
424
+ include CryptIdent
425
+
426
+ def self.included(other)
427
+ other.class_eval do
428
+ before :validate_session
429
+ end
430
+ end
431
+
432
+ private
433
+
434
+ def validate_session
435
+ updates = update_session_expiry(session)
436
+ if !session_expired?(session)
437
+ session[:expires_at] = updates[:expires_at]
438
+ return
439
+ end
440
+
441
+ @redirect_url ||= routes.root_path
442
+ config = CryptIdent.config
443
+ session[:current_user] = config.guest_user
444
+ session[:expires_at] = updates[:expires_at]
445
+ error_message = 'Your session has expired. You have been signed out.'
446
+ flash[config.error_key] = error_message
447
+ redirect_to @redirect_url
448
+ end
449
+ end
450
+ end
451
+ ```
452
+
453
+ This code should be fairly self-explanatory. Including the module adds the private `#validate_session` method to the client controller action class, adding a call to that method before the action class' `#call` method is entered. (One can argue that this violates the spirit if not the letter of the [Hanami Guide's](https://guides.hanamirb.org/actions/control-flow/) warning not to "use callbacks for model domain logic operations". We would argue that this callback's functionality is common to essentially all client applications and, by providing a reference example, allows individual project teams to modify it as required for their use.) If the session-expiry time has been previously set and is not before the current time, then that session-expiry time is reset based on the current time, and no further action is taken. Otherwise:
454
+
455
+ 1. The `current_user` setting in the session data is overwritten with the [`config.guest_user`](#configuration) value (defaulting to `nil`);
456
+ 2. A flash error message is set, which **should** be rendered within the controller action's view; and
457
+ 3. Control is redirected to the path or URL specified by `@redirect_url`, defaulting to the root path (`/`).
458
+
459
+ This code will be instantly familiar to anyone coming from another framework like Rails, where the conventional way to ensure authentication before a controller action is executed is to add a `:before` hook. Adding this module to the controller action class is also justifiable Hanami, since it depends on and interacts with session data. (Just don't let any actual domain logic [taint](http://hanamirb.org/guides/1.2/actions/control-flow/#proc) your controller callbacks; that's begging for difficult-to-debug problems going forward.
460
+
461
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
462
+
463
+ ### Session Expired
464
+
465
+ Method involved:
466
+
467
+ ```ruby
468
+ module CryptIdent
469
+ def session_expired?(session_data={})
470
+ # ...
471
+ end
472
+ end
473
+ ```
474
+
475
+ This is one of two methods in `CryptIdent` (the other being [`#update_session_expiry `](#update-session-expiry), below) which *does not* follow the `result`/success/failure [monad workflow](#interfaces). Like that method:
476
+
477
+ * there is no success/failure division in the workflow;
478
+ * calling this method only makes sense if there is an Authenticated User;
479
+ * it is intended for use in session-management code as described in the [Overview](#session-management-overview) above.
480
+
481
+ This method checks the passed-in `session_data[:start_time]` value against the current time. If the difference is *greater than* the [configured](#configuration) _Session Expiry_ value, then the method returns `true`; otherwise, it returns `false`. No change is attempted to the contents of the passed-in `session_data`.
482
+
483
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
484
+
485
+ ### Update Session Expiry
486
+
487
+ #### Overview
488
+
489
+ Method involved:
490
+
491
+ ```ruby
492
+ module CryptIdent
493
+ def update_session_expiry(session_data={})
494
+ # ...
495
+ end
496
+ end
497
+ ```
498
+
499
+ This is one of two methods in `CryptIdent` (the other being [`#session_expired?`](#session-expired), above) which *does not* follow the `result`/success/failure [monad workflow](#interfaces). This is because there is no success/failure division in the workflow. Calling the method only makes sense if there is an Authenticated User, but *all this method does* is return a `Hash` as defined below.
500
+
501
+ It is intended for use in session-management code as described in the [Overview](#session-management-overview) above.
502
+
503
+ #### Parameter
504
+
505
+ The parameter, `session_data`, is a Hash-like object which **should** have existing entries for `:current_user` (defaulting to the Guest User if not found) and for `:expires_at` (defaulting to the [epoch](https://en.wikipedia.org/wiki/Unix_time) if not found).
506
+
507
+ #### Return
508
+
509
+ The return value is a `Hash` which:
510
+
511
+ 1. `:current_user` value is the same as the passed-in parameter's `:current_user` value *if* that is a Registered User, or the Guest User if it isn't; and
512
+ 2. `start_time` value is a `Time` instance based on the current time when called.
513
+
514
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
515
+
516
+ # API Documentation
517
+
518
+ See [the Documentation Index](/CryptIdent.html).
519
+
520
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
521
+
522
+ # Development
523
+
524
+ After checking out the repo, run `bin/setup` to install dependencies. If you use [`rbenv`](https://github.com/rbenv/rbenv) and [`rbenv-gemset`](https://github.com/jf/rbenv-gemset), the `setup` script will create a new Gemset (in `./tmp/gemset`) to keep your system Gem repository pristine. Then, run `bin/rake test` to run the tests, or `bin/rake` without arguments to run tests and all static-analysis tools ([Flog](https://github.com/seattlerb/flog), [Flay](https://github.com/seattlerb/flay), [Reek](https://github.com/troessner/reek), and [RuboCop](https://github.com/rubocop-hq/rubocop/)). Running `bin/rake inch` will let [Inch](http://trivelop.de/inch/) comment on the amount of internal documentation in the project.
525
+
526
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
527
+
528
+ To install this gem onto your local machine, run `bin/rake install` or `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bin/rake release` or `bundle exec rake release`, which will create a Git tag for the version, push Git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
529
+
530
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
531
+
532
+ # Contributing
533
+
534
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jdickey/crypt_ident. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
535
+
536
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
537
+
538
+ # Copyright and License
539
+
540
+ This Gem, its source code, and all supporting documents and artefacts are Copyright &copy;2019 by Jeff Dickey. They are available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
541
+
542
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>
543
+
544
+ # Code of Conduct
545
+
546
+ Everyone interacting in the CryptIdent project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jdickey/crypt_ident/blob/master/CODE_OF_CONDUCT.md).
547
+
548
+ <sub style="font-size: 0.75rem;">[Back to Top](#CryptIdent)</sub>