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,62 @@
1
+ module Masks
2
+ module Client
3
+ class Rack
4
+ CLAIMS = "masks.claims".freeze
5
+ ERROR = "masks.error".freeze
6
+
7
+ def initialize(app, resource:, scope: nil, only: nil, optional: false)
8
+ @app = app
9
+ @resource = resource
10
+ @scope = scope
11
+ @only = only
12
+ @optional = optional
13
+ end
14
+
15
+ def call(env)
16
+ return @app.call(env) unless guards?(env)
17
+
18
+ env[CLAIMS] = resource(env).authenticate(env["HTTP_AUTHORIZATION"], scope: @scope)
19
+
20
+ @app.call(env)
21
+ rescue Challenge => e
22
+ return passthrough(env, e) if @optional && e.is_a?(Unauthenticated)
23
+
24
+ refuse(resource(env), e)
25
+ end
26
+
27
+ private
28
+
29
+ def guards?(env)
30
+ return true if @only.nil?
31
+
32
+ @only.call(env)
33
+ end
34
+
35
+ def passthrough(env, error)
36
+ env[ERROR] = error
37
+
38
+ @app.call(env)
39
+ end
40
+
41
+ def resource(env)
42
+ @resource.respond_to?(:call) ? @resource.call(env) : @resource
43
+ end
44
+
45
+ def refuse(resource, error)
46
+ body = JSON.generate(
47
+ { "error" => error.code, "error_description" => error.description }.compact
48
+ )
49
+
50
+ [
51
+ error.status,
52
+ {
53
+ "content-type" => "application/json",
54
+ "cache-control" => "no-store",
55
+ "www-authenticate" => resource.challenge(error)
56
+ },
57
+ [ body ]
58
+ ]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,91 @@
1
+ module Masks
2
+ module Client
3
+ class Registration
4
+ attr_reader :issuer, :metadata, :access_token
5
+
6
+ def self.create(issuer, token: nil, **attributes)
7
+ issuer = Issuer.resolve(issuer)
8
+ headers = token ? { "Authorization" => "Bearer #{token}" } : {}
9
+ body = HTTP.post_json(
10
+ issuer.endpoint("registration_endpoint"), stringify(attributes), headers
11
+ )
12
+
13
+ new(issuer, body)
14
+ end
15
+
16
+ def self.stringify(attributes)
17
+ {
18
+ "client_name" => attributes[:name] || attributes[:client_name],
19
+ "redirect_uris" => Array(attributes[:redirect_uris]),
20
+ "grant_types" => fallback(attributes[:grant_types], %w[authorization_code]),
21
+ "response_types" => fallback(attributes[:response_types], %w[code]),
22
+ "scope" => Array(attributes[:scope]).join(" "),
23
+ "token_endpoint_auth_method" => attributes[:token_endpoint_auth_method],
24
+ "application_type" => attributes[:application_type],
25
+ "client_uri" => attributes[:client_uri],
26
+ "logo_uri" => attributes[:logo_uri]
27
+ }.reject { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
28
+ end
29
+
30
+ def self.fallback(value, default)
31
+ list = Array(value)
32
+ list.empty? ? default : list
33
+ end
34
+
35
+ # Rebuild a registration from what a consumer stored, so RFC 7592's read,
36
+ # update and delete are reachable without having just created it.
37
+ def self.held(issuer, credentials)
38
+ held = credentials.to_h.transform_keys(&:to_s)
39
+ return nil if held["registration_client_uri"].blank? || held["registration_access_token"].blank?
40
+
41
+ new(Issuer.resolve(issuer), held)
42
+ end
43
+
44
+ def initialize(issuer, body)
45
+ @issuer = issuer
46
+ @metadata = body
47
+ @access_token = body["registration_access_token"]
48
+ end
49
+
50
+ def client_id
51
+ metadata["client_id"]
52
+ end
53
+
54
+ def client_secret
55
+ metadata["client_secret"]
56
+ end
57
+
58
+ def uri
59
+ metadata["registration_client_uri"]
60
+ end
61
+
62
+ def read
63
+ HTTP.get(uri, authorization)
64
+ end
65
+
66
+ def update(**attributes)
67
+ @metadata = metadata.merge(HTTP.put_json(uri, self.class.stringify(attributes), authorization))
68
+ self
69
+ end
70
+
71
+ def delete
72
+ HTTP.delete(uri, authorization)
73
+ true
74
+ end
75
+
76
+ def authorization
77
+ { "Authorization" => "Bearer #{access_token}" }
78
+ end
79
+
80
+ def session(redirect_uri:, scope: Session::DEFAULT_SCOPE)
81
+ Session.new(
82
+ issuer: issuer,
83
+ client_id: client_id,
84
+ client_secret: client_secret,
85
+ redirect_uri: redirect_uri,
86
+ scope: scope
87
+ )
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,25 @@
1
+ module Masks
2
+ module Client
3
+ class Registry
4
+ def initialize
5
+ @issuers = {}
6
+ @lock = Mutex.new
7
+ end
8
+
9
+ def [](url, ttl: Issuer::TTL)
10
+ key = Issuer.normalize(url)
11
+
12
+ @lock.synchronize { @issuers[key] ||= Issuer.new(key, ttl: ttl) }
13
+ end
14
+
15
+ def clear!
16
+ @lock.synchronize { @issuers.clear }
17
+ self
18
+ end
19
+
20
+ def urls
21
+ @lock.synchronize { @issuers.keys }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,92 @@
1
+ module Masks
2
+ module Client
3
+ class Resource
4
+ BEARER = /\ABearer[ \t]+([^\s,]+)[ \t]*\z/i.freeze
5
+ METADATA_PATH = "/.well-known/oauth-protected-resource".freeze
6
+ REQUIRED = %w[iss sub exp].freeze
7
+
8
+ attr_reader :issuer, :url, :scopes
9
+
10
+ def initialize(issuer:, url:, scopes: [], metadata_url: nil,
11
+ algorithms: Verifier::ALGORITHMS, required: REQUIRED, verifier: nil)
12
+ @issuer = Issuer.resolve(issuer)
13
+ @url = url.to_s
14
+ @descriptions = describe(scopes)
15
+ @scopes = (@descriptions.any? ? @descriptions.keys : Array(scopes).map(&:to_s)).freeze
16
+ @metadata_url = metadata_url
17
+ @required = Array(required)
18
+ @verifier = verifier || Verifier.new(@issuer, audience: @url, algorithms: algorithms)
19
+ end
20
+
21
+ def authenticate(authorization, scope: nil)
22
+ claims = Claims.new(@verifier.verify(token!(authorization), required: @required))
23
+
24
+ Array(scope).each { |name| claims.permit!(name) }
25
+
26
+ claims
27
+ rescue InvalidToken => e
28
+ raise Unauthorized.new(e.message)
29
+ end
30
+
31
+ def token(authorization)
32
+ authorization.to_s[BEARER, 1]
33
+ end
34
+
35
+ def metadata_url
36
+ @metadata_url ||= "#{origin}#{METADATA_PATH}"
37
+ end
38
+
39
+ def metadata
40
+ {
41
+ "resource" => url,
42
+ "authorization_servers" => [ issuer.url ],
43
+ "scopes_supported" => scopes,
44
+ "scope_descriptions" => @descriptions,
45
+ "bearer_methods_supported" => [ "header" ]
46
+ }.reject { |_, value| value.respond_to?(:empty?) && value.empty? }
47
+ end
48
+
49
+ def challenge(error = nil)
50
+ error = Unauthorized.new(error.to_s) unless error.is_a?(Challenge)
51
+
52
+ parameters = [
53
+ [ "error", error.code ],
54
+ [ "error_description", error.description ],
55
+ [ "scope", error.scope || (scopes.join(" ") if scopes.any?) ],
56
+ [ "resource_metadata", metadata_url ]
57
+ ]
58
+
59
+ "Bearer " + parameters.filter_map { |name, value|
60
+ %(#{name}="#{quote(value)}") if value && !value.to_s.empty?
61
+ }.join(", ")
62
+ end
63
+
64
+ private
65
+
66
+ def describe(scopes)
67
+ return {} unless scopes.is_a?(Hash)
68
+
69
+ scopes.each_with_object({}) do |(scope, description), held|
70
+ held[scope.to_s] = description.to_s
71
+ end.freeze
72
+ end
73
+
74
+ def token!(authorization)
75
+ token(authorization) || raise(Unauthenticated.new)
76
+ end
77
+
78
+ def origin
79
+ uri = URI.parse(url)
80
+ port = ":#{uri.port}" unless uri.port.nil? || uri.default_port == uri.port
81
+
82
+ "#{uri.scheme}://#{uri.host}#{port}"
83
+ rescue URI::InvalidURIError
84
+ url
85
+ end
86
+
87
+ def quote(value)
88
+ value.to_s.gsub(/[\\"]/, " ").gsub(/[[:cntrl:]]/, " ").strip
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,146 @@
1
+ module Masks
2
+ module Client
3
+ class Session
4
+ DEFAULT_SCOPE = %w[openid profile email].freeze
5
+
6
+ attr_reader :issuer, :client_id, :client_secret, :redirect_uri, :scope
7
+
8
+ def initialize(issuer:, client_id:, redirect_uri:, client_secret: nil, scope: DEFAULT_SCOPE)
9
+ @issuer = Issuer.resolve(issuer)
10
+ @client_id = client_id
11
+ @client_secret = client_secret
12
+ @redirect_uri = redirect_uri
13
+ @scope = Array(scope)
14
+ end
15
+
16
+ def start(resource: nil, prompt: nil, scope: nil, state: SecureRandom.urlsafe_base64(24),
17
+ nonce: SecureRandom.urlsafe_base64(24))
18
+ pkce = Pkce.generate
19
+ scopes = Array(scope || self.scope)
20
+ nonce = nil unless scopes.include?("openid")
21
+
22
+ pairs = [
23
+ [ "response_type", "code" ],
24
+ [ "client_id", client_id ],
25
+ [ "redirect_uri", redirect_uri ],
26
+ [ "scope", scopes.join(" ") ],
27
+ [ "state", state ],
28
+ [ "code_challenge", pkce.challenge ],
29
+ [ "code_challenge_method", pkce.method ]
30
+ ]
31
+
32
+ pairs << [ "nonce", nonce ] if nonce
33
+
34
+ Array(resource).each { |value| pairs << [ "resource", value ] }
35
+ pairs << [ "prompt", prompt ] if prompt
36
+
37
+ {
38
+ url: "#{issuer.endpoint('authorization_endpoint')}?#{URI.encode_www_form(pairs)}",
39
+ state: state,
40
+ nonce: nonce,
41
+ verifier: pkce.verifier
42
+ }
43
+ end
44
+
45
+ def complete(code:, verifier:, resource: nil)
46
+ form = [
47
+ [ "grant_type", "authorization_code" ],
48
+ [ "code", code ],
49
+ [ "client_id", client_id ],
50
+ [ "redirect_uri", redirect_uri ],
51
+ [ "code_verifier", verifier ]
52
+ ]
53
+
54
+ Array(resource).each { |value| form << [ "resource", value ] }
55
+
56
+ Tokens.granted(HTTP.post_form(issuer.endpoint("token_endpoint"), form, authorization))
57
+ end
58
+
59
+ def refresh(refresh_token, resource: nil, scope: nil)
60
+ form = [
61
+ [ "grant_type", "refresh_token" ],
62
+ [ "refresh_token", refresh_token ],
63
+ [ "client_id", client_id ]
64
+ ]
65
+
66
+ form << [ "scope", Array(scope).join(" ") ] if scope
67
+ Array(resource).each { |value| form << [ "resource", value ] }
68
+
69
+ Tokens.granted(HTTP.post_form(issuer.endpoint("token_endpoint"), form, authorization))
70
+ end
71
+
72
+ def exchange(subject_token, scope: nil, resource: nil, lifetime: nil)
73
+ form = [
74
+ [ "grant_type", Tokens::EXCHANGE ],
75
+ [ "client_id", client_id ],
76
+ [ "subject_token", subject_token ],
77
+ [ "subject_token_type", Tokens::ACCESS_TOKEN ]
78
+ ]
79
+
80
+ form << [ "scope", Array(scope).join(" ") ] if scope
81
+ form << [ "requested_lifetime", lifetime.to_i ] if lifetime
82
+ Array(resource).each { |value| form << [ "resource", value ] }
83
+
84
+ Tokens.granted(HTTP.post_form(issuer.endpoint("token_endpoint"), form, authorization))
85
+ end
86
+
87
+ def revoke(token, hint: nil)
88
+ form = [ [ "token", token ], [ "client_id", client_id ] ]
89
+ form << [ "token_type_hint", hint ] if hint
90
+
91
+ HTTP.post_form(issuer.endpoint("revocation_endpoint"), form, authorization)
92
+ true
93
+ end
94
+
95
+ def end_session_url(post_logout_redirect_uri: nil, state: nil, id_token_hint: nil)
96
+ pairs = [ [ "client_id", client_id ] ]
97
+ pairs << [ "id_token_hint", id_token_hint ] if id_token_hint
98
+ pairs << [ "post_logout_redirect_uri", post_logout_redirect_uri ] if post_logout_redirect_uri
99
+ pairs << [ "state", state ] if state
100
+
101
+ "#{issuer.endpoint('end_session_endpoint')}?#{URI.encode_www_form(pairs)}"
102
+ end
103
+
104
+ def introspect(token, hint: nil)
105
+ form = [ [ "token", token ], [ "client_id", client_id ] ]
106
+ form << [ "token_type_hint", hint ] if hint
107
+
108
+ Introspection.new(
109
+ HTTP.post_form(issuer.endpoint("introspection_endpoint"), form, authorization)
110
+ )
111
+ end
112
+
113
+ def userinfo(access_token)
114
+ HTTP.get(issuer.endpoint("userinfo_endpoint"), "Authorization" => "Bearer #{access_token}")
115
+ end
116
+
117
+ def identity(tokens)
118
+ return nil if tokens.id_token.nil?
119
+
120
+ claims = Verifier.new(issuer, audience: client_id).verify(tokens.id_token)
121
+
122
+ profile(tokens).merge(claims)
123
+ end
124
+
125
+ def profile(tokens)
126
+ return {} if tokens.access_token.nil?
127
+
128
+ userinfo(tokens.access_token)
129
+ rescue Masks::Client::Error
130
+ {}
131
+ end
132
+
133
+ private
134
+
135
+ def authorization
136
+ return {} if client_secret.nil?
137
+
138
+ encoded = Base64.strict_encode64(
139
+ "#{URI.encode_www_form_component(client_id)}:#{URI.encode_www_form_component(client_secret)}"
140
+ )
141
+
142
+ { "Authorization" => "Basic #{encoded}" }
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,40 @@
1
+ module Masks
2
+ module Client
3
+ module Stores
4
+ class Memory
5
+ attr_reader :held
6
+
7
+ def initialize(held = {})
8
+ @held = held
9
+ end
10
+
11
+ def read
12
+ held
13
+ end
14
+
15
+ def write(value)
16
+ @held = value
17
+ end
18
+ end
19
+
20
+ class Session
21
+ def initialize(session, key)
22
+ @session = session
23
+ @key = key
24
+ end
25
+
26
+ def read
27
+ @session[@key]
28
+ end
29
+
30
+ def write(value)
31
+ if value.empty?
32
+ @session.delete(@key)
33
+ else
34
+ @session[@key] = value
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,70 @@
1
+ module Masks
2
+ module Client
3
+ class Tokens
4
+ EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange".freeze
5
+ ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token".freeze
6
+
7
+ attr_reader :access_token, :id_token, :refresh_token, :token_type, :scope, :expires_in, :obtained_at
8
+
9
+ def self.granted(body)
10
+ token = new(body)
11
+
12
+ if token.access_token.to_s.empty?
13
+ raise Rejected.new(
14
+ body["error"] || "invalid_token_response",
15
+ body["error_description"] || "the token endpoint answered without an access_token"
16
+ )
17
+ end
18
+
19
+ token
20
+ end
21
+
22
+ def initialize(body)
23
+ @access_token = body["access_token"]
24
+ @id_token = body["id_token"]
25
+ @refresh_token = body["refresh_token"]
26
+ @token_type = body["token_type"] || "Bearer"
27
+ @scope = body["scope"].to_s
28
+ @expires_in = body["expires_in"].to_i
29
+ @obtained_at = Time.now.to_i
30
+ end
31
+
32
+ def expires_at
33
+ obtained_at + expires_in
34
+ end
35
+
36
+ def expired?(leeway: 30)
37
+ Time.now.to_i + leeway >= expires_at
38
+ end
39
+
40
+ def scopes
41
+ scope.split(/\s+/).reject(&:empty?)
42
+ end
43
+
44
+ def authorization
45
+ "#{token_type} #{access_token}"
46
+ end
47
+
48
+ def to_h
49
+ {
50
+ "access_token" => access_token,
51
+ "id_token" => id_token,
52
+ "refresh_token" => refresh_token,
53
+ "token_type" => token_type,
54
+ "scope" => scope,
55
+ "expires_in" => expires_in,
56
+ "obtained_at" => obtained_at
57
+ }.compact
58
+ end
59
+
60
+ def self.from_h(data)
61
+ return nil if data.nil? || data.empty?
62
+
63
+ tokens = allocate
64
+ tokens.send(:initialize, data)
65
+ tokens.instance_variable_set(:@obtained_at, data["obtained_at"] || Time.now.to_i)
66
+ tokens
67
+ end
68
+ end
69
+ end
70
+ end
@@ -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