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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE +21 -0
  4. data/README.md +215 -29
  5. data/app/controllers/masks/rails/base_controller.rb +13 -0
  6. data/app/controllers/masks/rails/handshakes_controller.rb +87 -0
  7. data/app/controllers/masks/rails/sessions_controller.rb +113 -0
  8. data/app/views/layouts/masks/rails/plain.html.erb +47 -0
  9. data/app/views/masks/rails/handshakes/refused.html.erb +12 -0
  10. data/app/views/masks/rails/handshakes/show.html.erb +40 -0
  11. data/config/routes.rb +10 -45
  12. data/lib/generators/masks/install/install_generator.rb +35 -22
  13. data/lib/generators/masks/install/templates/masks.rb.tt +38 -0
  14. data/lib/masks/client/claims.rb +103 -0
  15. data/lib/masks/client/errors.rb +52 -0
  16. data/lib/masks/client/handshake.rb +104 -0
  17. data/lib/masks/client/http.rb +81 -0
  18. data/lib/masks/client/introspection.rb +29 -0
  19. data/lib/masks/client/issuer.rb +76 -0
  20. data/lib/masks/client/pkce.rb +27 -0
  21. data/lib/masks/client/rack.rb +62 -0
  22. data/lib/masks/client/registration.rb +91 -0
  23. data/lib/masks/client/registry.rb +25 -0
  24. data/lib/masks/client/resource.rb +92 -0
  25. data/lib/masks/client/session.rb +146 -0
  26. data/lib/masks/client/stores.rb +40 -0
  27. data/lib/masks/client/tokens.rb +70 -0
  28. data/lib/masks/client/tracker.rb +122 -0
  29. data/lib/masks/client/verifier.rb +42 -0
  30. data/lib/masks/client.rb +51 -0
  31. data/lib/masks/rails/authentication.rb +217 -0
  32. data/lib/masks/rails/configurable.rb +11 -0
  33. data/lib/masks/rails/configuration.rb +172 -0
  34. data/lib/masks/rails/credentials.rb +60 -0
  35. data/lib/masks/rails/engine.rb +20 -0
  36. data/lib/masks/rails/protected_resource.rb +51 -0
  37. data/lib/masks/rails.rb +10 -0
  38. data/lib/masks/version.rb +1 -3
  39. data/lib/masks.rb +3 -182
  40. metadata +47 -368
  41. data/MIT-LICENSE +0 -20
  42. data/Rakefile +0 -11
  43. data/app/assets/builds/application.css +0 -4764
  44. data/app/assets/builds/application.js +0 -8236
  45. data/app/assets/builds/application.js.map +0 -7
  46. data/app/assets/builds/masks/application.css +0 -1
  47. data/app/assets/builds/masks/application.js +0 -8462
  48. data/app/assets/builds/masks/application.js.map +0 -7
  49. data/app/assets/images/masks.png +0 -0
  50. data/app/assets/javascripts/application.js +0 -2
  51. data/app/assets/javascripts/controllers/application.js +0 -9
  52. data/app/assets/javascripts/controllers/emails_controller.js +0 -28
  53. data/app/assets/javascripts/controllers/index.js +0 -12
  54. data/app/assets/javascripts/controllers/keys_controller.js +0 -20
  55. data/app/assets/javascripts/controllers/recover_controller.js +0 -21
  56. data/app/assets/javascripts/controllers/recover_password_controller.js +0 -21
  57. data/app/assets/javascripts/controllers/session_controller.js +0 -94
  58. data/app/assets/manifest.js +0 -2
  59. data/app/assets/masks_manifest.js +0 -2
  60. data/app/assets/stylesheets/application.css +0 -26
  61. data/app/controllers/concerns/masks/controller.rb +0 -114
  62. data/app/controllers/masks/actors_controller.rb +0 -15
  63. data/app/controllers/masks/application_controller.rb +0 -35
  64. data/app/controllers/masks/backup_codes_controller.rb +0 -34
  65. data/app/controllers/masks/debug_controller.rb +0 -9
  66. data/app/controllers/masks/devices_controller.rb +0 -20
  67. data/app/controllers/masks/emails_controller.rb +0 -60
  68. data/app/controllers/masks/error_controller.rb +0 -14
  69. data/app/controllers/masks/keys_controller.rb +0 -45
  70. data/app/controllers/masks/manage/actor_controller.rb +0 -35
  71. data/app/controllers/masks/manage/actors_controller.rb +0 -12
  72. data/app/controllers/masks/manage/base_controller.rb +0 -12
  73. data/app/controllers/masks/one_time_code_controller.rb +0 -49
  74. data/app/controllers/masks/passwords_controller.rb +0 -33
  75. data/app/controllers/masks/recoveries_controller.rb +0 -43
  76. data/app/controllers/masks/sessions_controller.rb +0 -53
  77. data/app/helpers/masks/application_helper.rb +0 -49
  78. data/app/jobs/masks/application_job.rb +0 -7
  79. data/app/jobs/masks/expire_actors_job.rb +0 -15
  80. data/app/jobs/masks/expire_recoveries_job.rb +0 -15
  81. data/app/mailers/masks/actor_mailer.rb +0 -22
  82. data/app/mailers/masks/application_mailer.rb +0 -15
  83. data/app/models/concerns/masks/access.rb +0 -162
  84. data/app/models/concerns/masks/actor.rb +0 -132
  85. data/app/models/concerns/masks/adapter.rb +0 -68
  86. data/app/models/concerns/masks/role.rb +0 -9
  87. data/app/models/concerns/masks/scoped.rb +0 -54
  88. data/app/models/masks/access/actor_password.rb +0 -20
  89. data/app/models/masks/access/actor_scopes.rb +0 -18
  90. data/app/models/masks/access/actor_signup.rb +0 -22
  91. data/app/models/masks/actors/anonymous.rb +0 -40
  92. data/app/models/masks/actors/system.rb +0 -24
  93. data/app/models/masks/adapters/active_record.rb +0 -85
  94. data/app/models/masks/application_model.rb +0 -15
  95. data/app/models/masks/application_record.rb +0 -8
  96. data/app/models/masks/check.rb +0 -192
  97. data/app/models/masks/credential.rb +0 -166
  98. data/app/models/masks/credentials/backup_code.rb +0 -30
  99. data/app/models/masks/credentials/device.rb +0 -59
  100. data/app/models/masks/credentials/email.rb +0 -48
  101. data/app/models/masks/credentials/factor2.rb +0 -71
  102. data/app/models/masks/credentials/key.rb +0 -38
  103. data/app/models/masks/credentials/last_login.rb +0 -12
  104. data/app/models/masks/credentials/masquerade.rb +0 -32
  105. data/app/models/masks/credentials/nickname.rb +0 -63
  106. data/app/models/masks/credentials/one_time_code.rb +0 -34
  107. data/app/models/masks/credentials/password.rb +0 -28
  108. data/app/models/masks/credentials/recovery.rb +0 -71
  109. data/app/models/masks/credentials/session.rb +0 -67
  110. data/app/models/masks/device.rb +0 -30
  111. data/app/models/masks/error.rb +0 -51
  112. data/app/models/masks/event.rb +0 -14
  113. data/app/models/masks/mask.rb +0 -255
  114. data/app/models/masks/rails/actor.rb +0 -190
  115. data/app/models/masks/rails/actor_role.rb +0 -12
  116. data/app/models/masks/rails/device.rb +0 -47
  117. data/app/models/masks/rails/email.rb +0 -96
  118. data/app/models/masks/rails/key.rb +0 -61
  119. data/app/models/masks/rails/recovery.rb +0 -116
  120. data/app/models/masks/rails/role.rb +0 -20
  121. data/app/models/masks/rails/scope.rb +0 -15
  122. data/app/models/masks/session.rb +0 -447
  123. data/app/models/masks/sessions/access.rb +0 -26
  124. data/app/models/masks/sessions/inline.rb +0 -16
  125. data/app/models/masks/sessions/request.rb +0 -42
  126. data/app/resources/masks/actor_resource.rb +0 -9
  127. data/app/resources/masks/session_resource.rb +0 -15
  128. data/app/views/layouts/masks/application.html.erb +0 -17
  129. data/app/views/layouts/masks/mailer.html.erb +0 -17
  130. data/app/views/layouts/masks/mailer.text.erb +0 -1
  131. data/app/views/layouts/masks/manage.html.erb +0 -25
  132. data/app/views/masks/actor_mailer/recover_credentials.html.erb +0 -33
  133. data/app/views/masks/actor_mailer/recover_credentials.text.erb +0 -1
  134. data/app/views/masks/actor_mailer/verify_email.html.erb +0 -34
  135. data/app/views/masks/actor_mailer/verify_email.text.erb +0 -8
  136. data/app/views/masks/actors/current.html.erb +0 -152
  137. data/app/views/masks/application/_header.html.erb +0 -31
  138. data/app/views/masks/backup_codes/new.html.erb +0 -103
  139. data/app/views/masks/emails/new.html.erb +0 -103
  140. data/app/views/masks/emails/verify.html.erb +0 -51
  141. data/app/views/masks/keys/new.html.erb +0 -127
  142. data/app/views/masks/manage/actor/show.html.erb +0 -126
  143. data/app/views/masks/manage/actors/index.html.erb +0 -40
  144. data/app/views/masks/one_time_code/new.html.erb +0 -150
  145. data/app/views/masks/passwords/edit.html.erb +0 -58
  146. data/app/views/masks/recoveries/new.html.erb +0 -71
  147. data/app/views/masks/recoveries/password.html.erb +0 -64
  148. data/app/views/masks/sessions/new.html.erb +0 -153
  149. data/config/brakeman.ignore +0 -28
  150. data/config/locales/en.yml +0 -286
  151. data/db/migrate/20231205173845_create_actors.rb +0 -94
  152. data/lib/generators/masks/install/USAGE +0 -8
  153. data/lib/generators/masks/install/templates/initializer.rb +0 -5
  154. data/lib/generators/masks/install/templates/masks.json +0 -6
  155. data/lib/masks/configuration.rb +0 -236
  156. data/lib/masks/engine.rb +0 -25
  157. data/lib/masks/middleware.rb +0 -70
  158. data/lib/tasks/masks_tasks.rake +0 -72
  159. data/masks.json +0 -277
@@ -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,11 @@
1
+ module Masks
2
+ module Rails
3
+ module Configurable
4
+ extend ActiveSupport::Concern
5
+
6
+ def masks_config
7
+ Masks::Rails.config
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -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
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
1
  module Masks
4
- VERSION = Gem::Version.new("0.3.2")
2
+ VERSION = "0.5.0".freeze
5
3
  end