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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +28 -461
- data/app/controllers/masks/rails/handshakes_controller.rb +2 -10
- data/app/controllers/masks/rails/logouts_controller.rb +11 -8
- data/app/controllers/masks/rails/sessions_controller.rb +46 -5
- data/config/routes.rb +1 -0
- data/lib/masks/client/embedded.rb +1 -1
- data/lib/masks/client/handshake.rb +12 -4
- data/lib/masks/client/http.rb +2 -13
- data/lib/masks/client/logout.rb +5 -1
- data/lib/masks/client/resource.rb +1 -1
- data/lib/masks/client/verifier.rb +10 -2
- data/lib/masks/client.rb +0 -4
- data/lib/masks/rails/authentication.rb +98 -33
- data/lib/masks/rails/cache.rb +173 -0
- data/lib/masks/rails/configuration.rb +24 -14
- data/lib/masks/rails/renewal.rb +54 -0
- data/lib/masks/rails/session_cookies.rb +145 -0
- data/lib/masks/rails.rb +5 -4
- data/lib/masks/version.rb +1 -1
- data/lib/masks.rb +0 -1
- metadata +4 -1
|
@@ -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, :
|
|
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
|
|
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:
|
|
138
|
+
replay: Cache.new(cache)
|
|
144
139
|
)
|
|
145
140
|
end
|
|
146
141
|
|
|
147
|
-
def
|
|
148
|
-
@
|
|
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
|
-
|
|
198
|
-
|
|
199
|
+
"#{origin_for(request)}#{routes.handshake_callback_path}"
|
|
200
|
+
end
|
|
199
201
|
|
|
200
|
-
|
|
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
|
|
17
|
-
#
|
|
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
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.
|
|
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:
|