authrocket 2.4.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,7 +5,11 @@ module AuthRocket
5
5
  require_relative 'controller_helper'
6
6
 
7
7
  ActiveSupport.on_load(:action_controller) do
8
- include AuthRocket::ControllerHelper
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
 
@@ -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 :login_policies
13
+ has_many :named_permissions
10
14
  has_many :orgs
15
+ has_many :resource_links
11
16
  has_many :users
12
17
 
13
- attr :api_key_minutes, :api_key_policy, :api_key_prefix, :custom, :name
14
- attr :jwt_fields, :require_unique_emails, :resource_links, :session_minutes
15
- attr :session_type, :state, :username_validation_human
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
- attr :jwt_secret # readonly, deprecated
18
- attr :jwt_data # deprecated
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).merge credentials: api_creds
23
- parsed, _ = request(:post, "#{url}/reset", params)
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
@@ -0,0 +1,10 @@
1
+ module AuthRocket
2
+ class ResourceLink < Resource
3
+ crud :find, :create, :update, :delete
4
+
5
+ belongs_to :realm
6
+
7
+ attr :link_url, :resource_type, :title
8
+
9
+ end
10
+ end
@@ -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 # readonly
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 - :within - (in seconds) Maximum time since the token was originally issued
19
- # - credentials: {jwt_secret: StringOrKey} - used to verify the token
20
- # - :algo - one of HS256, RS256 (default: auto-detect based on :jwt_secret)
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 = (options[:credentials]||credentials||{})[:jwt_secret]
23
- raise Error, "missing :jwt_secret (or AUTHROCKET_JWT_SECRET)" unless secret
24
- return unless token
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
- jwt, _ = JWT.decode token, secret, true, algorithm: algo
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
- if within = options.delete(:within)
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['uid'],
41
- realm_id: jwt['aud'],
42
- username: jwt['un'],
43
- first_name: jwt['fn'],
44
- last_name: jwt['ln'],
45
- name: jwt['n'],
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['m'] && jwt['m'].map do |m|
100
+ memberships: jwt['orgs'] && jwt['orgs'].map do |m|
48
101
  Membership.new({
49
- permissions: m['p'],
50
- custom: m['cs'],
51
- user_id: jwt['uid'],
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: m['o'] && Org.new({
107
+ org: Org.new({
54
108
  id: m['oid'],
55
- realm_id: jwt['aud'],
56
- name: m['o'],
57
- custom: m['ocs'],
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
- }, options[:credentials])
116
+ }, local_creds)
62
117
  session = new({
63
- id: jwt['tk'],
118
+ id: jwt['sid'],
64
119
  created_at: jwt['iat'],
65
120
  expires_at: jwt['exp'],
66
121
  token: token,
67
- user_id: jwt['uid'],
122
+ user_id: jwt['sub'],
68
123
  user: user
69
- }, options[:credentials])
70
-
124
+ }, local_creds)
125
+
71
126
  session
72
- rescue JWT::DecodeError
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
@@ -0,0 +1,9 @@
1
+ module AuthRocket
2
+ class Token < Resource
3
+
4
+ belongs_to :user
5
+
6
+ attr :token
7
+
8
+ end
9
+ end
@@ -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 :last_name, :name, :password, :password_confirmation
13
- attr :reference, :state, :user_type, :username
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).compact
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
- def authenticate(username, password, params={})
37
- params = parse_request_params(params).merge password: password
38
- parsed, creds = request(:post, "#{url}/#{CGI.escape username}/authenticate", params)
39
- if parsed[:errors].any?
40
- raise ValidationError, parsed[:errors]
41
- end
42
- new(parsed, creds)
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
- def authenticate_key(api_key, params={})
46
- params = parse_request_params(params).merge api_key: api_key
47
- parsed, creds = request(:post, "#{url}/authenticate_key", params)
48
- if parsed[:errors].any?
49
- raise ValidationError, parsed[:errors]
50
- end
51
- new(parsed, creds)
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 - {username: '...', token: 'kli_...', code: '000000'}
55
- def authenticate_code(params)
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
- username = params[json_root].delete(:username) || '--'
58
- parsed, creds = request(:post, "#{url}/#{CGI.escape username}/authenticate_code", params)
59
- if parsed[:errors].any?
60
- raise ValidationError, parsed[:errors]
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
- def generate_password_token(username, params={})
73
+ # returns: Token
74
+ def request_email_verification(id, params={})
66
75
  params = parse_request_params(params)
67
- parsed, creds = request(:post, "#{url}/#{CGI.escape username}/generate_password_token", params)
68
- if parsed[:errors].any?
69
- raise ValidationError, parsed[:errors]
70
- end
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 - {username: '...', token: '...', password: '...', password_confirmation: '...'}
75
- def reset_password_with_token(params)
82
+ # params - {token: '...'}
83
+ # returns: User
84
+ def verify_email(params)
76
85
  params = parse_request_params(params, json_root: json_root)
77
- username = params[json_root].delete(:username) || '--'
78
- parsed, creds = request(:post, "#{url}/#{CGI.escape username}/reset_password_with_token", params)
79
- if parsed[:errors].any?
80
- raise ValidationError, parsed[:errors]
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
- # params - {current_password: 'old', password: 'new', password_confirmation: 'new'}
88
- def update_password(params)
89
- params = parse_request_params(params, json_root: json_root).merge credentials: api_creds
90
- parsed, _ = request(:put, "#{url}/update_password", params)
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
- def request_email_verification(params={})
96
- params = parse_request_params(params).merge credentials: api_creds
97
- parsed, _ = request(:post, "#{url}/request_email_verification", params)
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 - {token: '...'}
103
- def verify_email(params)
104
- params = parse_request_params(params, json_root: json_root).merge credentials: api_creds
105
- parsed, _ = request(:post, "#{url}/verify_email", params)
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