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.
@@ -5,7 +5,12 @@ 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
+ before_action :process_authorization_header
13
+ end
9
14
  end
10
15
  end
11
16
 
@@ -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 :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 :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
- attr :jwt_secret # readonly, deprecated
18
- attr :jwt_data # deprecated
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).merge credentials: api_creds
23
- parsed, _ = request(:post, "#{url}/reset", params)
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
@@ -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,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 # 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
22
+ # returns Session or nil
21
23
  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
25
-
26
- algo = options[:algo]
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
- jwt, _ = JWT.decode token, secret, true, algorithm: algo
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
- if within = options.delete(:within)
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['uid'],
41
- realm_id: jwt['aud'],
42
- username: jwt['un'],
43
- first_name: jwt['fn'],
44
- last_name: jwt['ln'],
45
- name: jwt['n'],
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['m'] && jwt['m'].map do |m|
88
+ memberships: jwt['orgs'] && jwt['orgs'].map do |m|
48
89
  Membership.new({
49
- permissions: m['p'],
50
- custom: m['cs'],
51
- user_id: jwt['uid'],
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: m['o'] && Org.new({
95
+ org: Org.new({
54
96
  id: m['oid'],
55
- realm_id: jwt['aud'],
56
- name: m['o'],
57
- custom: m['ocs'],
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
- }, options[:credentials])
104
+ }, local_creds)
62
105
  session = new({
63
- id: jwt['tk'],
106
+ id: jwt['sid'],
64
107
  created_at: jwt['iat'],
65
108
  expires_at: jwt['exp'],
66
109
  token: token,
67
- user_id: jwt['uid'],
110
+ user_id: jwt['sub'],
68
111
  user: user
69
- }, options[:credentials])
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
@@ -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
@@ -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 :last_name, :name, :password, :password_confirmation
13
- attr :reference, :state, :user_type, :username
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).compact
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
- 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)
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
- 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)
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 - {username: '...', token: 'kli_...', code: '000000'}
55
- def authenticate_code(params)
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
- 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)
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
- def generate_password_token(username, params={})
74
+ # returns: Token
75
+ def request_email_verification(id, params={})
66
76
  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)
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 - {username: '...', token: '...', password: '...', password_confirmation: '...'}
75
- def reset_password_with_token(params)
83
+ # params - {token: '...'}
84
+ # returns: User
85
+ def verify_email(params)
76
86
  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)
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
- # 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)
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
- def request_email_verification(params={})
96
- params = parse_request_params(params).merge credentials: api_creds
97
- parsed, _ = request(:post, "#{url}/request_email_verification", params)
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 - {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)
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