yoti 1.6.4 → 1.7.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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/Rakefile +0 -8
  4. data/lib/yoti.rb +50 -1
  5. data/lib/yoti/client.rb +13 -4
  6. data/lib/yoti/configuration.rb +7 -2
  7. data/lib/yoti/data_type/age_verification.rb +1 -1
  8. data/lib/yoti/data_type/base_profile.rb +1 -1
  9. data/lib/yoti/data_type/image.rb +4 -12
  10. data/lib/yoti/data_type/image_jpeg.rb +2 -0
  11. data/lib/yoti/data_type/image_png.rb +2 -0
  12. data/lib/yoti/data_type/media.rb +19 -0
  13. data/lib/yoti/doc_scan/client.rb +163 -0
  14. data/lib/yoti/doc_scan/constants.rb +28 -0
  15. data/lib/yoti/doc_scan/session/create/create_session_result.rb +50 -0
  16. data/lib/yoti/doc_scan/session/create/document_filter.rb +31 -0
  17. data/lib/yoti/doc_scan/session/create/document_restrictions_filter.rb +140 -0
  18. data/lib/yoti/doc_scan/session/create/notification_config.rb +142 -0
  19. data/lib/yoti/doc_scan/session/create/orthogonal_restrictions_filter.rb +150 -0
  20. data/lib/yoti/doc_scan/session/create/requested_check.rb +39 -0
  21. data/lib/yoti/doc_scan/session/create/requested_document_authenticity_check.rb +53 -0
  22. data/lib/yoti/doc_scan/session/create/requested_face_match_check.rb +95 -0
  23. data/lib/yoti/doc_scan/session/create/requested_liveness_check.rb +108 -0
  24. data/lib/yoti/doc_scan/session/create/requested_task.rb +39 -0
  25. data/lib/yoti/doc_scan/session/create/requested_text_extraction_task.rb +94 -0
  26. data/lib/yoti/doc_scan/session/create/required_document.rb +31 -0
  27. data/lib/yoti/doc_scan/session/create/required_id_document.rb +53 -0
  28. data/lib/yoti/doc_scan/session/create/sdk_config.rb +221 -0
  29. data/lib/yoti/doc_scan/session/create/session_specification.rb +203 -0
  30. data/lib/yoti/doc_scan/session/retrieve/authenticity_check_response.rb +12 -0
  31. data/lib/yoti/doc_scan/session/retrieve/breakdown_response.rb +38 -0
  32. data/lib/yoti/doc_scan/session/retrieve/check_response.rb +63 -0
  33. data/lib/yoti/doc_scan/session/retrieve/details_response.rb +28 -0
  34. data/lib/yoti/doc_scan/session/retrieve/document_fields_response.rb +21 -0
  35. data/lib/yoti/doc_scan/session/retrieve/face_map_response.rb +21 -0
  36. data/lib/yoti/doc_scan/session/retrieve/face_match_check_response.rb +12 -0
  37. data/lib/yoti/doc_scan/session/retrieve/frame_response.rb +21 -0
  38. data/lib/yoti/doc_scan/session/retrieve/generated_check_response.rb +28 -0
  39. data/lib/yoti/doc_scan/session/retrieve/generated_media.rb +28 -0
  40. data/lib/yoti/doc_scan/session/retrieve/generated_text_data_check_response.rb +12 -0
  41. data/lib/yoti/doc_scan/session/retrieve/get_session_result.rb +113 -0
  42. data/lib/yoti/doc_scan/session/retrieve/id_document_resource_response.rb +52 -0
  43. data/lib/yoti/doc_scan/session/retrieve/liveness_check_response.rb +12 -0
  44. data/lib/yoti/doc_scan/session/retrieve/liveness_resource_response.rb +24 -0
  45. data/lib/yoti/doc_scan/session/retrieve/media_response.rb +38 -0
  46. data/lib/yoti/doc_scan/session/retrieve/page_response.rb +27 -0
  47. data/lib/yoti/doc_scan/session/retrieve/recommendation_response.rb +34 -0
  48. data/lib/yoti/doc_scan/session/retrieve/report_response.rb +31 -0
  49. data/lib/yoti/doc_scan/session/retrieve/resource_container.rb +50 -0
  50. data/lib/yoti/doc_scan/session/retrieve/resource_response.rb +39 -0
  51. data/lib/yoti/doc_scan/session/retrieve/task_response.rb +87 -0
  52. data/lib/yoti/doc_scan/session/retrieve/text_data_check_response.rb +12 -0
  53. data/lib/yoti/doc_scan/session/retrieve/text_extraction_task_response.rb +18 -0
  54. data/lib/yoti/doc_scan/session/retrieve/zoom_liveness_resource_response.rb +33 -0
  55. data/lib/yoti/doc_scan/support/supported_documents.rb +60 -0
  56. data/lib/yoti/dynamic_share_service/extension/thirdparty_attribute_extension.rb +1 -1
  57. data/lib/yoti/dynamic_share_service/policy/dynamic_policy.rb +4 -4
  58. data/lib/yoti/dynamic_share_service/share_url.rb +26 -33
  59. data/lib/yoti/errors.rb +17 -2
  60. data/lib/yoti/http/aml_check_request.rb +12 -6
  61. data/lib/yoti/http/payloads/aml_address.rb +4 -0
  62. data/lib/yoti/http/payloads/aml_profile.rb +7 -1
  63. data/lib/yoti/http/profile_request.rb +11 -6
  64. data/lib/yoti/http/request.rb +218 -18
  65. data/lib/yoti/http/signed_request.rb +13 -4
  66. data/lib/yoti/ssl.rb +2 -2
  67. data/lib/yoti/util/anchor_processor.rb +1 -1
  68. data/lib/yoti/util/validation.rb +41 -0
  69. data/lib/yoti/version.rb +1 -1
  70. data/rubocop.yml +9 -1
  71. data/yoti.gemspec +1 -2
  72. metadata +49 -18
@@ -38,7 +38,7 @@ module Yoti
38
38
  end
39
39
 
40
40
  #
41
- # @param [String] *names
41
+ # @param [String] names
42
42
  #
43
43
  # @return [self]
44
44
  #
@@ -16,8 +16,8 @@ module Yoti
16
16
  false
17
17
  end
18
18
 
19
- def to_json(*args)
20
- as_json.to_json(*args)
19
+ def to_json(*_args)
20
+ as_json.to_json
21
21
  end
22
22
 
23
23
  def as_json(*_args)
@@ -135,14 +135,14 @@ module Yoti
135
135
  end
136
136
 
137
137
  #
138
- # @param [Integer] derivation
138
+ # @param [Integer] age
139
139
  #
140
140
  def with_age_over(age, options = {})
141
141
  with_age_derived_attribute(Attribute::AGE_OVER + age.to_s, **options)
142
142
  end
143
143
 
144
144
  #
145
- # @param [Integer] derivation
145
+ # @param [Integer] age
146
146
  #
147
147
  def with_age_under(age, options = {})
148
148
  with_age_derived_attribute(Attribute::AGE_UNDER + age.to_s, **options)
@@ -13,50 +13,33 @@ module Yoti
13
13
  end
14
14
  end
15
15
 
16
- def self.create_share_url_endpoint
17
- "/qrcodes/apps/#{Yoti.configuration.client_sdk_id}"
18
- end
19
-
20
- def self.create_share_url_query
21
- "?nonce=#{SecureRandom.uuid}&timestamp=#{Time.now.to_i}"
22
- end
23
-
24
16
  def self.create_share_url(scenario)
25
- endpoint = "#{create_share_url_endpoint}#{create_share_url_query}"
26
- uri = URI("#{Yoti.configuration.api_endpoint}#{endpoint}")
27
-
28
- unsigned = Net::HTTP::Post.new uri
29
- unsigned.body = scenario.to_json
30
-
31
- signed_request = Yoti::SignedRequest.new(
32
- unsigned,
33
- endpoint,
34
- scenario
35
- ).sign
36
-
37
- response = Net::HTTP.start(
38
- uri.hostname,
39
- uri.port,
40
- use_ssl: true
41
- ) do |http|
42
- http.request signed_request
43
- end
17
+ yoti_request = Yoti::Request
18
+ .builder
19
+ .with_http_method('POST')
20
+ .with_base_url(Yoti.configuration.api_endpoint)
21
+ .with_endpoint("qrcodes/apps/#{Yoti.configuration.client_sdk_id}")
22
+ .with_query_param('appId', Yoti.configuration.client_sdk_id)
23
+ .with_payload(scenario)
24
+ .build
44
25
 
45
- create_share_url_parse_response response
46
- end
26
+ begin
27
+ create_share_url_parse_response yoti_request.execute
28
+ rescue Yoti::RequestError => e
29
+ raise if e.response.nil?
47
30
 
48
- def self.create_share_url_parse_response(response)
49
- if response.code.to_i < 200 || response.code.to_i >= 300
50
- case response.code
31
+ case e.response.code
51
32
  when '400'
52
33
  raise InvalidDataError
53
34
  when '404'
54
35
  raise ApplicationNotFoundError
55
36
  else
56
- raise UnknownHTTPError, response.code
37
+ raise UnknownHTTPError, e.response.code
57
38
  end
58
39
  end
40
+ end
59
41
 
42
+ def self.create_share_url_parse_response(response)
60
43
  Share.new JSON.parse response.body
61
44
  end
62
45
 
@@ -78,5 +61,15 @@ module Yoti
78
61
  super msg
79
62
  end
80
63
  end
64
+
65
+ # @deprecated no longer used - will be removed in 2.0.0
66
+ def self.create_share_url_query
67
+ "?nonce=#{SecureRandom.uuid}&timestamp=#{Time.now.to_i}"
68
+ end
69
+
70
+ # @deprecated no longer used - will be removed in 2.0.0
71
+ def self.create_share_url_endpoint
72
+ "/qrcodes/apps/#{Yoti.configuration.client_sdk_id}"
73
+ end
81
74
  end
82
75
  end
@@ -3,12 +3,27 @@ module Yoti
3
3
  class ProtobufError < StandardError; end
4
4
 
5
5
  # Raises exceptions related to API requests
6
- class RequestError < StandardError; end
6
+ class RequestError < StandardError
7
+ attr_reader :response
8
+
9
+ def initialize(message, response = nil)
10
+ super(append_response_message(message, response))
11
+ @response = response
12
+ end
13
+
14
+ private
15
+
16
+ def append_response_message(message, response)
17
+ return message if response.nil? || response.body.empty?
18
+
19
+ "#{message}: #{response.body}"
20
+ end
21
+ end
7
22
 
8
23
  # Raises exceptions related to OpenSSL actions
9
24
  class SslError < StandardError; end
10
25
 
11
- # Raises exceptions realted to an incorrect gem configuration value
26
+ # Raises exceptions related to an incorrect gem configuration value
12
27
  class ConfigurationError < StandardError; end
13
28
 
14
29
  # Raises exceptions related to AML actions
@@ -1,13 +1,16 @@
1
1
  module Yoti
2
2
  # Manage the API's AML check requests
3
3
  class AmlCheckRequest
4
+ #
5
+ # @param [AmlProfile] aml_profile
6
+ #
4
7
  def initialize(aml_profile)
5
8
  @aml_profile = aml_profile
6
9
  @payload = aml_profile.payload
7
10
  @request = request
8
11
  end
9
12
 
10
- # @return [String] a JSON representation of the AML check response
13
+ # @return [Hash] a JSON representation of the AML check response
11
14
  def response
12
15
  JSON.parse(@request.body)
13
16
  end
@@ -15,11 +18,14 @@ module Yoti
15
18
  private
16
19
 
17
20
  def request
18
- yoti_request = Yoti::Request.new
19
- yoti_request.http_method = 'POST'
20
- yoti_request.endpoint = 'aml-check'
21
- yoti_request.payload = @payload
22
- yoti_request
21
+ Yoti::Request
22
+ .builder
23
+ .with_http_method('POST')
24
+ .with_base_url(Yoti.configuration.api_endpoint)
25
+ .with_endpoint('aml-check')
26
+ .with_query_param('appId', Yoti.configuration.client_sdk_id)
27
+ .with_payload(@payload)
28
+ .build
23
29
  end
24
30
  end
25
31
  end
@@ -7,6 +7,10 @@ module Yoti
7
7
  # @return [String] the postcode required for USA, optional otherwise
8
8
  attr_accessor :post_code
9
9
 
10
+ #
11
+ # @param [String] country
12
+ # @param [String] post_code
13
+ #
10
14
  def initialize(country, post_code = nil)
11
15
  raise AmlError, 'AmlAddress requires a country.' if country.to_s.empty?
12
16
 
@@ -1,6 +1,12 @@
1
1
  module Yoti
2
2
  # Manages the AML check Profile object
3
3
  class AmlProfile
4
+ #
5
+ # @param [String] given_names
6
+ # @param [String] family_name
7
+ # @param [AmlAddress] aml_address
8
+ # @param [String] ssn
9
+ #
4
10
  def initialize(given_names, family_name, aml_address, ssn = nil)
5
11
  @given_names = given_names
6
12
  @family_name = family_name
@@ -11,7 +17,7 @@ module Yoti
11
17
  raise AmlError, 'Request for USA require a valid SSN and postcode.' if usa_invalid
12
18
  end
13
19
 
14
- # @return [Object] the AML check request body
20
+ # @return [Hash] the AML check request body
15
21
  def payload
16
22
  {
17
23
  given_names: @given_names,
@@ -1,6 +1,9 @@
1
1
  module Yoti
2
2
  # Manage the API's profile requests
3
3
  class ProfileRequest
4
+ #
5
+ # @param [String] encrypted_connect_token
6
+ #
4
7
  def initialize(encrypted_connect_token)
5
8
  @encrypted_connect_token = encrypted_connect_token
6
9
  @request = request
@@ -14,12 +17,14 @@ module Yoti
14
17
  private
15
18
 
16
19
  def request
17
- yoti_request = Yoti::Request.new
18
- yoti_request.add_header('X-Yoti-Auth-Key', Yoti::SSL.auth_key_from_pem)
19
- yoti_request.encrypted_connect_token = @encrypted_connect_token
20
- yoti_request.http_method = 'GET'
21
- yoti_request.endpoint = 'profile'
22
- yoti_request
20
+ Yoti::Request
21
+ .builder
22
+ .with_http_method('GET')
23
+ .with_base_url(Yoti.configuration.api_endpoint)
24
+ .with_endpoint("profile/#{Yoti::SSL.decrypt_token(@encrypted_connect_token)}")
25
+ .with_query_param('appId', Yoti.configuration.client_sdk_id)
26
+ .with_header('X-Yoti-Auth-Key', Yoti::SSL.auth_key_from_pem)
27
+ .build
23
28
  end
24
29
  end
25
30
  end
@@ -3,9 +3,16 @@ require 'securerandom'
3
3
  module Yoti
4
4
  # Manage the API's HTTPS requests
5
5
  class Request
6
+ # @deprecated will be removed in 2.0.0 - token is now provided with the endpoint
6
7
  # @return [String] the URL token received from Yoti Connect
7
8
  attr_accessor :encrypted_connect_token
8
9
 
10
+ # @return [String] the base URL
11
+ attr_writer :base_url
12
+
13
+ # @return [Hash] query params to add to the request
14
+ attr_accessor :query_params
15
+
9
16
  # @return [String] the HTTP method used for the request
10
17
  # The allowed methods are: GET, DELETE, POST, PUT, PATCH
11
18
  attr_accessor :http_method
@@ -13,36 +20,93 @@ module Yoti
13
20
  # @return [String] the API endpoint for the request
14
21
  attr_accessor :endpoint
15
22
 
16
- # @return [Hash] the body sent with the request
23
+ # @return [#to_json,String] the body sent with the request
17
24
  attr_accessor :payload
18
25
 
19
26
  def initialize
20
27
  @headers = {}
21
28
  end
22
29
 
30
+ #
31
+ # @return [RequestBuilder]
32
+ #
33
+ def self.builder
34
+ RequestBuilder.new
35
+ end
36
+
37
+ #
23
38
  # Adds a HTTP header to the request
39
+ #
40
+ # @param [String] header
41
+ # @param [String] value
42
+ #
24
43
  def add_header(header, value)
25
44
  @headers[header] = value
26
45
  end
27
46
 
47
+ #
28
48
  # Makes a HTTP request after signing the headers
29
- # @return [Hash] the body from the HTTP request
30
- def body
49
+ #
50
+ # @return [HTTPResponse]
51
+ #
52
+ def execute
31
53
  raise RequestError, 'The request requires a HTTP method.' unless @http_method
32
- raise RequestError, 'The payload needs to be a hash.' unless @payload.to_s.empty? || @payload.is_a?(Hash)
33
54
 
34
- res = Net::HTTP.start(uri.hostname, Yoti.configuration.api_port, use_ssl: https_uri?) do |http|
55
+ http_res = Net::HTTP.start(uri.hostname, Yoti.configuration.api_port, use_ssl: https_uri?) do |http|
35
56
  signed_request = SignedRequest.new(unsigned_request, path, @payload).sign
36
57
  http.request(signed_request)
37
58
  end
38
59
 
39
- raise RequestError, "Unsuccessful Yoti API call: #{res.message}" unless res.code == '200'
60
+ raise RequestError.new("Unsuccessful Yoti API call: #{http_res.message}", http_res) unless response_is_success(http_res)
40
61
 
41
- res.body
62
+ http_res
63
+ end
64
+
65
+ #
66
+ # Makes a HTTP request and returns the body after signing the headers
67
+ #
68
+ # @return [String]
69
+ #
70
+ def body
71
+ execute.body
72
+ end
73
+
74
+ #
75
+ # @return [String] the base URL
76
+ #
77
+ def base_url
78
+ @base_url ||= Yoti.configuration.api_endpoint
42
79
  end
43
80
 
44
81
  private
45
82
 
83
+ #
84
+ # @param [Net::HTTPResponse] http_res
85
+ #
86
+ # @return [Boolean]
87
+ #
88
+ def response_is_success(http_res)
89
+ http_res.code.to_i >= 200 && http_res.code.to_i < 300
90
+ end
91
+
92
+ #
93
+ # Adds payload to provided HTTP request
94
+ #
95
+ # @param [Net::HTTPRequest] http_req
96
+ #
97
+ def add_payload(http_req)
98
+ return if @payload.to_s.empty?
99
+
100
+ if @payload.is_a?(String)
101
+ http_req.body = @payload
102
+ elsif @payload.respond_to?(:to_json)
103
+ http_req.body = @payload.to_json
104
+ end
105
+ end
106
+
107
+ #
108
+ # @return [Net::HTTPRequest] the unsigned HTTP request
109
+ #
46
110
  def unsigned_request
47
111
  case @http_method
48
112
  when 'GET'
@@ -51,13 +115,13 @@ module Yoti
51
115
  http_req = Net::HTTP::Delete.new(uri)
52
116
  when 'POST'
53
117
  http_req = Net::HTTP::Post.new(uri)
54
- http_req.body = @payload.to_json unless @payload.to_s.empty?
118
+ add_payload(http_req)
55
119
  when 'PUT'
56
120
  http_req = Net::HTTP::Put.new(uri)
57
- http_req.body = @payload.to_json unless @payload.to_s.empty?
121
+ add_payload(http_req)
58
122
  when 'PATCH'
59
123
  http_req = Net::HTTP::Patch.new(uri)
60
- http_req.body = @payload.to_json unless @payload.to_s.empty?
124
+ add_payload(http_req)
61
125
  else
62
126
  raise RequestError, "Request method not allowed: #{@http_method}"
63
127
  end
@@ -69,22 +133,27 @@ module Yoti
69
133
  http_req
70
134
  end
71
135
 
136
+ #
137
+ # @return [URI] the full request URI
138
+ #
72
139
  def uri
73
- @uri ||= URI(Yoti.configuration.api_endpoint + path)
140
+ @uri ||= URI(base_url + path)
74
141
  end
75
142
 
143
+ #
144
+ # @return [String] the path with query string
145
+ #
76
146
  def path
77
147
  @path ||= begin
78
- nonce = SecureRandom.uuid
79
- timestamp = Time.now.to_i
80
-
81
- "/#{@endpoint}/#{token}"\
82
- "?nonce=#{nonce}"\
83
- "&timestamp=#{timestamp}"\
84
- "&appId=#{Yoti.configuration.client_sdk_id}"
148
+ "/#{@endpoint}/#{token}".chomp('/') + "?#{query_string}"
85
149
  end
86
150
  end
87
151
 
152
+ #
153
+ # @deprecated will be removed in 2.0.0 - token is now provided with the endpoint
154
+ #
155
+ # @return [String] the decrypted connect token
156
+ #
88
157
  def token
89
158
  return '' unless @encrypted_connect_token
90
159
 
@@ -94,5 +163,136 @@ module Yoti
94
163
  def https_uri?
95
164
  uri.scheme == 'https'
96
165
  end
166
+
167
+ #
168
+ # Builds query string including nonce and timestamp
169
+ #
170
+ # @return [String]
171
+ #
172
+ def query_string
173
+ params = {
174
+ nonce: SecureRandom.uuid,
175
+ timestamp: Time.now.to_i
176
+ }
177
+
178
+ if @query_params.nil?
179
+ # @deprecated this default will be removed in 2.0.0
180
+ # Append appId when no custom query params are provided.
181
+ params.merge!(appId: Yoti.configuration.client_sdk_id)
182
+ else
183
+ Validation.assert_is_a(Hash, @query_params, 'query_params')
184
+ params.merge!(@query_params)
185
+ end
186
+
187
+ params.map do |k, v|
188
+ CGI.escape(k.to_s) + '=' + CGI.escape(v.to_s)
189
+ end.join('&')
190
+ end
191
+ end
192
+
193
+ #
194
+ # Builder for {Request}
195
+ #
196
+ class RequestBuilder
197
+ def initialize
198
+ @headers = {}
199
+ @query_params = {}
200
+ end
201
+
202
+ #
203
+ # Sets the base URL
204
+ #
205
+ # @param [String] base_url
206
+ #
207
+ # @return [self]
208
+ #
209
+ def with_base_url(base_url)
210
+ Validation.assert_is_a(String, base_url, 'base_url')
211
+ @base_url = base_url
212
+ self
213
+ end
214
+
215
+ #
216
+ # Adds a HTTP header to the request
217
+ #
218
+ # @param [String] header
219
+ # @param [String] value
220
+ #
221
+ # @return [self]
222
+ #
223
+ def with_header(header, value)
224
+ Validation.assert_is_a(String, header, 'header')
225
+ Validation.assert_is_a(String, value, 'value')
226
+ @headers[header] = value
227
+ self
228
+ end
229
+
230
+ #
231
+ # Adds a query parameter to the request
232
+ #
233
+ # @param [String] key
234
+ # @param [String] value
235
+ #
236
+ # @return [self]
237
+ #
238
+ def with_query_param(key, value)
239
+ Validation.assert_is_a(String, key, 'key')
240
+ Validation.assert_is_a(String, value, 'value')
241
+ @query_params[key] = value
242
+ self
243
+ end
244
+
245
+ #
246
+ # Sets the HTTP method
247
+ #
248
+ # @param [String] http_method
249
+ #
250
+ # @return [self]
251
+ #
252
+ def with_http_method(http_method)
253
+ Validation.assert_is_a(String, http_method, 'http_method')
254
+ @http_method = http_method
255
+ self
256
+ end
257
+
258
+ #
259
+ # Sets the API endpoint for the request
260
+ #
261
+ # @param [String] endpoint
262
+ #
263
+ # @return [self]
264
+ #
265
+ def with_endpoint(endpoint)
266
+ Validation.assert_is_a(String, endpoint, 'endpoint')
267
+ @endpoint = endpoint
268
+ self
269
+ end
270
+
271
+ #
272
+ # Sets the body sent with the request
273
+ #
274
+ # @param [#to_json,String] payload
275
+ #
276
+ # @return [self]
277
+ #
278
+ def with_payload(payload)
279
+ Validation.assert_respond_to(:to_json, payload, 'payload') unless payload.is_a?(String)
280
+ @payload = payload
281
+ self
282
+ end
283
+
284
+ #
285
+ # @return [Request]
286
+ #
287
+ def build
288
+ request = Request.new
289
+ request.base_url = @base_url
290
+ request.endpoint = @endpoint
291
+ request.query_params = @query_params
292
+ request.http_method = @http_method
293
+ request.payload = @payload
294
+ @headers.map { |k, v| request.add_header(k, v) }
295
+ request
296
+ end
97
297
  end
98
298
  end