masks 0.3.2 → 0.5.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 +16 -0
- data/LICENSE +21 -0
- data/README.md +215 -29
- data/app/controllers/masks/rails/base_controller.rb +13 -0
- data/app/controllers/masks/rails/handshakes_controller.rb +87 -0
- data/app/controllers/masks/rails/sessions_controller.rb +113 -0
- data/app/views/layouts/masks/rails/plain.html.erb +47 -0
- data/app/views/masks/rails/handshakes/refused.html.erb +12 -0
- data/app/views/masks/rails/handshakes/show.html.erb +40 -0
- data/config/routes.rb +10 -45
- data/lib/generators/masks/install/install_generator.rb +35 -22
- data/lib/generators/masks/install/templates/masks.rb.tt +38 -0
- data/lib/masks/client/claims.rb +103 -0
- data/lib/masks/client/errors.rb +52 -0
- data/lib/masks/client/handshake.rb +104 -0
- data/lib/masks/client/http.rb +81 -0
- data/lib/masks/client/introspection.rb +29 -0
- data/lib/masks/client/issuer.rb +76 -0
- data/lib/masks/client/pkce.rb +27 -0
- data/lib/masks/client/rack.rb +62 -0
- data/lib/masks/client/registration.rb +91 -0
- data/lib/masks/client/registry.rb +25 -0
- data/lib/masks/client/resource.rb +92 -0
- data/lib/masks/client/session.rb +146 -0
- data/lib/masks/client/stores.rb +40 -0
- data/lib/masks/client/tokens.rb +70 -0
- data/lib/masks/client/tracker.rb +122 -0
- data/lib/masks/client/verifier.rb +42 -0
- data/lib/masks/client.rb +51 -0
- data/lib/masks/rails/authentication.rb +217 -0
- data/lib/masks/rails/configurable.rb +11 -0
- data/lib/masks/rails/configuration.rb +172 -0
- data/lib/masks/rails/credentials.rb +60 -0
- data/lib/masks/rails/engine.rb +20 -0
- data/lib/masks/rails/protected_resource.rb +51 -0
- data/lib/masks/rails.rb +10 -0
- data/lib/masks/version.rb +1 -3
- data/lib/masks.rb +3 -182
- metadata +47 -368
- data/MIT-LICENSE +0 -20
- data/Rakefile +0 -11
- data/app/assets/builds/application.css +0 -4764
- data/app/assets/builds/application.js +0 -8236
- data/app/assets/builds/application.js.map +0 -7
- data/app/assets/builds/masks/application.css +0 -1
- data/app/assets/builds/masks/application.js +0 -8462
- data/app/assets/builds/masks/application.js.map +0 -7
- data/app/assets/images/masks.png +0 -0
- data/app/assets/javascripts/application.js +0 -2
- data/app/assets/javascripts/controllers/application.js +0 -9
- data/app/assets/javascripts/controllers/emails_controller.js +0 -28
- data/app/assets/javascripts/controllers/index.js +0 -12
- data/app/assets/javascripts/controllers/keys_controller.js +0 -20
- data/app/assets/javascripts/controllers/recover_controller.js +0 -21
- data/app/assets/javascripts/controllers/recover_password_controller.js +0 -21
- data/app/assets/javascripts/controllers/session_controller.js +0 -94
- data/app/assets/manifest.js +0 -2
- data/app/assets/masks_manifest.js +0 -2
- data/app/assets/stylesheets/application.css +0 -26
- data/app/controllers/concerns/masks/controller.rb +0 -114
- data/app/controllers/masks/actors_controller.rb +0 -15
- data/app/controllers/masks/application_controller.rb +0 -35
- data/app/controllers/masks/backup_codes_controller.rb +0 -34
- data/app/controllers/masks/debug_controller.rb +0 -9
- data/app/controllers/masks/devices_controller.rb +0 -20
- data/app/controllers/masks/emails_controller.rb +0 -60
- data/app/controllers/masks/error_controller.rb +0 -14
- data/app/controllers/masks/keys_controller.rb +0 -45
- data/app/controllers/masks/manage/actor_controller.rb +0 -35
- data/app/controllers/masks/manage/actors_controller.rb +0 -12
- data/app/controllers/masks/manage/base_controller.rb +0 -12
- data/app/controllers/masks/one_time_code_controller.rb +0 -49
- data/app/controllers/masks/passwords_controller.rb +0 -33
- data/app/controllers/masks/recoveries_controller.rb +0 -43
- data/app/controllers/masks/sessions_controller.rb +0 -53
- data/app/helpers/masks/application_helper.rb +0 -49
- data/app/jobs/masks/application_job.rb +0 -7
- data/app/jobs/masks/expire_actors_job.rb +0 -15
- data/app/jobs/masks/expire_recoveries_job.rb +0 -15
- data/app/mailers/masks/actor_mailer.rb +0 -22
- data/app/mailers/masks/application_mailer.rb +0 -15
- data/app/models/concerns/masks/access.rb +0 -162
- data/app/models/concerns/masks/actor.rb +0 -132
- data/app/models/concerns/masks/adapter.rb +0 -68
- data/app/models/concerns/masks/role.rb +0 -9
- data/app/models/concerns/masks/scoped.rb +0 -54
- data/app/models/masks/access/actor_password.rb +0 -20
- data/app/models/masks/access/actor_scopes.rb +0 -18
- data/app/models/masks/access/actor_signup.rb +0 -22
- data/app/models/masks/actors/anonymous.rb +0 -40
- data/app/models/masks/actors/system.rb +0 -24
- data/app/models/masks/adapters/active_record.rb +0 -85
- data/app/models/masks/application_model.rb +0 -15
- data/app/models/masks/application_record.rb +0 -8
- data/app/models/masks/check.rb +0 -192
- data/app/models/masks/credential.rb +0 -166
- data/app/models/masks/credentials/backup_code.rb +0 -30
- data/app/models/masks/credentials/device.rb +0 -59
- data/app/models/masks/credentials/email.rb +0 -48
- data/app/models/masks/credentials/factor2.rb +0 -71
- data/app/models/masks/credentials/key.rb +0 -38
- data/app/models/masks/credentials/last_login.rb +0 -12
- data/app/models/masks/credentials/masquerade.rb +0 -32
- data/app/models/masks/credentials/nickname.rb +0 -63
- data/app/models/masks/credentials/one_time_code.rb +0 -34
- data/app/models/masks/credentials/password.rb +0 -28
- data/app/models/masks/credentials/recovery.rb +0 -71
- data/app/models/masks/credentials/session.rb +0 -67
- data/app/models/masks/device.rb +0 -30
- data/app/models/masks/error.rb +0 -51
- data/app/models/masks/event.rb +0 -14
- data/app/models/masks/mask.rb +0 -255
- data/app/models/masks/rails/actor.rb +0 -190
- data/app/models/masks/rails/actor_role.rb +0 -12
- data/app/models/masks/rails/device.rb +0 -47
- data/app/models/masks/rails/email.rb +0 -96
- data/app/models/masks/rails/key.rb +0 -61
- data/app/models/masks/rails/recovery.rb +0 -116
- data/app/models/masks/rails/role.rb +0 -20
- data/app/models/masks/rails/scope.rb +0 -15
- data/app/models/masks/session.rb +0 -447
- data/app/models/masks/sessions/access.rb +0 -26
- data/app/models/masks/sessions/inline.rb +0 -16
- data/app/models/masks/sessions/request.rb +0 -42
- data/app/resources/masks/actor_resource.rb +0 -9
- data/app/resources/masks/session_resource.rb +0 -15
- data/app/views/layouts/masks/application.html.erb +0 -17
- data/app/views/layouts/masks/mailer.html.erb +0 -17
- data/app/views/layouts/masks/mailer.text.erb +0 -1
- data/app/views/layouts/masks/manage.html.erb +0 -25
- data/app/views/masks/actor_mailer/recover_credentials.html.erb +0 -33
- data/app/views/masks/actor_mailer/recover_credentials.text.erb +0 -1
- data/app/views/masks/actor_mailer/verify_email.html.erb +0 -34
- data/app/views/masks/actor_mailer/verify_email.text.erb +0 -8
- data/app/views/masks/actors/current.html.erb +0 -152
- data/app/views/masks/application/_header.html.erb +0 -31
- data/app/views/masks/backup_codes/new.html.erb +0 -103
- data/app/views/masks/emails/new.html.erb +0 -103
- data/app/views/masks/emails/verify.html.erb +0 -51
- data/app/views/masks/keys/new.html.erb +0 -127
- data/app/views/masks/manage/actor/show.html.erb +0 -126
- data/app/views/masks/manage/actors/index.html.erb +0 -40
- data/app/views/masks/one_time_code/new.html.erb +0 -150
- data/app/views/masks/passwords/edit.html.erb +0 -58
- data/app/views/masks/recoveries/new.html.erb +0 -71
- data/app/views/masks/recoveries/password.html.erb +0 -64
- data/app/views/masks/sessions/new.html.erb +0 -153
- data/config/brakeman.ignore +0 -28
- data/config/locales/en.yml +0 -286
- data/db/migrate/20231205173845_create_actors.rb +0 -94
- data/lib/generators/masks/install/USAGE +0 -8
- data/lib/generators/masks/install/templates/initializer.rb +0 -5
- data/lib/generators/masks/install/templates/masks.json +0 -6
- data/lib/masks/configuration.rb +0 -236
- data/lib/masks/engine.rb +0 -25
- data/lib/masks/middleware.rb +0 -70
- data/lib/tasks/masks_tasks.rake +0 -72
- data/masks.json +0 -277
data/lib/masks/client.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "json"
|
|
3
|
+
require "jwt"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
require_relative "version"
|
|
10
|
+
require_relative "client/errors"
|
|
11
|
+
require_relative "client/http"
|
|
12
|
+
require_relative "client/pkce"
|
|
13
|
+
require_relative "client/stores"
|
|
14
|
+
require_relative "client/tracker"
|
|
15
|
+
require_relative "client/issuer"
|
|
16
|
+
require_relative "client/registry"
|
|
17
|
+
require_relative "client/tokens"
|
|
18
|
+
require_relative "client/claims"
|
|
19
|
+
require_relative "client/introspection"
|
|
20
|
+
require_relative "client/session"
|
|
21
|
+
require_relative "client/verifier"
|
|
22
|
+
require_relative "client/resource"
|
|
23
|
+
require_relative "client/rack"
|
|
24
|
+
require_relative "client/registration"
|
|
25
|
+
require_relative "client/handshake"
|
|
26
|
+
|
|
27
|
+
module Masks
|
|
28
|
+
module Client
|
|
29
|
+
class << self
|
|
30
|
+
def registry
|
|
31
|
+
@registry ||= Registry.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def issuer(url, **options)
|
|
35
|
+
Issuer.resolve(url, **options)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def verifier(url, audience:, **options)
|
|
39
|
+
Verifier.new(issuer(url), audience: audience, **options)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def resource(url, issuer:, **options)
|
|
43
|
+
Resource.new(issuer: issuer, url: url, **options)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handshake(url, **options)
|
|
47
|
+
Handshake.new(url, **options)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Rails
|
|
3
|
+
module Authentication
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
include Masks::Rails::Configurable
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
if respond_to?(:helper_method)
|
|
9
|
+
helper_method :masks_signed_in?, :masks_identity, :masks_tenant, :masks_scopes
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
REQUESTS = "masks_requests".freeze
|
|
14
|
+
HANDSHAKES = "masks_handshakes".freeze
|
|
15
|
+
|
|
16
|
+
def masks_session
|
|
17
|
+
@masks_session ||= masks_config.session_for(request)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def masks_requests
|
|
21
|
+
@masks_requests ||= masks_tracker(REQUESTS)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def masks_handshakes
|
|
25
|
+
@masks_handshakes ||= masks_tracker(HANDSHAKES)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def masks_tracker(key)
|
|
29
|
+
Masks::Client::Tracker.new(Masks::Client::Stores::Session.new(session, key))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def masks_tokens
|
|
33
|
+
return @masks_tokens if defined?(@masks_tokens)
|
|
34
|
+
|
|
35
|
+
@masks_tokens = Masks::Client::Tokens.from_h(session[masks_config.session_key])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def masks_store(tokens, identity: nil)
|
|
39
|
+
held = tokens.to_h.except("id_token")
|
|
40
|
+
held["identity"] = identity&.slice(*IDENTITY) || masks_held["identity"]
|
|
41
|
+
|
|
42
|
+
session[masks_config.session_key] = held.compact
|
|
43
|
+
@masks_tokens = tokens
|
|
44
|
+
@masks_identity = held["identity"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def masks_held
|
|
48
|
+
session[masks_config.session_key] || {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
IDENTITY = %w[sub name preferred_username email email_verified tenant].freeze
|
|
52
|
+
|
|
53
|
+
def masks_identity_from(tokens)
|
|
54
|
+
return nil if tokens.id_token.nil?
|
|
55
|
+
|
|
56
|
+
masks_session.identity(tokens)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def masks_forget
|
|
60
|
+
session.delete(masks_config.session_key)
|
|
61
|
+
@masks_tokens = nil
|
|
62
|
+
@masks_identity = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def masks_signed_in?
|
|
66
|
+
masks_tokens.present? && !masks_tokens.expired?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def masks_configured?
|
|
70
|
+
masks_config.configured?(request)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def masks_handshake_path
|
|
74
|
+
Masks::Rails::Engine.routes.url_helpers.handshake_path
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def masks_identity
|
|
78
|
+
return @masks_identity if defined?(@masks_identity)
|
|
79
|
+
|
|
80
|
+
@masks_identity = masks_held["identity"]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def masks_tenant
|
|
84
|
+
masks_identity&.dig("tenant")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def masks_scopes
|
|
88
|
+
masks_tokens&.scopes || []
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def masks_permits?(scope)
|
|
92
|
+
masks_scopes.include?(scope.to_s)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def masks_access_token
|
|
96
|
+
masks_tokens&.access_token
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def masks_refresh!
|
|
100
|
+
return false unless masks_configured?
|
|
101
|
+
return false if masks_tokens&.refresh_token.nil?
|
|
102
|
+
|
|
103
|
+
masks_store(
|
|
104
|
+
masks_session.refresh(
|
|
105
|
+
masks_tokens.refresh_token,
|
|
106
|
+
resource: masks_config.resource_for(request)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
true
|
|
110
|
+
rescue Masks::Client::Rejected
|
|
111
|
+
masks_forget
|
|
112
|
+
false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def masks_logout_url(return_to: nil)
|
|
116
|
+
return nil unless masks_configured?
|
|
117
|
+
|
|
118
|
+
masks_session.end_session_url(
|
|
119
|
+
post_logout_redirect_uri: masks_post_logout_redirect_uri,
|
|
120
|
+
state: SecureRandom.urlsafe_base64(16)
|
|
121
|
+
)
|
|
122
|
+
rescue Masks::Client::Error
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def masks_post_logout_redirect_uri
|
|
127
|
+
"#{request.base_url}#{masks_local_path(masks_config.after_sign_out) || '/'}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def masks_login_url(return_to: nil)
|
|
131
|
+
path = Masks::Rails::Engine.routes.url_helpers.start_path
|
|
132
|
+
target = masks_local_path(return_to)
|
|
133
|
+
|
|
134
|
+
target ? "#{path}?return_to=#{CGI.escape(target)}" : path
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def masks_account
|
|
138
|
+
identity = masks_identity || {}
|
|
139
|
+
|
|
140
|
+
{
|
|
141
|
+
"signed_in" => masks_signed_in?,
|
|
142
|
+
"subject" => identity["sub"],
|
|
143
|
+
"name" => identity["name"],
|
|
144
|
+
"nickname" => identity["preferred_username"],
|
|
145
|
+
"email" => identity["email"],
|
|
146
|
+
"email_verified" => identity["email_verified"],
|
|
147
|
+
"tenant" => masks_tenant,
|
|
148
|
+
"scopes" => masks_scopes,
|
|
149
|
+
"expires_at" => masks_tokens&.expires_at
|
|
150
|
+
}.compact
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def authenticate_masks!
|
|
154
|
+
return true if masks_configured? && (masks_signed_in? || (masks_tokens && masks_refresh!))
|
|
155
|
+
|
|
156
|
+
masks_refuse
|
|
157
|
+
false
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def masks_refuse
|
|
163
|
+
return masks_refuse_json if masks_wants_json?
|
|
164
|
+
return redirect_to(masks_handshake_path) unless masks_configured?
|
|
165
|
+
|
|
166
|
+
session[:masks_return_to] = request.fullpath if request.get?
|
|
167
|
+
redirect_to Masks::Rails::Engine.routes.url_helpers.start_path
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def masks_refuse_json
|
|
171
|
+
response.headers["Cache-Control"] = "no-store"
|
|
172
|
+
|
|
173
|
+
render json: masks_refusal, status: :unauthorized
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def masks_refusal
|
|
177
|
+
unless masks_configured?
|
|
178
|
+
return {
|
|
179
|
+
"signed_in" => false,
|
|
180
|
+
"error" => "handshake_required",
|
|
181
|
+
"handshake_url" => masks_handshake_path
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
{
|
|
186
|
+
"signed_in" => false,
|
|
187
|
+
"error" => "login_required",
|
|
188
|
+
"login_url" => masks_login_url(return_to: masks_referring_path)
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def masks_wants_json?
|
|
193
|
+
request.xhr? ||
|
|
194
|
+
request.format.json? ||
|
|
195
|
+
request.content_mime_type&.symbol == :json ||
|
|
196
|
+
!request.format.html?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def masks_referring_path
|
|
200
|
+
return nil if request.referer.blank?
|
|
201
|
+
|
|
202
|
+
masks_local_path(URI.parse(request.referer).request_uri)
|
|
203
|
+
rescue URI::InvalidURIError
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def masks_local_path(value)
|
|
208
|
+
path = value.to_s
|
|
209
|
+
|
|
210
|
+
return nil unless path.start_with?("/")
|
|
211
|
+
return nil if path.start_with?("//", "/\\")
|
|
212
|
+
|
|
213
|
+
path
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Rails
|
|
3
|
+
class Configuration
|
|
4
|
+
class Unconfigured < Masks::Client::Error; end
|
|
5
|
+
|
|
6
|
+
attr_accessor :scope, :resource, :resource_scopes, :after_sign_in, :after_sign_out,
|
|
7
|
+
:session_key, :sign_out_of_issuer, :parent_controller,
|
|
8
|
+
:credentials_path, :authenticate_everything
|
|
9
|
+
attr_writer :issuer, :redirect_uri, :name, :credentials, :store, :forget
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@scope = Masks::Client::Session::DEFAULT_SCOPE
|
|
13
|
+
@resource_scopes = []
|
|
14
|
+
@after_sign_in = "/"
|
|
15
|
+
@after_sign_out = "/"
|
|
16
|
+
@session_key = "masks"
|
|
17
|
+
@sign_out_of_issuer = false
|
|
18
|
+
@parent_controller = "ActionController::Base"
|
|
19
|
+
@authenticate_everything = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# An app that has not said where to keep its credentials gets one file
|
|
23
|
+
# under the Rails root, so the handshake works before anybody writes a
|
|
24
|
+
# `store` lambda. `things` overrides both because it is multi-tenant,
|
|
25
|
+
# which is the interesting case rather than the common one.
|
|
26
|
+
def default_credentials
|
|
27
|
+
@default_credentials ||= Credentials.new(
|
|
28
|
+
credentials_path || ::Rails.root.join("config", "masks.json")
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def name_for(request)
|
|
33
|
+
resolve(@name, request).presence || ::Rails.application.class.module_parent_name
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def issuer_for(request)
|
|
37
|
+
resolve(@issuer, request) ||
|
|
38
|
+
raise(Unconfigured, "Masks::Rails.config.issuer is not set")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def redirect_uri_for(request)
|
|
42
|
+
resolve(@redirect_uri, request) ||
|
|
43
|
+
"#{request.base_url}#{routes.callback_path}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def resource_for(request)
|
|
47
|
+
resolve(@resource, request)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def credentials_for(request)
|
|
51
|
+
held = @credentials ? resolve(@credentials, request) : default_credentials.read
|
|
52
|
+
held ||= {}
|
|
53
|
+
|
|
54
|
+
held.respond_to?(:to_h) ? held.to_h.transform_keys(&:to_s) : {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def client_id_for(request)
|
|
58
|
+
credentials_for(request)["client_id"].presence
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def client_secret_for(request)
|
|
62
|
+
credentials_for(request)["client_secret"].presence
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def configured?(request)
|
|
66
|
+
client_id_for(request).present?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# An app that keeps its own credentials has to say how to drop them, and
|
|
70
|
+
# until it does the engine does not offer a button it cannot honour.
|
|
71
|
+
def can_forget?
|
|
72
|
+
@forget.respond_to?(:call) || !@store.respond_to?(:call)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def forget!(request)
|
|
76
|
+
return @forget.call(request) if @forget.respond_to?(:call)
|
|
77
|
+
raise Unconfigured, "Masks::Rails.config.forget is not set" if @store.respond_to?(:call)
|
|
78
|
+
|
|
79
|
+
default_credentials.clear!
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def store!(request, registration)
|
|
83
|
+
return default_credentials.write(registration) unless @store.respond_to?(:call)
|
|
84
|
+
|
|
85
|
+
@store.call(request, registration)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The engine depends on `handshake_endpoint` being in the discovery
|
|
89
|
+
# document, and on the approval flow behind it. An issuer that predates
|
|
90
|
+
# both should say so here rather than at the one screen that exists to
|
|
91
|
+
# be the first thing anybody touches.
|
|
92
|
+
MINIMUM_ISSUER = 1
|
|
93
|
+
|
|
94
|
+
def issuer_speaks!(request)
|
|
95
|
+
spoken = Masks::Client::Issuer.resolve(issuer_for(request))
|
|
96
|
+
.discovery["masks_protocol_version"].to_i
|
|
97
|
+
|
|
98
|
+
return true if spoken >= MINIMUM_ISSUER
|
|
99
|
+
|
|
100
|
+
raise Unconfigured,
|
|
101
|
+
"#{issuer_for(request)} speaks masks protocol #{spoken}, and masks " \
|
|
102
|
+
"#{Masks::VERSION} needs at least #{MINIMUM_ISSUER}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def session_for(request)
|
|
106
|
+
raise Unconfigured, "this app has not shaken hands with #{issuer_for(request)}" unless configured?(request)
|
|
107
|
+
|
|
108
|
+
Masks::Client::Session.new(
|
|
109
|
+
issuer: issuer_for(request),
|
|
110
|
+
client_id: client_id_for(request),
|
|
111
|
+
client_secret: client_secret_for(request),
|
|
112
|
+
redirect_uri: redirect_uri_for(request),
|
|
113
|
+
scope: scope
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def resource_server_for(request)
|
|
118
|
+
url = Array(resource_for(request)).first
|
|
119
|
+
|
|
120
|
+
raise Unconfigured, "Masks::Rails.config.resource is not set" if url.nil?
|
|
121
|
+
|
|
122
|
+
Masks::Client::Resource.new(
|
|
123
|
+
issuer: issuer_for(request),
|
|
124
|
+
url: url,
|
|
125
|
+
scopes: resource_scopes
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def handshake_for(request)
|
|
130
|
+
Masks::Client::Handshake.new(
|
|
131
|
+
issuer_for(request),
|
|
132
|
+
name: name_for(request),
|
|
133
|
+
resource: Array(resource_for(request)).first ||
|
|
134
|
+
raise(Unconfigured, "Masks::Rails.config.resource is not set"),
|
|
135
|
+
redirect_uris: [ redirect_uri_for(request) ],
|
|
136
|
+
return_to: return_to_for(request),
|
|
137
|
+
scope: scope
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def return_to_for(request)
|
|
142
|
+
uri = URI.parse(redirect_uri_for(request))
|
|
143
|
+
port = ":#{uri.port}" unless uri.port == uri.default_port
|
|
144
|
+
|
|
145
|
+
"#{uri.scheme}://#{uri.host}#{port}#{routes.handshake_callback_path}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def routes
|
|
151
|
+
Masks::Rails::Engine.routes.url_helpers
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def resolve(value, request)
|
|
155
|
+
return value unless value.respond_to?(:call)
|
|
156
|
+
|
|
157
|
+
value.arity.zero? ? value.call : value.call(request)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class << self
|
|
162
|
+
def config
|
|
163
|
+
@config ||= Configuration.new
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def configure
|
|
167
|
+
yield config
|
|
168
|
+
config
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Rails
|
|
3
|
+
# The default place a single-tenant app keeps what the handshake gives it,
|
|
4
|
+
# so `config.store` has something to be before a consumer writes one. An
|
|
5
|
+
# app with more than one issuer wants its own — `things` keeps these on
|
|
6
|
+
# `tenants`, because a handshake is per tenant there.
|
|
7
|
+
class Credentials
|
|
8
|
+
KEYS = %w[client_id client_secret registration_access_token registration_client_uri].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :path
|
|
11
|
+
|
|
12
|
+
def initialize(path)
|
|
13
|
+
@path = Pathname.new(path)
|
|
14
|
+
@lock = Mutex.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def read
|
|
18
|
+
@lock.synchronize { held }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write(registration)
|
|
22
|
+
@lock.synchronize do
|
|
23
|
+
@held = {
|
|
24
|
+
"client_id" => registration.client_id,
|
|
25
|
+
"client_secret" => registration.client_secret,
|
|
26
|
+
"registration_access_token" => registration.access_token,
|
|
27
|
+
"registration_client_uri" => registration.uri,
|
|
28
|
+
"connected_at" => Time.now.utc.iso8601
|
|
29
|
+
}.compact
|
|
30
|
+
|
|
31
|
+
path.dirname.mkpath
|
|
32
|
+
path.write(JSON.pretty_generate(@held))
|
|
33
|
+
path.chmod(0o600)
|
|
34
|
+
@held
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def clear!
|
|
39
|
+
@lock.synchronize do
|
|
40
|
+
@held = nil
|
|
41
|
+
path.delete if path.exist?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def connected?
|
|
46
|
+
read["client_id"].present?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def held
|
|
52
|
+
return @held if defined?(@held) && @held
|
|
53
|
+
|
|
54
|
+
@held = path.exist? ? JSON.parse(path.read) : {}
|
|
55
|
+
rescue JSON::ParserError
|
|
56
|
+
@held = {}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Rails
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
isolate_namespace Masks::Rails
|
|
5
|
+
|
|
6
|
+
config.masks = Masks::Rails.config
|
|
7
|
+
|
|
8
|
+
# Opt-in, not blanket. This used to `include Masks::Rails::Authentication`
|
|
9
|
+
# into every controller in the host app, which lands twenty-five `masks_*`
|
|
10
|
+
# methods and four helper_methods on code the consumer owns, with no way
|
|
11
|
+
# to say no — and a name collision found that way is the kind of thing
|
|
12
|
+
# `grants:` versus `scope:` already cost this project once.
|
|
13
|
+
initializer "masks.authentication" do
|
|
14
|
+
ActiveSupport.on_load(:action_controller) do
|
|
15
|
+
include Masks::Rails::Authentication if Masks::Rails.config.authenticate_everything
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Masks
|
|
2
|
+
module Rails
|
|
3
|
+
module ProtectedResource
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
include Masks::Rails::Configurable
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
def masks_protect!(**options)
|
|
9
|
+
before_action(-> { masks_authenticate!(**options) }, **options.slice(:only, :except))
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def masks_resource
|
|
14
|
+
@masks_resource ||= masks_config.resource_server_for(request)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def masks_claims
|
|
18
|
+
@masks_claims if defined?(@masks_claims)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def masks_authenticate!(scope: nil, **)
|
|
22
|
+
@masks_claims = masks_resource.authenticate(request.authorization, scope: scope)
|
|
23
|
+
rescue Masks::Client::Challenge => e
|
|
24
|
+
masks_challenge(e)
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def masks_authenticate(scope: nil)
|
|
29
|
+
masks_resource.authenticate(request.authorization, scope: scope)
|
|
30
|
+
rescue Masks::Client::Unauthenticated
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def masks_challenge(error)
|
|
35
|
+
error = Masks::Client::Unauthorized.new(error.to_s) unless error.is_a?(Masks::Client::Challenge)
|
|
36
|
+
|
|
37
|
+
response.headers["WWW-Authenticate"] = masks_resource.challenge(error)
|
|
38
|
+
response.headers["Cache-Control"] = "no-store"
|
|
39
|
+
|
|
40
|
+
render json: {
|
|
41
|
+
"error" => error.code || "invalid_token",
|
|
42
|
+
"error_description" => error.description
|
|
43
|
+
}, status: error.status
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def masks_resource_metadata
|
|
47
|
+
masks_resource.metadata
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/masks/rails.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
require "rails"
|
|
2
|
+
|
|
3
|
+
require_relative "version"
|
|
4
|
+
require_relative "client"
|
|
5
|
+
require_relative "rails/credentials"
|
|
6
|
+
require_relative "rails/configuration"
|
|
7
|
+
require_relative "rails/configurable"
|
|
8
|
+
require_relative "rails/authentication"
|
|
9
|
+
require_relative "rails/protected_resource"
|
|
10
|
+
require_relative "rails/engine"
|
data/lib/masks/version.rb
CHANGED