masks 0.11.0 → 0.12.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.
@@ -11,7 +11,7 @@ module Masks
11
11
  :after_sign_out, :session_key, :sign_out_of_issuer, :parent_controller,
12
12
  :credentials_path, :authenticate_everything, :delegates
13
13
  attr_writer :issuer, :redirect_uri, :name, :credentials, :store, :forget, :logged_out,
14
- :delegation_redirect_uri, :organization, :manages, :replay
14
+ :delegation_redirect_uri, :organization, :manages, :cache
15
15
 
16
16
  def initialize
17
17
  @scope = Masks::Client::Session::DEFAULT_SCOPE
@@ -80,18 +80,13 @@ module Masks
80
80
  end
81
81
 
82
82
  def backchannel_logout_uri_for(request)
83
- return nil unless @logged_out.respond_to?(:call)
84
- return nil unless Masks::Client::Issuer.resolve(issuer_for(request)).backchannel_logout?
85
-
86
83
  "#{request.base_url}#{routes.backchannel_logout_path}"
87
84
  end
88
85
 
89
86
  def logged_out!(request, logout)
90
- return false unless @logged_out.respond_to?(:call)
87
+ return unless @logged_out.respond_to?(:call)
91
88
 
92
89
  @logged_out.arity == 1 ? @logged_out.call(logout) : @logged_out.call(request, logout)
93
-
94
- true
95
90
  end
96
91
 
97
92
  def manages?(request, masked = nil)
@@ -140,12 +135,18 @@ module Masks
140
135
  issuer: issuer_for(request),
141
136
  url: url,
142
137
  scopes: resource_scopes,
143
- replay: replay
138
+ replay: Cache.new(cache)
144
139
  )
145
140
  end
146
141
 
147
- def replay
148
- @replay || Masks::Client::Proof.memory
142
+ def cache
143
+ return @cache if @cache
144
+
145
+ held = ::Rails.cache if ::Rails.respond_to?(:cache)
146
+
147
+ return held unless held.nil? || held.is_a?(ActiveSupport::Cache::NullStore)
148
+
149
+ @process_cache ||= ActiveSupport::Cache::MemoryStore.new
149
150
  end
150
151
 
151
152
  def handshake_for(request)
@@ -157,7 +158,8 @@ module Masks
157
158
  redirect_uris: [ redirect_uri_for(request), delegation_redirect_uri_for(request) ].compact.uniq,
158
159
  return_to: return_to_for(request),
159
160
  scope: approved_scope,
160
- backchannel_logout_uri: backchannel_logout_uri_for(request)
161
+ backchannel_logout_uri: backchannel_logout_uri_for(request),
162
+ post_logout_redirect_uri: post_logout_redirect_uri_for(request)
161
163
  )
162
164
  end
163
165
 
@@ -194,14 +196,22 @@ module Masks
194
196
  end
195
197
 
196
198
  def return_to_for(request)
197
- uri = URI.parse(redirect_uri_for(request))
198
- port = ":#{uri.port}" unless uri.port == uri.default_port
199
+ "#{origin_for(request)}#{routes.handshake_callback_path}"
200
+ end
199
201
 
200
- "#{uri.scheme}://#{uri.host}#{port}#{routes.handshake_callback_path}"
202
+ def post_logout_redirect_uri_for(request)
203
+ "#{origin_for(request)}#{routes.logout_return_path}"
201
204
  end
202
205
 
203
206
  private
204
207
 
208
+ def origin_for(request)
209
+ uri = URI.parse(redirect_uri_for(request))
210
+ port = ":#{uri.port}" unless uri.port == uri.default_port
211
+
212
+ "#{uri.scheme}://#{uri.host}#{port}"
213
+ end
214
+
205
215
  def routes
206
216
  Masks::Rails::Engine.routes.url_helpers
207
217
  end
@@ -0,0 +1,54 @@
1
+ module Masks
2
+ module Rails
3
+ class Renewal
4
+ OUTCOMES = %i[renewed refused unregistered unreachable].freeze
5
+
6
+ attr_reader :outcome, :tokens, :identity
7
+
8
+ class << self
9
+ def renewed(tokens, identity)
10
+ new(:renewed, tokens: tokens, identity: identity)
11
+ end
12
+
13
+ def refused
14
+ new(:refused)
15
+ end
16
+
17
+ def unregistered
18
+ new(:unregistered)
19
+ end
20
+
21
+ def unreachable
22
+ new(:unreachable)
23
+ end
24
+ end
25
+
26
+ def initialize(outcome, tokens: nil, identity: nil)
27
+ raise ArgumentError, "#{outcome.inspect} is not one of #{OUTCOMES.join(', ')}" unless OUTCOMES.include?(outcome)
28
+
29
+ @outcome = outcome
30
+ @tokens = tokens
31
+ @identity = identity
32
+ end
33
+
34
+ def to_h
35
+ { "outcome" => outcome.to_s, "tokens" => tokens&.to_h&.except("id_token"), "identity" => identity }
36
+ end
37
+
38
+ def self.from_h(body)
39
+ return unreachable unless body.is_a?(Hash)
40
+
41
+ outcome = OUTCOMES.find { |known| known.to_s == body["outcome"] }
42
+
43
+ return unreachable if outcome.nil? || outcome == :unreachable
44
+ return new(outcome) unless outcome == :renewed
45
+
46
+ tokens = Masks::Client::Tokens.from_h(body["tokens"])
47
+
48
+ return unreachable if tokens.nil? || tokens.access_token.to_s.empty?
49
+
50
+ renewed(tokens, body["identity"].is_a?(Hash) ? body["identity"] : nil)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,145 @@
1
+ require "zlib"
2
+
3
+ module Masks
4
+ module Rails
5
+ class SessionCookies
6
+ class Overflow < Masks::Client::Error; end
7
+
8
+ VERSION = "v1".freeze
9
+ PIECE = 3800
10
+ PIECES = 8
11
+ CIPHER = "aes-256-gcm".freeze
12
+ NONCE = 12
13
+ TAG = 16
14
+ PURPOSE = "masks session cookie".freeze
15
+ UNPACKED = PIECE * PIECES * 8
16
+
17
+ class << self
18
+ def seal(held, bound)
19
+ sealing = OpenSSL::Cipher.new(CIPHER).encrypt
20
+ sealing.key = key
21
+ nonce = SecureRandom.random_bytes(NONCE)
22
+ sealing.iv = nonce
23
+ sealing.auth_data = bound.to_s
24
+ body = sealing.update(Zlib::Deflate.deflate(JSON.generate(held))) + sealing.final
25
+
26
+ "#{VERSION}.#{Base64.urlsafe_encode64(nonce + sealing.auth_tag(TAG) + body, padding: false)}"
27
+ end
28
+
29
+ def open(value, bound)
30
+ version, sealed = value.to_s.split(".", 2)
31
+
32
+ return nil unless version == VERSION && sealed.present?
33
+
34
+ raw = Base64.urlsafe_decode64(sealed)
35
+
36
+ return nil if raw.bytesize < NONCE + TAG
37
+
38
+ opening = OpenSSL::Cipher.new(CIPHER).decrypt
39
+ opening.key = key
40
+ opening.iv = raw.byteslice(0, NONCE)
41
+ opening.auth_tag = raw.byteslice(NONCE, TAG)
42
+ opening.auth_data = bound.to_s
43
+ packed = opening.update(raw.byteslice((NONCE + TAG)..)) + opening.final
44
+ held = JSON.parse(inflated(packed))
45
+
46
+ held.is_a?(Hash) ? held : nil
47
+ rescue ArgumentError, OpenSSL::Cipher::CipherError, Zlib::Error, JSON::ParserError
48
+ nil
49
+ end
50
+
51
+ def key
52
+ ::Rails.application.key_generator.generate_key(PURPOSE, 32)
53
+ end
54
+
55
+ private
56
+
57
+ def inflated(packed)
58
+ inflater = Zlib::Inflate.new
59
+ held = +""
60
+
61
+ inflater.inflate(packed) do |chunk|
62
+ held << chunk
63
+ raise Zlib::DataError, "the session cookie unpacks to more than #{UNPACKED} bytes" if held.bytesize > UNPACKED
64
+ end
65
+
66
+ inflater.finish
67
+ held
68
+ ensure
69
+ inflater&.close
70
+ end
71
+ end
72
+
73
+ def initialize(request, key)
74
+ @request = request
75
+ @key = key
76
+ end
77
+
78
+ def read
79
+ bound = session[@key]
80
+ value = pieces.join
81
+
82
+ return nil if value.empty?
83
+
84
+ held = self.class.open(value, bound) if bound.is_a?(String)
85
+
86
+ clear_pieces if held.nil?
87
+ held
88
+ end
89
+
90
+ def write(held)
91
+ bound = session[@key]
92
+ bound = session[@key] = SecureRandom.hex(32) unless bound.is_a?(String)
93
+ value = self.class.seal(held, bound)
94
+ written = value.scan(/.{1,#{PIECE}}/o)
95
+
96
+ raise Overflow, "this session is #{value.bytesize} bytes, more than #{PIECES} cookies can hold" if written.size > PIECES
97
+
98
+ written.each_with_index { |piece, index| jar[name(index)] = options.merge(value: piece) }
99
+ (written.size...PIECES).each { |index| drop(name(index)) }
100
+ end
101
+
102
+ def clear
103
+ session.delete(@key)
104
+ clear_pieces
105
+ end
106
+
107
+ def name(index)
108
+ base = @request.ssl? ? "__Host-#{@key}" : @key.to_s
109
+
110
+ index.zero? ? base : "#{base}-#{index}"
111
+ end
112
+
113
+ private
114
+
115
+ def session
116
+ @request.session
117
+ end
118
+
119
+ def jar
120
+ @request.cookie_jar
121
+ end
122
+
123
+ def pieces
124
+ (0...PIECES).map { |index| jar[name(index)] }.take_while(&:present?)
125
+ end
126
+
127
+ def clear_pieces
128
+ (0...PIECES).each { |index| drop(name(index)) }
129
+ end
130
+
131
+ def drop(named)
132
+ jar.delete(named, path: "/", secure: @request.ssl?, same_site: :lax) if jar.key?(named)
133
+ end
134
+
135
+ def options
136
+ lifetime = @request.session_options[:expire_after] if @request.respond_to?(:session_options)
137
+
138
+ {
139
+ path: "/", httponly: true, secure: @request.ssl?, same_site: :lax,
140
+ expires: lifetime ? Time.now + lifetime : nil
141
+ }.compact
142
+ end
143
+ end
144
+ end
145
+ end
data/lib/masks/rails.rb CHANGED
@@ -13,10 +13,8 @@ module Masks
13
13
  # Three pieces do the work:
14
14
  #
15
15
  # [Configuration] the issuer, credentials and routes, set once in an
16
- # initializer and validated on boot rather than on the
17
- # first request that needs them
18
- # [Authentication] +masks_login_url+, the callback, and the session the
19
- # app reads +current_actor+ from
16
+ # initializer
17
+ # [Authentication] +masks_login_url+, the callback, and the session
20
18
  # [ProtectedResource] the other direction — checking a bearer this app was
21
19
  # handed, for an API rather than a browser
22
20
  #
@@ -29,6 +27,9 @@ end
29
27
  require_relative "version"
30
28
  require_relative "client"
31
29
  require_relative "rails/credentials"
30
+ require_relative "rails/renewal"
31
+ require_relative "rails/cache"
32
+ require_relative "rails/session_cookies"
32
33
  require_relative "rails/configuration"
33
34
  require_relative "rails/configurable"
34
35
  require_relative "rails/masked"
data/lib/masks/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Masks
2
- VERSION = "0.11.0".freeze
2
+ VERSION = "0.12.0".freeze
3
3
  end
data/lib/masks.rb CHANGED
@@ -8,7 +8,6 @@
8
8
  # defined, so requiring this gem outside \Rails costs nothing.
9
9
  #
10
10
  # issuer = Masks::Client.issuer("https://auth.example")
11
- # issuer.authorization_url(client_id: id, redirect_uri: uri)
12
11
  #
13
12
  # The provider itself is not in here. It is the masks-server gem, which runs
14
13
  # as an app of its own or mounted inside another Rails app.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: masks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - the masks authors
@@ -75,6 +75,7 @@ files:
75
75
  - lib/masks/client/verifier.rb
76
76
  - lib/masks/rails.rb
77
77
  - lib/masks/rails/authentication.rb
78
+ - lib/masks/rails/cache.rb
78
79
  - lib/masks/rails/configurable.rb
79
80
  - lib/masks/rails/configuration.rb
80
81
  - lib/masks/rails/credentials.rb
@@ -82,6 +83,8 @@ files:
82
83
  - lib/masks/rails/masked.rb
83
84
  - lib/masks/rails/privileged.rb
84
85
  - lib/masks/rails/protected_resource.rb
86
+ - lib/masks/rails/renewal.rb
87
+ - lib/masks/rails/session_cookies.rb
85
88
  - lib/masks/version.rb
86
89
  homepage: https://github.com/masksrb/masks
87
90
  licenses: