forget-passwords 0.2.9
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 +27 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/LICENSE +202 -0
- data/README.md +603 -0
- data/Rakefile +6 -0
- data/behaviour.org +112 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/content/basic-401.xhtml +15 -0
- data/content/basic-404.xhtml +10 -0
- data/content/basic-409.xhtml +14 -0
- data/content/basic-500.xhtml +10 -0
- data/content/cookie-expired.xhtml +15 -0
- data/content/email-409.xhtml +15 -0
- data/content/email-sent.xhtml +11 -0
- data/content/email.xhtml +10 -0
- data/content/logged-out-all.xhtml +10 -0
- data/content/logged-out.xhtml +10 -0
- data/content/nonce-expired.xhtml +15 -0
- data/content/not-on-list.xhtml +15 -0
- data/content/post-405.xhtml +10 -0
- data/content/uri-409.xhtml +10 -0
- data/etc/text-only.xsl +105 -0
- data/exe/forgetpw +7 -0
- data/forget-passwords.gemspec +67 -0
- data/lib/forget-passwords/cli.rb +514 -0
- data/lib/forget-passwords/fastcgi.rb +28 -0
- data/lib/forget-passwords/state.rb +535 -0
- data/lib/forget-passwords/template.rb +269 -0
- data/lib/forget-passwords/types.rb +118 -0
- data/lib/forget-passwords/version.rb +3 -0
- data/lib/forget-passwords.rb +635 -0
- metadata +312 -0
data/README.md
ADDED
@@ -0,0 +1,603 @@
|
|
1
|
+
# Forget Passwords: Log in the Lazy Way
|
2
|
+
|
3
|
+
**Forget Passwords** is a stand-alone Web authentication module that
|
4
|
+
replicates the "forgot-my-password" user flow, which will, on request,
|
5
|
+
e-mail a special link to an address on a predefined list, in lieu of
|
6
|
+
password-based authentication. This module makes use of [a
|
7
|
+
lesser-known feature of the FastCGI
|
8
|
+
protocol](https://fastcgi-archives.github.io/FastCGI_Specification.html#S6.3)
|
9
|
+
to do its work, and plugs into a receiving end such as [Apache's
|
10
|
+
`mod_authnz_fcgi`](https://httpd.apache.org/docs/2.4/mod/mod_authnz_fcgi.html).
|
11
|
+
|
12
|
+
## Rationale & Goal
|
13
|
+
|
14
|
+
I have various Web properties littered around the internet in various
|
15
|
+
stages of development. Sometimes I want to show these properties to
|
16
|
+
people, but only _certain_ people—an example being both existing and
|
17
|
+
prospective clients.
|
18
|
+
|
19
|
+
Providing meaningful protection to a website almost always entails
|
20
|
+
some kind of authentication mechanism, and unless you go exotic, this
|
21
|
+
almost always means some kind of password. A single, shared password
|
22
|
+
is often inadequate protection because it can be leaked. This means
|
23
|
+
everybody to whom I would want to access one of these Web properties
|
24
|
+
would need their own password.
|
25
|
+
|
26
|
+
In this particular case, I am looking to support a relatively small
|
27
|
+
number of people, the total extent of whom is not necessarily known up
|
28
|
+
front. Under a password-based authentication regime, not only would I
|
29
|
+
be burdening clients and prospects with yet another set of
|
30
|
+
authentication credentials to manage, but I would also be burdening
|
31
|
+
_myself_ with the chore of fielding requests for new accounts* as
|
32
|
+
stragglers trickle in, as well as reset or retrieve lost passwords.
|
33
|
+
|
34
|
+
> \* I suppose I could set up UI for _them_ to create their own
|
35
|
+
> accounts and retrieve their lost passwords, but that would actually
|
36
|
+
> be more work than the solution I propose here, and the net effect
|
37
|
+
> would be to _further_ burden my users.
|
38
|
+
|
39
|
+
The solution to this problem stems from observing that the long tail
|
40
|
+
of Web authentication is serviced by the archetypal **forgot my
|
41
|
+
password** flow _itself_, so why force people to go through the extra
|
42
|
+
step of _creating_, and then _remembering_, a password?
|
43
|
+
|
44
|
+
The goal, then, is to create an authentication module that replicates
|
45
|
+
the forgot-my-password flow, provides about the same security as
|
46
|
+
`Basic` authentication over SSL, has a generic-enough user interface
|
47
|
+
to be merged seamlessly into any existing system, and otherwise
|
48
|
+
interacts minimally with any downstream access control mechanism or
|
49
|
+
Web application, including static content. An additional requirement
|
50
|
+
is that a mapping scheme (e.g. website domain to e-mail domain) can be
|
51
|
+
set up to provision identities (accounts) automatically.
|
52
|
+
|
53
|
+
## How It Works
|
54
|
+
|
55
|
+
This module mainly operates as a FastCGI application in the
|
56
|
+
`AUTHORIZER` role, intended to plug into Apache's `mod_authnz_fcgi` or
|
57
|
+
any workalike, and configured in the server just as one would any
|
58
|
+
other authentication module. In addition to the authentication module,
|
59
|
+
there a couple of dynamic pages (namely login, logout) that need to be
|
60
|
+
surfaced as well. (Their locations are configurable.)
|
61
|
+
|
62
|
+
When unauthenticated users hit the protected area, they are met with a
|
63
|
+
form entreating them to enter their e-mail address. When they submit
|
64
|
+
the form, they are mailed a link with a random token attached to it
|
65
|
+
that provides the authentication. When the user visits the link, the
|
66
|
+
authentication handler trades the token attached to the URL (marking
|
67
|
+
it as used in the process) for a cookie. Responses to subsequent
|
68
|
+
requests then match the cookie to the user's e-mail address, and use
|
69
|
+
that to populate the `REMOTE_USER` field, which can then be picked up
|
70
|
+
by any downstream authorization handler or Web application.
|
71
|
+
|
72
|
+
Users of this system must be pre-authorized. The `forgetpw`
|
73
|
+
command-line tool that ships with this package has a verb for doing
|
74
|
+
this. Since the primary use case for this module is client extranets,
|
75
|
+
and it is customary that everybody at Widgets, Inc., will have a
|
76
|
+
`@widgets.biz` address, entire e-mail domains can be mapped to Web
|
77
|
+
domains. In other words, you can say "grant access to anybody at
|
78
|
+
widgets dot biz to `widgets-inc.extranet.my.company`", and then not
|
79
|
+
have to think subsequently about whether this or that person at the
|
80
|
+
company has access.
|
81
|
+
|
82
|
+
> Also, if necessary, specific addresses can be blocked.
|
83
|
+
|
84
|
+
This module uses an SQL database as its primary storage mechanism. It
|
85
|
+
has been tested with SQLite and PostgreSQL, though in principle it
|
86
|
+
should work with anything for which there is a
|
87
|
+
[Sequel](https://sequel.jeremyevans.net/) driver. Use of SQLite is
|
88
|
+
discouraged in production, due to its well-known inability to handle
|
89
|
+
concurrent transactions. There is a secondary storage in the module
|
90
|
+
itself for the little over a dozen user interface templates. The
|
91
|
+
locations of these (and thus their contents) can be overridden in a
|
92
|
+
configuration file, along with a number of other parameters, a few of
|
93
|
+
which (e.g., data source name, e-mail sender) are necessary for the
|
94
|
+
module to operate.
|
95
|
+
|
96
|
+
## Usage
|
97
|
+
|
98
|
+
To start using ForgetPasswords, we'll assume you have done the necessary
|
99
|
+
setup on the server (below), as well as all the necessary setup for an
|
100
|
+
address to send e-mail from. After that, we'll need a database (this
|
101
|
+
example uses PostgreSQL; you can of course skip this step for SQLite):
|
102
|
+
|
103
|
+
$ createdb forgetpw
|
104
|
+
|
105
|
+
Now we initialize the configuration file and create the tables:
|
106
|
+
|
107
|
+
$ forgetpw -c ~/.forgetpw.yml init -d postgres:///forgetpw \
|
108
|
+
-f noreply@my.company
|
109
|
+
|
110
|
+
> Note: the `init` command uses the `-c` flag as the location to
|
111
|
+
> _write_ a _new_ configuration file, while all other commands use the
|
112
|
+
> flag as the source to _read_ from an existing one. The program
|
113
|
+
> otherwise looks for `forgetpw.yml` in the current directory.
|
114
|
+
|
115
|
+
Now we privilege some e-mail addresses:
|
116
|
+
|
117
|
+
$ forgetpw -c ~/.forgetpw.yml privilege \
|
118
|
+
-d widgets-inc.extranet.my.company widgets.biz some@other.person
|
119
|
+
|
120
|
+
Now, assuming we have configured the server, we start the daemon:
|
121
|
+
|
122
|
+
$ forgetpw -c ~/.forgetpw.yml fcgi
|
123
|
+
Running authenticator daemon on fcgi://localhost:10101/
|
124
|
+
|
125
|
+
> You can use `-z` to detach the process. Listener IP and port are of
|
126
|
+
> course also configurable.
|
127
|
+
|
128
|
+
## Server Configuration
|
129
|
+
|
130
|
+
Currently the only known receptacle for this module is
|
131
|
+
`mod_authnz_fcgi`, which ships with Apache, though the interface is
|
132
|
+
standard (to the extent that FastCGI is a standard), and so in
|
133
|
+
principle it is usable in other systems. What follows is the
|
134
|
+
configuration for Apache 2.4.x or newer.
|
135
|
+
|
136
|
+
First, we need to declare the authenticator (here it can be called
|
137
|
+
anything but we are appropriately calling it `ForgetPasswords`) and where
|
138
|
+
it's listening:
|
139
|
+
|
140
|
+
```apache
|
141
|
+
AuthnzFcgiDefineProvider authn ForgetPasswords fcgi://localhost:10101/
|
142
|
+
```
|
143
|
+
|
144
|
+
> On Debian systems and their derivatives, this is in a separate file,
|
145
|
+
> `mods-available/authnz_fcgi.conf`. Note that you will also have to
|
146
|
+
> `a2enmod authnz_fcgi` or none of this configuration will work.
|
147
|
+
|
148
|
+
Then, in the virtual host (or main server configuration in lieu
|
149
|
+
thereof), we can use any standard configuration mechanism we want to
|
150
|
+
delineate the protected area. We invoke the module with the
|
151
|
+
`AuthnzFcgiCheckAuthnProvider` directive, and then tune it with
|
152
|
+
`Require`. `mod_authnz_fcgi` has a number of idiosyncrasies, one of
|
153
|
+
which is that it always must return a user, so we have to give it a
|
154
|
+
throwaway user like `nobody`, and then subsequently deny that user. (I
|
155
|
+
would consider this a design flaw in `mod_authnz_fcgi`.) The
|
156
|
+
expression `%{reqenv:FCGI_USER}` (where the slug `FCGI_USER` is
|
157
|
+
configurable on our side) is how the identity gets transmitted
|
158
|
+
upstream from ForgetPasswords to the server.
|
159
|
+
|
160
|
+
```apache
|
161
|
+
<Location /protected>
|
162
|
+
# unfortunately mod_authnz_fcgi won't let you have a blank default user
|
163
|
+
AuthnzFcgiCheckAuthnProvider ForgetPasswords Authoritative On RequireBasicAuth Off UserExpr "%{reqenv:FCGI_USER}" DefaultUser nobody
|
164
|
+
<RequireAll>
|
165
|
+
Require valid-user
|
166
|
+
# that's fine, we just outlaw 'nobody'
|
167
|
+
Require not user nobody
|
168
|
+
</RequireAll>
|
169
|
+
</Location>
|
170
|
+
```
|
171
|
+
|
172
|
+
Another idiosyncrasy of `mod_authnz_fcgi` is that while it uses the
|
173
|
+
`200` response code to indicate a success, the _actual_ response back
|
174
|
+
to the client necessarily has to come from the downstram content
|
175
|
+
handler. As such, any other information from a _successful_
|
176
|
+
authentication response needs to be smuggled out through environment
|
177
|
+
variables. Since ForgetPasswords performs a redirect to remove the
|
178
|
+
authentication token from the URL upon successful authentication, the
|
179
|
+
following `mod_rewrite` configuration needs to be in place to turn the
|
180
|
+
environment variable back into an actual redirect:
|
181
|
+
|
182
|
+
```apache
|
183
|
+
RewriteCond %{QUERY_STRING} .+
|
184
|
+
RewriteCond %{ENV:FCGI_REDIRECT} .+
|
185
|
+
RewriteRule .* %{ENV:FCGI_REDIRECT} [R=307,L,QSD]
|
186
|
+
```
|
187
|
+
|
188
|
+
> Note that `mod_rewrite` syntax is different from `ap_expr` syntax,
|
189
|
+
> and the prefix `ENV` is used in the expression instead of `reqenv`
|
190
|
+
> above. We also use `QSD` to remove the query string from the
|
191
|
+
> _currently-requested_ URI, and redirect with `307` to preserve the
|
192
|
+
> request method.
|
193
|
+
>
|
194
|
+
> Note as well that URL rewriting typically happens _before_
|
195
|
+
> authorization, so the environment variable will not yet be set
|
196
|
+
> unless you trick the `RewriteRule` to run in a later phase (e.g. by
|
197
|
+
> putting it in a `<Directory>` block or `.htaccess`).
|
198
|
+
|
199
|
+
We also need to account for _unsuccessful_ responses from the
|
200
|
+
authentication module, since certain headers (notably `Content-Type`)
|
201
|
+
are either getting overwritten by an unfortunate interaction with the
|
202
|
+
default error handler, or are otherwise not being transmitted (which
|
203
|
+
would be another bug in `mod_authnz_fcgi`).
|
204
|
+
|
205
|
+
```apache
|
206
|
+
Header always set Content-Type "expr=%{resp:Variable-FCGI_CONTENT_TYPE}" "expr=%{resp:Variable-FCGI_CONTENT_TYPE} != ''"
|
207
|
+
Header always unset Variable-FCGI_CONTENT_TYPE
|
208
|
+
```
|
209
|
+
|
210
|
+
Finally, the module provides two dynamic resources that need to be
|
211
|
+
mapped to content handlers; here we use `mod_proxy_fcgi` (remember to
|
212
|
+
enable it):
|
213
|
+
|
214
|
+
```apache
|
215
|
+
ProxyPass /email-link fcgi://localhost:10101/email-link
|
216
|
+
ProxyPass /logout fcgi://localhost:10101/logout
|
217
|
+
```
|
218
|
+
|
219
|
+
> An earlier design had these operations controlled exclusively by
|
220
|
+
> `POST` parameters on _any_ resource, and therefore these
|
221
|
+
> purpose-made resources were ostensibly not necessary. However, it
|
222
|
+
> turns out that `mod_authnz_fcgi` does not convey request body
|
223
|
+
> content to the downstream FastCGI script, causing the latter to
|
224
|
+
> crash with a protocol error. While the handling is less than
|
225
|
+
> delicate, this is actually a reasonable expectation, as request
|
226
|
+
> bodies are only read once off the wire and will thus be already
|
227
|
+
> consumed (whether or not they contain the fields to which ForgetPasswords
|
228
|
+
> is sensitive) when the content handler is invoked. (The way Apache
|
229
|
+
> handles the request body, it _can_ be duplicated and reinserted into
|
230
|
+
> the input stream, but that is a whole project unto itself.
|
231
|
+
|
232
|
+
## Templates
|
233
|
+
|
234
|
+
ForgetPasswords has a number of UI states that are embedded in the gem. These
|
235
|
+
take the form of template files. The functionality of these templates
|
236
|
+
is currently at the absolute bare minimum required to do the job. The
|
237
|
+
templates are XHTML, with a basic placeholder substitution
|
238
|
+
functionality, which can take place either in processing instructions
|
239
|
+
(`<?var $WHATEVER?>`), or attribute values (`<elem
|
240
|
+
attr="$WHATEVER"/>`).
|
241
|
+
|
242
|
+
> I did this deliberately for a few reasons, the first being that the
|
243
|
+
> substitutions occur in a way such that the input _and_ the output
|
244
|
+
> always validates, i.e., there is no way to produce broken markup.
|
245
|
+
> The second is that this system neither needs nor merits a more
|
246
|
+
> sophisticated templating system. Each state is directly addressable;
|
247
|
+
> it gets its own template file. Anything that needs to be addressed
|
248
|
+
> in any individual state, save for a small number of substitutions in
|
249
|
+
> text nodes or attribute values, can be done by supplanting its file
|
250
|
+
> with a different one. Any styling or page composition needed to knit
|
251
|
+
> these states into their surroundings can be handled through an
|
252
|
+
> exterior mechanism, which I will endeavour to write up separately. I
|
253
|
+
> may consider different or additional template mechanisms
|
254
|
+
> (e.g. markdown, or any of the zillion non-standard template engines)
|
255
|
+
> at some point in the future.
|
256
|
+
>
|
257
|
+
> Note as well that the templates are not currently internationalized,
|
258
|
+
> but I am open to making them so if there is sufficient demand.
|
259
|
+
|
260
|
+
The configuration parameter `transform` under `templates` will cause
|
261
|
+
an `xml-stylesheet` processing instruction to be inserted into all
|
262
|
+
outgoing templates with the location of an XSLT stylesheet, enabling
|
263
|
+
arbitrary manipulations (and also the main reason why these templates
|
264
|
+
are XHTML and not regular HTML).
|
265
|
+
|
266
|
+
> **NOTE 2022-04-22** this `forgetpw extract` business is still
|
267
|
+
> under construction.
|
268
|
+
|
269
|
+
The default templates for all states are embedded in the gem
|
270
|
+
distribution, and can be overridden individually or en masse in the
|
271
|
+
configuration file by specifying the location of a supplanting file.
|
272
|
+
The command-line verb `forgetpw extract $DESTINATION` will extract
|
273
|
+
the full set of templates from the gem, and deposit copies of them
|
274
|
+
wherever you tell it to.
|
275
|
+
|
276
|
+
In addition to these templates that get piped out from arbitrary
|
277
|
+
locations, there are a couple resources, namely two logout states
|
278
|
+
(`/logged-out` for current device; `/logged-out-all` for all devices),
|
279
|
+
which can be completely static. Boilerplate for these states is
|
280
|
+
included in the distribution and can be retrieved by running
|
281
|
+
`forgetpw extract --static`. The URLs of these resources can
|
282
|
+
naturally be overridden in the configuration file.
|
283
|
+
|
284
|
+
> Out of an abundance of prudence I should also remark that to
|
285
|
+
> eliminate file extensions in static resources (at least in Apache),
|
286
|
+
> enable `mod_negotiation` and add `MultiViews` to any `Options`
|
287
|
+
> directive in scope.
|
288
|
+
|
289
|
+
What follows is the list of states, when they show up, and roughly
|
290
|
+
what they say. Most of them are specific error conditions:
|
291
|
+
|
292
|
+
### `default_401` (currently handled by `basic-401.xhtml`)
|
293
|
+
|
294
|
+
This page is the one everybody sees when they are not logged in,
|
295
|
+
unless a more specific page is more appropriate. It explains that the
|
296
|
+
area is protected, and the way to get access (assuming that you're on
|
297
|
+
the list) is to enter your e-mail address. It then provides said
|
298
|
+
form. Note that the `action=` of the form **must** point to the
|
299
|
+
location of the `email-link` resource, and there must also be a hidden
|
300
|
+
form field by the name of `forward` that contains the current URL.
|
301
|
+
|
302
|
+
### `default_404` (currently handled by `basic-404.xhtml`)
|
303
|
+
|
304
|
+
This resource should actually never be seen, as it currently only
|
305
|
+
arises when outside content-handling traffic is directed to locations
|
306
|
+
other than the two specified by ForgetPasswords.
|
307
|
+
|
308
|
+
### `knock_bad` (currently handled by `basic-409.xhtml`)
|
309
|
+
|
310
|
+
This is shown when the knock-knock token attached to the URL is
|
311
|
+
malformed. It is an undifferentiated `409 Conflict` message, which
|
312
|
+
also includes a form like the one found in the default `401`.
|
313
|
+
|
314
|
+
### `knock_not_found` (currently handled by `basic-409.xhtml`)
|
315
|
+
|
316
|
+
This is shown when the token is _not_ malformed, but also not present
|
317
|
+
in the database. (This is treated as a `403 Forbidden`, but the error
|
318
|
+
message is not meaningfully different from `409`, so it gets the same
|
319
|
+
message by default.)
|
320
|
+
|
321
|
+
### `knock_expired` (currently handled by `nonce-expired.xhtml`)
|
322
|
+
|
323
|
+
Here, the token attached to the link sent out in the e-mail has
|
324
|
+
expired, i.e., the user has not claimed it in time (by default, 10
|
325
|
+
minutes). Again we notify themm, and show them the form to generate a
|
326
|
+
new one.
|
327
|
+
|
328
|
+
### `cookie_bad` (currently handled by `basic-409.xhtml`)
|
329
|
+
|
330
|
+
This recapitulates the `knock_bad` scenario, but with a cookie.
|
331
|
+
|
332
|
+
### `cookie_not_found` (currently handled by `basic-409.xhtml`)
|
333
|
+
|
334
|
+
The cookie equivalent of `knock_not_found`.
|
335
|
+
|
336
|
+
### `cookie_expired` (currently handled by `cookie-expired.xhtml`)
|
337
|
+
|
338
|
+
This message is shown when the user has a cookie which has been
|
339
|
+
invalidated either by a logout or has been expired on the server
|
340
|
+
side. The user is given an opportunity to log back in.
|
341
|
+
|
342
|
+
### `no_user` (currently handled by `not-on-list.xhtml`)
|
343
|
+
|
344
|
+
This message is returned when the cookie is valid but the user is not,
|
345
|
+
e.g. their access was revoked since they hit the site last. They are
|
346
|
+
given an opportunity to log back in.
|
347
|
+
|
348
|
+
### `forward_bad` (currently handled by `uri-409.xhtml`)
|
349
|
+
|
350
|
+
This message is shown as the result of the user submitting their
|
351
|
+
e-mail when the forwarding address (URL), which should have been
|
352
|
+
included in the submitted form, is malformed (e.g. does not match the
|
353
|
+
domain). This is nominally a client error but it should never be
|
354
|
+
reached by normal operation. The only way a user would get here is a
|
355
|
+
misconfiguration on our part, or an attempt at abuse. We tell them to
|
356
|
+
go back and try again.
|
357
|
+
|
358
|
+
### `email` (currently handled by `email.xhtml`)
|
359
|
+
|
360
|
+
This is the actual e-mail that gets sent to the user. Note that the
|
361
|
+
`<title>` gets turned into the subject, and the entire thing is also
|
362
|
+
stripped to plain text.
|
363
|
+
|
364
|
+
### `email_bad` (currently handled by `email-409.xhtml`)
|
365
|
+
|
366
|
+
This status is returned after a user submits an e-mail address that is
|
367
|
+
syntactically bad.
|
368
|
+
|
369
|
+
### `email_not_listed` (currently handled by `not-on-list.xhtml`)
|
370
|
+
|
371
|
+
This happens when the e-mail address is not on the permit list. Users
|
372
|
+
are given an opportunity to try a different one.
|
373
|
+
|
374
|
+
### `email_failed` (currently handled by `basic-500.xhtml`)
|
375
|
+
|
376
|
+
This happens when the e-mailing process _itself_ fails, e.g. when the
|
377
|
+
script can't connectd to the specified SMTP server.
|
378
|
+
|
379
|
+
### `email_sent` (currently handled by `email-sent.xhtml`)
|
380
|
+
|
381
|
+
This is the confirmation page people see when ForgetPasswords has accepted
|
382
|
+
their e-mmail address and sent the link-containing e-mail.
|
383
|
+
|
384
|
+
### `post_only` (currently handled by `post-405.xhtml`)
|
385
|
+
|
386
|
+
This error only occurs when somebody tries to access one of the two
|
387
|
+
targets (by default `/email-link` and `/logout`) by a request method
|
388
|
+
other than `POST`, which should never happen outside of normal operation.
|
389
|
+
|
390
|
+
## All Configuration Options
|
391
|
+
|
392
|
+
### `host`
|
393
|
+
|
394
|
+
The host to listen on; defaults (as expected) to `localhost`.
|
395
|
+
|
396
|
+
### `port`
|
397
|
+
|
398
|
+
The TCP port, default `10101`.
|
399
|
+
|
400
|
+
### `state`
|
401
|
+
|
402
|
+
This is the configuration group involving the persistent state,
|
403
|
+
i.e. the database.
|
404
|
+
|
405
|
+
* `dsn` is the DSN (data source name), i.e., the connection string
|
406
|
+
that gets passed into Sequel.
|
407
|
+
* `user` is the user name, which can be rolled into the DSN or
|
408
|
+
separated out.
|
409
|
+
* Same goes for the `password`.
|
410
|
+
* `options` are additional options that get passed directly to the
|
411
|
+
Sequel constructor.
|
412
|
+
* `expiry` deals with the expiration times of the different kinds of
|
413
|
+
token, which are represented as ISO 8601 durations:
|
414
|
+
* `query` handles the expiry for the token in the link's query
|
415
|
+
string, defaulting to 10 minutes (`PT10M`)
|
416
|
+
* `cookie` handles the expiry for the cookie, defaulting to two weeks
|
417
|
+
(which gets refreshed by accessing the site; `P2W`)
|
418
|
+
|
419
|
+
### `keys`
|
420
|
+
|
421
|
+
These are overrides for different keys in query strings and HTML
|
422
|
+
forms.
|
423
|
+
|
424
|
+
* `query` is the key for the URL query string component that contains
|
425
|
+
the nonce token; it defaults to `knock`.
|
426
|
+
* `cookie` is the key for the cookie, which defaults to `forgetpw`.
|
427
|
+
* `email` is the form key where the user's e-mail address is expected,
|
428
|
+
defaulting to `email`.
|
429
|
+
* `logout` is the form key which would be set to something true-ish
|
430
|
+
(`true`, `yes`, `on`, `1`) for whether to log out all tokens or just
|
431
|
+
the current one, defaulting to `logout`.
|
432
|
+
|
433
|
+
### `vars`
|
434
|
+
|
435
|
+
These are overrides for the names of the environment variables that
|
436
|
+
are handed back to `mod_authnz_fcgi`, in case anything collides with
|
437
|
+
an existing setup and needs to be called something else.
|
438
|
+
|
439
|
+
* `user`is what gets retrieved and turned into `REMOTE_USER`,
|
440
|
+
defaulting to `FCGI_USER`.
|
441
|
+
* `redirect` is what gets retrieved and turned into a `Location:`
|
442
|
+
header, defaulting to `FCGI_REDIRECT`.
|
443
|
+
* `type` is what gets retrieved and turned into a `Content-Type:`
|
444
|
+
header, defaulting to `FCGI_CONTENT_TYPE`.
|
445
|
+
|
446
|
+
### `targets`
|
447
|
+
|
448
|
+
These are (relative, but not necessarily) URLs to pages that perform
|
449
|
+
specific functions within the system, and have a stable location.
|
450
|
+
|
451
|
+
* `login` is the target that accepts the `POST` request from the `401`
|
452
|
+
page and others, that sends the e-mail and issues a confirmation. It
|
453
|
+
defaults to `/email-link`. This resource is powered by ForgetPasswords and
|
454
|
+
is used internally to configure the location of that resource.
|
455
|
+
* `logout` is the target that accepts the `POST` request to log
|
456
|
+
out. It (rather predictably) defaults to `/logout`. This location is
|
457
|
+
also handled by ForgetPasswords.
|
458
|
+
* `logout_one` is a _static_ (or other arbitrary) target (i.e., _not_
|
459
|
+
handled by ForgetPasswords) that confirms the user has logged out their
|
460
|
+
current session. It defaults to `/logged-out`.
|
461
|
+
* `logout_all` is another static target that confirms the user has
|
462
|
+
logged out of all devices.
|
463
|
+
|
464
|
+
### `templates`
|
465
|
+
|
466
|
+
This is configuration for the various templates.
|
467
|
+
|
468
|
+
* `path` is the template root, that defaults to `content/` under the
|
469
|
+
gem root.
|
470
|
+
* `transform` is the URL of an XSLT stylesheet. Omitted if omitted.
|
471
|
+
* `mapping` is a key-value structure of templates (listed above) to
|
472
|
+
file names, relative to `path`.
|
473
|
+
|
474
|
+
### `email`
|
475
|
+
|
476
|
+
This is configuration for the e-mail sender.
|
477
|
+
|
478
|
+
* `from` is the sender's address; it has no default.
|
479
|
+
* `method` is how the sender will send mail, defaults to `sendmail`.
|
480
|
+
* `options` is a key-value structure of additional options, e.g. for
|
481
|
+
when the `method` is `smtp`. It is fed directly into
|
482
|
+
`Mail::Message#delivery_method`.
|
483
|
+
|
484
|
+
## Minimal Configuration
|
485
|
+
|
486
|
+
This is the absolute bare minimum configuration you will need supply
|
487
|
+
directly. All other values have defaults:
|
488
|
+
|
489
|
+
```yaml
|
490
|
+
state:
|
491
|
+
dsn: whatever://database
|
492
|
+
templates:
|
493
|
+
# this is actually optional, but there is no default value.
|
494
|
+
transform: /transform.xsl
|
495
|
+
email:
|
496
|
+
from: robot@my.company
|
497
|
+
# additional SMTP configuration would go here, if applicable.
|
498
|
+
```
|
499
|
+
|
500
|
+
## Future Directions
|
501
|
+
|
502
|
+
This project began on something of a lark, with the intent to make a
|
503
|
+
quick-and-easy passwordless authentication mechanism with zero UI, or
|
504
|
+
rather, _I_ was the UI, manually e-mailing magic links to people. What
|
505
|
+
I found when I put this scheme into production was that people balked
|
506
|
+
because the experience was actually *too* seamless: a prospective
|
507
|
+
client insisted on believing a confidential proposal was just out on
|
508
|
+
the open internet for anybody to see, even though this was not the
|
509
|
+
case. As a result, I shelved this code for three years because I
|
510
|
+
didn't have time to do what was necessary to ameliorate it.
|
511
|
+
|
512
|
+
What I had here was an _optics_ problem: the user needs to _see_ that
|
513
|
+
the content is protected, and logging in has to be a positive action;
|
514
|
+
something that they _do_. This meant going from _zero_ UI, to rather
|
515
|
+
quite a bit of it. As such, I anticipate what was once a one-off
|
516
|
+
endeavour is now a significant Project™ that will have to be
|
517
|
+
maintained and expanded upon.
|
518
|
+
|
519
|
+
What follows are some remarks around where things might go.
|
520
|
+
|
521
|
+
### How about a test suite?
|
522
|
+
|
523
|
+
My philosophy around automated tests is that they are useful for
|
524
|
+
ensuring the behaviour of a piece of code without having to look
|
525
|
+
directly at it. In my experience, getting little products like these
|
526
|
+
to a functioning state is *system*-heavy, which has a crapload of
|
527
|
+
overhead setting up a test regime, and furthermore the various
|
528
|
+
constituent parts either very obviously work or very obviously do
|
529
|
+
not. In other words, eyeballing it is a perfectly satisfactory quality
|
530
|
+
assurance regime in the early stages of development (at least until it
|
531
|
+
gets out of hand, which in this case it didn't). Now that it works (as
|
532
|
+
of 2022-04-22), the focus can shift to keeping it that way.
|
533
|
+
|
534
|
+
### How about expanding out the templates?
|
535
|
+
|
536
|
+
Localizing the templates is definitely a possibility, as well as
|
537
|
+
making domain-specific overrides so a single ForgetPasswords daemon could
|
538
|
+
handle multiple domains with tailor-fit responses for each. I am less
|
539
|
+
sanguine about going hog-wild with the templates but I could see some
|
540
|
+
kind of future plug-in interface so people could use their favourite
|
541
|
+
flavour of templating engine.
|
542
|
+
|
543
|
+
### Reconcile with OAuth
|
544
|
+
|
545
|
+
The authentication cookie used by ForgetPasswords bears a striking
|
546
|
+
resemblance to an [OAuth](https://oauth.net/) bearer token, such that
|
547
|
+
could actually _be_ (at least a proxy for) an OAuth bearer token.
|
548
|
+
Indeed, bearer tokens would make for an _excellent_ cleavage plane for
|
549
|
+
_segmented_ authentication: Method X to bearer token, then bearer
|
550
|
+
token to `REMOTE_USER`. This means we could have multiple
|
551
|
+
authentication mechanisms (ForgetPasswords, OAuth, X.509, Kerberos, boring
|
552
|
+
old password, whatever) operating in the same space at once.
|
553
|
+
|
554
|
+
### The really interesting thing is `mod_authnz_fcgi`
|
555
|
+
|
556
|
+
At least in principle. The actual module itself is a bit of a dog
|
557
|
+
(although not un-groomable), but the fact that the FastCGI people had
|
558
|
+
the presence of mind to design modes for things other than content
|
559
|
+
(there is a `FILTER` role as well) is actually quite interesting.
|
560
|
+
|
561
|
+
The vast majority of Web development happens exclusively inside what
|
562
|
+
can be termed a _content handler_. This is where all server-side
|
563
|
+
platforms and frameworks operate. In reality, Web servers (like Apache
|
564
|
+
and `nginx`) have a number of phases, most of them happening _before_
|
565
|
+
the content handler, that can be addressed directly—provided you write
|
566
|
+
your module in C. What `mod_authnz_fcgi` does is tap the
|
567
|
+
_authentication_ phase of Apache's request-handling loop and open it
|
568
|
+
up to cheap scripts written in any language that speak FastCGI. This
|
569
|
+
means that stand-alone modules like ForgetPasswords can be used in
|
570
|
+
conjunction with *any* downstream Web application framework or
|
571
|
+
development strategy. Some additional observations:
|
572
|
+
|
573
|
+
* **It doesn't have to be FastCGI**: There is really no reason in
|
574
|
+
principle why, with some creative reading of the HTTP protocol, that
|
575
|
+
this functionality couldn't be handled 100% by a stand-alone Web
|
576
|
+
service that the main workhorse server proxies to.
|
577
|
+
* **This could be done for any phase**: Using said creative reading of
|
578
|
+
the HTTP protocol, this puts _any_ phase in the request-handling
|
579
|
+
process, for either Apache _or_ `nginx`, on the table, assuming the
|
580
|
+
appropriate module (in C) is written for each.
|
581
|
+
|
582
|
+
So, yeah, _big_ opportunity there to take modularity in Web
|
583
|
+
development to the next level.
|
584
|
+
|
585
|
+
## Installation
|
586
|
+
|
587
|
+
You know how to do this:
|
588
|
+
|
589
|
+
$ gem install forget-passwords
|
590
|
+
|
591
|
+
Or, [download it off rubygems.org](https://rubygems.org/gems/forget-passwords).
|
592
|
+
|
593
|
+
## Contributing
|
594
|
+
|
595
|
+
Bug reports and pull requests are welcome at
|
596
|
+
[the GitHub repository](https://github.com/doriantaylor/rb-forget-passwords).
|
597
|
+
|
598
|
+
## Copyright & License
|
599
|
+
|
600
|
+
©2019-2022 [Dorian Taylor](https://doriantaylor.com/)
|
601
|
+
|
602
|
+
This software is provided under
|
603
|
+
the [Apache License, 2.0](https://www.apache.org/licenses/LICENSE-2.0).
|