identisure-client 0.1.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.
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Identisure
4
+ module BankIdSe
5
+ # --- Requests ---
6
+
7
+ class AuthRequest
8
+ attr_reader :ssn, :ip_address, :callback_url, :pin_required, :intent,
9
+ :org_number, :request_address, :ref_id
10
+
11
+ def initialize(ssn: nil, ip_address: nil, callback_url: nil, pin_required: nil,
12
+ intent: nil, org_number: nil, request_address: nil, ref_id: nil)
13
+ @ssn = ssn
14
+ @ip_address = ip_address
15
+ @callback_url = callback_url
16
+ @pin_required = pin_required
17
+ @intent = intent
18
+ @org_number = org_number
19
+ @request_address = request_address
20
+ @ref_id = ref_id
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ ssn: @ssn, ipAddress: @ip_address, callbackUrl: @callback_url,
26
+ pinRequired: @pin_required, intent: @intent, orgNumber: @org_number,
27
+ requestAddress: @request_address, refId: @ref_id
28
+ }.compact
29
+ end
30
+ end
31
+
32
+ class PhoneAuthRequest
33
+ attr_reader :ssn, :call_initiator, :callback_url, :pin_required, :intent,
34
+ :org_number, :request_address, :ref_id
35
+
36
+ def initialize(ssn:, call_initiator:, callback_url: nil, pin_required: nil,
37
+ intent: nil, org_number: nil, request_address: nil, ref_id: nil)
38
+ @ssn = ssn
39
+ @call_initiator = call_initiator
40
+ @callback_url = callback_url
41
+ @pin_required = pin_required
42
+ @intent = intent
43
+ @org_number = org_number
44
+ @request_address = request_address
45
+ @ref_id = ref_id
46
+ end
47
+
48
+ def to_h
49
+ {
50
+ ssn: @ssn, callInitiator: @call_initiator, callbackUrl: @callback_url,
51
+ pinRequired: @pin_required, intent: @intent, orgNumber: @org_number,
52
+ requestAddress: @request_address, refId: @ref_id
53
+ }.compact
54
+ end
55
+ end
56
+
57
+ class SignRequest
58
+ attr_reader :text, :ssn, :ip_address, :callback_url, :pin_required, :digest,
59
+ :org_number, :request_address, :ref_id
60
+
61
+ def initialize(text:, ssn: nil, ip_address: nil, callback_url: nil, pin_required: nil,
62
+ digest: nil, org_number: nil, request_address: nil, ref_id: nil)
63
+ @text = text; @ssn = ssn; @ip_address = ip_address; @callback_url = callback_url
64
+ @pin_required = pin_required; @digest = digest; @org_number = org_number
65
+ @request_address = request_address; @ref_id = ref_id
66
+ end
67
+
68
+ def to_h
69
+ {
70
+ text: @text, ssn: @ssn, ipAddress: @ip_address, callbackUrl: @callback_url,
71
+ pinRequired: @pin_required, digest: @digest, orgNumber: @org_number,
72
+ requestAddress: @request_address, refId: @ref_id
73
+ }.compact
74
+ end
75
+ end
76
+
77
+ class PhoneSignRequest
78
+ attr_reader :ssn, :call_initiator, :text, :callback_url, :pin_required, :digest,
79
+ :org_number, :request_address, :ref_id
80
+
81
+ def initialize(ssn:, call_initiator:, text:, callback_url: nil, pin_required: nil,
82
+ digest: nil, org_number: nil, request_address: nil, ref_id: nil)
83
+ @ssn = ssn; @call_initiator = call_initiator; @text = text
84
+ @callback_url = callback_url; @pin_required = pin_required; @digest = digest
85
+ @org_number = org_number; @request_address = request_address; @ref_id = ref_id
86
+ end
87
+
88
+ def to_h
89
+ {
90
+ ssn: @ssn, callInitiator: @call_initiator, text: @text,
91
+ callbackUrl: @callback_url, pinRequired: @pin_required, digest: @digest,
92
+ orgNumber: @org_number, requestAddress: @request_address, refId: @ref_id
93
+ }.compact
94
+ end
95
+ end
96
+
97
+ class VerifyRequest
98
+ attr_reader :qr_code
99
+
100
+ def initialize(qr_code:) = @qr_code = qr_code
101
+ def to_h = { qrCode: @qr_code }
102
+ end
103
+
104
+ class VerifyResponse
105
+ attr_reader :ssn, :name, :given_name, :surname, :age, :verified_at
106
+
107
+ def initialize(ssn:, name:, given_name:, surname:, age: nil, verified_at: nil)
108
+ @ssn = ssn; @name = name; @given_name = given_name; @surname = surname
109
+ @age = age; @verified_at = verified_at
110
+ end
111
+
112
+ def deconstruct_keys(_)
113
+ { ssn: @ssn, name: @name, given_name: @given_name, surname: @surname,
114
+ age: @age, verified_at: @verified_at }
115
+ end
116
+
117
+ def self.from_json(d)
118
+ new(ssn: d['ssn'], name: d['name'], given_name: d['givenName'],
119
+ surname: d['surname'], age: d['age'], verified_at: d['verifiedAt'])
120
+ end
121
+ end
122
+
123
+ # --- Status variants ---
124
+
125
+ class Pending
126
+ attr_reader :id, :ref_id, :auto_start_token, :qr_start_token, :qr_start_secret, :hint_code
127
+
128
+ def initialize(id:, ref_id: nil, auto_start_token: nil, qr_start_token: nil,
129
+ qr_start_secret: nil, hint_code: nil)
130
+ @id = id; @ref_id = ref_id; @auto_start_token = auto_start_token
131
+ @qr_start_token = qr_start_token; @qr_start_secret = qr_start_secret
132
+ @hint_code = hint_code
133
+ end
134
+
135
+ def deconstruct_keys(_)
136
+ { id: @id, ref_id: @ref_id, auto_start_token: @auto_start_token,
137
+ qr_start_token: @qr_start_token, qr_start_secret: @qr_start_secret,
138
+ hint_code: @hint_code }
139
+ end
140
+
141
+ def self.from_json(d)
142
+ new(id: d['id'], ref_id: d['refId'], auto_start_token: d['autoStartToken'],
143
+ qr_start_token: d['qrStartToken'], qr_start_secret: d['qrStartSecret'],
144
+ hint_code: d['hintCode'])
145
+ end
146
+ end
147
+
148
+ class PendingPhone
149
+ attr_reader :id, :ref_id, :hint_code
150
+
151
+ def initialize(id:, ref_id: nil, hint_code: nil)
152
+ @id = id; @ref_id = ref_id; @hint_code = hint_code
153
+ end
154
+
155
+ def deconstruct_keys(_) = { id: @id, ref_id: @ref_id, hint_code: @hint_code }
156
+
157
+ def self.from_json(d)
158
+ new(id: d['id'], ref_id: d['refId'], hint_code: d['hintCode'])
159
+ end
160
+ end
161
+
162
+ class Device
163
+ attr_reader :ip_address, :uhi
164
+
165
+ def initialize(ip_address: nil, uhi: nil)
166
+ @ip_address = ip_address; @uhi = uhi
167
+ end
168
+
169
+ def deconstruct_keys(_) = { ip_address: @ip_address, uhi: @uhi }
170
+
171
+ def self.from_json(d) = new(ip_address: d['ipAddress'], uhi: d['uhi'])
172
+ end
173
+
174
+ class StepUp
175
+ attr_reader :mrtd, :face
176
+
177
+ def initialize(mrtd: nil, face: nil)
178
+ @mrtd = mrtd; @face = face
179
+ end
180
+
181
+ def deconstruct_keys(_) = { mrtd: @mrtd, face: @face }
182
+
183
+ def self.from_json(d) = new(mrtd: d['mrtd'], face: d['face'])
184
+ end
185
+
186
+ class Completed
187
+ attr_reader :id, :ref_id, :ssn, :name, :given_name, :surname,
188
+ :cert_start_date, :ocsp_response, :risk, :device, :step_up,
189
+ :address, :company_signatory_text,
190
+ :authenticated_at, :signed_at
191
+
192
+ def initialize(id:, ssn:, name:, given_name:, surname:, ocsp_response:,
193
+ ref_id: nil, cert_start_date: nil, risk: nil, device: nil, step_up: nil,
194
+ address: nil, company_signatory_text: nil,
195
+ authenticated_at: nil, signed_at: nil)
196
+ @id = id; @ref_id = ref_id; @ssn = ssn; @name = name
197
+ @given_name = given_name; @surname = surname
198
+ @cert_start_date = cert_start_date; @ocsp_response = ocsp_response
199
+ @risk = risk; @device = device; @step_up = step_up; @address = address
200
+ @company_signatory_text = company_signatory_text
201
+ @authenticated_at = authenticated_at; @signed_at = signed_at
202
+ end
203
+
204
+ def deconstruct_keys(_)
205
+ { id: @id, ref_id: @ref_id, ssn: @ssn, name: @name, given_name: @given_name,
206
+ surname: @surname, cert_start_date: @cert_start_date,
207
+ ocsp_response: @ocsp_response, risk: @risk, device: @device, step_up: @step_up,
208
+ address: @address,
209
+ company_signatory_text: @company_signatory_text,
210
+ authenticated_at: @authenticated_at, signed_at: @signed_at }
211
+ end
212
+
213
+ def self.from_json(d)
214
+ new(id: d['id'], ref_id: d['refId'], ssn: d['ssn'], name: d['name'],
215
+ given_name: d['givenName'], surname: d['surname'],
216
+ cert_start_date: d['certStartDate'], ocsp_response: d['ocspResponse'],
217
+ risk: d['risk'],
218
+ device: d['device'] ? Device.from_json(d['device']) : nil,
219
+ step_up: d['stepUp'] ? StepUp.from_json(d['stepUp']) : nil,
220
+ address: d['address'],
221
+ company_signatory_text: d['companySignatoryText'],
222
+ authenticated_at: d['authenticatedAt'], signed_at: d['signedAt'])
223
+ end
224
+ end
225
+
226
+ class Failed
227
+ attr_reader :id, :ref_id, :error
228
+
229
+ def initialize(id:, error:, ref_id: nil)
230
+ @id = id; @ref_id = ref_id; @error = error
231
+ end
232
+
233
+ def deconstruct_keys(_) = { id: @id, ref_id: @ref_id, error: @error }
234
+
235
+ def self.from_json(d) = new(id: d['id'], ref_id: d['refId'], error: d['error'])
236
+ end
237
+
238
+ class Unknown
239
+ attr_reader :status
240
+
241
+ def initialize(status:)
242
+ @status = status
243
+ end
244
+
245
+ def deconstruct_keys(_) = { status: @status }
246
+ end
247
+
248
+ module Status
249
+ def self.from_json(d)
250
+ case d['status']
251
+ when 'PENDING' then Pending.from_json(d)
252
+ when 'COMPLETED' then Completed.from_json(d)
253
+ when 'FAILED' then Failed.from_json(d)
254
+ when String then Unknown.new(status: d['status'])
255
+ else raise IdentisureError.new(0, "Invalid bankid-se status: #{d['status'].inspect}")
256
+ end
257
+ end
258
+ end
259
+
260
+ module PhoneStatus
261
+ def self.from_json(d)
262
+ case d['status']
263
+ when 'PENDING' then PendingPhone.from_json(d)
264
+ when 'COMPLETED' then Completed.from_json(d)
265
+ when 'FAILED' then Failed.from_json(d)
266
+ when String then Unknown.new(status: d['status'])
267
+ else raise IdentisureError.new(0, "Invalid bankid-se phone status: #{d['status'].inspect}")
268
+ end
269
+ end
270
+ end
271
+
272
+ # --- Endpoint ---
273
+
274
+ class Endpoint
275
+ # @api private
276
+ def initialize(transport)
277
+ @transport = transport
278
+ end
279
+
280
+ def auth(req) = Status.from_json(@transport.post('/v3/bankid-se/auth', req.to_h))
281
+ def phone_auth(req) = PhoneStatus.from_json(@transport.post('/v3/bankid-se/phone/auth', req.to_h))
282
+ def sign(req) = Status.from_json(@transport.post('/v3/bankid-se/sign', req.to_h))
283
+ def phone_sign(req) = PhoneStatus.from_json(@transport.post('/v3/bankid-se/phone/sign', req.to_h))
284
+ def verify(req) = VerifyResponse.from_json(@transport.post('/v3/bankid-se/verify', req.to_h))
285
+
286
+ def age_verification(req)
287
+ Identisure::AgeVerification::Status.from_json(@transport.post('/v3/bankid-se/age-verification', req.to_h))
288
+ end
289
+
290
+ def auth_status(id) = Status.from_json(@transport.get("/v3/bankid-se/auth/#{id}"))
291
+ def sign_status(id) = Status.from_json(@transport.get("/v3/bankid-se/sign/#{id}"))
292
+
293
+ def age_verification_status(id)
294
+ Identisure::AgeVerification::Status.from_json(@transport.get("/v3/bankid-se/age-verification/#{id}"))
295
+ end
296
+
297
+ def cancel_auth(id) = @transport.delete("/v3/bankid-se/auth/#{id}")
298
+ def cancel_sign(id) = @transport.delete("/v3/bankid-se/sign/#{id}")
299
+ def cancel_age_verification(id) = @transport.delete("/v3/bankid-se/age-verification/#{id}")
300
+
301
+ def wait_for_auth(id, opts: PollOptions.new)
302
+ poll_status(opts) { auth_status(id) }
303
+ end
304
+
305
+ def wait_for_sign(id, opts: PollOptions.new)
306
+ poll_status(opts) { sign_status(id) }
307
+ end
308
+
309
+ def wait_for_age_verification(id, opts: PollOptions.new)
310
+ poll_age(opts) { age_verification_status(id) }
311
+ end
312
+
313
+ private
314
+
315
+ def poll_status(opts)
316
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + opts.timeout
317
+ loop do
318
+ status = yield
319
+ return status unless status.is_a?(Pending)
320
+ raise WaitError.new(timeout: true) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
321
+
322
+ sleep opts.interval
323
+ end
324
+ end
325
+
326
+ def poll_age(opts)
327
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + opts.timeout
328
+ loop do
329
+ status = yield
330
+ return status unless status.is_a?(Identisure::AgeVerification::Pending)
331
+ raise WaitError.new(timeout: true) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
332
+
333
+ sleep opts.interval
334
+ end
335
+ end
336
+ end
337
+ end
338
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Identisure
6
+ # Verifies the X-Signature-SHA256 header of a callback request. Pass the raw request body as
7
+ # received, before parsing it.
8
+ def self.verify_callback_signature(body, signature, secret)
9
+ expected = OpenSSL::HMAC.digest('SHA256', secret, body)
10
+ actual = signature.unpack1('m0')
11
+ actual.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(actual, expected)
12
+ rescue ArgumentError
13
+ false
14
+ end
15
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+
5
+ require_relative 'version'
6
+ require_relative 'environment'
7
+ require_relative 'poll_options'
8
+ require_relative 'error'
9
+ require_relative 'transport'
10
+ require_relative 'age_verification'
11
+ require_relative 'bank_id_se'
12
+ require_relative 'bank_id_no'
13
+ require_relative 'freja'
14
+ require_relative 'mit_id'
15
+ require_relative 'ftn'
16
+ require_relative 'vipps'
17
+ require_relative 'document'
18
+ require_relative 'callback'
19
+
20
+ module Identisure
21
+ class Client
22
+ # @api private
23
+ def initialize(transport)
24
+ @transport = transport
25
+ end
26
+
27
+ def bank_id_se = BankIdSe::Endpoint.new(@transport)
28
+ def bank_id_no = BankIdNo::Endpoint.new(@transport)
29
+ def freja = Freja::Endpoint.new(@transport)
30
+ def mit_id = MitId::Endpoint.new(@transport)
31
+ def ftn = Ftn::Endpoint.new(@transport)
32
+ def vipps = Vipps::Endpoint.new(@transport)
33
+ def document = Document::Endpoint.new(@transport)
34
+ end
35
+
36
+ class ClientBuilder
37
+ def initialize(client_id, client_secret)
38
+ @client_id = client_id
39
+ @client_secret = client_secret
40
+ @environment = Environment::PRODUCTION
41
+ @base_url = nil
42
+ @http_client = nil
43
+ @timeout = 30
44
+ end
45
+
46
+ def environment(env)
47
+ @environment = env
48
+ self
49
+ end
50
+
51
+ def base_url(url)
52
+ @base_url = url
53
+ self
54
+ end
55
+
56
+ def http_client(conn)
57
+ @http_client = conn
58
+ self
59
+ end
60
+
61
+ # Timeout in seconds for each HTTP request. Defaults to 30.
62
+ def timeout(seconds)
63
+ raise ArgumentError, 'Timeout must be positive' unless seconds.positive?
64
+
65
+ @timeout = seconds
66
+ self
67
+ end
68
+
69
+ def build
70
+ base = @base_url || Environment.base_url(@environment)
71
+ conn = @http_client || Faraday.new
72
+ Client.new(Transport.new(conn, base, @client_id, @client_secret, @timeout))
73
+ end
74
+
75
+ def inspect = "#<#{self.class.name} client_id=#{@client_id.inspect}>"
76
+ end
77
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Identisure
4
+ module Document
5
+ class UploadResponse
6
+ attr_reader :id, :document_hash
7
+
8
+ def initialize(id:, document_hash:)
9
+ @id = id; @document_hash = document_hash
10
+ end
11
+
12
+ def deconstruct_keys(_) = { id: @id, document_hash: @document_hash }
13
+ def self.from_json(d) = new(id: d['id'], document_hash: d['hash'])
14
+ end
15
+
16
+ class Endpoint
17
+ # @api private
18
+ def initialize(transport)
19
+ @transport = transport
20
+ end
21
+
22
+ def upload(data, filename, mime_type: 'application/pdf')
23
+ UploadResponse.from_json(@transport.post_multipart('/v3/document', data, filename, mime_type))
24
+ end
25
+
26
+ def download(id)
27
+ @transport.get_raw("/v3/document/#{id}")
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Identisure
4
+ module Environment
5
+ PRODUCTION = :production
6
+ STAGING = :staging
7
+
8
+ BASE_URLS = {
9
+ PRODUCTION => 'https://api.idkollen.se',
10
+ STAGING => 'https://stgapi.idkollen.se',
11
+ }.freeze
12
+
13
+ def self.base_url(env)
14
+ BASE_URLS.fetch(env)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Identisure
4
+ class IdentisureError < StandardError
5
+ attr_reader :status_code
6
+
7
+ def initialize(status_code, message)
8
+ @status_code = status_code
9
+ super("identisure: #{status_code} #{message}")
10
+ end
11
+ end
12
+
13
+ class WaitError < StandardError
14
+ attr_reader :timeout
15
+
16
+ def initialize(timeout:)
17
+ @timeout = timeout
18
+ super('Poll timed out')
19
+ end
20
+ end
21
+ end