forget-passwords 0.2.9
Sign up to get free protection for your applications and to get access to all the features.
- 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).
|