identikey 0.7.1 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7eb968c0e5dd54488d15939d79d81ebccbf7052c93e3da22ff77bad6fc623106
4
- data.tar.gz: d90c163b2fdebaf21e6386a721c8f17b4ec0248dd44394e234c646007842a5fd
3
+ metadata.gz: 7ca7bba53578f15faed142b15d7938712cc93733eb62dce480a139fe42f49a91
4
+ data.tar.gz: b90c1e4451d845d80c2de47d9bf4ad5e6d17318819b2201aa0afca6f0fcc9090
5
5
  SHA512:
6
- metadata.gz: 984d1e2f3b20b1d882568d70e338420062ba54b89b26b5f7c9495fd92ee2f5507db7d1da38c4d45662c32fc478c68ac88622d3b69a8c50bb6a617b01a9761d7b
7
- data.tar.gz: 1c8092afbd35a441141c9830422a26467fc1fd851cbb9eee3109e8e69edea710de97b1ab22041c66869f5a84ea2bc6a29d6e465142b0dba3727f2453cd0831a5
6
+ metadata.gz: b1f3749db40bb5656106906424383f94dd05887587bca7897ba68d2c936e8f5ee7641aba9be8730a19a03b37f80c3df5f4cad757308e37d07320e70e0d8e5f28
7
+ data.tar.gz: 3a4a6ef801813a63153a14c93f8365efd08f2d450fa562a86cc6dc4415a44ede0026168c5a1cc7097de8f3ce48bf8c355084adacbda0d3ab8be89c2ae9adbb16
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Identikey
2
2
 
3
- This library is a thin yet incomplete wrapper of the VASCO Identikey SOAP API.
3
+ This library is a thin yet featureful wrapper of the VASCO Identikey SOAP API.
4
4
 
5
5
  Vasco Identikey has been recently re-branded as OneSpan Authentication Server.
6
6
 
@@ -25,6 +25,49 @@ And then execute:
25
25
 
26
26
  $ bundle
27
27
 
28
+ ## Features
29
+
30
+ This client implements the Authentication, Administration and Provisioning
31
+ SOAP APIs.
32
+
33
+ ### Authentication
34
+
35
+ * `auth_user`: end user authentication with OTP / static password / back-end
36
+
37
+
38
+ ### Administration
39
+
40
+ * `logon` / `logoff`: log on or log off an administrative session.
41
+ You are advised to use a connection pool ([such as
42
+ mperham's](https://github.com/mperham/connection_pool)) to keep multiple
43
+ instances of administration sessions alive. This gem is used in production
44
+ with puma, and has been extensively tested so it is thread-safe.
45
+
46
+ * `alive?`: checks whether an administrative session is alive. You can use
47
+ `.logon` again when `.alive?` returns `false`.
48
+
49
+ * `admin_session_query`: returns active admin sessions
50
+
51
+ * `user_execute`: `view`, `create`, `update`, `delete`, `reset_password`,
52
+ `set_password`, and `unlock` user accounts.
53
+
54
+ * `user_query`: search for users
55
+
56
+ * `digipass_execute`: `view`, `assign`, `unassign` digipasses
57
+
58
+ * `digipass_query`: search for digipasses
59
+
60
+ * `digipassappl_execute`: `test_otp`, `set_pin` on applicable digipasses
61
+
62
+
63
+ ### Provisioning
64
+
65
+ * `provisioning_execute`: `mdl_register`, `dsapp_srp_register`. bonus:
66
+ generation of CRONTO images for online activation, for use with the push
67
+ notification gateways. You can use [this gem](https://github.com/ifad/cronto)
68
+ to generate the PNG to serve to your users.
69
+
70
+
28
71
  ## Configuration
29
72
 
30
73
  By default the client expects WSDL files in the current working directory,
@@ -53,7 +96,14 @@ Identikey::Authentication.configure do
53
96
  end
54
97
 
55
98
  Identikey::Administration.configure do
56
- wsdl './path/to/your/administrtaion.wsdl'
99
+ wsdl './path/to/your/administrtation.wsdl'
100
+ endpoint 'https://your-identikey.example.com:8888'
101
+
102
+ # ... more configuration options as needed ...
103
+ end
104
+
105
+ Identikey::Provisioning.configure do
106
+ wsdl './path/to/your/provisioning.wsdl'
57
107
  endpoint 'https://your-identikey.example.com:8888'
58
108
 
59
109
  # ... more configuration options as needed ...
@@ -17,6 +17,13 @@ end
17
17
 
18
18
  puts "Configured Admin WSDL #{ENV.fetch('IK_WSDL_ADMIN')} against #{ENV.fetch('IK_HOST')}"
19
19
 
20
+ Identikey::Provisioning.configure do
21
+ wsdl ENV.fetch('IK_WSDL_PROVN')
22
+ endpoint ENV.fetch('IK_HOST')
23
+ end
24
+
25
+ puts "Configured Provisioning WSDL #{ENV.fetch('IK_WSDL_PROVN')} against #{ENV.fetch('IK_HOST')}"
26
+
20
27
  $ik = Identikey::Administration::Session.new(
21
28
  username: ENV.fetch('IK_USER'),
22
29
  password: ENV.fetch('IK_PASS'),
@@ -18,11 +18,10 @@ Gem::Specification.new do |spec|
18
18
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
19
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
20
  end
21
- spec.bindir = "exe"
22
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
21
  spec.require_paths = ["lib"]
24
22
 
25
23
  spec.add_dependency "savon", "~> 2.0"
24
+ spec.add_dependency "wasabi", "~> 3.5.0"
26
25
 
27
26
  spec.add_development_dependency "bundler", "~> 2.0"
28
27
  spec.add_development_dependency "rake", ">= 12.3.3"
@@ -5,3 +5,4 @@ require 'identikey/error'
5
5
  require 'identikey/unsigned'
6
6
  require 'identikey/authentication'
7
7
  require 'identikey/administration'
8
+ require 'identikey/provisioning'
@@ -18,6 +18,10 @@ module Identikey
18
18
  :admin_session_query, :user_execute, :user_query,
19
19
  :digipass_execute, :digipass_query, :digipassappl_execute
20
20
 
21
+ ###
22
+ ## LOGON/LOGOFF/PING
23
+ ###
24
+
21
25
  def logon(username:, password:, domain:)
22
26
  resp = super(message: {
23
27
  attributeSet: {
@@ -95,6 +99,10 @@ module Identikey
95
99
  parse_response resp, :admin_session_query_response
96
100
  end
97
101
 
102
+ ###
103
+ ## USER_EXECUTE
104
+ ###
105
+
98
106
  def user_execute(session_id:, cmd:, attributes: [])
99
107
  resp = super(message: {
100
108
  sessionID: session_id,
@@ -180,6 +188,10 @@ module Identikey
180
188
  )
181
189
  end
182
190
 
191
+ ###
192
+ ## USER_QUERY
193
+ ###
194
+
183
195
  # Executes a userQuery command that searches users. By default, it doesn't
184
196
  # log anywhere. To enable logging to a specific destination, pass a logger
185
197
  # as the log: option. To log to the default destination, pass `true` as
@@ -199,6 +211,9 @@ module Identikey
199
211
  end
200
212
  end
201
213
 
214
+ ###
215
+ ## DIGIPASS_EXECUTE
216
+ ###
202
217
 
203
218
  def digipass_execute(session_id:, cmd:, attributes: [])
204
219
  resp = super(message: {
@@ -245,6 +260,9 @@ module Identikey
245
260
  )
246
261
  end
247
262
 
263
+ ###
264
+ ## DIGIPASS_QUERY
265
+ ###
248
266
 
249
267
  def digipass_query(session_id:, attributes:, query_options:)
250
268
  resp = super(message: {
@@ -258,6 +276,9 @@ module Identikey
258
276
  parse_response resp, :digipass_query_response
259
277
  end
260
278
 
279
+ ###
280
+ ## DIGIPASSAPPL_EXECUTE
281
+ ###
261
282
 
262
283
  def digipassappl_execute(session_id:, cmd:, attributes:)
263
284
  resp = super(message: {
@@ -314,6 +335,5 @@ module Identikey
314
335
  client.globals[:log] = old_log
315
336
  end
316
337
 
317
-
318
338
  end
319
339
  end
@@ -6,12 +6,21 @@ module Identikey
6
6
  attr_reader :session_id, :product, :version
7
7
  attr_reader :privileges, :location
8
8
 
9
- def initialize(username:, password:, domain: 'master')
9
+ def initialize(username:, password: nil, apikey: nil, domain: 'master')
10
+ if password.nil? && apikey.nil?
11
+ raise Identikey::UsageError, "Either a password or an API Key is required"
12
+ end
13
+
10
14
  @client = Identikey::Administration.new
11
15
 
12
16
  @username = username
13
17
  @password = password
14
18
  @domain = domain
19
+
20
+ if apikey
21
+ @service_user = true
22
+ @session_id = "Apikey #{username}:#{apikey}"
23
+ end
15
24
  end
16
25
 
17
26
  def endpoint
@@ -23,6 +32,8 @@ module Identikey
23
32
  end
24
33
 
25
34
  def logon
35
+ require_classic_user!
36
+
26
37
  stat, sess, error = @client.logon(username: @username, password: @password, domain: @domain)
27
38
 
28
39
  if stat != 'STAT_SUCCESS'
@@ -42,6 +53,7 @@ module Identikey
42
53
  end
43
54
 
44
55
  def logoff
56
+ require_classic_user!
45
57
  require_logged_on!
46
58
 
47
59
  stat, _, error = @client.logoff session_id: @session_id
@@ -60,6 +72,8 @@ module Identikey
60
72
  end
61
73
 
62
74
  def alive?(log: true)
75
+ require_classic_user!
76
+
63
77
  return false unless logged_on?
64
78
 
65
79
  stat, _ = @client.ping session_id: @session_id, log: log
@@ -108,7 +122,17 @@ module Identikey
108
122
  end
109
123
 
110
124
  def inspect
111
- "#<#{self.class.name} sid=#@session_id username=#@username domain=#@domain product=#@product>"
125
+ descr = if service_user?
126
+ "SERVICE USER"
127
+ else
128
+ "domain=#@domain product=#@product"
129
+ end
130
+
131
+ "#<#{self.class.name} sid=#@session_id username=#@username #{descr}>"
132
+ end
133
+
134
+ def service_user?
135
+ !!@service_user
112
136
  end
113
137
 
114
138
  alias sid session_id
@@ -123,6 +147,12 @@ module Identikey
123
147
  end
124
148
  end
125
149
 
150
+ def require_classic_user!
151
+ if service_user?
152
+ raise Identikey::UsageError, "This command is not supported with Service users"
153
+ end
154
+ end
155
+
126
156
  def parse_privileges(privileges)
127
157
  privileges.split(', ').inject({}) do |h, priv|
128
158
  privilege, status = priv.split(' ')
@@ -161,6 +161,16 @@ module Identikey
161
161
  true
162
162
  end
163
163
 
164
+ def set_local_auth!(value)
165
+ ensure_persisted!
166
+
167
+ self.local_auth = value
168
+
169
+ self.save!
170
+
171
+ self
172
+ end
173
+
164
174
  def unlock!
165
175
  ensure_persisted!
166
176
 
@@ -6,11 +6,13 @@ module Identikey
6
6
 
7
7
  operations :auth_user
8
8
 
9
- def auth_user(user, domain, otp)
9
+ def auth_user(user, domain, otp, client = nil)
10
+ client ||= 'Administration Program'
11
+
10
12
  resp = super(message: {
11
13
  credentialAttributeSet: {
12
14
  attributes: typed_attributes_list_from(
13
- CREDFLD_COMPONENT_TYPE: 'Administration Program',
15
+ CREDFLD_COMPONENT_TYPE: client,
14
16
  CREDFLD_USERID: user,
15
17
  CREDFLD_DOMAIN: domain,
16
18
  CREDFLD_PASSWORD_FORMAT: Unsigned(0),
@@ -22,18 +24,18 @@ module Identikey
22
24
  parse_response resp, :auth_user_response
23
25
  end
24
26
 
25
- def self.valid_otp?(user, domain, otp)
26
- status, result, _ = new.auth_user(user, domain, otp)
27
+ def self.valid_otp?(user, domain, otp, client = nil)
28
+ status, result, _ = new.auth_user(user, domain, otp, client)
27
29
  return otp_validated_ok?(status, result)
28
30
  end
29
31
 
30
- def self.validate!(user, domain, otp)
31
- status, result, error_stack = new.auth_user(user, domain, otp)
32
+ def self.validate!(user, domain, otp, client = nil)
33
+ status, result, error_stack = new.auth_user(user, domain, otp, client)
32
34
 
33
35
  if otp_validated_ok?(status, result)
34
36
  return true
35
37
  else
36
- error_message = result['CREDFLD_STATUS_MESSAGE']
38
+ error_message = result ? result['CREDFLD_STATUS_MESSAGE'] : 'no status returned'
37
39
  raise Identikey::OperationFailed.new("OTP Validation error (#{status}): #{error_message}", error_stack)
38
40
  end
39
41
  end
@@ -52,5 +54,16 @@ module Identikey
52
54
  def self.otp_validated_ok?(status, result)
53
55
  status == 'STAT_SUCCESS' && !result.key?('CREDFLD_STATUS_MESSAGE')
54
56
  end
57
+
58
+
59
+ protected
60
+ def parse_result_root(body, root_element)
61
+ root = super
62
+
63
+ # The authentication API wraps the results with another element
64
+ #
65
+ results_key = root_element.to_s.sub(/_response$/, '_results').to_sym
66
+ return root[results_key]
67
+ end
55
68
  end
56
69
  end
@@ -82,6 +82,7 @@ module Identikey
82
82
 
83
83
  filters: [
84
84
  'sessionID',
85
+ 'staticPassword',
85
86
  'identikey:CREDFLD_PASSWORD',
86
87
  'identikey:CREDFLD_STATIC_PASSWORD',
87
88
  'identikey:CREDFLD_SESSION_ID'
@@ -100,7 +101,7 @@ module Identikey
100
101
 
101
102
  # Parse the generic response types that the API returns.
102
103
  #
103
- # The returned attributes (up to now...) are always:
104
+ # The returned attributes in the Administration API are:
104
105
  #
105
106
  # - The given root element, whose name is derived from the SOAP command
106
107
  # that was invoked
@@ -111,6 +112,11 @@ module Identikey
111
112
  # or multiple ones.
112
113
  # - :error_stack, a list of error that occurred
113
114
  #
115
+ # The authentication API wraps the results element in another one
116
+ #
117
+ # The provisioning API uses a status element for the result code and
118
+ # error stack, and a result element for the results.
119
+ #
114
120
  # The returned value is a three-elements array, containing:
115
121
  #
116
122
  # [Response code, Attribute(s) list, Errors list]
@@ -129,11 +135,19 @@ module Identikey
129
135
  # formats. TODO maybe create a separate class for errors, that includes
130
136
  # the error code.
131
137
  #
132
- # TODO refactor and split in separate methods
133
- #
134
138
  def parse_response(resp, root_element)
135
- body = resp.body
139
+ root = parse_result_root(resp.body, root_element)
140
+
141
+ results = parse_result_element(root, root_element)
142
+
143
+ result_code = parse_result_code(results, root_element)
144
+ result_attributes = parse_result_attributes(results, root_element)
145
+ result_errors = parse_result_errors(results, root_element)
136
146
 
147
+ return result_code, result_attributes, result_errors
148
+ end
149
+
150
+ def parse_result_root(body, root_element)
137
151
  if body.size.zero?
138
152
  raise Identikey::ParseError, "Empty response received"
139
153
  end
@@ -144,40 +158,35 @@ module Identikey
144
158
 
145
159
  # The root results element
146
160
  #
147
- root = body[root_element]
148
-
149
- # ... that the authentication API wraps with another element
150
- #
151
- results_key = root_element.to_s.sub(/_response$/, '_results').to_sym
152
- if root.keys.size == 1 && root.key?(results_key)
153
- root = root[results_key]
154
- end
161
+ return body[root_element]
162
+ end
155
163
 
164
+ def parse_result_element(root, root_element)
156
165
  # The results element
157
166
  #
158
167
  unless root.key?(:results)
159
168
  raise Identikey::ParseError, "Results element not found below #{root_element}"
160
169
  end
161
170
 
162
- results = root[:results]
171
+ return root[:results]
172
+ end
163
173
 
164
- # Result code
165
- #
174
+ def parse_result_code(results, root_element)
166
175
  unless results.key?(:result_codes)
167
176
  raise Identikey::ParseError, "Result codes not found below #{root_element}"
168
177
  end
169
178
 
170
- result_code = results[:result_codes][:status_code_enum] || 'STAT_UNKNOWN'
179
+ results[:result_codes][:status_code_enum] || 'STAT_UNKNOWN'
180
+ end
171
181
 
172
- # Result attributes
173
- #
182
+ def parse_result_attributes(results, root_element)
174
183
  unless results.key?(:result_attribute)
175
184
  raise Identikey::ParseError, "Result attribute not found below #{root_element}"
176
185
  end
177
186
 
178
187
  results_attr = results[:result_attribute]
179
188
 
180
- result_attributes = if results_attr.key?(:attributes)
189
+ if results_attr.key?(:attributes)
181
190
  entries = [ results_attr[:attributes] ].flatten
182
191
  parse_attributes entries
183
192
 
@@ -193,16 +202,14 @@ module Identikey
193
202
  else
194
203
  nil
195
204
  end
205
+ end
196
206
 
197
- # Errors
198
- #
199
- errors = if results[:error_stack].key?(:errors)
207
+ def parse_result_errors(results, root_element)
208
+ if results[:error_stack].key?(:errors)
200
209
  parse_errors results[:error_stack][:errors]
201
210
  else
202
211
  nil
203
212
  end
204
-
205
- return result_code, result_attributes, errors
206
213
  end
207
214
 
208
215
  def parse_attributes(attributes)
@@ -0,0 +1,150 @@
1
+ require 'identikey/base'
2
+
3
+ module Identikey
4
+ # This class wraps the Provisioning API.
5
+ #
6
+ class Provisioning < Base
7
+ client wsdl: './sdk/wsdl/provisioning.wsdl'
8
+
9
+ operations :provisioning_execute, :dsapp_srp_register
10
+
11
+ ###
12
+ ## PROVISIONING_EXECUTE
13
+ ###
14
+
15
+ def provisioning_execute(cmd:, attributes:)
16
+ resp = super(message: {
17
+ cmd: cmd,
18
+ attributeSet: {
19
+ attributes: attributes
20
+ }
21
+ })
22
+
23
+ parse_response resp, :provisioning_execute_response
24
+ end
25
+
26
+ def provisioning_execute_MDL_REGISTER(component:, user:, domain:, password:)
27
+ provisioning_execute(
28
+ cmd: 'PROVISIONCMD_MDL_REGISTER',
29
+ attributes: typed_attributes_list_from(
30
+ PROVFLD_USERID: user,
31
+ PROVFLD_DOMAIN: domain,
32
+ PROVFLD_COMPONENT_TYPE: component,
33
+ PROVFLD_STATIC_PASSWORD: password,
34
+ PROVFLD_ACTIVATION_TYPE: Unsigned(0),
35
+ )
36
+ )
37
+ end
38
+
39
+ ###
40
+ ## dsappSRPRegister
41
+ ###
42
+
43
+ def dsapp_srp_register(component:, user:, domain:, password:)
44
+ resp = super(message: {
45
+ componentType: component,
46
+ user: {
47
+ userID: user,
48
+ domain: domain,
49
+ },
50
+ credential: {
51
+ staticPassword: password
52
+ }
53
+ })
54
+
55
+ parse_response resp, :dsapp_srp_register_response
56
+ end
57
+
58
+ ###
59
+ ## Wraps dsapp_srp_register and returns directly the activation
60
+ ## message through which a CRONTO image can be generated, to be
61
+ ## used for push notifications setups in combination with a MDC
62
+ ## configured on your OneSpan control panel and on Identikey.
63
+ ##
64
+ ## You may want to look into https://github.com/ifad/cronto for
65
+ ## a generator to produce PNGs to deliver to your clients.
66
+ ####
67
+ def self.cronto_code_for_srp_registration(gateway:, **kwargs)
68
+ status, result, error = new.dsapp_srp_register(**kwargs)
69
+
70
+ if status != 'STAT_SUCCESS'
71
+ raise Identikey::OperationFailed, "Error while assigning DAL: #{status} - #{[error].flatten.join('; ')}"
72
+ end
73
+
74
+ # Compose proprietary string
75
+ message = '01;01;%s;%s;%s;%s;%s' % [
76
+ result[:user][:user_id],
77
+ result[:user][:domain],
78
+ result[:registration_id],
79
+ result[:activation_password],
80
+ gateway
81
+ ]
82
+
83
+ # Encode it as hex
84
+ return message.split(//).map {|c| '%x' % c.ord}.join
85
+ end
86
+
87
+ protected
88
+ # The provisioningExecute command has the same
89
+ # design as the rest of the API, with a single
90
+ # multi-purpose `results` element that carries
91
+ # key-value results.
92
+ #
93
+ # Instead, dsappSRPRegister and other commands
94
+ # use a different design with return types and
95
+ # values predefined in the WSDL.
96
+ #
97
+ # So if we have a `results` element, this is a
98
+ # old-style response, and we delegate parsing
99
+ # to the parent class, otherwise if we detect
100
+ # the `result` and `status` elements, we parse
101
+ # in this class, basically just returning the
102
+ # values that Savon parsed for us.
103
+ #
104
+ def parse_result_element(root, root_element)
105
+ if root.key?(:results)
106
+ root[:results]
107
+ else
108
+ root
109
+ end
110
+ end
111
+
112
+ def parse_result_code(root, root_element)
113
+ if root.key?(:status)
114
+ super root[:status], root_element
115
+ else
116
+ super
117
+ end
118
+ end
119
+
120
+ # This may be an old or a new style response.
121
+ #
122
+ # New style responses may have both `result`
123
+ # and `status` elements, or only a `status`
124
+ # element.
125
+ #
126
+ # If there is no `result` but there is a
127
+ # `status`, then consider this a new style
128
+ # response and return an empty result. Else,
129
+ # pass along to the superclass to parse the
130
+ # old style response.
131
+ #
132
+ def parse_result_attributes(root, root_element)
133
+ if root.key?(:result)
134
+ root[:result]
135
+ elsif root.key?(:status)
136
+ nil
137
+ else
138
+ super
139
+ end
140
+ end
141
+
142
+ def parse_result_errors(root, root_element)
143
+ if root.key?(:status)
144
+ super root[:status], root_element
145
+ else
146
+ super
147
+ end
148
+ end
149
+ end
150
+ end
@@ -1,3 +1,3 @@
1
1
  module Identikey
2
- VERSION = "0.7.1"
2
+ VERSION = "0.9.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: identikey
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marcello Barnaba
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-14 00:00:00.000000000 Z
11
+ date: 2020-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: savon
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: wasabi
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.5.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.5.0
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -206,6 +220,7 @@ files:
206
220
  - lib/identikey/authentication.rb
207
221
  - lib/identikey/base.rb
208
222
  - lib/identikey/error.rb
223
+ - lib/identikey/provisioning.rb
209
224
  - lib/identikey/unsigned.rb
210
225
  - lib/identikey/version.rb
211
226
  - log/.keep