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.
- checksums.yaml +7 -0
- data/LICENSE +19 -0
- data/README.md +105 -0
- data/lib/identisure/age_verification.rb +87 -0
- data/lib/identisure/bank_id_no.rb +219 -0
- data/lib/identisure/bank_id_se.rb +338 -0
- data/lib/identisure/callback.rb +15 -0
- data/lib/identisure/client.rb +77 -0
- data/lib/identisure/document.rb +31 -0
- data/lib/identisure/environment.rb +17 -0
- data/lib/identisure/error.rb +21 -0
- data/lib/identisure/freja.rb +224 -0
- data/lib/identisure/ftn.rb +168 -0
- data/lib/identisure/mit_id.rb +205 -0
- data/lib/identisure/poll_options.rb +12 -0
- data/lib/identisure/transport.rb +90 -0
- data/lib/identisure/version.rb +5 -0
- data/lib/identisure/vipps.rb +153 -0
- metadata +75 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Identisure
|
|
4
|
+
module Freja
|
|
5
|
+
class AuthRequest
|
|
6
|
+
attr_reader :ssn, :callback_url, :min_registration_level, :org_number,
|
|
7
|
+
:request_address, :ref_id
|
|
8
|
+
|
|
9
|
+
def initialize(ssn: nil, callback_url: nil, min_registration_level: nil,
|
|
10
|
+
org_number: nil, request_address: nil, ref_id: nil)
|
|
11
|
+
@ssn = ssn; @callback_url = callback_url
|
|
12
|
+
@min_registration_level = min_registration_level; @org_number = org_number
|
|
13
|
+
@request_address = request_address; @ref_id = ref_id
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
{
|
|
18
|
+
ssn: @ssn, callbackUrl: @callback_url,
|
|
19
|
+
minRegistrationLevel: @min_registration_level, orgNumber: @org_number,
|
|
20
|
+
requestAddress: @request_address, refId: @ref_id
|
|
21
|
+
}.compact
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class BackchannelAuthRequest
|
|
26
|
+
attr_reader :ssn, :country, :callback_url, :min_registration_level,
|
|
27
|
+
:org_number, :request_address, :ref_id
|
|
28
|
+
|
|
29
|
+
def initialize(ssn:, country:, callback_url: nil, min_registration_level: nil,
|
|
30
|
+
org_number: nil, request_address: nil, ref_id: nil)
|
|
31
|
+
@ssn = ssn; @country = country; @callback_url = callback_url
|
|
32
|
+
@min_registration_level = min_registration_level; @org_number = org_number
|
|
33
|
+
@request_address = request_address; @ref_id = ref_id
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_h
|
|
37
|
+
{
|
|
38
|
+
ssn: @ssn, country: @country, callbackUrl: @callback_url,
|
|
39
|
+
minRegistrationLevel: @min_registration_level, orgNumber: @org_number,
|
|
40
|
+
requestAddress: @request_address, refId: @ref_id
|
|
41
|
+
}.compact
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class SignRequest
|
|
46
|
+
attr_reader :text, :ssn, :callback_url, :min_registration_level,
|
|
47
|
+
:org_number, :request_address, :ref_id
|
|
48
|
+
|
|
49
|
+
def initialize(text:, ssn: nil, callback_url: nil, min_registration_level: nil,
|
|
50
|
+
org_number: nil, request_address: nil, ref_id: nil)
|
|
51
|
+
@text = text; @ssn = ssn; @callback_url = callback_url
|
|
52
|
+
@min_registration_level = min_registration_level; @org_number = org_number
|
|
53
|
+
@request_address = request_address; @ref_id = ref_id
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
{
|
|
58
|
+
text: @text, ssn: @ssn, callbackUrl: @callback_url,
|
|
59
|
+
minRegistrationLevel: @min_registration_level, orgNumber: @org_number,
|
|
60
|
+
requestAddress: @request_address, refId: @ref_id
|
|
61
|
+
}.compact
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class BackchannelSignRequest
|
|
66
|
+
attr_reader :ssn, :country, :text, :callback_url, :min_registration_level,
|
|
67
|
+
:org_number, :request_address, :ref_id
|
|
68
|
+
|
|
69
|
+
def initialize(ssn:, country:, text:, callback_url: nil, min_registration_level: nil,
|
|
70
|
+
org_number: nil, request_address: nil, ref_id: nil)
|
|
71
|
+
@ssn = ssn; @country = country; @text = text; @callback_url = callback_url
|
|
72
|
+
@min_registration_level = min_registration_level; @org_number = org_number
|
|
73
|
+
@request_address = request_address; @ref_id = ref_id
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def to_h
|
|
77
|
+
{
|
|
78
|
+
ssn: @ssn, country: @country, text: @text, callbackUrl: @callback_url,
|
|
79
|
+
minRegistrationLevel: @min_registration_level, orgNumber: @org_number,
|
|
80
|
+
requestAddress: @request_address, refId: @ref_id
|
|
81
|
+
}.compact
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class Pending
|
|
86
|
+
attr_reader :id, :ref_id, :auto_start_token, :qr_data
|
|
87
|
+
|
|
88
|
+
def initialize(id:, auto_start_token:, qr_data:, ref_id: nil)
|
|
89
|
+
@id = id; @ref_id = ref_id
|
|
90
|
+
@auto_start_token = auto_start_token; @qr_data = qr_data
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def deconstruct_keys(_)
|
|
94
|
+
{ id: @id, ref_id: @ref_id, auto_start_token: @auto_start_token, qr_data: @qr_data }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.from_json(d)
|
|
98
|
+
new(id: d['id'], ref_id: d['refId'],
|
|
99
|
+
auto_start_token: d['autoStartToken'], qr_data: d['qrData'])
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class Completed
|
|
104
|
+
attr_reader :id, :ref_id, :ssn, :country, :name, :given_name, :surname,
|
|
105
|
+
:address, :company_signatory_text, :authenticated_at, :signed_at
|
|
106
|
+
|
|
107
|
+
def initialize(id:, ssn:, country:, name:, given_name:, surname:,
|
|
108
|
+
ref_id: nil, address: nil, company_signatory_text: nil,
|
|
109
|
+
authenticated_at: nil, signed_at: nil)
|
|
110
|
+
@id = id; @ref_id = ref_id; @ssn = ssn; @country = country
|
|
111
|
+
@name = name; @given_name = given_name; @surname = surname
|
|
112
|
+
@address = address; @company_signatory_text = company_signatory_text
|
|
113
|
+
@authenticated_at = authenticated_at; @signed_at = signed_at
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def deconstruct_keys(_)
|
|
117
|
+
{ id: @id, ref_id: @ref_id, ssn: @ssn, country: @country,
|
|
118
|
+
name: @name, given_name: @given_name, surname: @surname,
|
|
119
|
+
address: @address, company_signatory_text: @company_signatory_text,
|
|
120
|
+
authenticated_at: @authenticated_at, signed_at: @signed_at }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.from_json(d)
|
|
124
|
+
new(
|
|
125
|
+
id: d['id'], ref_id: d['refId'], ssn: d['ssn'], country: d['country'],
|
|
126
|
+
name: d['name'], given_name: d['givenName'], surname: d['surname'],
|
|
127
|
+
address: d['address'], company_signatory_text: d['companySignatoryText'],
|
|
128
|
+
authenticated_at: d['authenticatedAt'], signed_at: d['signedAt']
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
class Failed
|
|
134
|
+
attr_reader :id, :ref_id, :error
|
|
135
|
+
|
|
136
|
+
def initialize(id:, error:, ref_id: nil)
|
|
137
|
+
@id = id; @ref_id = ref_id; @error = error
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def deconstruct_keys(_) = { id: @id, ref_id: @ref_id, error: @error }
|
|
141
|
+
|
|
142
|
+
def self.from_json(d) = new(id: d['id'], ref_id: d['refId'], error: d['error'])
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
class Unknown
|
|
146
|
+
attr_reader :status
|
|
147
|
+
|
|
148
|
+
def initialize(status:)
|
|
149
|
+
@status = status
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def deconstruct_keys(_) = { status: @status }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
module Status
|
|
156
|
+
def self.from_json(d)
|
|
157
|
+
case d['status']
|
|
158
|
+
when 'PENDING' then Pending.from_json(d)
|
|
159
|
+
when 'COMPLETED' then Completed.from_json(d)
|
|
160
|
+
when 'FAILED' then Failed.from_json(d)
|
|
161
|
+
when String then Unknown.new(status: d['status'])
|
|
162
|
+
else raise IdentisureError.new(0, "Invalid freja status: #{d['status'].inspect}")
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
class Endpoint
|
|
168
|
+
# @api private
|
|
169
|
+
def initialize(transport)
|
|
170
|
+
@transport = transport
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def auth(req) = Status.from_json(@transport.post('/v3/freja/auth', req.to_h))
|
|
174
|
+
def backchannel_auth(req) = Status.from_json(@transport.post('/v3/freja/backchannel/auth', req.to_h))
|
|
175
|
+
def sign(req) = Status.from_json(@transport.post('/v3/freja/sign', req.to_h))
|
|
176
|
+
def backchannel_sign(req) = Status.from_json(@transport.post('/v3/freja/backchannel/sign', req.to_h))
|
|
177
|
+
def auth_status(id) = Status.from_json(@transport.get("/v3/freja/auth/#{id}"))
|
|
178
|
+
def sign_status(id) = Status.from_json(@transport.get("/v3/freja/sign/#{id}"))
|
|
179
|
+
def cancel_auth(id) = @transport.delete("/v3/freja/auth/#{id}")
|
|
180
|
+
def cancel_sign(id) = @transport.delete("/v3/freja/sign/#{id}")
|
|
181
|
+
|
|
182
|
+
def age_verification(req)
|
|
183
|
+
Identisure::AgeVerification::Status.from_json(@transport.post('/v3/freja/age-verification', req.to_h))
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def age_verification_status(id)
|
|
187
|
+
Identisure::AgeVerification::Status.from_json(@transport.get("/v3/freja/age-verification/#{id}"))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def cancel_age_verification(id) = @transport.delete("/v3/freja/age-verification/#{id}")
|
|
191
|
+
|
|
192
|
+
def wait_for_auth(id, opts: PollOptions.new) = poll(opts) { auth_status(id) }
|
|
193
|
+
def wait_for_sign(id, opts: PollOptions.new) = poll(opts) { sign_status(id) }
|
|
194
|
+
|
|
195
|
+
def wait_for_age_verification(id, opts: PollOptions.new)
|
|
196
|
+
poll_age(opts) { age_verification_status(id) }
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def poll(opts)
|
|
202
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + opts.timeout
|
|
203
|
+
loop do
|
|
204
|
+
status = yield
|
|
205
|
+
return status unless status.is_a?(Pending)
|
|
206
|
+
raise WaitError.new(timeout: true) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
207
|
+
|
|
208
|
+
sleep opts.interval
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def poll_age(opts)
|
|
213
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + opts.timeout
|
|
214
|
+
loop do
|
|
215
|
+
status = yield
|
|
216
|
+
return status unless status.is_a?(Identisure::AgeVerification::Pending)
|
|
217
|
+
raise WaitError.new(timeout: true) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
218
|
+
|
|
219
|
+
sleep opts.interval
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Identisure
|
|
4
|
+
module Ftn
|
|
5
|
+
# --- Request ---
|
|
6
|
+
|
|
7
|
+
class AuthRequest
|
|
8
|
+
attr_reader :redirect_url, :request_phone, :request_email, :request_address, :ref_id
|
|
9
|
+
|
|
10
|
+
def initialize(redirect_url: nil, request_phone: nil, request_email: nil,
|
|
11
|
+
request_address: nil, ref_id: nil)
|
|
12
|
+
@redirect_url = redirect_url
|
|
13
|
+
@request_phone = request_phone
|
|
14
|
+
@request_email = request_email
|
|
15
|
+
@request_address = request_address
|
|
16
|
+
@ref_id = ref_id
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
redirectUrl: @redirect_url,
|
|
22
|
+
requestPhone: @request_phone,
|
|
23
|
+
requestEmail: @request_email,
|
|
24
|
+
requestAddress: @request_address,
|
|
25
|
+
refId: @ref_id,
|
|
26
|
+
}.compact
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# --- Status variants ---
|
|
31
|
+
|
|
32
|
+
class Pending
|
|
33
|
+
attr_reader :id, :ref_id, :url
|
|
34
|
+
|
|
35
|
+
def initialize(id:, url:, ref_id: nil)
|
|
36
|
+
@id = id; @ref_id = ref_id; @url = url
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def deconstruct_keys(_) = { id: @id, ref_id: @ref_id, url: @url }
|
|
40
|
+
|
|
41
|
+
def self.from_json(d) = new(id: d['id'], ref_id: d['refId'], url: d['url'])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Completed
|
|
45
|
+
attr_reader :id, :ref_id, :ssn, :name, :given_name, :surname,
|
|
46
|
+
:phone, :email, :address, :birth_date, :pid, :bank_id,
|
|
47
|
+
:bank_name, :authenticated_at
|
|
48
|
+
|
|
49
|
+
def initialize(id:, ssn:, name:, given_name:, surname:,
|
|
50
|
+
ref_id: nil, phone: nil, email: nil, address: nil,
|
|
51
|
+
birth_date: nil, pid: nil, bank_id: nil, bank_name: nil,
|
|
52
|
+
authenticated_at: nil)
|
|
53
|
+
@id = id; @ref_id = ref_id; @ssn = ssn; @name = name
|
|
54
|
+
@given_name = given_name; @surname = surname
|
|
55
|
+
@phone = phone; @email = email; @address = address
|
|
56
|
+
@birth_date = birth_date; @pid = pid; @bank_id = bank_id
|
|
57
|
+
@bank_name = bank_name; @authenticated_at = authenticated_at
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def deconstruct_keys(_)
|
|
61
|
+
{ id: @id, ref_id: @ref_id, ssn: @ssn, name: @name, given_name: @given_name,
|
|
62
|
+
surname: @surname, phone: @phone, email: @email, address: @address,
|
|
63
|
+
birth_date: @birth_date, pid: @pid, bank_id: @bank_id,
|
|
64
|
+
bank_name: @bank_name, authenticated_at: @authenticated_at }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.from_json(d)
|
|
68
|
+
new(id: d['id'], ref_id: d['refId'], ssn: d['ssn'], name: d['name'],
|
|
69
|
+
given_name: d['givenName'], surname: d['surname'],
|
|
70
|
+
phone: d['phone'], email: d['email'], address: d['address'],
|
|
71
|
+
birth_date: d['birthDate'], pid: d['pid'], bank_id: d['bankId'],
|
|
72
|
+
bank_name: d['bankName'], authenticated_at: d['authenticatedAt'])
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class Failed
|
|
77
|
+
attr_reader :id, :ref_id, :error
|
|
78
|
+
|
|
79
|
+
def initialize(id:, error:, ref_id: nil)
|
|
80
|
+
@id = id; @ref_id = ref_id; @error = error
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def deconstruct_keys(_) = { id: @id, ref_id: @ref_id, error: @error }
|
|
84
|
+
|
|
85
|
+
def self.from_json(d) = new(id: d['id'], ref_id: d['refId'], error: d['error'])
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class Unknown
|
|
89
|
+
attr_reader :status
|
|
90
|
+
|
|
91
|
+
def initialize(status:)
|
|
92
|
+
@status = status
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def deconstruct_keys(_) = { status: @status }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
module Status
|
|
99
|
+
def self.from_json(d)
|
|
100
|
+
case d['status']
|
|
101
|
+
when 'PENDING' then Pending.from_json(d)
|
|
102
|
+
when 'COMPLETED' then Completed.from_json(d)
|
|
103
|
+
when 'FAILED' then Failed.from_json(d)
|
|
104
|
+
when String then Unknown.new(status: d['status'])
|
|
105
|
+
else raise IdentisureError.new(0, "Invalid ftn status: #{d['status'].inspect}")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# --- Endpoint ---
|
|
111
|
+
|
|
112
|
+
class Endpoint
|
|
113
|
+
# @api private
|
|
114
|
+
def initialize(transport)
|
|
115
|
+
@transport = transport
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def auth(req)
|
|
119
|
+
Status.from_json(@transport.post('/v3/ftn/auth', req.to_h))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def age_verification(req)
|
|
123
|
+
Identisure::AgeVerification::Status.from_json(@transport.post('/v3/ftn/age-verification', req.to_h))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def auth_status(id) = Status.from_json(@transport.get("/v3/ftn/auth/#{id}"))
|
|
127
|
+
|
|
128
|
+
def age_verification_status(id)
|
|
129
|
+
Identisure::AgeVerification::Status.from_json(@transport.get("/v3/ftn/age-verification/#{id}"))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def cancel_auth(id) = @transport.delete("/v3/ftn/auth/#{id}")
|
|
133
|
+
def cancel_age_verification(id) = @transport.delete("/v3/ftn/age-verification/#{id}")
|
|
134
|
+
|
|
135
|
+
def wait_for_auth(id, opts: PollOptions.new)
|
|
136
|
+
poll_ftn(opts) { auth_status(id) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def wait_for_age_verification(id, opts: PollOptions.new)
|
|
140
|
+
poll_age(opts) { age_verification_status(id) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def poll_ftn(opts)
|
|
146
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + opts.timeout
|
|
147
|
+
loop do
|
|
148
|
+
status = yield
|
|
149
|
+
return status unless status.is_a?(Pending)
|
|
150
|
+
raise WaitError.new(timeout: true) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
151
|
+
|
|
152
|
+
sleep opts.interval
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def poll_age(opts)
|
|
157
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + opts.timeout
|
|
158
|
+
loop do
|
|
159
|
+
status = yield
|
|
160
|
+
return status unless status.is_a?(Identisure::AgeVerification::Pending)
|
|
161
|
+
raise WaitError.new(timeout: true) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
162
|
+
|
|
163
|
+
sleep opts.interval
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Identisure
|
|
4
|
+
module MitId
|
|
5
|
+
class AuthRequest
|
|
6
|
+
attr_reader :redirect_url, :reference_text, :request_phone, :request_email,
|
|
7
|
+
:request_address, :ref_id
|
|
8
|
+
|
|
9
|
+
def initialize(redirect_url: nil, reference_text: nil, request_phone: nil,
|
|
10
|
+
request_email: nil, request_address: nil, ref_id: nil)
|
|
11
|
+
@redirect_url = redirect_url; @reference_text = reference_text
|
|
12
|
+
@request_phone = request_phone; @request_email = request_email
|
|
13
|
+
@request_address = request_address; @ref_id = ref_id
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
{
|
|
18
|
+
redirectUrl: @redirect_url, referenceText: @reference_text,
|
|
19
|
+
requestPhone: @request_phone, requestEmail: @request_email,
|
|
20
|
+
requestAddress: @request_address, refId: @ref_id
|
|
21
|
+
}.compact
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class BackchannelAuthRequest
|
|
26
|
+
attr_reader :ssn, :binding_message, :callback_url, :ref_id
|
|
27
|
+
|
|
28
|
+
def initialize(ssn:, binding_message:, callback_url: nil, ref_id: nil)
|
|
29
|
+
@ssn = ssn; @binding_message = binding_message
|
|
30
|
+
@callback_url = callback_url; @ref_id = ref_id
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
{
|
|
35
|
+
ssn: @ssn, bindingMessage: @binding_message,
|
|
36
|
+
callbackUrl: @callback_url, refId: @ref_id
|
|
37
|
+
}.compact
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class SignRequest
|
|
42
|
+
attr_reader :text, :redirect_url, :ref_id
|
|
43
|
+
|
|
44
|
+
def initialize(text:, redirect_url: nil, ref_id: nil)
|
|
45
|
+
@text = text; @redirect_url = redirect_url; @ref_id = ref_id
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_h
|
|
49
|
+
{ text: @text, redirectUrl: @redirect_url, refId: @ref_id }.compact
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class SignResult
|
|
54
|
+
attr_reader :checksum
|
|
55
|
+
|
|
56
|
+
def initialize(checksum:)
|
|
57
|
+
@checksum = checksum
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def deconstruct_keys(_) = { checksum: @checksum }
|
|
61
|
+
def self.from_json(d) = new(checksum: d['checksum'])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class Pending
|
|
65
|
+
attr_reader :id, :ref_id, :url, :binding_message
|
|
66
|
+
|
|
67
|
+
def initialize(id:, ref_id: nil, url: nil, binding_message: nil)
|
|
68
|
+
@id = id; @ref_id = ref_id; @url = url; @binding_message = binding_message
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def deconstruct_keys(_)
|
|
72
|
+
{ id: @id, ref_id: @ref_id, url: @url, binding_message: @binding_message }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.from_json(d)
|
|
76
|
+
new(id: d['id'], ref_id: d['refId'], url: d['url'], binding_message: d['bindingMessage'])
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class Completed
|
|
81
|
+
attr_reader :id, :ref_id, :ssn, :name, :given_name, :surname,
|
|
82
|
+
:phone, :email, :address, :birth_date, :pid, :bank_id,
|
|
83
|
+
:sign_result, :authenticated_at, :signed_at
|
|
84
|
+
|
|
85
|
+
def initialize(id:, ssn:, name:, given_name:, surname:, ref_id: nil,
|
|
86
|
+
phone: nil, email: nil, address: nil, birth_date: nil,
|
|
87
|
+
pid: nil, bank_id: nil, sign_result: nil,
|
|
88
|
+
authenticated_at: nil, signed_at: nil)
|
|
89
|
+
@id = id; @ref_id = ref_id; @ssn = ssn; @name = name
|
|
90
|
+
@given_name = given_name; @surname = surname; @phone = phone
|
|
91
|
+
@email = email; @address = address; @birth_date = birth_date
|
|
92
|
+
@pid = pid; @bank_id = bank_id; @sign_result = sign_result
|
|
93
|
+
@authenticated_at = authenticated_at; @signed_at = signed_at
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def deconstruct_keys(_)
|
|
97
|
+
{ id: @id, ref_id: @ref_id, ssn: @ssn, name: @name, given_name: @given_name,
|
|
98
|
+
surname: @surname, phone: @phone, email: @email, address: @address,
|
|
99
|
+
birth_date: @birth_date, pid: @pid, bank_id: @bank_id,
|
|
100
|
+
sign_result: @sign_result, authenticated_at: @authenticated_at,
|
|
101
|
+
signed_at: @signed_at }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.from_json(d)
|
|
105
|
+
new(
|
|
106
|
+
id: d['id'], ref_id: d['refId'], ssn: d['ssn'], name: d['name'],
|
|
107
|
+
given_name: d['givenName'], surname: d['surname'], phone: d['phone'],
|
|
108
|
+
email: d['email'], address: d['address'], birth_date: d['birthDate'],
|
|
109
|
+
pid: d['pid'], bank_id: d['bankId'],
|
|
110
|
+
sign_result: d['signResult'] ? SignResult.from_json(d['signResult']) : nil,
|
|
111
|
+
authenticated_at: d['authenticatedAt'], signed_at: d['signedAt']
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class Failed
|
|
117
|
+
attr_reader :id, :ref_id, :error
|
|
118
|
+
|
|
119
|
+
def initialize(id:, error:, ref_id: nil)
|
|
120
|
+
@id = id; @ref_id = ref_id; @error = error
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def deconstruct_keys(_) = { id: @id, ref_id: @ref_id, error: @error }
|
|
124
|
+
def self.from_json(d) = new(id: d['id'], ref_id: d['refId'], error: d['error'])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class Unknown
|
|
128
|
+
attr_reader :status
|
|
129
|
+
|
|
130
|
+
def initialize(status:)
|
|
131
|
+
@status = status
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def deconstruct_keys(_) = { status: @status }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
module Status
|
|
138
|
+
def self.from_json(d)
|
|
139
|
+
case d['status']
|
|
140
|
+
when 'PENDING' then Pending.from_json(d)
|
|
141
|
+
when 'COMPLETED' then Completed.from_json(d)
|
|
142
|
+
when 'FAILED' then Failed.from_json(d)
|
|
143
|
+
when String then Unknown.new(status: d['status'])
|
|
144
|
+
else raise IdentisureError.new(0, "Invalid mitid status: #{d['status'].inspect}")
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class Endpoint
|
|
150
|
+
# @api private
|
|
151
|
+
def initialize(transport)
|
|
152
|
+
@transport = transport
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def auth(req) = Status.from_json(@transport.post('/v3/mitid/auth', req.to_h))
|
|
156
|
+
def backchannel_auth(req) = Status.from_json(@transport.post('/v3/mitid/backchannel/auth', req.to_h))
|
|
157
|
+
def sign(req) = Status.from_json(@transport.post('/v3/mitid/sign', req.to_h))
|
|
158
|
+
def auth_status(id) = Status.from_json(@transport.get("/v3/mitid/auth/#{id}"))
|
|
159
|
+
def sign_status(id) = Status.from_json(@transport.get("/v3/mitid/sign/#{id}"))
|
|
160
|
+
def cancel_auth(id) = @transport.delete("/v3/mitid/auth/#{id}")
|
|
161
|
+
def cancel_sign(id) = @transport.delete("/v3/mitid/sign/#{id}")
|
|
162
|
+
|
|
163
|
+
def age_verification(req)
|
|
164
|
+
Identisure::AgeVerification::Status.from_json(@transport.post('/v3/mitid/age-verification', req.to_h))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def age_verification_status(id)
|
|
168
|
+
Identisure::AgeVerification::Status.from_json(@transport.get("/v3/mitid/age-verification/#{id}"))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def cancel_age_verification(id) = @transport.delete("/v3/mitid/age-verification/#{id}")
|
|
172
|
+
|
|
173
|
+
def wait_for_auth(id, opts: PollOptions.new) = poll(opts) { auth_status(id) }
|
|
174
|
+
def wait_for_sign(id, opts: PollOptions.new) = poll(opts) { sign_status(id) }
|
|
175
|
+
|
|
176
|
+
def wait_for_age_verification(id, opts: PollOptions.new)
|
|
177
|
+
poll_age(opts) { age_verification_status(id) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def poll(opts)
|
|
183
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + opts.timeout
|
|
184
|
+
loop do
|
|
185
|
+
status = yield
|
|
186
|
+
return status unless status.is_a?(Pending)
|
|
187
|
+
raise WaitError.new(timeout: true) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
188
|
+
|
|
189
|
+
sleep opts.interval
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def poll_age(opts)
|
|
194
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + opts.timeout
|
|
195
|
+
loop do
|
|
196
|
+
status = yield
|
|
197
|
+
return status unless status.is_a?(Identisure::AgeVerification::Pending)
|
|
198
|
+
raise WaitError.new(timeout: true) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
199
|
+
|
|
200
|
+
sleep opts.interval
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
module Identisure
|
|
8
|
+
# @api private
|
|
9
|
+
USER_AGENT = "identisure-client-ruby/#{Identisure::VERSION}".freeze
|
|
10
|
+
|
|
11
|
+
# @api private
|
|
12
|
+
class Transport
|
|
13
|
+
def initialize(connection, base_url, client_id, client_secret, timeout)
|
|
14
|
+
@conn = connection
|
|
15
|
+
@timeout = timeout
|
|
16
|
+
@base_url = base_url.chomp('/')
|
|
17
|
+
@authorization = "Basic #{["#{client_id}:#{client_secret}"].pack('m0')}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def inspect = "#<#{self.class.name}>"
|
|
21
|
+
|
|
22
|
+
def post(path, body)
|
|
23
|
+
parse(send_request(:post, path, JSON.generate(body), 'Content-Type' => 'application/json'))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get(path)
|
|
27
|
+
parse(send_request(:get, path))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get_raw(path)
|
|
31
|
+
send_request(:get, path).body.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def delete(path)
|
|
35
|
+
send_request(:delete, path)
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def post_multipart(path, data, filename, mime_type)
|
|
40
|
+
boundary = "identisure-#{SecureRandom.hex(16)}"
|
|
41
|
+
body = multipart_body(boundary, data, filename, mime_type)
|
|
42
|
+
parse(send_request(:post, path, body, 'Content-Type' => "multipart/form-data; boundary=#{boundary}"))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def multipart_body(boundary, data, filename, mime_type)
|
|
48
|
+
escaped = filename.gsub('"', '%22').gsub("\r", '%0D').gsub("\n", '%0A')
|
|
49
|
+
body = String.new(encoding: Encoding::BINARY)
|
|
50
|
+
body << "--#{boundary}\r\n".b
|
|
51
|
+
body << %(Content-Disposition: form-data; name="file"; filename="#{escaped}"\r\n).b
|
|
52
|
+
body << "Content-Type: #{mime_type}\r\n\r\n".b
|
|
53
|
+
body << data.b
|
|
54
|
+
body << "\r\n--#{boundary}--\r\n".b
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def send_request(method, path, body = nil, headers = {})
|
|
58
|
+
headers = headers.merge('Authorization' => @authorization, 'User-Agent' => USER_AGENT)
|
|
59
|
+
res = @conn.run_request(method, @base_url + path, body, headers) do |req|
|
|
60
|
+
req.options.timeout = @timeout
|
|
61
|
+
end
|
|
62
|
+
raise IdentisureError.new(res.status, error_message(res.body)) unless res.success?
|
|
63
|
+
|
|
64
|
+
res
|
|
65
|
+
rescue Faraday::Error => e
|
|
66
|
+
raise IdentisureError.new(0, e.message)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse(res)
|
|
70
|
+
body = res.body
|
|
71
|
+
return body if body.is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
parsed = JSON.parse(body.to_s)
|
|
74
|
+
raise IdentisureError.new(res.status, 'Response body is not a JSON object') unless parsed.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
parsed
|
|
77
|
+
rescue JSON::ParserError => e
|
|
78
|
+
raise IdentisureError.new(res.status, "Invalid JSON response: #{e.message}")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def error_message(body)
|
|
82
|
+
return body['message'] || body.to_s if body.is_a?(Hash)
|
|
83
|
+
|
|
84
|
+
parsed = JSON.parse(body.to_s)
|
|
85
|
+
parsed.is_a?(Hash) && parsed['message'] ? parsed['message'] : body.to_s
|
|
86
|
+
rescue JSON::ParserError
|
|
87
|
+
body.to_s
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|