authrocket 2.4.1 → 3.3.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 +39 -0
- data/LICENSE +1 -1
- 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 +23 -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 +48 -49
- data/lib/authrocket/client_app.rb +14 -0
- data/lib/authrocket/connection.rb +13 -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 +42 -0
- data/lib/authrocket/hook_state.rb +26 -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 +80 -20
- data/lib/authrocket/rails/engine.rb +6 -1
- data/lib/authrocket/realm.rb +26 -9
- data/lib/authrocket/resource_link.rb +10 -0
- data/lib/authrocket/session.rb +104 -33
- data/lib/authrocket/token.rb +9 -0
- data/lib/authrocket/user.rb +89 -54
- metadata +37 -22
- 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,12 @@ 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
|
+
before_action :process_authorization_header
|
13
|
+
end
|
9
14
|
end
|
10
15
|
end
|
11
16
|
|
data/lib/authrocket/realm.rb
CHANGED
@@ -2,25 +2,42 @@ 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 :available_locales, :default_locale
|
20
|
+
attr :email_verification, :org_mode, :signup
|
21
|
+
attr :name_field, :org_name_field, :password_field, :username_field
|
22
|
+
attr :branding, :color_1, :logo, :logo_icon, :privacy_policy, :stylesheet, :terms_of_service
|
23
|
+
attr :access_token_minutes, :jwt_algo, :jwt_minutes, :jwt_scopes, :session_minutes
|
16
24
|
attr :jwt_key # readonly
|
17
|
-
|
18
|
-
|
25
|
+
|
26
|
+
|
27
|
+
def named_permissions
|
28
|
+
reload unless @attribs[:named_permissions]
|
29
|
+
@attribs[:named_permissions]
|
30
|
+
end
|
31
|
+
|
32
|
+
def resource_links
|
33
|
+
reload unless @attribs[:resource_links]
|
34
|
+
@attribs[:resource_links]
|
35
|
+
end
|
19
36
|
|
20
37
|
|
21
38
|
def reset!(params={})
|
22
|
-
params = parse_request_params(params).
|
23
|
-
parsed, _ = request(:post, "#{
|
39
|
+
params = parse_request_params(params).reverse_merge credentials: api_creds
|
40
|
+
parsed, _ = request(:post, "#{resource_path}/reset", params)
|
24
41
|
load(parsed)
|
25
42
|
errors.empty? ? self : false
|
26
43
|
end
|
data/lib/authrocket/session.rb
CHANGED
@@ -5,73 +5,144 @@ 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
|
22
|
+
# returns Session or nil
|
21
23
|
def self.from_token(token, options={})
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
29
|
+
secret = options.dig(:credentials, :jwt_key) || credentials[:jwt_key]
|
27
30
|
if secret.is_a?(String) && secret.length > 256
|
31
|
+
unless secret.starts_with?('-----BEGIN ')
|
32
|
+
secret = "-----BEGIN PUBLIC KEY-----\n#{secret}\n-----END PUBLIC KEY-----"
|
33
|
+
end
|
28
34
|
secret = OpenSSL::PKey.read secret
|
29
35
|
end
|
36
|
+
algo = options[:algo]
|
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?
|
44
|
+
|
45
|
+
base_params = {token: token, within: options[:within], local_creds: options[:credentials]}
|
46
|
+
if jwks_eligible
|
47
|
+
kid = JSON.parse(JWT::Base64.url_decode(token.split('.')[0]))['kid'] rescue nil
|
48
|
+
return if kid.blank?
|
49
|
+
|
50
|
+
load_jwk_set(lr_url) unless @_jwks[kid]
|
51
|
+
if key_set = @_jwks[kid]
|
52
|
+
parse_jwt **key_set, **base_params
|
53
|
+
end
|
54
|
+
else
|
55
|
+
parse_jwt secret: secret, algo: algo, **base_params
|
56
|
+
end
|
57
|
+
end
|
32
58
|
|
33
|
-
|
59
|
+
# private
|
60
|
+
# returns Session or nil
|
61
|
+
def self.parse_jwt(token:, secret:, algo:, within:, local_creds: nil)
|
62
|
+
opts = {
|
63
|
+
algorithm: algo,
|
64
|
+
leeway: 5,
|
65
|
+
iss: "https://authrocket.com",
|
66
|
+
verify_iss: true,
|
67
|
+
}
|
34
68
|
|
35
|
-
|
69
|
+
jwt, _ = JWT.decode token, secret, true, opts
|
70
|
+
|
71
|
+
if within
|
72
|
+
# this ensures token was created recently
|
73
|
+
# :iat is set to Time.now every time a token is created by the AR api
|
36
74
|
return if jwt['iat'] < Time.now.to_i - within
|
37
75
|
end
|
38
76
|
|
39
77
|
user = User.new({
|
40
|
-
id: jwt['
|
41
|
-
realm_id: jwt['
|
42
|
-
username: jwt['
|
43
|
-
first_name: jwt['
|
44
|
-
last_name: jwt['
|
45
|
-
name: jwt['
|
78
|
+
id: jwt['sub'],
|
79
|
+
realm_id: jwt['rid'],
|
80
|
+
username: jwt['preferred_username'],
|
81
|
+
first_name: jwt['given_name'],
|
82
|
+
last_name: jwt['family_name'],
|
83
|
+
name: jwt['name'],
|
84
|
+
email: jwt['email'],
|
85
|
+
email_verification: jwt['email_verified'] ? 'verified' : 'none',
|
86
|
+
reference: jwt['ref'],
|
46
87
|
custom: jwt['cs'],
|
47
|
-
memberships: jwt['
|
88
|
+
memberships: jwt['orgs'] && jwt['orgs'].map do |m|
|
48
89
|
Membership.new({
|
49
|
-
|
50
|
-
|
51
|
-
|
90
|
+
id: m['mid'],
|
91
|
+
permissions: m['perm'],
|
92
|
+
selected: m['selected'],
|
93
|
+
user_id: jwt['sub'],
|
52
94
|
org_id: m['oid'],
|
53
|
-
org:
|
95
|
+
org: Org.new({
|
54
96
|
id: m['oid'],
|
55
|
-
realm_id: jwt['
|
56
|
-
name: m['
|
57
|
-
|
58
|
-
|
59
|
-
|
97
|
+
realm_id: jwt['rid'],
|
98
|
+
name: m['name'],
|
99
|
+
reference: m['ref'],
|
100
|
+
custom: m['cs'],
|
101
|
+
}, local_creds),
|
102
|
+
}, local_creds)
|
60
103
|
end,
|
61
|
-
},
|
104
|
+
}, local_creds)
|
62
105
|
session = new({
|
63
|
-
id: jwt['
|
106
|
+
id: jwt['sid'],
|
64
107
|
created_at: jwt['iat'],
|
65
108
|
expires_at: jwt['exp'],
|
66
109
|
token: token,
|
67
|
-
user_id: jwt['
|
110
|
+
user_id: jwt['sub'],
|
68
111
|
user: user
|
69
|
-
},
|
70
|
-
|
112
|
+
}, local_creds)
|
113
|
+
|
71
114
|
session
|
72
115
|
rescue JWT::DecodeError
|
73
116
|
nil
|
74
117
|
end
|
75
118
|
|
119
|
+
@_jwks ||= {}
|
120
|
+
JWKS_MUTEX = Mutex.new
|
121
|
+
|
122
|
+
# private
|
123
|
+
def self.load_jwk_set(uri)
|
124
|
+
JWKS_MUTEX.synchronize do
|
125
|
+
path = URI.parse(uri).path
|
126
|
+
headers = build_headers({}, {})
|
127
|
+
rest_opts = {
|
128
|
+
connect_timeout: 8,
|
129
|
+
headers: headers,
|
130
|
+
method: :get,
|
131
|
+
path: path,
|
132
|
+
read_timeout: 15,
|
133
|
+
url: uri,
|
134
|
+
write_timeout: 15,
|
135
|
+
}
|
136
|
+
response = execute_request(rest_opts)
|
137
|
+
parsed = parse_response(response)
|
138
|
+
# => {data: json, errors: errors, metadata: metadata}
|
139
|
+
parsed[:data][:keys].each do |h|
|
140
|
+
crt = "-----BEGIN PUBLIC KEY-----\n#{h['x5c'][0]}\n-----END PUBLIC KEY-----"
|
141
|
+
@_jwks[h['kid']] = {secret: OpenSSL::PKey.read(crt), algo: h['alg']}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
@_jwks
|
145
|
+
end
|
146
|
+
|
76
147
|
end
|
77
148
|
end
|
data/lib/authrocket/user.rb
CHANGED
@@ -5,12 +5,13 @@ module AuthRocket
|
|
5
5
|
belongs_to :realm
|
6
6
|
has_many :credentials
|
7
7
|
has_many :events
|
8
|
+
has_many :hook_states
|
8
9
|
has_many :memberships
|
9
10
|
has_many :sessions
|
10
11
|
|
11
|
-
attr :custom, :email, :email_verification, :first_name
|
12
|
-
attr :
|
13
|
-
attr :
|
12
|
+
attr :custom, :email, :email_verification, :first_name, :last_name, :locale, :name
|
13
|
+
attr :reference, :state, :username
|
14
|
+
attr :password, :password_confirmation # writeonly
|
14
15
|
attr_datetime :created_at, :last_login_at
|
15
16
|
|
16
17
|
|
@@ -20,93 +21,127 @@ module AuthRocket
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def orgs
|
23
|
-
memberships.map(&:org)
|
24
|
+
memberships.map(&:org)
|
24
25
|
end
|
25
26
|
|
26
27
|
def find_org(id)
|
27
28
|
orgs.detect{|o| o.id == id } || raise(RecordNotFound)
|
28
29
|
end
|
29
30
|
|
30
|
-
def human? ; user_type=='human' ; end
|
31
|
-
def api? ; user_type=='api' ; end
|
32
|
-
|
33
31
|
|
34
32
|
class << self
|
33
|
+
# id - email|username|id
|
34
|
+
|
35
|
+
# params - {password: '...'}
|
36
|
+
# returns: Session || Token
|
37
|
+
def authenticate(id, params)
|
38
|
+
params = parse_request_params(params, json_root: json_root)
|
39
|
+
parsed, creds = request(:post, "#{resource_path}/#{CGI.escape id}/authenticate", params)
|
40
|
+
obj = factory(parsed, creds)
|
41
|
+
raise RecordInvalid, obj if obj.errors?
|
42
|
+
obj
|
43
|
+
end
|
35
44
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
45
|
+
# params - {token: 'kli:...', code: '000000'}
|
46
|
+
# returns: Session
|
47
|
+
def authenticate_token(params)
|
48
|
+
params = parse_request_params(params, json_root: json_root)
|
49
|
+
parsed, creds = request(:post, "#{resource_path}/authenticate_token", params)
|
50
|
+
obj = factory(parsed, creds)
|
51
|
+
raise RecordInvalid, obj if obj.errors?
|
52
|
+
obj
|
43
53
|
end
|
44
54
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
55
|
+
# returns: Token
|
56
|
+
def generate_password_token(id, params={})
|
57
|
+
params = parse_request_params(params)
|
58
|
+
parsed, creds = request(:post, "#{resource_path}/#{CGI.escape id}/generate_password_token", params)
|
59
|
+
obj = factory(parsed, creds)
|
60
|
+
raise RecordInvalid, obj if obj.errors?
|
61
|
+
obj
|
52
62
|
end
|
53
63
|
|
54
|
-
# params - {
|
55
|
-
|
64
|
+
# params - {token: '...', password: '...', password_confirmation: '...'}
|
65
|
+
# returns: Session || Token
|
66
|
+
def reset_password_with_token(params)
|
56
67
|
params = parse_request_params(params, json_root: json_root)
|
57
|
-
|
58
|
-
|
59
|
-
if
|
60
|
-
|
61
|
-
end
|
62
|
-
new(parsed, creds)
|
68
|
+
parsed, creds = request(:post, "#{resource_path}/reset_password_with_token", params)
|
69
|
+
obj = factory(parsed, creds)
|
70
|
+
raise RecordInvalid, obj if obj.errors?
|
71
|
+
obj
|
63
72
|
end
|
64
73
|
|
65
|
-
|
74
|
+
# returns: Token
|
75
|
+
def request_email_verification(id, params={})
|
66
76
|
params = parse_request_params(params)
|
67
|
-
parsed, creds = request(:post, "#{
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
new(parsed, creds)
|
77
|
+
parsed, creds = request(:post, "#{resource_path}/#{CGI.escape id}/request_email_verification", params)
|
78
|
+
obj = factory(parsed, creds)
|
79
|
+
raise RecordInvalid, obj if obj.errors?
|
80
|
+
obj
|
72
81
|
end
|
73
82
|
|
74
|
-
# params - {
|
75
|
-
|
83
|
+
# params - {token: '...'}
|
84
|
+
# returns: User
|
85
|
+
def verify_email(params)
|
76
86
|
params = parse_request_params(params, json_root: json_root)
|
77
|
-
|
78
|
-
|
79
|
-
if
|
80
|
-
|
81
|
-
end
|
82
|
-
new(parsed, creds)
|
87
|
+
parsed, creds = request(:post, "#{resource_path}/verify_email", params)
|
88
|
+
obj = factory(parsed, creds)
|
89
|
+
raise RecordInvalid, obj if obj.errors?
|
90
|
+
obj
|
83
91
|
end
|
84
92
|
|
85
93
|
end
|
86
94
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
95
|
+
|
96
|
+
# params - {token: '...'}
|
97
|
+
def accept_invitation(params)
|
98
|
+
params = parse_request_params(params, json_root: json_root).reverse_merge credentials: api_creds
|
99
|
+
parsed, _ = request(:post, "#{resource_path}/accept_invitation", params)
|
91
100
|
load(parsed)
|
92
101
|
errors.empty? ? self : false
|
93
102
|
end
|
94
103
|
|
95
|
-
|
96
|
-
|
97
|
-
|
104
|
+
# params - {current_password: 'old', password: 'new', password_confirmation: 'new'}
|
105
|
+
def update_password(params)
|
106
|
+
params = parse_request_params(params, json_root: json_root).reverse_merge credentials: api_creds
|
107
|
+
parsed, _ = request(:put, "#{resource_path}/update_password", params)
|
98
108
|
load(parsed)
|
99
109
|
errors.empty? ? self : false
|
100
110
|
end
|
101
111
|
|
102
|
-
# params - {
|
103
|
-
def
|
104
|
-
params = parse_request_params(params, json_root: json_root).
|
105
|
-
parsed, _ = request(:
|
112
|
+
# params - {email:, first_name:, last_name:, password:, password_confirmation:, username:}
|
113
|
+
def update_profile(params)
|
114
|
+
params = parse_request_params(params, json_root: json_root).reverse_merge credentials: api_creds
|
115
|
+
parsed, _ = request(:put, "#{resource_path}/profile", params)
|
106
116
|
load(parsed)
|
107
117
|
errors.empty? ? self : false
|
108
118
|
end
|
109
119
|
|
110
120
|
|
121
|
+
# returns: Session || Token
|
122
|
+
# (Session.user !== self)
|
123
|
+
def authenticate(params)
|
124
|
+
self.class.authenticate id, params.reverse_merge(credentials: api_creds)
|
125
|
+
rescue RecordInvalid => ex
|
126
|
+
errors.merge! ex.errors
|
127
|
+
false
|
128
|
+
end
|
129
|
+
|
130
|
+
# returns: Token
|
131
|
+
def generate_password_token(params={})
|
132
|
+
self.class.generate_password_token id, params.reverse_merge(credentials: api_creds)
|
133
|
+
rescue RecordInvalid => ex
|
134
|
+
errors.merge! ex.errors
|
135
|
+
false
|
136
|
+
end
|
137
|
+
|
138
|
+
# returns: Token
|
139
|
+
def request_email_verification(params={})
|
140
|
+
self.class.request_email_verification id, params.reverse_merge(credentials: api_creds)
|
141
|
+
rescue RecordInvalid => ex
|
142
|
+
errors.merge! ex.errors
|
143
|
+
false
|
144
|
+
end
|
145
|
+
|
111
146
|
end
|
112
147
|
end
|