authrocket 2.4.1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|