forget-passwords 0.2.12 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: []