authrocket 2.4.1 → 3.0.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 +18 -2
- data/README.md +72 -62
- data/app/controllers/auth_rocket/ar_controller.rb +11 -33
- data/app/controllers/logins_controller.rb +1 -8
- data/authrocket.gemspec +4 -3
- data/config/routes.rb +0 -1
- data/lib/authrocket.rb +22 -1
- data/lib/authrocket/api/api_config.rb +17 -18
- data/lib/authrocket/api/client.rb +1 -1
- data/lib/authrocket/api/version.rb +1 -1
- data/lib/authrocket/auth_provider.rb +49 -50
- data/lib/authrocket/client_app.rb +14 -0
- data/lib/authrocket/connection.rb +12 -0
- data/lib/authrocket/credential.rb +12 -6
- data/lib/authrocket/domain.rb +19 -0
- data/lib/authrocket/event.rb +2 -3
- data/lib/authrocket/hook.rb +39 -0
- data/lib/authrocket/invitation.rb +35 -0
- data/lib/authrocket/membership.rb +1 -1
- data/lib/authrocket/named_permission.rb +10 -0
- data/lib/authrocket/notification.rb +1 -1
- data/lib/authrocket/oauth2_session.rb +26 -0
- data/lib/authrocket/org.rb +2 -1
- data/lib/authrocket/rails/controller_helper.rb +73 -21
- data/lib/authrocket/rails/engine.rb +5 -1
- data/lib/authrocket/realm.rb +25 -9
- data/lib/authrocket/resource_link.rb +10 -0
- data/lib/authrocket/session.rb +139 -32
- data/lib/authrocket/token.rb +9 -0
- data/lib/authrocket/user.rb +88 -54
- metadata +33 -19
- data/lib/authrocket/app_hook.rb +0 -28
- data/lib/authrocket/login_policy.rb +0 -14
- data/lib/authrocket/user_token.rb +0 -9
@@ -5,7 +5,11 @@ module AuthRocket
|
|
5
5
|
require_relative 'controller_helper'
|
6
6
|
|
7
7
|
ActiveSupport.on_load(:action_controller) do
|
8
|
-
|
8
|
+
if self == ActionController::Base
|
9
|
+
include AuthRocket::ControllerHelper
|
10
|
+
helper AuthRocket::ControllerHelper
|
11
|
+
before_action :process_inbound_token
|
12
|
+
end
|
9
13
|
end
|
10
14
|
end
|
11
15
|
|
data/lib/authrocket/realm.rb
CHANGED
@@ -2,25 +2,41 @@ module AuthRocket
|
|
2
2
|
class Realm < Resource
|
3
3
|
crud :all, :find, :create, :update, :delete
|
4
4
|
|
5
|
-
has_many :app_hooks
|
6
5
|
has_many :auth_providers
|
6
|
+
has_many :client_apps
|
7
|
+
has_many :connections
|
8
|
+
has_many :domains
|
7
9
|
has_many :events
|
10
|
+
has_many :hooks
|
11
|
+
has_many :invitations
|
8
12
|
has_many :jwt_keys
|
9
|
-
has_many :
|
13
|
+
has_many :named_permissions
|
10
14
|
has_many :orgs
|
15
|
+
has_many :resource_links
|
11
16
|
has_many :users
|
12
17
|
|
13
|
-
attr :
|
14
|
-
attr :
|
15
|
-
attr :
|
18
|
+
attr :custom, :environment, :name, :public_name, :state
|
19
|
+
attr :email_verification, :org_mode, :signup
|
20
|
+
attr :name_field, :org_name_field, :password_field, :username_field
|
21
|
+
attr :branding, :color_1, :logo, :logo_icon, :privacy_policy, :stylesheet, :terms_of_service
|
22
|
+
attr :access_token_minutes, :jwt_algo, :jwt_minutes, :jwt_scopes, :session_minutes
|
16
23
|
attr :jwt_key # readonly
|
17
|
-
|
18
|
-
|
24
|
+
|
25
|
+
|
26
|
+
def named_permissions
|
27
|
+
reload unless @attribs[:named_permissions]
|
28
|
+
@attribs[:named_permissions]
|
29
|
+
end
|
30
|
+
|
31
|
+
def resource_links
|
32
|
+
reload unless @attribs[:resource_links]
|
33
|
+
@attribs[:resource_links]
|
34
|
+
end
|
19
35
|
|
20
36
|
|
21
37
|
def reset!(params={})
|
22
|
-
params = parse_request_params(params).
|
23
|
-
parsed, _ = request(:post, "#{
|
38
|
+
params = parse_request_params(params).reverse_merge credentials: api_creds
|
39
|
+
parsed, _ = request(:post, "#{resource_path}/reset", params)
|
24
40
|
load(parsed)
|
25
41
|
errors.empty? ? self : false
|
26
42
|
end
|
data/lib/authrocket/session.rb
CHANGED
@@ -5,73 +5,180 @@ module AuthRocket
|
|
5
5
|
class Session < Resource
|
6
6
|
crud :all, :find, :create, :delete
|
7
7
|
|
8
|
+
belongs_to :client_app
|
8
9
|
belongs_to :user
|
9
10
|
|
10
11
|
attr :token # readonly
|
11
|
-
attr_datetime :created_at, :expires_at
|
12
|
+
attr_datetime :created_at, :expires_at
|
12
13
|
|
13
14
|
def request_data
|
14
15
|
self[:request]
|
15
16
|
end
|
16
17
|
|
17
18
|
|
18
|
-
# options - :
|
19
|
-
# -
|
20
|
-
# - :
|
19
|
+
# options - :algo - one of HS256, RS256 (default: auto-detect based on :jwt_key)
|
20
|
+
# - :within - (in seconds) Maximum time since the token was (re)issued
|
21
|
+
# - credentials: {jwt_key: StringOrKey} - used to verify the token
|
21
22
|
def self.from_token(token, options={})
|
22
|
-
secret = (
|
23
|
-
|
24
|
-
|
23
|
+
secret = options.dig(:credentials, :jwt_key) || credentials[:jwt_key]
|
24
|
+
if lr_url = options.dig(:credentials, :loginrocket_url) || credentials[:loginrocket_url]
|
25
|
+
lr_url = lr_url.dup
|
26
|
+
lr_url.concat '/' unless lr_url.ends_with?('/')
|
27
|
+
lr_url.concat 'connect/jwks'
|
28
|
+
end
|
25
29
|
|
26
30
|
algo = options[:algo]
|
27
31
|
if secret.is_a?(String) && secret.length > 256
|
32
|
+
unless secret.starts_with?('-----BEGIN ')
|
33
|
+
secret = "-----BEGIN PUBLIC KEY-----\n#{secret}\n-----END PUBLIC KEY-----"
|
34
|
+
end
|
28
35
|
secret = OpenSSL::PKey.read secret
|
29
36
|
end
|
30
37
|
algo ||= 'RS256' if secret.is_a?(OpenSSL::PKey::RSA)
|
31
|
-
algo ||= 'HS256'
|
38
|
+
algo ||= 'HS256' if secret
|
39
|
+
|
40
|
+
jwks_eligible = algo.in?([nil, 'RS256']) && secret.blank? && lr_url
|
41
|
+
|
42
|
+
raise Error, "Missing jwt_key; set LOGINROCKET_URL, AUTHROCKET_JWT_KEY, or pass in credentials: {loginrocket_url: ...} or {jwt_key: ...}" if secret.blank? && !jwks_eligible
|
43
|
+
return if token.blank?
|
32
44
|
|
33
|
-
|
45
|
+
if jwks_eligible
|
46
|
+
base_params = {token: token, algo: 'RS256', within: options[:within], local_creds: options[:credentials]}
|
47
|
+
load_jwk_set(lr_url, use_cached: true).each do |secret|
|
48
|
+
begin
|
49
|
+
return parse_jwt secret: secret, **base_params
|
50
|
+
rescue JWT::DecodeError
|
51
|
+
end
|
52
|
+
end
|
53
|
+
load_jwk_set(lr_url, use_cached: false).each do |secret|
|
54
|
+
begin
|
55
|
+
return parse_jwt secret: secret, **base_params
|
56
|
+
rescue JWT::DecodeError
|
57
|
+
end
|
58
|
+
end
|
59
|
+
nil
|
60
|
+
else
|
61
|
+
begin
|
62
|
+
parse_jwt token: token, secret: secret, algo: algo, within: options[:within], local_creds: options[:credentials]
|
63
|
+
rescue JWT::DecodeError
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
34
68
|
|
35
|
-
|
69
|
+
# private
|
70
|
+
# raises an exception if eligible for retry using different token
|
71
|
+
# returns Session on success
|
72
|
+
# returns nil on a definitive token-parsed-but-invalid
|
73
|
+
def self.parse_jwt(token:, secret:, algo:, within:, local_creds: nil)
|
74
|
+
opts = {
|
75
|
+
algorithm: algo,
|
76
|
+
leeway: 5,
|
77
|
+
iss: "https://authrocket.com",
|
78
|
+
verify_iss: true,
|
79
|
+
}
|
80
|
+
|
81
|
+
jwt, _ = JWT.decode token, secret, true, opts
|
82
|
+
|
83
|
+
if within
|
84
|
+
# this ensures token was created recently
|
85
|
+
# :iat is set to Time.now every time a token is created by the AR api
|
36
86
|
return if jwt['iat'] < Time.now.to_i - within
|
37
87
|
end
|
38
88
|
|
39
89
|
user = User.new({
|
40
|
-
id: jwt['
|
41
|
-
realm_id: jwt['
|
42
|
-
username: jwt['
|
43
|
-
first_name: jwt['
|
44
|
-
last_name: jwt['
|
45
|
-
name: jwt['
|
90
|
+
id: jwt['sub'],
|
91
|
+
realm_id: jwt['rid'],
|
92
|
+
username: jwt['preferred_username'],
|
93
|
+
first_name: jwt['given_name'],
|
94
|
+
last_name: jwt['family_name'],
|
95
|
+
name: jwt['name'],
|
96
|
+
email: jwt['email'],
|
97
|
+
email_verification: jwt['email_verified'] ? 'verified' : 'none',
|
98
|
+
reference: jwt['ref'],
|
46
99
|
custom: jwt['cs'],
|
47
|
-
memberships: jwt['
|
100
|
+
memberships: jwt['orgs'] && jwt['orgs'].map do |m|
|
48
101
|
Membership.new({
|
49
|
-
|
50
|
-
|
51
|
-
|
102
|
+
id: m['mid'],
|
103
|
+
permissions: m['perm'],
|
104
|
+
selected: m['selected'],
|
105
|
+
user_id: jwt['sub'],
|
52
106
|
org_id: m['oid'],
|
53
|
-
org:
|
107
|
+
org: Org.new({
|
54
108
|
id: m['oid'],
|
55
|
-
realm_id: jwt['
|
56
|
-
name: m['
|
57
|
-
|
58
|
-
|
59
|
-
|
109
|
+
realm_id: jwt['rid'],
|
110
|
+
name: m['name'],
|
111
|
+
reference: m['ref'],
|
112
|
+
custom: m['cs'],
|
113
|
+
}, local_creds),
|
114
|
+
}, local_creds)
|
60
115
|
end,
|
61
|
-
},
|
116
|
+
}, local_creds)
|
62
117
|
session = new({
|
63
|
-
id: jwt['
|
118
|
+
id: jwt['sid'],
|
64
119
|
created_at: jwt['iat'],
|
65
120
|
expires_at: jwt['exp'],
|
66
121
|
token: token,
|
67
|
-
user_id: jwt['
|
122
|
+
user_id: jwt['sub'],
|
68
123
|
user: user
|
69
|
-
},
|
70
|
-
|
124
|
+
}, local_creds)
|
125
|
+
|
71
126
|
session
|
72
|
-
rescue JWT::
|
127
|
+
rescue JWT::ExpiredSignature, JWT::ImmatureSignature, JWT::InvalidAudError,
|
128
|
+
JWT::InvalidIatError, JWT::InvalidIssuerError
|
129
|
+
# successfully parsed, but invalid claims
|
73
130
|
nil
|
74
131
|
end
|
75
132
|
|
133
|
+
@_jwks ||= {}
|
134
|
+
JWKS_MUTEX = Mutex.new
|
135
|
+
MIN_ATTEMPT_WINDOW = 71 # seconds
|
136
|
+
|
137
|
+
# private
|
138
|
+
# use_cached - if there is a cached result, use it regardless of last cache load time
|
139
|
+
def self.load_jwk_set(uri, use_cached:)
|
140
|
+
keys, last_time = @_jwks.dig(uri, :keys), @_jwks.dig(uri, :time)
|
141
|
+
last_time ||= 0
|
142
|
+
|
143
|
+
return keys if use_cached && last_time > 0
|
144
|
+
return keys if Time.now.to_f - MIN_ATTEMPT_WINDOW < last_time
|
145
|
+
|
146
|
+
JWKS_MUTEX.synchronize do
|
147
|
+
# recheck in case we locked while being loaded in another process
|
148
|
+
newer_keys, newer_time = @_jwks.dig(uri, :keys), @_jwks.dig(uri, :time)
|
149
|
+
newer_time ||= 0
|
150
|
+
|
151
|
+
return newer_keys if newer_time > last_time
|
152
|
+
|
153
|
+
path = URI.parse(uri).path
|
154
|
+
headers = build_headers({}, {})
|
155
|
+
rest_opts = {
|
156
|
+
connect_timeout: 8,
|
157
|
+
headers: headers,
|
158
|
+
method: :get,
|
159
|
+
path: path,
|
160
|
+
read_timeout: 15,
|
161
|
+
url: uri,
|
162
|
+
write_timeout: 15,
|
163
|
+
}
|
164
|
+
response = execute_request(rest_opts)
|
165
|
+
parsed = parse_response(response)
|
166
|
+
# => {data: json, errors: errors, metadata: metadata}
|
167
|
+
certs = parsed[:data][:keys].map do |h|
|
168
|
+
crt = "-----BEGIN PUBLIC KEY-----\n#{h['x5c'][0]}\n-----END PUBLIC KEY-----"
|
169
|
+
OpenSSL::PKey.read crt
|
170
|
+
end
|
171
|
+
|
172
|
+
@_jwks[uri] = {keys: certs, time: Time.now.to_f}
|
173
|
+
keys ||= []
|
174
|
+
just_added = certs - keys
|
175
|
+
if just_added.any?
|
176
|
+
just_added
|
177
|
+
else
|
178
|
+
certs
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
76
183
|
end
|
77
184
|
end
|
data/lib/authrocket/user.rb
CHANGED
@@ -8,9 +8,9 @@ module AuthRocket
|
|
8
8
|
has_many :memberships
|
9
9
|
has_many :sessions
|
10
10
|
|
11
|
-
attr :custom, :email, :email_verification, :first_name
|
12
|
-
attr :
|
13
|
-
attr :
|
11
|
+
attr :custom, :email, :email_verification, :first_name, :last_name, :name
|
12
|
+
attr :reference, :state, :username
|
13
|
+
attr :password, :password_confirmation # writeonly
|
14
14
|
attr_datetime :created_at, :last_login_at
|
15
15
|
|
16
16
|
|
@@ -20,93 +20,127 @@ module AuthRocket
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def orgs
|
23
|
-
memberships.map(&:org)
|
23
|
+
memberships.map(&:org)
|
24
24
|
end
|
25
25
|
|
26
26
|
def find_org(id)
|
27
27
|
orgs.detect{|o| o.id == id } || raise(RecordNotFound)
|
28
28
|
end
|
29
29
|
|
30
|
-
def human? ; user_type=='human' ; end
|
31
|
-
def api? ; user_type=='api' ; end
|
32
|
-
|
33
30
|
|
34
31
|
class << self
|
32
|
+
# id - email|username|id
|
33
|
+
|
34
|
+
# params - {password: '...'}
|
35
|
+
# returns: Session || Token
|
36
|
+
def authenticate(id, params)
|
37
|
+
params = parse_request_params(params, json_root: json_root)
|
38
|
+
parsed, creds = request(:post, "#{resource_path}/#{CGI.escape id}/authenticate", params)
|
39
|
+
obj = factory(parsed, creds)
|
40
|
+
raise RecordInvalid, obj if obj.errors?
|
41
|
+
obj
|
42
|
+
end
|
35
43
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
# params - {token: 'kli:...', code: '000000'}
|
45
|
+
# returns: Session
|
46
|
+
def authenticate_token(params)
|
47
|
+
params = parse_request_params(params, json_root: json_root)
|
48
|
+
parsed, creds = request(:post, "#{resource_path}/authenticate_token", params)
|
49
|
+
obj = factory(parsed, creds)
|
50
|
+
raise RecordInvalid, obj if obj.errors?
|
51
|
+
obj
|
43
52
|
end
|
44
53
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
54
|
+
# returns: Token
|
55
|
+
def generate_password_token(id, params={})
|
56
|
+
params = parse_request_params(params)
|
57
|
+
parsed, creds = request(:post, "#{resource_path}/#{CGI.escape id}/generate_password_token", params)
|
58
|
+
obj = factory(parsed, creds)
|
59
|
+
raise RecordInvalid, obj if obj.errors?
|
60
|
+
obj
|
52
61
|
end
|
53
62
|
|
54
|
-
# params - {
|
55
|
-
|
63
|
+
# params - {token: '...', password: '...', password_confirmation: '...'}
|
64
|
+
# returns: Session || Token
|
65
|
+
def reset_password_with_token(params)
|
56
66
|
params = parse_request_params(params, json_root: json_root)
|
57
|
-
|
58
|
-
|
59
|
-
if
|
60
|
-
|
61
|
-
end
|
62
|
-
new(parsed, creds)
|
67
|
+
parsed, creds = request(:post, "#{resource_path}/reset_password_with_token", params)
|
68
|
+
obj = factory(parsed, creds)
|
69
|
+
raise RecordInvalid, obj if obj.errors?
|
70
|
+
obj
|
63
71
|
end
|
64
72
|
|
65
|
-
|
73
|
+
# returns: Token
|
74
|
+
def request_email_verification(id, params={})
|
66
75
|
params = parse_request_params(params)
|
67
|
-
parsed, creds = request(:post, "#{
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
new(parsed, creds)
|
76
|
+
parsed, creds = request(:post, "#{resource_path}/#{CGI.escape id}/request_email_verification", params)
|
77
|
+
obj = factory(parsed, creds)
|
78
|
+
raise RecordInvalid, obj if obj.errors?
|
79
|
+
obj
|
72
80
|
end
|
73
81
|
|
74
|
-
# params - {
|
75
|
-
|
82
|
+
# params - {token: '...'}
|
83
|
+
# returns: User
|
84
|
+
def verify_email(params)
|
76
85
|
params = parse_request_params(params, json_root: json_root)
|
77
|
-
|
78
|
-
|
79
|
-
if
|
80
|
-
|
81
|
-
end
|
82
|
-
new(parsed, creds)
|
86
|
+
parsed, creds = request(:post, "#{resource_path}/verify_email", params)
|
87
|
+
obj = factory(parsed, creds)
|
88
|
+
raise RecordInvalid, obj if obj.errors?
|
89
|
+
obj
|
83
90
|
end
|
84
91
|
|
85
92
|
end
|
86
93
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
94
|
+
|
95
|
+
# params - {token: '...'}
|
96
|
+
def accept_invitation(params)
|
97
|
+
params = parse_request_params(params, json_root: json_root).reverse_merge credentials: api_creds
|
98
|
+
parsed, _ = request(:post, "#{resource_path}/accept_invitation", params)
|
91
99
|
load(parsed)
|
92
100
|
errors.empty? ? self : false
|
93
101
|
end
|
94
102
|
|
95
|
-
|
96
|
-
|
97
|
-
|
103
|
+
# params - {current_password: 'old', password: 'new', password_confirmation: 'new'}
|
104
|
+
def update_password(params)
|
105
|
+
params = parse_request_params(params, json_root: json_root).reverse_merge credentials: api_creds
|
106
|
+
parsed, _ = request(:put, "#{resource_path}/update_password", params)
|
98
107
|
load(parsed)
|
99
108
|
errors.empty? ? self : false
|
100
109
|
end
|
101
110
|
|
102
|
-
# params - {
|
103
|
-
def
|
104
|
-
params = parse_request_params(params, json_root: json_root).
|
105
|
-
parsed, _ = request(:
|
111
|
+
# params - {email:, first_name:, last_name:, password:, password_confirmation:, username:}
|
112
|
+
def update_profile(params)
|
113
|
+
params = parse_request_params(params, json_root: json_root).reverse_merge credentials: api_creds
|
114
|
+
parsed, _ = request(:put, "#{resource_path}/profile", params)
|
106
115
|
load(parsed)
|
107
116
|
errors.empty? ? self : false
|
108
117
|
end
|
109
118
|
|
110
119
|
|
120
|
+
# returns: Session || Token
|
121
|
+
# (Session.user !== self)
|
122
|
+
def authenticate(params)
|
123
|
+
self.class.authenticate id, params.reverse_merge(credentials: api_creds)
|
124
|
+
rescue RecordInvalid => ex
|
125
|
+
errors.merge! ex.errors
|
126
|
+
false
|
127
|
+
end
|
128
|
+
|
129
|
+
# returns: Token
|
130
|
+
def generate_password_token(params={})
|
131
|
+
self.class.generate_password_token id, params.reverse_merge(credentials: api_creds)
|
132
|
+
rescue RecordInvalid => ex
|
133
|
+
errors.merge! ex.errors
|
134
|
+
false
|
135
|
+
end
|
136
|
+
|
137
|
+
# returns: Token
|
138
|
+
def request_email_verification(params={})
|
139
|
+
self.class.request_email_verification id, params.reverse_merge(credentials: api_creds)
|
140
|
+
rescue RecordInvalid => ex
|
141
|
+
errors.merge! ex.errors
|
142
|
+
false
|
143
|
+
end
|
144
|
+
|
111
145
|
end
|
112
146
|
end
|