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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 82d1c302c12ff4ecf26256974dc59b2bffe3aecdfe60b2353d3f0966832a7d13
4
+ data.tar.gz: c3065889a77aba5439388bc6efdd0bb9eac2d11a65a194beaf767ab4c6261775
5
+ SHA512:
6
+ metadata.gz: 1a6191c000b7770c389de61ebb66b3037aed2e06078d7d16402bca6a673d740777ecb6f64635e08a2323bea5db8f53c4708b21b4b7eb37f16c0e52ad72bc3784
7
+ data.tar.gz: c5440e580d6521272a861e250e2f41f894aaef827989e14ba8e44d35688dcefc6aa84054a37ac485bf9fe791f10180d5b13319076bfcb96060e26a662e940858
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ <img align="left" height="100" style="margin-right: 10px;" src="img/icon.png">
2
+
3
+ # Identisure Ruby Client
4
+
5
+ [![Gem version][gem-image]][gem-url]
6
+ [![Documentation][docs-image]][docs-url]
7
+ [![MIT license][license-image]][license-url]
8
+
9
+ [gem-image]: https://img.shields.io/gem/v/identisure-client?style=flat-square
10
+ [gem-url]: https://rubygems.org/gems/identisure-client
11
+ [docs-image]: https://img.shields.io/badge/github-docs-orange?logo=github&style=flat-square
12
+ [docs-url]: https://identisure.github.io/api-client-ruby
13
+ [license-image]: https://img.shields.io/badge/license-MIT-blue?style=flat-square
14
+ [license-url]: https://opensource.org/license/mit
15
+
16
+ API client for the [Identisure](https://developers.identisure.com) REST API.
17
+
18
+ Documentation: [identisure.github.io/api-client-ruby](https://identisure.github.io/api-client-ruby)
19
+
20
+ ## Requirements
21
+
22
+ Ruby 3.2 or later.
23
+
24
+ ## Supported providers
25
+
26
+ - **BankID SE**: auth, sign, phone auth and sign, QR verify, age verification
27
+ - **BankID NO**: auth, backchannel auth, sign, age verification
28
+ - **Freja eID**: auth, backchannel auth, sign, backchannel sign, age verification
29
+ - **MitID**: auth, backchannel auth, sign, age verification
30
+ - **FTN** (Finnish Trust Network): auth, age verification
31
+ - **Vipps MobilePay**: auth, backchannel auth
32
+ - **Documents**: upload and download documents for signing
33
+
34
+ ## Installation
35
+
36
+ ```sh
37
+ gem install identisure-client
38
+ ```
39
+
40
+ Or add to your `Gemfile`:
41
+
42
+ ```ruby
43
+ gem 'identisure-client', '~> 0.1'
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ ```ruby
49
+ require 'identisure/client'
50
+
51
+ client = Identisure::ClientBuilder.new('client_id', 'client_secret')
52
+ .environment(Identisure::Environment::STAGING)
53
+ .build
54
+
55
+ session = client.bank_id_se.auth(Identisure::BankIdSe::AuthRequest.new)
56
+ puts "Session ID: #{session.id}" if session.is_a?(Identisure::BankIdSe::Pending)
57
+
58
+ result = client.bank_id_se.wait_for_auth(session.id)
59
+
60
+ case result
61
+ in Identisure::BankIdSe::Completed(ssn:, name:)
62
+ puts "Authenticated: #{name} (#{ssn})"
63
+ in Identisure::BankIdSe::Failed(error:)
64
+ puts "Failed: #{error}"
65
+ end
66
+ ```
67
+
68
+ ### Verifying callbacks
69
+
70
+ Verify the `X-Signature-SHA256` header against the raw request body before parsing it:
71
+
72
+ ```ruby
73
+ valid = Identisure.verify_callback_signature(raw_body, signature_header, 'hmac_secret')
74
+ ```
75
+
76
+ ## Development
77
+
78
+ ### Prerequisites
79
+
80
+ - Ruby 3.2+
81
+ - Bundler
82
+
83
+ ### Setup / Build / Test
84
+
85
+ The test fixtures live in a Git submodule. Clone with `--recurse-submodules`, or run `git submodule update --init` in an existing clone.
86
+
87
+ ```sh
88
+ bundle install
89
+ bundle exec rake test
90
+ ```
91
+
92
+ ### Release
93
+
94
+ Set the version in `lib/identisure/version.rb` and commit. Then push a tag:
95
+
96
+ ```sh
97
+ git tag v0.1.0
98
+ git push origin v0.1.0
99
+ ```
100
+
101
+ The release workflow checks that the tag matches the version, runs CI, publishes to RubyGems and creates a GitHub release.
102
+
103
+ ## Other languages
104
+
105
+ [C#](https://github.com/identisure/api-client-csharp), [Dart](https://github.com/identisure/api-client-dart), [Go](https://github.com/identisure/api-client-go), [TypeScript/JavaScript](https://github.com/identisure/api-client-js), [Java/Kotlin](https://github.com/identisure/api-client-jvm), [PHP](https://github.com/identisure/api-client-php), [Python](https://github.com/identisure/api-client-py), [Rust](https://github.com/identisure/api-client-rust), [Swift](https://github.com/identisure/api-client-swift)
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Identisure
4
+ module AgeVerification
5
+ class Request
6
+ attr_reader :min_age, :max_age, :ref_id, :callback_url, :redirect_url
7
+
8
+ def initialize(min_age: nil, max_age: nil, ref_id: nil, callback_url: nil, redirect_url: nil)
9
+ @min_age = min_age
10
+ @max_age = max_age
11
+ @ref_id = ref_id
12
+ @callback_url = callback_url
13
+ @redirect_url = redirect_url
14
+ end
15
+
16
+ def to_h
17
+ {
18
+ minAge: @min_age,
19
+ maxAge: @max_age,
20
+ refId: @ref_id,
21
+ callbackUrl: @callback_url,
22
+ redirectUrl: @redirect_url,
23
+ }.compact
24
+ end
25
+ end
26
+
27
+ class Pending
28
+ attr_reader :id, :url, :min_age, :max_age
29
+
30
+ def initialize(id:, url: nil, min_age: nil, max_age: nil)
31
+ @id = id; @url = url; @min_age = min_age; @max_age = max_age
32
+ end
33
+
34
+ def deconstruct_keys(_) = { id: @id, url: @url, min_age: @min_age, max_age: @max_age }
35
+
36
+ def self.from_json(d)
37
+ new(id: d['id'], url: d['url'], min_age: d['minAge'], max_age: d['maxAge'])
38
+ end
39
+ end
40
+
41
+ class Completed
42
+ attr_reader :id, :age_verified
43
+
44
+ def initialize(id:, age_verified:)
45
+ @id = id; @age_verified = age_verified
46
+ end
47
+
48
+ def deconstruct_keys(_) = { id: @id, age_verified: @age_verified }
49
+
50
+ def self.from_json(d) = new(id: d['id'], age_verified: d['ageVerified'])
51
+ end
52
+
53
+ class Failed
54
+ attr_reader :id, :error
55
+
56
+ def initialize(id:, error:)
57
+ @id = id; @error = error
58
+ end
59
+
60
+ def deconstruct_keys(_) = { id: @id, error: @error }
61
+
62
+ def self.from_json(d) = new(id: d['id'], error: d['error'])
63
+ end
64
+
65
+ class Unknown
66
+ attr_reader :status
67
+
68
+ def initialize(status:)
69
+ @status = status
70
+ end
71
+
72
+ def deconstruct_keys(_) = { status: @status }
73
+ end
74
+
75
+ module Status
76
+ def self.from_json(d)
77
+ case d['status']
78
+ when 'PENDING' then Pending.from_json(d)
79
+ when 'COMPLETED' then Completed.from_json(d)
80
+ when 'FAILED' then Failed.from_json(d)
81
+ when String then Unknown.new(status: d['status'])
82
+ else raise IdentisureError.new(0, "Invalid age verification status: #{d['status'].inspect}")
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Identisure
4
+ module BankIdNo
5
+ class AuthRequest
6
+ attr_reader :redirect_url, :request_ssn, :request_phone, :request_email,
7
+ :request_address, :ref_id, :app_callback_uri
8
+
9
+ def initialize(redirect_url: nil, request_ssn: nil, request_phone: nil,
10
+ request_email: nil, request_address: nil, ref_id: nil,
11
+ app_callback_uri: nil)
12
+ @redirect_url = redirect_url; @request_ssn = request_ssn
13
+ @request_phone = request_phone; @request_email = request_email
14
+ @request_address = request_address; @ref_id = ref_id
15
+ @app_callback_uri = app_callback_uri
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ redirectUrl: @redirect_url, requestSsn: @request_ssn,
21
+ requestPhone: @request_phone, requestEmail: @request_email,
22
+ requestAddress: @request_address, refId: @ref_id,
23
+ appCallbackUri: @app_callback_uri
24
+ }.compact
25
+ end
26
+ end
27
+
28
+ class BackchannelAuthRequest
29
+ attr_reader :ssn, :callback_url, :ref_id
30
+
31
+ def initialize(ssn:, callback_url: nil, ref_id: nil)
32
+ @ssn = ssn; @callback_url = callback_url; @ref_id = ref_id
33
+ end
34
+
35
+ def to_h
36
+ { ssn: @ssn, callbackUrl: @callback_url, refId: @ref_id }.compact
37
+ end
38
+ end
39
+
40
+ class SignRequest
41
+ attr_reader :redirect_url, :text, :documents, :ref_id
42
+
43
+ def initialize(redirect_url: nil, text: nil, documents: nil, ref_id: nil)
44
+ @redirect_url = redirect_url; @text = text; @documents = documents
45
+ @ref_id = ref_id
46
+ end
47
+
48
+ def to_h
49
+ {
50
+ redirectUrl: @redirect_url, text: @text, documents: @documents,
51
+ refId: @ref_id
52
+ }.compact
53
+ end
54
+ end
55
+
56
+ class SignResult
57
+ attr_reader :end_user, :merchant, :document_hash
58
+
59
+ def initialize(end_user:, merchant:, document_hash:)
60
+ @end_user = end_user; @merchant = merchant; @document_hash = document_hash
61
+ end
62
+
63
+ def deconstruct_keys(_) = { end_user: @end_user, merchant: @merchant, document_hash: @document_hash }
64
+ def self.from_json(d) = new(end_user: d['endUser'], merchant: d['merchant'], document_hash: d['hash'])
65
+ end
66
+
67
+ class SignedDocument
68
+ attr_reader :id, :document_hash
69
+
70
+ def initialize(id:, document_hash:)
71
+ @id = id; @document_hash = document_hash
72
+ end
73
+
74
+ def deconstruct_keys(_) = { id: @id, document_hash: @document_hash }
75
+ def self.from_json(d) = new(id: d['id'], document_hash: d['hash'])
76
+ end
77
+
78
+ class Pending
79
+ attr_reader :id, :ref_id, :url, :binding_message
80
+
81
+ def initialize(id:, ref_id: nil, url: nil, binding_message: nil)
82
+ @id = id; @ref_id = ref_id; @url = url; @binding_message = binding_message
83
+ end
84
+
85
+ def deconstruct_keys(_) = { id: @id, ref_id: @ref_id, url: @url, binding_message: @binding_message }
86
+
87
+ def self.from_json(d)
88
+ new(id: d['id'], ref_id: d['refId'], url: d['url'], binding_message: d['bindingMessage'])
89
+ end
90
+ end
91
+
92
+ class Completed
93
+ attr_reader :id, :ref_id, :ssn, :name, :given_name, :surname,
94
+ :phone, :email, :address, :birth_date, :pid, :bank_id,
95
+ :sign_result, :signed_documents, :authenticated_at, :signed_at
96
+
97
+ def initialize(id:, ssn:, name:, given_name:, surname:, ref_id: nil,
98
+ phone: nil, email: nil, address: nil, birth_date: nil,
99
+ pid: nil, bank_id: nil, sign_result: nil, signed_documents: nil,
100
+ authenticated_at: nil, signed_at: nil)
101
+ @id = id; @ref_id = ref_id; @ssn = ssn; @name = name
102
+ @given_name = given_name; @surname = surname; @phone = phone
103
+ @email = email; @address = address; @birth_date = birth_date
104
+ @pid = pid; @bank_id = bank_id; @sign_result = sign_result
105
+ @signed_documents = signed_documents
106
+ @authenticated_at = authenticated_at; @signed_at = signed_at
107
+ end
108
+
109
+ def deconstruct_keys(_)
110
+ { id: @id, ref_id: @ref_id, ssn: @ssn, name: @name, given_name: @given_name,
111
+ surname: @surname, phone: @phone, email: @email, address: @address,
112
+ birth_date: @birth_date, pid: @pid, bank_id: @bank_id,
113
+ sign_result: @sign_result, signed_documents: @signed_documents,
114
+ authenticated_at: @authenticated_at, signed_at: @signed_at }
115
+ end
116
+
117
+ def self.from_json(d)
118
+ new(
119
+ id: d['id'], ref_id: d['refId'], ssn: d['ssn'], name: d['name'],
120
+ given_name: d['givenName'], surname: d['surname'], phone: d['phone'],
121
+ email: d['email'], address: d['address'], birth_date: d['birthDate'],
122
+ pid: d['pid'], bank_id: d['bankId'],
123
+ sign_result: d['signResult'] ? SignResult.from_json(d['signResult']) : nil,
124
+ signed_documents: d['signedDocuments']&.map { SignedDocument.from_json(_1) },
125
+ authenticated_at: d['authenticatedAt'], signed_at: d['signedAt']
126
+ )
127
+ end
128
+ end
129
+
130
+ class Failed
131
+ attr_reader :id, :ref_id, :error
132
+
133
+ def initialize(id:, error:, ref_id: nil)
134
+ @id = id; @ref_id = ref_id; @error = error
135
+ end
136
+
137
+ def deconstruct_keys(_) = { id: @id, ref_id: @ref_id, error: @error }
138
+ def self.from_json(d) = new(id: d['id'], ref_id: d['refId'], error: d['error'])
139
+ end
140
+
141
+ class Unknown
142
+ attr_reader :status
143
+
144
+ def initialize(status:)
145
+ @status = status
146
+ end
147
+
148
+ def deconstruct_keys(_) = { status: @status }
149
+ end
150
+
151
+ module Status
152
+ def self.from_json(d)
153
+ case d['status']
154
+ when 'PENDING' then Pending.from_json(d)
155
+ when 'COMPLETED' then Completed.from_json(d)
156
+ when 'FAILED' then Failed.from_json(d)
157
+ when String then Unknown.new(status: d['status'])
158
+ else raise IdentisureError.new(0, "Invalid bankid-no status: #{d['status'].inspect}")
159
+ end
160
+ end
161
+ end
162
+
163
+ class Endpoint
164
+ # @api private
165
+ def initialize(transport)
166
+ @transport = transport
167
+ end
168
+
169
+ def auth(req) = Status.from_json(@transport.post('/v3/bankid-no/auth', req.to_h))
170
+ def backchannel_auth(req) = Status.from_json(@transport.post('/v3/bankid-no/backchannel/auth', req.to_h))
171
+ def sign(req) = Status.from_json(@transport.post('/v3/bankid-no/sign', req.to_h))
172
+ def auth_status(id) = Status.from_json(@transport.get("/v3/bankid-no/auth/#{id}"))
173
+ def sign_status(id) = Status.from_json(@transport.get("/v3/bankid-no/sign/#{id}"))
174
+ def cancel_auth(id) = @transport.delete("/v3/bankid-no/auth/#{id}")
175
+ def cancel_sign(id) = @transport.delete("/v3/bankid-no/sign/#{id}")
176
+
177
+ def age_verification(req)
178
+ Identisure::AgeVerification::Status.from_json(@transport.post('/v3/bankid-no/age-verification', req.to_h))
179
+ end
180
+
181
+ def age_verification_status(id)
182
+ Identisure::AgeVerification::Status.from_json(@transport.get("/v3/bankid-no/age-verification/#{id}"))
183
+ end
184
+
185
+ def cancel_age_verification(id) = @transport.delete("/v3/bankid-no/age-verification/#{id}")
186
+
187
+ def wait_for_auth(id, opts: PollOptions.new) = poll(opts) { auth_status(id) }
188
+ def wait_for_sign(id, opts: PollOptions.new) = poll(opts) { sign_status(id) }
189
+
190
+ def wait_for_age_verification(id, opts: PollOptions.new)
191
+ poll_age(opts) { age_verification_status(id) }
192
+ end
193
+
194
+ private
195
+
196
+ def poll(opts)
197
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + opts.timeout
198
+ loop do
199
+ status = yield
200
+ return status unless status.is_a?(Pending)
201
+ raise WaitError.new(timeout: true) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
202
+
203
+ sleep opts.interval
204
+ end
205
+ end
206
+
207
+ def poll_age(opts)
208
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + opts.timeout
209
+ loop do
210
+ status = yield
211
+ return status unless status.is_a?(Identisure::AgeVerification::Pending)
212
+ raise WaitError.new(timeout: true) if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
213
+
214
+ sleep opts.interval
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end