authrocket 2.4.1 → 3.3.0

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