forget-passwords 0.2.12 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3215dc2df30a7f53896530fe46e576ccc68e69a9bdeb9dfdcc1d8d55f482077
4
- data.tar.gz: 56479273ba73c937bda4449b3a9ebbae2aa41736cf8e01bc58c3c209f3179b51
3
+ metadata.gz: dacb56a002005cc14bb46809690604cccd905eee5c5fc8c5efb78557629611e6
4
+ data.tar.gz: 704b521acb64f33a934c8768e8bc1496a86ae55d27a0598f4172a7ddc6c8d0e0
5
5
  SHA512:
6
- metadata.gz: 4c95ee03bc4b33e7bce7ef05ea11633defac41664eecec042b0ff15ccdd2ef5dbd95ad3faa9336e9f2dcbd583adf0db4cf1674a585007027a31c73d3a732e9b9
7
- data.tar.gz: 4f98e7c9feb6fd09470c93f820ff5aca5bdfaa27a4e845e6a8efb484cde2ff319224cb3a18435c805ac4998f0ad1f6975d22c8ccb813e76343beadc981625de3
6
+ metadata.gz: 1e79d40cbf1151f1a6fb056e17e90f022f65cf575e9eac4a2d1931b4c9c32723924a7e27f83ec893acbafa46710c6da68de3e2791005066ba74eea2a880f0f39
7
+ data.tar.gz: c306b865ed182f590338d26a7a41a165c38626519dbfc627b77fac4f8a271f7a9d83274a10ec8ae7629a9992407fe92524a4b2a9eb5e4c74ebf1b3b5907e67de
data/README.md CHANGED
@@ -524,6 +524,15 @@ email:
524
524
  # additional SMTP configuration would go here, if applicable.
525
525
  ```
526
526
 
527
+ ## Alternate Authentication Methods
528
+
529
+ It is possible to take the token in the cookie and feed it in as
530
+ either a `Basic` authentication password or `Bearer` token. In the
531
+ case of `Basic`, the username is ignored. This enables `curl` or API
532
+ access, or other automated things like feed readers. There is
533
+ currently no UI for this, but an "app password" management screen is
534
+ potentially on the horizon.
535
+
527
536
  ## Future Directions
528
537
 
529
538
  This project began on something of a lark, with the intent to make a
@@ -569,9 +578,11 @@ favourite flavour of templating engine.
569
578
 
570
579
  ### Reconcile with OAuth
571
580
 
572
- The authentication cookie used by Forget Passwords bears a striking
573
- resemblance to an [OAuth](https://oauth.net/) bearer token, such that
574
- could actually _be_ (at least a proxy for) an OAuth bearer token.
581
+ Let's face it: this thing is 98% of what [OAuth](https://oauth.net/)
582
+ does: it trades one token for another over a more-or-less secure side
583
+ channel. It could be made a heck of a lot simpler by just…wrapping
584
+ OAuth.
585
+
575
586
  Indeed, bearer tokens would make for an _excellent_ cleavage plane for
576
587
  _segmented_ authentication: Method X to bearer token, then bearer
577
588
  token to `REMOTE_USER`. This means we could have multiple
@@ -4,6 +4,7 @@ require 'xml-mixup'
4
4
  require 'http-negotiate'
5
5
  require 'forget-passwords/types'
6
6
  require 'uri'
7
+ require 'time'
7
8
 
8
9
  module ForgetPasswords
9
10
 
@@ -18,6 +19,8 @@ module ForgetPasswords
18
19
  DEFAULT_PATH = (
19
20
  Pathname(__FILE__).parent + '../../content').expand_path.freeze
20
21
 
22
+ # this is a default document root subpath
23
+ DEFAULT_DOCROOT = '.forgetpw'.freeze
21
24
 
22
25
  # Normalize the input to symbol `:like_this`.
23
26
  #
@@ -57,21 +60,67 @@ module ForgetPasswords
57
60
  @path = Pathname(path).expand_path
58
61
  @base = base
59
62
  @transform = transform
60
- @mapping = mapping.map do |k, v|
61
- name = normalize k
62
- template = v.is_a?(ForgetPasswords::Template) ? v :
63
- ForgetPasswords::Template.new(self, k, @path + v)
64
- [name, template]
65
- end.to_h
63
+ @mapping = mapping
64
+ @templates = {
65
+ @path => mapping.map do |k, v|
66
+ name = normalize k
67
+ template = v.is_a?(ForgetPasswords::Template) ? v :
68
+ ForgetPasswords::Template.new(self, k, @path + v)
69
+ [name, template]
70
+ end.to_h
71
+ }
66
72
  end
67
73
 
68
- def [] key
69
- @mapping[normalize key]
74
+ # Fetch the appropriate template, optionally relative to a given root.
75
+ #
76
+ # @param key [Symbol, #to_sym] the template key.
77
+ # @param root [nil, String] an optional document root.
78
+ #
79
+ # @return [nil, ForgetPasswords::Template] a template
80
+ #
81
+ def [] key, root = nil
82
+ key = normalize key
83
+ # bail early if we don't know the key
84
+ return unless @mapping[key]
85
+
86
+ # obtain optional root
87
+ root = if root
88
+ r = root.respond_to?(:env) ? root.env['DOCUMENT_ROOT'] : root
89
+ r = (Pathname(r) + DEFAULT_DOCROOT).expand_path
90
+ r.readable? ? r : nil
91
+ end
92
+
93
+ if root
94
+ # get the full file path
95
+ fp = root + @mapping[key]
96
+ if fp.readable?
97
+ mt = fp.mtime
98
+ rootmap = @templates[root] ||= {}
99
+ template = rootmap[key]
100
+
101
+ # congratulations, you found it
102
+ return template if template and mt <= template.modified
103
+
104
+ # XXX this could explode obvs
105
+ begin
106
+ template = ForgetPasswords::Template.new(self, key, fp, mt)
107
+ rootmap[key] = template
108
+ return template
109
+ rescue
110
+ # XXX duhh what do we do here
111
+ nil
112
+ end
113
+ end
114
+ end
115
+
116
+ # otherwise just return the default
117
+ @templates[@path][key]
70
118
  end
71
119
 
72
120
  def []= key, path
73
121
  name = normalize key
74
- @mapping[name] = path.is_a?(ForgetPasswords::Template) ? path :
122
+ # XXX do something less dumb here
123
+ @templates[@path][name] = path.is_a?(ForgetPasswords::Template) ? path :
75
124
  ForgetPasswords::Template.new(self, key, @path + path)
76
125
  end
77
126
 
@@ -132,12 +181,13 @@ module ForgetPasswords
132
181
 
133
182
  public
134
183
 
135
- attr_reader :name, :doc, :mapper
184
+ attr_reader :name, :doc, :mapper, :modified
136
185
 
137
- def initialize mapper, name, content
186
+ def initialize mapper, name, content, modified = Time.now
138
187
  # boring members
139
- @mapper = mapper
140
- @name = name
188
+ @mapper = mapper
189
+ @name = name
190
+ @modified = modified
141
191
 
142
192
  # resolve content
143
193
  @doc = case content
@@ -253,6 +303,7 @@ module ForgetPasswords
253
303
  # @return [Rack::Response] the response object, updated in place
254
304
  #
255
305
  def populate resp, headers = {}, vars = {}, base: nil
306
+
256
307
  if (body, type = serialize(
257
308
  process(vars: vars, base: base), headers, full: true))
258
309
  #resp.length = body.bytesize # not sure if necessary
@@ -1,3 +1,3 @@
1
1
  module ForgetPasswords
2
- VERSION = '0.2.12'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -11,6 +11,7 @@ require 'rack'
11
11
  require 'rack/request'
12
12
  require 'rack/response'
13
13
 
14
+ require 'base64'
14
15
  require 'mail'
15
16
 
16
17
  module ForgetPasswords
@@ -310,7 +311,7 @@ module ForgetPasswords
310
311
  }
311
312
 
312
313
  # grab the template since we'll use it
313
- template = @templates[:email]
314
+ template = @templates[:email, req]
314
315
 
315
316
  # process the templates
316
317
  doc = template.process vars: vars
@@ -335,7 +336,7 @@ module ForgetPasswords
335
336
  uri = req_uri req
336
337
  resp = Rack::Response.new
337
338
  resp.status = status
338
- @templates[key].populate resp, req, vars, base: uri
339
+ @templates[key, req].populate resp, req, vars, base: uri
339
340
  resp.set_header "Variable-#{@vars[:type]}", resp.content_type
340
341
  raise ForgetPasswords::ErrorResponse, resp
341
342
  end
@@ -346,7 +347,7 @@ module ForgetPasswords
346
347
  uri = req_uri req
347
348
  resp = Rack::Response.new
348
349
  resp.status = 401
349
- @templates[:default_401].populate resp, req, {
350
+ @templates[:default_401, req].populate resp, req, {
350
351
  FORWARD: req_uri(req).to_s, LOGIN: @targets[:login] }, base: uri
351
352
  resp.set_header "Variable-#{@vars[:type]}", resp.content_type
352
353
  resp
@@ -385,7 +386,7 @@ module ForgetPasswords
385
386
  target != uri # (note this should always be true)
386
387
  resp.set_cookie @keys[:cookie], {
387
388
  value: token, secure: req.ssl?, httponly: true,
388
- domain: uri.host, path: ?/, same_site: :strict,
389
+ domain: uri.host, path: ?/, same_site: :lax, # strict is too strict
389
390
  expires: time_delta(@state.expiry[:cookie]),
390
391
  }
391
392
 
@@ -443,7 +444,7 @@ module ForgetPasswords
443
444
  # update the cookie expiration
444
445
  resp.set_cookie @keys[:cookie], {
445
446
  value: token, secure: req.ssl?, httponly: true,
446
- domain: uri.host, path: ?/, same_site: :strict,
447
+ domain: uri.host, path: ?/, same_site: :lax, # strict is too strict
447
448
  expires: time_delta(@state.expiry[:cookie], now),
448
449
  }
449
450
 
@@ -494,7 +495,7 @@ module ForgetPasswords
494
495
 
495
496
  # return 200 now because this is now a content handler
496
497
  resp.status = 200
497
- @templates[:email_sent].populate resp, req, {
498
+ @templates[:email_sent, req].populate resp, req, {
498
499
  FORWARD: forward.to_s, FROM: @email[:from].to_s, EMAIL: address.to_s },
499
500
  base: uri
500
501
  end
@@ -531,27 +532,33 @@ module ForgetPasswords
531
532
  end
532
533
 
533
534
  def handle_auth req
534
- if auth = req.get_header('Authorization')
535
- token = if req.basic?
536
- # auto-decodes (XXX do we care about the username??)
537
- req.credentials.last
538
- elsif auth.strip.downcase.start_with? 'bearer'
539
- auth.strip.split[1]
535
+ auth = req.get_header('Authorization') || req.env['HTTP_AUTHORIZATION']
536
+ if auth and !auth.strip.empty?
537
+ # warn "has authorization header #{auth}"
538
+ mech, *auth = auth.strip.split
539
+ token = case mech.downcase
540
+ when 'basic'
541
+ # can't trust/use rack here
542
+ Base64.decode64(auth.first || '').split(?:, 2).last
543
+ when 'bearer'
544
+ auth.first
540
545
  end
546
+
541
547
  if token
542
548
  handle_token req, token
543
549
  else
544
- # XXX one day maybe this can be more descriptive??
545
550
  default_401 req
546
551
  end
547
552
  elsif knock = req.GET[@keys[:query]]
548
- # check for a knock first; this overrides everything
553
+ # check for a knock; this overrides an existing cookie
554
+ # warn "has knock token #{knock}"
549
555
  handle_knock req, knock
550
556
  # elsif req.post?
551
557
  # # next check for a login/logout attempt
552
558
  # handle_post req
553
559
  elsif token = req.cookies[@keys[:cookie]]
554
560
  # next check for a cookie
561
+ # warn "has cookie #{token}"
555
562
  handle_cookie req, token
556
563
  else
557
564
  default_401 req
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forget-passwords
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.12
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dorian Taylor
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-12-07 00:00:00.000000000 Z
11
+ date: 2023-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -290,7 +290,7 @@ homepage: https://github.com/doriantaylor/rb-forget-passwords
290
290
  licenses:
291
291
  - Apache-2.0
292
292
  metadata: {}
293
- post_install_message:
293
+ post_install_message:
294
294
  rdoc_options: []
295
295
  require_paths:
296
296
  - lib
@@ -305,8 +305,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
305
305
  - !ruby/object:Gem::Version
306
306
  version: '0'
307
307
  requirements: []
308
- rubygems_version: 3.3.11
309
- signing_key:
308
+ rubygems_version: 3.1.2
309
+ signing_key:
310
310
  specification_version: 4
311
311
  summary: Web authentication module for the extremely lazy
312
312
  test_files: []