masks 0.5.0 → 0.7.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.
@@ -6,7 +6,8 @@ module Masks
6
6
 
7
7
  included do
8
8
  if respond_to?(:helper_method)
9
- helper_method :masks_signed_in?, :masks_identity, :masks_tenant, :masks_scopes
9
+ helper_method :masks_signed_in?, :masks_identity, :masks_tenant, :masks_scopes,
10
+ :masks_claims
10
11
  end
11
12
  end
12
13
 
@@ -48,7 +49,10 @@ module Masks
48
49
  session[masks_config.session_key] || {}
49
50
  end
50
51
 
51
- IDENTITY = %w[sub name preferred_username email email_verified tenant].freeze
52
+ IDENTITY = [
53
+ "sub", "name", "preferred_username", "email", "email_verified", "tenant",
54
+ "picture", Masks::Client::Claims::AVATARS
55
+ ].freeze
52
56
 
53
57
  def masks_identity_from(tokens)
54
58
  return nil if tokens.id_token.nil?
@@ -70,6 +74,36 @@ module Masks
70
74
  masks_config.configured?(request)
71
75
  end
72
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
+
73
107
  def masks_handshake_path
74
108
  Masks::Rails::Engine.routes.url_helpers.handshake_path
75
109
  end
@@ -84,6 +118,10 @@ module Masks
84
118
  masks_identity&.dig("tenant")
85
119
  end
86
120
 
121
+ def masks_claims
122
+ @masks_claims ||= Masks::Client::Claims.new(masks_identity || {})
123
+ end
124
+
87
125
  def masks_scopes
88
126
  masks_tokens&.scopes || []
89
127
  end
@@ -107,6 +145,9 @@ module Masks
107
145
  )
108
146
  )
109
147
  true
148
+ rescue Masks::Client::Unregistered
149
+ masks_disconnect!
150
+ false
110
151
  rescue Masks::Client::Rejected
111
152
  masks_forget
112
153
  false
@@ -144,6 +185,8 @@ module Masks
144
185
  "nickname" => identity["preferred_username"],
145
186
  "email" => identity["email"],
146
187
  "email_verified" => identity["email_verified"],
188
+ "picture" => masks_claims.picture,
189
+ "avatars" => masks_claims.avatars.to_h.presence,
147
190
  "tenant" => masks_tenant,
148
191
  "scopes" => masks_scopes,
149
192
  "expires_at" => masks_tokens&.expires_at
@@ -3,10 +3,11 @@ module Masks
3
3
  class Configuration
4
4
  class Unconfigured < Masks::Client::Error; end
5
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
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, :delegates
9
+ attr_writer :issuer, :redirect_uri, :name, :credentials, :store, :forget, :logged_out,
10
+ :delegation_redirect_uri
10
11
 
11
12
  def initialize
12
13
  @scope = Masks::Client::Session::DEFAULT_SCOPE
@@ -17,12 +18,9 @@ module Masks
17
18
  @sign_out_of_issuer = false
18
19
  @parent_controller = "ActionController::Base"
19
20
  @authenticate_everything = false
21
+ @delegates = false
20
22
  end
21
23
 
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
24
  def default_credentials
27
25
  @default_credentials ||= Credentials.new(
28
26
  credentials_path || ::Rails.root.join("config", "masks.json")
@@ -43,6 +41,12 @@ module Masks
43
41
  "#{request.base_url}#{routes.callback_path}"
44
42
  end
45
43
 
44
+ def delegation_redirect_uri_for(request)
45
+ return nil unless delegates
46
+
47
+ resolve(@delegation_redirect_uri, request)
48
+ end
49
+
46
50
  def resource_for(request)
47
51
  resolve(@resource, request)
48
52
  end
@@ -66,8 +70,21 @@ module Masks
66
70
  client_id_for(request).present?
67
71
  end
68
72
 
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.
73
+ def backchannel_logout_uri_for(request)
74
+ return nil unless @logged_out.respond_to?(:call)
75
+ return nil unless Masks::Client::Issuer.resolve(issuer_for(request)).backchannel_logout?
76
+
77
+ "#{request.base_url}#{routes.backchannel_logout_path}"
78
+ end
79
+
80
+ def logged_out!(request, logout)
81
+ return false unless @logged_out.respond_to?(:call)
82
+
83
+ @logged_out.arity == 1 ? @logged_out.call(logout) : @logged_out.call(request, logout)
84
+
85
+ true
86
+ end
87
+
71
88
  def can_forget?
72
89
  @forget.respond_to?(:call) || !@store.respond_to?(:call)
73
90
  end
@@ -85,10 +102,6 @@ module Masks
85
102
  @store.call(request, registration)
86
103
  end
87
104
 
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
105
  MINIMUM_ISSUER = 1
93
106
 
94
107
  def issuer_speaks!(request)
@@ -132,12 +145,23 @@ module Masks
132
145
  name: name_for(request),
133
146
  resource: Array(resource_for(request)).first ||
134
147
  raise(Unconfigured, "Masks::Rails.config.resource is not set"),
135
- redirect_uris: [ redirect_uri_for(request) ],
148
+ redirect_uris: [ redirect_uri_for(request), delegation_redirect_uri_for(request) ].compact.uniq,
136
149
  return_to: return_to_for(request),
137
- scope: scope
150
+ scope: approved_scope,
151
+ backchannel_logout_uri: backchannel_logout_uri_for(request)
138
152
  )
139
153
  end
140
154
 
155
+ def approved_scope
156
+ delegated = delegates ? [ Masks::Client::Delegations::SCOPE ] : []
157
+
158
+ return Array(scope) + delegated if namespace.blank?
159
+
160
+ outside = Array(scope).reject { |name| name.to_s.start_with?(namespace) }
161
+
162
+ outside + [ namespace ] + delegated
163
+ end
164
+
141
165
  def return_to_for(request)
142
166
  uri = URI.parse(redirect_uri_for(request))
143
167
  port = ":#{uri.port}" unless uri.port == uri.default_port
@@ -1,9 +1,5 @@
1
1
  module Masks
2
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
3
  class Credentials
8
4
  KEYS = %w[client_id client_secret registration_access_token registration_client_uri].freeze
9
5
 
@@ -5,11 +5,6 @@ module Masks
5
5
 
6
6
  config.masks = Masks::Rails.config
7
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
8
  initializer "masks.authentication" do
14
9
  ActiveSupport.on_load(:action_controller) do
15
10
  include Masks::Rails::Authentication if Masks::Rails.config.authenticate_everything
data/lib/masks/rails.rb CHANGED
@@ -1,5 +1,31 @@
1
1
  require "rails"
2
2
 
3
+ module Masks
4
+ # = Masks::Rails
5
+ #
6
+ # The consumer half: a \Rails engine that mounts the code flow into an
7
+ # application, so signing in against a masks issuer is configuration rather
8
+ # than a controller you write.
9
+ #
10
+ # It loads only when +Rails::Engine+ is already defined. Requiring the gem
11
+ # from a plain Ruby process gets Masks::Client and nothing else.
12
+ #
13
+ # Three pieces do the work:
14
+ #
15
+ # [Configuration] the issuer, credentials and routes, set once in an
16
+ # initializer and validated on boot rather than on the
17
+ # first request that needs them
18
+ # [Authentication] +masks_login_url+, the callback, and the session the
19
+ # app reads +current_actor+ from
20
+ # [ProtectedResource] the other direction — checking a bearer this app was
21
+ # handed, for an API rather than a browser
22
+ #
23
+ # This engine is the client. It never runs in the same process as the
24
+ # provider, which is a standalone deployable holding its own database.
25
+ module Rails
26
+ end
27
+ end
28
+
3
29
  require_relative "version"
4
30
  require_relative "client"
5
31
  require_relative "rails/credentials"
data/lib/masks/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Masks
2
- VERSION = "0.5.0".freeze
2
+ VERSION = "0.7.0".freeze
3
3
  end
data/lib/masks.rb CHANGED
@@ -1,3 +1,20 @@
1
+ # = \Masks
2
+ #
3
+ # Signs an application in against a masks issuer.
4
+ #
5
+ # The gem is two halves. Masks::Client is plain Ruby and speaks the protocol —
6
+ # discovery, PKCE, the code exchange, token verification. Masks::Rails mounts
7
+ # that flow into a \Rails app, and loads only when +Rails::Engine+ is already
8
+ # defined, so requiring this gem outside \Rails costs nothing.
9
+ #
10
+ # issuer = Masks::Client.issuer("https://auth.example")
11
+ # issuer.authorization_url(client_id: id, redirect_uri: uri)
12
+ #
13
+ # The provider itself is not in here. It is a deployable that holds a database
14
+ # and the per-tenant signing keys, and it stays standalone.
15
+ module Masks
16
+ end
17
+
1
18
  require_relative "masks/version"
2
19
  require_relative "masks/client"
3
20
 
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: masks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
- - geiger
7
+ - the masks authors
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -33,8 +33,10 @@ files:
33
33
  - CHANGELOG.md
34
34
  - LICENSE
35
35
  - README.md
36
+ - app/controllers/masks/rails/avatars_controller.rb
36
37
  - app/controllers/masks/rails/base_controller.rb
37
38
  - app/controllers/masks/rails/handshakes_controller.rb
39
+ - app/controllers/masks/rails/logouts_controller.rb
38
40
  - app/controllers/masks/rails/sessions_controller.rb
39
41
  - app/views/layouts/masks/rails/plain.html.erb
40
42
  - app/views/masks/rails/handshakes/refused.html.erb
@@ -45,11 +47,14 @@ files:
45
47
  - lib/masks.rb
46
48
  - lib/masks/client.rb
47
49
  - lib/masks/client/claims.rb
50
+ - lib/masks/client/delegations.rb
51
+ - lib/masks/client/delegations/fake.rb
48
52
  - lib/masks/client/errors.rb
49
53
  - lib/masks/client/handshake.rb
50
54
  - lib/masks/client/http.rb
51
55
  - lib/masks/client/introspection.rb
52
56
  - lib/masks/client/issuer.rb
57
+ - lib/masks/client/logout.rb
53
58
  - lib/masks/client/pkce.rb
54
59
  - lib/masks/client/rack.rb
55
60
  - lib/masks/client/registration.rb