masks 0.4.0 → 0.6.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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/LICENSE +21 -0
  4. data/README.md +235 -29
  5. data/app/controllers/masks/rails/avatars_controller.rb +51 -0
  6. data/app/controllers/masks/rails/base_controller.rb +13 -0
  7. data/app/controllers/masks/rails/handshakes_controller.rb +86 -0
  8. data/app/controllers/masks/rails/sessions_controller.rb +118 -0
  9. data/app/views/layouts/masks/rails/plain.html.erb +47 -0
  10. data/app/views/masks/rails/handshakes/refused.html.erb +12 -0
  11. data/app/views/masks/rails/handshakes/show.html.erb +40 -0
  12. data/config/routes.rb +11 -82
  13. data/lib/generators/masks/install/install_generator.rb +35 -22
  14. data/lib/generators/masks/install/templates/masks.rb.tt +38 -0
  15. data/lib/masks/client/claims.rb +144 -0
  16. data/lib/masks/client/errors.rb +60 -0
  17. data/lib/masks/client/handshake.rb +104 -0
  18. data/lib/masks/client/http.rb +96 -0
  19. data/lib/masks/client/introspection.rb +29 -0
  20. data/lib/masks/client/issuer.rb +92 -0
  21. data/lib/masks/client/pkce.rb +27 -0
  22. data/lib/masks/client/rack.rb +62 -0
  23. data/lib/masks/client/registration.rb +115 -0
  24. data/lib/masks/client/registry.rb +25 -0
  25. data/lib/masks/client/resource.rb +92 -0
  26. data/lib/masks/client/session.rb +146 -0
  27. data/lib/masks/client/stores.rb +40 -0
  28. data/lib/masks/client/tokens.rb +70 -0
  29. data/lib/masks/client/tracker.rb +122 -0
  30. data/lib/masks/client/verifier.rb +42 -0
  31. data/lib/masks/client.rb +51 -0
  32. data/lib/masks/rails/authentication.rb +260 -0
  33. data/lib/masks/rails/configurable.rb +11 -0
  34. data/lib/masks/rails/configuration.rb +184 -0
  35. data/lib/masks/rails/credentials.rb +60 -0
  36. data/lib/masks/rails/engine.rb +20 -0
  37. data/lib/masks/rails/protected_resource.rb +51 -0
  38. data/lib/masks/rails.rb +10 -0
  39. data/lib/masks/version.rb +1 -3
  40. data/lib/masks.rb +3 -183
  41. metadata +48 -416
  42. data/MIT-LICENSE +0 -20
  43. data/Rakefile +0 -11
  44. data/app/assets/builds/masks/application.css +0 -1
  45. data/app/assets/builds/masks/application.js +0 -8549
  46. data/app/assets/builds/masks/application.js.map +0 -7
  47. data/app/assets/images/masks.png +0 -0
  48. data/app/assets/javascripts/application.js +0 -2
  49. data/app/assets/javascripts/controllers/application.js +0 -9
  50. data/app/assets/javascripts/controllers/emails_controller.js +0 -28
  51. data/app/assets/javascripts/controllers/index.js +0 -21
  52. data/app/assets/javascripts/controllers/keys_controller.js +0 -20
  53. data/app/assets/javascripts/controllers/recover_controller.js +0 -21
  54. data/app/assets/javascripts/controllers/recover_password_controller.js +0 -21
  55. data/app/assets/javascripts/controllers/session_controller.js +0 -94
  56. data/app/assets/javascripts/controllers/table_controller.js +0 -15
  57. data/app/assets/manifest.js +0 -2
  58. data/app/assets/masks_manifest.js +0 -2
  59. data/app/assets/stylesheets/application.css +0 -34
  60. data/app/controllers/concerns/masks/controller.rb +0 -114
  61. data/app/controllers/masks/actors_controller.rb +0 -15
  62. data/app/controllers/masks/application_controller.rb +0 -35
  63. data/app/controllers/masks/backup_codes_controller.rb +0 -34
  64. data/app/controllers/masks/debug_controller.rb +0 -9
  65. data/app/controllers/masks/devices_controller.rb +0 -20
  66. data/app/controllers/masks/emails_controller.rb +0 -60
  67. data/app/controllers/masks/error_controller.rb +0 -14
  68. data/app/controllers/masks/keys_controller.rb +0 -45
  69. data/app/controllers/masks/manage/actors_controller.rb +0 -83
  70. data/app/controllers/masks/manage/base_controller.rb +0 -20
  71. data/app/controllers/masks/manage/clients_controller.rb +0 -84
  72. data/app/controllers/masks/manage/dashboard_controller.rb +0 -15
  73. data/app/controllers/masks/manage/devices_controller.rb +0 -19
  74. data/app/controllers/masks/one_time_code_controller.rb +0 -49
  75. data/app/controllers/masks/openid/authorizations_controller.rb +0 -45
  76. data/app/controllers/masks/openid/discoveries_controller.rb +0 -55
  77. data/app/controllers/masks/openid/tokens_controller.rb +0 -45
  78. data/app/controllers/masks/openid/userinfo_controller.rb +0 -28
  79. data/app/controllers/masks/passwords_controller.rb +0 -33
  80. data/app/controllers/masks/recoveries_controller.rb +0 -43
  81. data/app/controllers/masks/sessions_controller.rb +0 -53
  82. data/app/helpers/masks/application_helper.rb +0 -49
  83. data/app/jobs/masks/application_job.rb +0 -7
  84. data/app/jobs/masks/expire_actors_job.rb +0 -15
  85. data/app/jobs/masks/expire_recoveries_job.rb +0 -15
  86. data/app/mailers/masks/actor_mailer.rb +0 -22
  87. data/app/mailers/masks/application_mailer.rb +0 -15
  88. data/app/models/concerns/masks/access.rb +0 -162
  89. data/app/models/concerns/masks/actor.rb +0 -132
  90. data/app/models/concerns/masks/adapter.rb +0 -68
  91. data/app/models/concerns/masks/role.rb +0 -9
  92. data/app/models/concerns/masks/scoped.rb +0 -54
  93. data/app/models/masks/access/actor_password.rb +0 -21
  94. data/app/models/masks/access/actor_scopes.rb +0 -18
  95. data/app/models/masks/access/actor_signup.rb +0 -21
  96. data/app/models/masks/actors/anonymous.rb +0 -40
  97. data/app/models/masks/actors/system.rb +0 -24
  98. data/app/models/masks/adapters/active_record.rb +0 -85
  99. data/app/models/masks/application_model.rb +0 -15
  100. data/app/models/masks/application_record.rb +0 -8
  101. data/app/models/masks/check.rb +0 -192
  102. data/app/models/masks/credential.rb +0 -166
  103. data/app/models/masks/credentials/access_token.rb +0 -60
  104. data/app/models/masks/credentials/backup_code.rb +0 -30
  105. data/app/models/masks/credentials/device.rb +0 -59
  106. data/app/models/masks/credentials/email.rb +0 -48
  107. data/app/models/masks/credentials/factor2.rb +0 -71
  108. data/app/models/masks/credentials/key.rb +0 -38
  109. data/app/models/masks/credentials/last_login.rb +0 -12
  110. data/app/models/masks/credentials/masquerade.rb +0 -32
  111. data/app/models/masks/credentials/nickname.rb +0 -63
  112. data/app/models/masks/credentials/one_time_code.rb +0 -34
  113. data/app/models/masks/credentials/password.rb +0 -28
  114. data/app/models/masks/credentials/recovery.rb +0 -71
  115. data/app/models/masks/credentials/return_to.rb +0 -27
  116. data/app/models/masks/credentials/session.rb +0 -67
  117. data/app/models/masks/device.rb +0 -30
  118. data/app/models/masks/error.rb +0 -51
  119. data/app/models/masks/event.rb +0 -14
  120. data/app/models/masks/mask.rb +0 -266
  121. data/app/models/masks/openid/authorization.rb +0 -116
  122. data/app/models/masks/openid/token.rb +0 -56
  123. data/app/models/masks/rails/actor.rb +0 -212
  124. data/app/models/masks/rails/actor_role.rb +0 -12
  125. data/app/models/masks/rails/device.rb +0 -47
  126. data/app/models/masks/rails/email.rb +0 -96
  127. data/app/models/masks/rails/key.rb +0 -61
  128. data/app/models/masks/rails/openid/access_token.rb +0 -55
  129. data/app/models/masks/rails/openid/authorization.rb +0 -45
  130. data/app/models/masks/rails/openid/client.rb +0 -186
  131. data/app/models/masks/rails/openid/id_token.rb +0 -43
  132. data/app/models/masks/rails/recovery.rb +0 -116
  133. data/app/models/masks/rails/role.rb +0 -20
  134. data/app/models/masks/rails/scope.rb +0 -15
  135. data/app/models/masks/session.rb +0 -447
  136. data/app/models/masks/sessions/access.rb +0 -27
  137. data/app/models/masks/sessions/inline.rb +0 -16
  138. data/app/models/masks/sessions/request.rb +0 -42
  139. data/app/resources/masks/actor_resource.rb +0 -9
  140. data/app/resources/masks/session_resource.rb +0 -15
  141. data/app/views/layouts/masks/application.html.erb +0 -17
  142. data/app/views/layouts/masks/mailer.html.erb +0 -17
  143. data/app/views/layouts/masks/mailer.text.erb +0 -1
  144. data/app/views/layouts/masks/manage.html.erb +0 -42
  145. data/app/views/masks/actor_mailer/recover_credentials.html.erb +0 -32
  146. data/app/views/masks/actor_mailer/recover_credentials.text.erb +0 -1
  147. data/app/views/masks/actor_mailer/verify_email.html.erb +0 -33
  148. data/app/views/masks/actor_mailer/verify_email.text.erb +0 -8
  149. data/app/views/masks/actors/current.html.erb +0 -145
  150. data/app/views/masks/application/_header.html.erb +0 -30
  151. data/app/views/masks/backup_codes/new.html.erb +0 -117
  152. data/app/views/masks/emails/new.html.erb +0 -109
  153. data/app/views/masks/emails/verify.html.erb +0 -51
  154. data/app/views/masks/keys/new.html.erb +0 -127
  155. data/app/views/masks/manage/actors/index.html.erb +0 -104
  156. data/app/views/masks/manage/actors/show.html.erb +0 -172
  157. data/app/views/masks/manage/clients/index.html.erb +0 -102
  158. data/app/views/masks/manage/clients/show.html.erb +0 -156
  159. data/app/views/masks/manage/dashboard/index.html.erb +0 -10
  160. data/app/views/masks/manage/devices/index.html.erb +0 -47
  161. data/app/views/masks/one_time_code/new.html.erb +0 -167
  162. data/app/views/masks/openid/authorizations/error.html.erb +0 -23
  163. data/app/views/masks/openid/authorizations/new.html.erb +0 -46
  164. data/app/views/masks/passwords/edit.html.erb +0 -71
  165. data/app/views/masks/recoveries/new.html.erb +0 -69
  166. data/app/views/masks/recoveries/password.html.erb +0 -63
  167. data/app/views/masks/sessions/new.html.erb +0 -152
  168. data/config/brakeman.ignore +0 -28
  169. data/config/initializers/inflections.rb +0 -5
  170. data/config/locales/en.yml +0 -307
  171. data/db/migrate/20231205173845_create_actors.rb +0 -94
  172. data/db/migrate/20240329182422_support_openid.rb +0 -64
  173. data/lib/generators/masks/install/USAGE +0 -8
  174. data/lib/generators/masks/install/templates/initializer.rb +0 -5
  175. data/lib/generators/masks/install/templates/masks.json +0 -9
  176. data/lib/masks/configuration.rb +0 -249
  177. data/lib/masks/engine.rb +0 -25
  178. data/lib/masks/middleware.rb +0 -70
  179. data/lib/tasks/masks_tasks.rake +0 -72
  180. data/masks.json +0 -315
@@ -0,0 +1,122 @@
1
+ module Masks
2
+ module Client
3
+ class Tracker
4
+ TTL = 3600
5
+ LIMIT = 5
6
+
7
+ class Entry
8
+ attr_reader :id, :data, :opened_at, :expires_at
9
+
10
+ def self.from_h(id, held)
11
+ return nil unless held.is_a?(Hash)
12
+
13
+ new(
14
+ id: id,
15
+ data: held["data"] || {},
16
+ opened_at: held["opened_at"].to_f,
17
+ expires_at: held["expires_at"].to_f
18
+ )
19
+ end
20
+
21
+ def initialize(id:, data:, opened_at:, expires_at:)
22
+ @id = id
23
+ @data = data
24
+ @opened_at = opened_at
25
+ @expires_at = expires_at
26
+ end
27
+
28
+ def live?(now = Time.now.to_f)
29
+ expires_at > now
30
+ end
31
+
32
+ def [](key)
33
+ data[key.to_s]
34
+ end
35
+
36
+ def to_h
37
+ { "data" => data, "opened_at" => opened_at, "expires_at" => expires_at }
38
+ end
39
+ end
40
+
41
+ attr_reader :store, :ttl, :limit
42
+
43
+ def initialize(store, ttl: TTL, limit: LIMIT)
44
+ @store = store
45
+ @ttl = ttl
46
+ @limit = limit
47
+ end
48
+
49
+ def open(**data)
50
+ id = SecureRandom.urlsafe_base64(32)
51
+ now = Time.now.to_f
52
+
53
+ entry = Entry.new(
54
+ id: id,
55
+ data: data.transform_keys(&:to_s).compact,
56
+ opened_at: now,
57
+ expires_at: now + ttl
58
+ )
59
+
60
+ write(sweep.merge(id => entry.to_h))
61
+ entry
62
+ end
63
+
64
+ def amend(id, **data)
65
+ held = sweep
66
+ entry = Entry.from_h(id, held[id])
67
+ return nil if entry.nil?
68
+
69
+ merged = Entry.new(
70
+ id: id,
71
+ data: entry.data.merge(data.transform_keys(&:to_s).compact),
72
+ opened_at: entry.opened_at,
73
+ expires_at: entry.expires_at
74
+ )
75
+
76
+ write(held.merge(id => merged.to_h))
77
+ merged
78
+ end
79
+
80
+ def claim(id)
81
+ return nil if id.nil? || id.empty?
82
+
83
+ held = sweep
84
+ entry = Entry.from_h(id, held[id])
85
+ return nil if entry.nil? || !entry.live?
86
+
87
+ write(held.except(id))
88
+ entry
89
+ end
90
+
91
+ def clear!
92
+ write({})
93
+ end
94
+
95
+ def size
96
+ sweep.size
97
+ end
98
+
99
+ private
100
+
101
+ def sweep
102
+ now = Time.now.to_f
103
+
104
+ held = read.select do |id, value|
105
+ entry = Entry.from_h(id, value)
106
+ entry&.live?(now)
107
+ end
108
+
109
+ held.to_a.last(limit).to_h
110
+ end
111
+
112
+ def read
113
+ value = store.read
114
+ value.is_a?(Hash) ? value : {}
115
+ end
116
+
117
+ def write(held)
118
+ store.write(held)
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,42 @@
1
+ module Masks
2
+ module Client
3
+ class Verifier
4
+ ALGORITHMS = %w[RS256 ES256].freeze
5
+
6
+ attr_reader :issuer, :audience
7
+
8
+ def initialize(issuer, audience:, algorithms: ALGORITHMS)
9
+ @issuer = Issuer.resolve(issuer)
10
+ @audience = Array(audience)
11
+ @algorithms = algorithms
12
+ end
13
+
14
+ def verify(token, required: %w[iss sub exp])
15
+ JWT.decode(
16
+ token, nil, true,
17
+ algorithms: @algorithms,
18
+ jwks: keys,
19
+ iss: issuer.url, verify_iss: true,
20
+ aud: audience, verify_aud: true,
21
+ verify_expiration: true,
22
+ required_claims: required
23
+ ).first
24
+ rescue JWT::DecodeError => e
25
+ raise InvalidToken, e.message
26
+ end
27
+
28
+ def tenant(token)
29
+ verify(token)["tenant"]
30
+ end
31
+
32
+ private
33
+
34
+ def keys
35
+ ->(options) do
36
+ issuer.refresh! if options[:invalidate]
37
+ issuer.jwks
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -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,260 @@
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
+ :masks_claims
11
+ end
12
+ end
13
+
14
+ REQUESTS = "masks_requests".freeze
15
+ HANDSHAKES = "masks_handshakes".freeze
16
+
17
+ def masks_session
18
+ @masks_session ||= masks_config.session_for(request)
19
+ end
20
+
21
+ def masks_requests
22
+ @masks_requests ||= masks_tracker(REQUESTS)
23
+ end
24
+
25
+ def masks_handshakes
26
+ @masks_handshakes ||= masks_tracker(HANDSHAKES)
27
+ end
28
+
29
+ def masks_tracker(key)
30
+ Masks::Client::Tracker.new(Masks::Client::Stores::Session.new(session, key))
31
+ end
32
+
33
+ def masks_tokens
34
+ return @masks_tokens if defined?(@masks_tokens)
35
+
36
+ @masks_tokens = Masks::Client::Tokens.from_h(session[masks_config.session_key])
37
+ end
38
+
39
+ def masks_store(tokens, identity: nil)
40
+ held = tokens.to_h.except("id_token")
41
+ held["identity"] = identity&.slice(*IDENTITY) || masks_held["identity"]
42
+
43
+ session[masks_config.session_key] = held.compact
44
+ @masks_tokens = tokens
45
+ @masks_identity = held["identity"]
46
+ end
47
+
48
+ def masks_held
49
+ session[masks_config.session_key] || {}
50
+ end
51
+
52
+ IDENTITY = [
53
+ "sub", "name", "preferred_username", "email", "email_verified", "tenant",
54
+ "picture", Masks::Client::Claims::AVATARS
55
+ ].freeze
56
+
57
+ def masks_identity_from(tokens)
58
+ return nil if tokens.id_token.nil?
59
+
60
+ masks_session.identity(tokens)
61
+ end
62
+
63
+ def masks_forget
64
+ session.delete(masks_config.session_key)
65
+ @masks_tokens = nil
66
+ @masks_identity = nil
67
+ end
68
+
69
+ def masks_signed_in?
70
+ masks_tokens.present? && !masks_tokens.expired?
71
+ end
72
+
73
+ def masks_configured?
74
+ masks_config.configured?(request)
75
+ end
76
+
77
+ def masks_registration
78
+ Masks::Client::Registration.held(
79
+ masks_config.issuer_for(request),
80
+ masks_config.credentials_for(request)
81
+ )
82
+ end
83
+
84
+ def masks_registered?
85
+ held = masks_registration
86
+
87
+ held.nil? || held.known?
88
+ rescue Masks::Client::Error
89
+ true
90
+ end
91
+
92
+ def masks_disconnect!
93
+ return false unless masks_configured? && masks_config.can_forget?
94
+
95
+ masks_config.forget!(request)
96
+ masks_forget
97
+
98
+ true
99
+ end
100
+
101
+ def masks_reconnect!
102
+ return false if masks_registered?
103
+
104
+ masks_disconnect!
105
+ end
106
+
107
+ def masks_handshake_path
108
+ Masks::Rails::Engine.routes.url_helpers.handshake_path
109
+ end
110
+
111
+ def masks_identity
112
+ return @masks_identity if defined?(@masks_identity)
113
+
114
+ @masks_identity = masks_held["identity"]
115
+ end
116
+
117
+ def masks_tenant
118
+ masks_identity&.dig("tenant")
119
+ end
120
+
121
+ def masks_claims
122
+ @masks_claims ||= Masks::Client::Claims.new(masks_identity || {})
123
+ end
124
+
125
+ def masks_scopes
126
+ masks_tokens&.scopes || []
127
+ end
128
+
129
+ def masks_permits?(scope)
130
+ masks_scopes.include?(scope.to_s)
131
+ end
132
+
133
+ def masks_access_token
134
+ masks_tokens&.access_token
135
+ end
136
+
137
+ def masks_refresh!
138
+ return false unless masks_configured?
139
+ return false if masks_tokens&.refresh_token.nil?
140
+
141
+ masks_store(
142
+ masks_session.refresh(
143
+ masks_tokens.refresh_token,
144
+ resource: masks_config.resource_for(request)
145
+ )
146
+ )
147
+ true
148
+ rescue Masks::Client::Unregistered
149
+ masks_disconnect!
150
+ false
151
+ rescue Masks::Client::Rejected
152
+ masks_forget
153
+ false
154
+ end
155
+
156
+ def masks_logout_url(return_to: nil)
157
+ return nil unless masks_configured?
158
+
159
+ masks_session.end_session_url(
160
+ post_logout_redirect_uri: masks_post_logout_redirect_uri,
161
+ state: SecureRandom.urlsafe_base64(16)
162
+ )
163
+ rescue Masks::Client::Error
164
+ nil
165
+ end
166
+
167
+ def masks_post_logout_redirect_uri
168
+ "#{request.base_url}#{masks_local_path(masks_config.after_sign_out) || '/'}"
169
+ end
170
+
171
+ def masks_login_url(return_to: nil)
172
+ path = Masks::Rails::Engine.routes.url_helpers.start_path
173
+ target = masks_local_path(return_to)
174
+
175
+ target ? "#{path}?return_to=#{CGI.escape(target)}" : path
176
+ end
177
+
178
+ def masks_account
179
+ identity = masks_identity || {}
180
+
181
+ {
182
+ "signed_in" => masks_signed_in?,
183
+ "subject" => identity["sub"],
184
+ "name" => identity["name"],
185
+ "nickname" => identity["preferred_username"],
186
+ "email" => identity["email"],
187
+ "email_verified" => identity["email_verified"],
188
+ "picture" => masks_claims.picture,
189
+ "avatars" => masks_claims.avatars.to_h.presence,
190
+ "tenant" => masks_tenant,
191
+ "scopes" => masks_scopes,
192
+ "expires_at" => masks_tokens&.expires_at
193
+ }.compact
194
+ end
195
+
196
+ def authenticate_masks!
197
+ return true if masks_configured? && (masks_signed_in? || (masks_tokens && masks_refresh!))
198
+
199
+ masks_refuse
200
+ false
201
+ end
202
+
203
+ private
204
+
205
+ def masks_refuse
206
+ return masks_refuse_json if masks_wants_json?
207
+ return redirect_to(masks_handshake_path) unless masks_configured?
208
+
209
+ session[:masks_return_to] = request.fullpath if request.get?
210
+ redirect_to Masks::Rails::Engine.routes.url_helpers.start_path
211
+ end
212
+
213
+ def masks_refuse_json
214
+ response.headers["Cache-Control"] = "no-store"
215
+
216
+ render json: masks_refusal, status: :unauthorized
217
+ end
218
+
219
+ def masks_refusal
220
+ unless masks_configured?
221
+ return {
222
+ "signed_in" => false,
223
+ "error" => "handshake_required",
224
+ "handshake_url" => masks_handshake_path
225
+ }
226
+ end
227
+
228
+ {
229
+ "signed_in" => false,
230
+ "error" => "login_required",
231
+ "login_url" => masks_login_url(return_to: masks_referring_path)
232
+ }
233
+ end
234
+
235
+ def masks_wants_json?
236
+ request.xhr? ||
237
+ request.format.json? ||
238
+ request.content_mime_type&.symbol == :json ||
239
+ !request.format.html?
240
+ end
241
+
242
+ def masks_referring_path
243
+ return nil if request.referer.blank?
244
+
245
+ masks_local_path(URI.parse(request.referer).request_uri)
246
+ rescue URI::InvalidURIError
247
+ nil
248
+ end
249
+
250
+ def masks_local_path(value)
251
+ path = value.to_s
252
+
253
+ return nil unless path.start_with?("/")
254
+ return nil if path.start_with?("//", "/\\")
255
+
256
+ path
257
+ end
258
+ end
259
+ end
260
+ 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,184 @@
1
+ module Masks
2
+ module Rails
3
+ class Configuration
4
+ class Unconfigured < Masks::Client::Error; end
5
+
6
+ attr_accessor :scope, :namespace, :resource, :resource_scopes, :after_sign_in,
7
+ :after_sign_out, :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: approved_scope
138
+ )
139
+ end
140
+
141
+ # What the handshake asks to be approved for, which is not what a sign-in
142
+ # requests. An app that owns a namespace asks for the namespace once, so
143
+ # that adding a capability later is a deployment rather than an approval
144
+ # round; the authorize request still names the scopes it actually wants.
145
+ def approved_scope
146
+ return scope if namespace.blank?
147
+
148
+ outside = Array(scope).reject { |name| name.to_s.start_with?(namespace) }
149
+
150
+ outside + [ namespace ]
151
+ end
152
+
153
+ def return_to_for(request)
154
+ uri = URI.parse(redirect_uri_for(request))
155
+ port = ":#{uri.port}" unless uri.port == uri.default_port
156
+
157
+ "#{uri.scheme}://#{uri.host}#{port}#{routes.handshake_callback_path}"
158
+ end
159
+
160
+ private
161
+
162
+ def routes
163
+ Masks::Rails::Engine.routes.url_helpers
164
+ end
165
+
166
+ def resolve(value, request)
167
+ return value unless value.respond_to?(:call)
168
+
169
+ value.arity.zero? ? value.call : value.call(request)
170
+ end
171
+ end
172
+
173
+ class << self
174
+ def config
175
+ @config ||= Configuration.new
176
+ end
177
+
178
+ def configure
179
+ yield config
180
+ config
181
+ end
182
+ end
183
+ end
184
+ end