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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/LICENSE +21 -0
- data/README.md +235 -29
- data/app/controllers/masks/rails/avatars_controller.rb +51 -0
- data/app/controllers/masks/rails/base_controller.rb +13 -0
- data/app/controllers/masks/rails/handshakes_controller.rb +86 -0
- data/app/controllers/masks/rails/sessions_controller.rb +118 -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 +11 -82
- 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 +144 -0
- data/lib/masks/client/errors.rb +60 -0
- data/lib/masks/client/handshake.rb +104 -0
- data/lib/masks/client/http.rb +96 -0
- data/lib/masks/client/introspection.rb +29 -0
- data/lib/masks/client/issuer.rb +92 -0
- data/lib/masks/client/pkce.rb +27 -0
- data/lib/masks/client/rack.rb +62 -0
- data/lib/masks/client/registration.rb +115 -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 +260 -0
- data/lib/masks/rails/configurable.rb +11 -0
- data/lib/masks/rails/configuration.rb +184 -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 -183
- metadata +48 -416
- data/MIT-LICENSE +0 -20
- data/Rakefile +0 -11
- data/app/assets/builds/masks/application.css +0 -1
- data/app/assets/builds/masks/application.js +0 -8549
- 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 -21
- 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/javascripts/controllers/table_controller.js +0 -15
- data/app/assets/manifest.js +0 -2
- data/app/assets/masks_manifest.js +0 -2
- data/app/assets/stylesheets/application.css +0 -34
- 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/actors_controller.rb +0 -83
- data/app/controllers/masks/manage/base_controller.rb +0 -20
- data/app/controllers/masks/manage/clients_controller.rb +0 -84
- data/app/controllers/masks/manage/dashboard_controller.rb +0 -15
- data/app/controllers/masks/manage/devices_controller.rb +0 -19
- data/app/controllers/masks/one_time_code_controller.rb +0 -49
- data/app/controllers/masks/openid/authorizations_controller.rb +0 -45
- data/app/controllers/masks/openid/discoveries_controller.rb +0 -55
- data/app/controllers/masks/openid/tokens_controller.rb +0 -45
- data/app/controllers/masks/openid/userinfo_controller.rb +0 -28
- 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 -21
- data/app/models/masks/access/actor_scopes.rb +0 -18
- data/app/models/masks/access/actor_signup.rb +0 -21
- 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/access_token.rb +0 -60
- 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/return_to.rb +0 -27
- 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 -266
- data/app/models/masks/openid/authorization.rb +0 -116
- data/app/models/masks/openid/token.rb +0 -56
- data/app/models/masks/rails/actor.rb +0 -212
- 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/openid/access_token.rb +0 -55
- data/app/models/masks/rails/openid/authorization.rb +0 -45
- data/app/models/masks/rails/openid/client.rb +0 -186
- data/app/models/masks/rails/openid/id_token.rb +0 -43
- 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 -27
- 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 -42
- data/app/views/masks/actor_mailer/recover_credentials.html.erb +0 -32
- data/app/views/masks/actor_mailer/recover_credentials.text.erb +0 -1
- data/app/views/masks/actor_mailer/verify_email.html.erb +0 -33
- data/app/views/masks/actor_mailer/verify_email.text.erb +0 -8
- data/app/views/masks/actors/current.html.erb +0 -145
- data/app/views/masks/application/_header.html.erb +0 -30
- data/app/views/masks/backup_codes/new.html.erb +0 -117
- data/app/views/masks/emails/new.html.erb +0 -109
- 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/actors/index.html.erb +0 -104
- data/app/views/masks/manage/actors/show.html.erb +0 -172
- data/app/views/masks/manage/clients/index.html.erb +0 -102
- data/app/views/masks/manage/clients/show.html.erb +0 -156
- data/app/views/masks/manage/dashboard/index.html.erb +0 -10
- data/app/views/masks/manage/devices/index.html.erb +0 -47
- data/app/views/masks/one_time_code/new.html.erb +0 -167
- data/app/views/masks/openid/authorizations/error.html.erb +0 -23
- data/app/views/masks/openid/authorizations/new.html.erb +0 -46
- data/app/views/masks/passwords/edit.html.erb +0 -71
- data/app/views/masks/recoveries/new.html.erb +0 -69
- data/app/views/masks/recoveries/password.html.erb +0 -63
- data/app/views/masks/sessions/new.html.erb +0 -152
- data/config/brakeman.ignore +0 -28
- data/config/initializers/inflections.rb +0 -5
- data/config/locales/en.yml +0 -307
- data/db/migrate/20231205173845_create_actors.rb +0 -94
- data/db/migrate/20240329182422_support_openid.rb +0 -64
- 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 -9
- data/lib/masks/configuration.rb +0 -249
- 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 -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
|
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,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,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
|