authrocket 2.4.1 → 3.3.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 +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
|