omniauth_openid_connect_test 0.3.6
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/.github/config/rubocop_linter_action.yml +59 -0
- data/.github/stale.yml +17 -0
- data/.github/workflows/rubocop.yml +22 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +58 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +47 -0
- data/Gemfile +4 -0
- data/Guardfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +126 -0
- data/Rakefile +10 -0
- data/lib/omniauth/openid_connect/errors.rb +9 -0
- data/lib/omniauth/openid_connect/version.rb +7 -0
- data/lib/omniauth/openid_connect.rb +5 -0
- data/lib/omniauth/strategies/openid_connect.rb +365 -0
- data/lib/omniauth_openid_connect.rb +3 -0
- data/omniauth_openid_connect_test.gemspec +35 -0
- data/test/fixtures/id_token.txt +1 -0
- data/test/fixtures/jwks.json +8 -0
- data/test/fixtures/test.crt +19 -0
- data/test/lib/omniauth/strategies/openid_connect_test.rb +620 -0
- data/test/strategy_test_case.rb +51 -0
- data/test/test_helper.rb +16 -0
- metadata +258 -0
@@ -0,0 +1,365 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'addressable/uri'
|
4
|
+
require 'timeout'
|
5
|
+
require 'net/http'
|
6
|
+
require 'open-uri'
|
7
|
+
require 'omniauth'
|
8
|
+
require 'openid_connect'
|
9
|
+
require 'forwardable'
|
10
|
+
|
11
|
+
module OmniAuth
|
12
|
+
module Strategies
|
13
|
+
class OpenIDConnect
|
14
|
+
include OmniAuth::Strategy
|
15
|
+
extend Forwardable
|
16
|
+
|
17
|
+
RESPONSE_TYPE_EXCEPTIONS = {
|
18
|
+
'id_token' => { exception_class: OmniAuth::OpenIDConnect::MissingIdTokenError, key: :missing_id_token }.freeze,
|
19
|
+
'code' => { exception_class: OmniAuth::OpenIDConnect::MissingCodeError, key: :missing_code }.freeze,
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def_delegator :request, :params
|
23
|
+
|
24
|
+
option :name, 'openid_connect'
|
25
|
+
option(:client_options, identifier: nil,
|
26
|
+
secret: nil,
|
27
|
+
redirect_uri: nil,
|
28
|
+
scheme: 'https',
|
29
|
+
host: nil,
|
30
|
+
port: 443,
|
31
|
+
authorization_endpoint: '/authorize',
|
32
|
+
token_endpoint: '/token',
|
33
|
+
userinfo_endpoint: '/userinfo',
|
34
|
+
jwks_uri: '/jwk',
|
35
|
+
end_session_endpoint: nil)
|
36
|
+
|
37
|
+
option :issuer
|
38
|
+
option :discovery, false
|
39
|
+
option :client_signing_alg
|
40
|
+
option :client_jwk_signing_key
|
41
|
+
option :client_x509_signing_key
|
42
|
+
option :scope, [:openid]
|
43
|
+
option :response_type, 'code' # ['code', 'id_token']
|
44
|
+
option :state
|
45
|
+
option :response_mode # [:query, :fragment, :form_post, :web_message]
|
46
|
+
option :display, nil # [:page, :popup, :touch, :wap]
|
47
|
+
option :prompt, nil # [:none, :login, :consent, :select_account]
|
48
|
+
option :hd, nil
|
49
|
+
option :max_age
|
50
|
+
option :ui_locales
|
51
|
+
option :id_token_hint
|
52
|
+
option :acr_values
|
53
|
+
option :send_nonce, true
|
54
|
+
option :send_scope_to_token_endpoint, true
|
55
|
+
option :client_auth_method
|
56
|
+
option :post_logout_redirect_uri
|
57
|
+
option :extra_authorize_params, {}
|
58
|
+
option :uid_field, 'sub'
|
59
|
+
|
60
|
+
def uid
|
61
|
+
user_info.raw_attributes[options.uid_field.to_sym] || user_info.sub
|
62
|
+
end
|
63
|
+
|
64
|
+
info do
|
65
|
+
{
|
66
|
+
name: user_info.name,
|
67
|
+
email: user_info.email,
|
68
|
+
nickname: user_info.preferred_username,
|
69
|
+
first_name: user_info.given_name,
|
70
|
+
last_name: user_info.family_name,
|
71
|
+
gender: user_info.gender,
|
72
|
+
image: user_info.picture,
|
73
|
+
phone: user_info.phone_number,
|
74
|
+
urls: { website: user_info.website },
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
extra do
|
79
|
+
{ raw_info: user_info.raw_attributes }
|
80
|
+
end
|
81
|
+
|
82
|
+
credentials do
|
83
|
+
{
|
84
|
+
id_token: access_token.id_token,
|
85
|
+
token: access_token.access_token,
|
86
|
+
refresh_token: access_token.refresh_token,
|
87
|
+
expires_in: access_token.expires_in,
|
88
|
+
scope: access_token.scope,
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def client
|
93
|
+
@client ||= ::OpenIDConnect::Client.new(client_options)
|
94
|
+
end
|
95
|
+
|
96
|
+
def config
|
97
|
+
@config ||= ::OpenIDConnect::Discovery::Provider::Config.discover!(options.issuer)
|
98
|
+
end
|
99
|
+
|
100
|
+
def request_phase
|
101
|
+
options.issuer = issuer if options.issuer.to_s.empty?
|
102
|
+
discover!
|
103
|
+
redirect authorize_uri
|
104
|
+
end
|
105
|
+
|
106
|
+
def callback_phase
|
107
|
+
error = params['error_reason'] || params['error']
|
108
|
+
error_description = params['error_description'] || params['error_reason']
|
109
|
+
invalid_state = params['state'].to_s.empty? || params['state'] != stored_state
|
110
|
+
|
111
|
+
raise CallbackError, error: params['error'], reason: error_description, uri: params['error_uri'] if error
|
112
|
+
raise CallbackError, error: :csrf_detected, reason: "Invalid 'state' parameter" if invalid_state
|
113
|
+
|
114
|
+
return unless valid_response_type?
|
115
|
+
|
116
|
+
options.issuer = issuer if options.issuer.nil? || options.issuer.empty?
|
117
|
+
|
118
|
+
verify_id_token!(params['id_token']) if configured_response_type == 'id_token'
|
119
|
+
discover!
|
120
|
+
client.redirect_uri = redirect_uri
|
121
|
+
|
122
|
+
return id_token_callback_phase if configured_response_type == 'id_token'
|
123
|
+
|
124
|
+
client.authorization_code = authorization_code
|
125
|
+
access_token
|
126
|
+
super
|
127
|
+
rescue CallbackError => e
|
128
|
+
fail!(e.error, e)
|
129
|
+
rescue ::Rack::OAuth2::Client::Error => e
|
130
|
+
fail!(e.response[:error], e)
|
131
|
+
rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
|
132
|
+
fail!(:timeout, e)
|
133
|
+
rescue ::SocketError => e
|
134
|
+
fail!(:failed_to_connect, e)
|
135
|
+
end
|
136
|
+
|
137
|
+
def other_phase
|
138
|
+
if logout_path_pattern.match?(current_path)
|
139
|
+
options.issuer = issuer if options.issuer.to_s.empty?
|
140
|
+
discover!
|
141
|
+
return redirect(end_session_uri) if end_session_uri
|
142
|
+
end
|
143
|
+
call_app!
|
144
|
+
end
|
145
|
+
|
146
|
+
def authorization_code
|
147
|
+
params['code']
|
148
|
+
end
|
149
|
+
|
150
|
+
def end_session_uri
|
151
|
+
return unless end_session_endpoint_is_valid?
|
152
|
+
|
153
|
+
end_session_uri = URI(client_options.end_session_endpoint)
|
154
|
+
end_session_uri.query = encoded_post_logout_redirect_uri
|
155
|
+
end_session_uri.to_s
|
156
|
+
end
|
157
|
+
|
158
|
+
def authorize_uri
|
159
|
+
client.redirect_uri = redirect_uri
|
160
|
+
opts = {
|
161
|
+
response_type: options.response_type,
|
162
|
+
response_mode: options.response_mode,
|
163
|
+
scope: options.scope,
|
164
|
+
state: new_state,
|
165
|
+
login_hint: params['login_hint'],
|
166
|
+
ui_locales: params['ui_locales'],
|
167
|
+
claims_locales: params['claims_locales'],
|
168
|
+
prompt: options.prompt,
|
169
|
+
nonce: (new_nonce if options.send_nonce),
|
170
|
+
hd: options.hd,
|
171
|
+
acr_values: options.acr_values,
|
172
|
+
}
|
173
|
+
|
174
|
+
opts.merge!(options.extra_authorize_params) unless options.extra_authorize_params.empty?
|
175
|
+
|
176
|
+
client.authorization_uri(opts.reject { |_k, v| v.nil? })
|
177
|
+
end
|
178
|
+
|
179
|
+
def public_key
|
180
|
+
return config.jwks if options.discovery
|
181
|
+
|
182
|
+
key_or_secret
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
def issuer
|
188
|
+
resource = "#{ client_options.scheme }://#{ client_options.host }"
|
189
|
+
resource = "#{ resource }:#{ client_options.port }" if client_options.port
|
190
|
+
::OpenIDConnect::Discovery::Provider.discover!(resource).issuer
|
191
|
+
end
|
192
|
+
|
193
|
+
def discover!
|
194
|
+
return unless options.discovery
|
195
|
+
|
196
|
+
client_options.authorization_endpoint = config.authorization_endpoint
|
197
|
+
client_options.token_endpoint = config.token_endpoint
|
198
|
+
client_options.userinfo_endpoint = config.userinfo_endpoint
|
199
|
+
client_options.jwks_uri = config.jwks_uri
|
200
|
+
client_options.end_session_endpoint = config.end_session_endpoint if config.respond_to?(:end_session_endpoint)
|
201
|
+
end
|
202
|
+
|
203
|
+
def user_info
|
204
|
+
return @user_info if @user_info
|
205
|
+
|
206
|
+
if access_token.id_token
|
207
|
+
decoded = decode_id_token(access_token.id_token).raw_attributes
|
208
|
+
|
209
|
+
@user_info = ::OpenIDConnect::ResponseObject::UserInfo.new access_token.userinfo!.raw_attributes.merge(decoded)
|
210
|
+
else
|
211
|
+
@user_info = access_token.userinfo!
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def access_token
|
216
|
+
return @access_token if @access_token
|
217
|
+
|
218
|
+
@access_token = client.access_token!(
|
219
|
+
scope: (options.scope if options.send_scope_to_token_endpoint),
|
220
|
+
client_auth_method: options.client_auth_method
|
221
|
+
)
|
222
|
+
|
223
|
+
verify_id_token!(@access_token.id_token) if configured_response_type == 'code'
|
224
|
+
|
225
|
+
@access_token
|
226
|
+
end
|
227
|
+
|
228
|
+
def decode_id_token(id_token)
|
229
|
+
::OpenIDConnect::ResponseObject::IdToken.decode(id_token, public_key)
|
230
|
+
end
|
231
|
+
|
232
|
+
def client_options
|
233
|
+
options.client_options
|
234
|
+
end
|
235
|
+
|
236
|
+
def new_state
|
237
|
+
state = if options.state.respond_to?(:call)
|
238
|
+
if options.state.arity == 1
|
239
|
+
options.state.call(env)
|
240
|
+
else
|
241
|
+
options.state.call
|
242
|
+
end
|
243
|
+
end
|
244
|
+
session['omniauth.state'] = state || SecureRandom.hex(16)
|
245
|
+
end
|
246
|
+
|
247
|
+
def stored_state
|
248
|
+
session.delete('omniauth.state')
|
249
|
+
end
|
250
|
+
|
251
|
+
def new_nonce
|
252
|
+
session['omniauth.nonce'] = SecureRandom.hex(16)
|
253
|
+
end
|
254
|
+
|
255
|
+
def stored_nonce
|
256
|
+
session.delete('omniauth.nonce')
|
257
|
+
end
|
258
|
+
|
259
|
+
def session
|
260
|
+
return {} if @env.nil?
|
261
|
+
|
262
|
+
super
|
263
|
+
end
|
264
|
+
|
265
|
+
def key_or_secret
|
266
|
+
case options.client_signing_alg
|
267
|
+
when :HS256, :HS384, :HS512
|
268
|
+
client_options.secret
|
269
|
+
when :RS256, :RS384, :RS512
|
270
|
+
if options.client_jwk_signing_key
|
271
|
+
parse_jwk_key(options.client_jwk_signing_key)
|
272
|
+
elsif options.client_x509_signing_key
|
273
|
+
parse_x509_key(options.client_x509_signing_key)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def parse_x509_key(key)
|
279
|
+
OpenSSL::X509::Certificate.new(key).public_key
|
280
|
+
end
|
281
|
+
|
282
|
+
def parse_jwk_key(key)
|
283
|
+
json = JSON.parse(key)
|
284
|
+
return JSON::JWK::Set.new(json['keys']) if json.key?('keys')
|
285
|
+
|
286
|
+
JSON::JWK.new(json)
|
287
|
+
end
|
288
|
+
|
289
|
+
def decode(str)
|
290
|
+
UrlSafeBase64.decode64(str).unpack1('B*').to_i(2).to_s
|
291
|
+
end
|
292
|
+
|
293
|
+
def redirect_uri
|
294
|
+
return client_options.redirect_uri unless params['redirect_uri']
|
295
|
+
|
296
|
+
"#{ client_options.redirect_uri }?redirect_uri=#{ CGI.escape(params['redirect_uri']) }"
|
297
|
+
end
|
298
|
+
|
299
|
+
def encoded_post_logout_redirect_uri
|
300
|
+
return unless options.post_logout_redirect_uri
|
301
|
+
|
302
|
+
URI.encode_www_form(
|
303
|
+
post_logout_redirect_uri: options.post_logout_redirect_uri
|
304
|
+
)
|
305
|
+
end
|
306
|
+
|
307
|
+
def end_session_endpoint_is_valid?
|
308
|
+
client_options.end_session_endpoint &&
|
309
|
+
client_options.end_session_endpoint =~ URI::DEFAULT_PARSER.make_regexp
|
310
|
+
end
|
311
|
+
|
312
|
+
def logout_path_pattern
|
313
|
+
@logout_path_pattern ||= %r{\A#{Regexp.quote(request_path)}(/logout)}
|
314
|
+
end
|
315
|
+
|
316
|
+
def id_token_callback_phase
|
317
|
+
user_data = decode_id_token(params['id_token']).raw_attributes
|
318
|
+
env['omniauth.auth'] = AuthHash.new(
|
319
|
+
provider: name,
|
320
|
+
uid: user_data['sub'],
|
321
|
+
info: { name: user_data['name'], email: user_data['email'] },
|
322
|
+
extra: { raw_info: user_data }
|
323
|
+
)
|
324
|
+
call_app!
|
325
|
+
end
|
326
|
+
|
327
|
+
def valid_response_type?
|
328
|
+
return true if params.key?(configured_response_type)
|
329
|
+
|
330
|
+
error_attrs = RESPONSE_TYPE_EXCEPTIONS[configured_response_type]
|
331
|
+
fail!(error_attrs[:key], error_attrs[:exception_class].new(params['error']))
|
332
|
+
|
333
|
+
false
|
334
|
+
end
|
335
|
+
|
336
|
+
def configured_response_type
|
337
|
+
@configured_response_type ||= options.response_type.to_s
|
338
|
+
end
|
339
|
+
|
340
|
+
def verify_id_token!(id_token)
|
341
|
+
return unless id_token
|
342
|
+
|
343
|
+
decode_id_token(id_token).verify!(issuer: options.issuer,
|
344
|
+
client_id: client_options.identifier,
|
345
|
+
nonce: stored_nonce)
|
346
|
+
end
|
347
|
+
|
348
|
+
class CallbackError < StandardError
|
349
|
+
attr_accessor :error, :error_reason, :error_uri
|
350
|
+
|
351
|
+
def initialize(data)
|
352
|
+
self.error = data[:error]
|
353
|
+
self.error_reason = data[:reason]
|
354
|
+
self.error_uri = data[:uri]
|
355
|
+
end
|
356
|
+
|
357
|
+
def message
|
358
|
+
[error, error_reason, error_uri].compact.join(' | ')
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
OmniAuth.config.add_camelization 'openid_connect', 'OpenIDConnect'
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'omniauth/openid_connect/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'omniauth_openid_connect_test'
|
9
|
+
spec.version = OmniAuth::OpenIDConnect::VERSION
|
10
|
+
spec.authors = ['John Bohn', 'Ilya Shcherbinin']
|
11
|
+
spec.email = ['jjbohn@gmail.com', 'm0n9oose@gmail.com','burak.akca834@gmail.com']
|
12
|
+
spec.summary = 'OpenID Connect Strategy for OmniAuth'
|
13
|
+
spec.description = 'OpenID Connect Strategy for OmniAuth.'
|
14
|
+
spec.homepage = 'https://github.com/burakakca/omniauth_openid_connect'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_dependency 'addressable', '~> 2.5'
|
23
|
+
spec.add_dependency 'omniauth', '~> 2.0'
|
24
|
+
spec.add_dependency 'openid_connect', '~> 1.1'
|
25
|
+
spec.add_development_dependency 'coveralls', '~> 0.8'
|
26
|
+
spec.add_development_dependency 'faker', '~> 1.6'
|
27
|
+
spec.add_development_dependency 'guard', '~> 2.14'
|
28
|
+
spec.add_development_dependency 'guard-bundler', '~> 2.2'
|
29
|
+
spec.add_development_dependency 'guard-minitest', '~> 2.4'
|
30
|
+
spec.add_development_dependency 'minitest', '~> 5.1'
|
31
|
+
spec.add_development_dependency 'mocha', '~> 1.7'
|
32
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
33
|
+
spec.add_development_dependency 'rubocop', '~> 0.63'
|
34
|
+
spec.add_development_dependency 'simplecov', '~> 0.12'
|
35
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg
|
@@ -0,0 +1,8 @@
|
|
1
|
+
{"keys": [{
|
2
|
+
"kty": "RSA",
|
3
|
+
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
|
4
|
+
"e": "AQAB",
|
5
|
+
"alg": "RS256",
|
6
|
+
"kid": "1e9gdk7"
|
7
|
+
}]
|
8
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIDJDCCAgwCCQC57Ob2JfXb+DANBgkqhkiG9w0BAQUFADBUMQswCQYDVQQGEwJK
|
3
|
+
UDEOMAwGA1UECBMFVG9reW8xITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5
|
4
|
+
IEx0ZDESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDgwMTA4NTAxM1oXDTE1MDgw
|
5
|
+
MTA4NTAxM1owVDELMAkGA1UEBhMCSlAxDjAMBgNVBAgTBVRva3lvMSEwHwYDVQQK
|
6
|
+
ExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDCC
|
7
|
+
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN+7czSGHN2087T+oX2kBCY/
|
8
|
+
XN6UOS/mdU2Gn//omZlyxsQXIqvgBLNWeCVt4QdlFUbgPLggfXUelECV/RUOCIIi
|
9
|
+
F2Th4t3x1LviN2XkUiva0DZBnOycqEaJdkyreEuGL1CLVZgZjKmSzNqLl0Yci3D0
|
10
|
+
zgVsXFZSadQebietm4CCmfJYREt9NJxXcrLxVDgat/Xm/KJBsohs3f+cbBT8EXer
|
11
|
+
7+2oZjZoVUgw1hu0alaOvAfE4mxsVwjn3g2mjDqRJLbbuWqgDobjMHah+d4zwJvN
|
12
|
+
ePK8E0hfaz/XBLsJ4e6bQA3M3bANEgSvsicup/qb/0th4gUdc/kj4aJGj0RP7oEC
|
13
|
+
AwEAATANBgkqhkiG9w0BAQUFAAOCAQEADuVec/8u2qJiq6K2W/gSLGYCBZq64OrA
|
14
|
+
s7L2+S82m9/3gAb62wGcDNZjIGFDQubXmO6RhHv7JUT5YZqv9/kRGTJcHDUrwwoN
|
15
|
+
IE99CIPizp7VfnrZ6GsYeszSsw3m+mKTETm+6ELmaSDbYAsrCg4IpGwUF0L88ATv
|
16
|
+
CJ8QzW4X7b9dYVc7UAYyCie2N65GXfesBbRlSwFLuVqIzZfMdNpNijTIUwUqGSME
|
17
|
+
b8IjLYzvekP53CO4wEBRrAVIPNXgftorxIE30OLWua2Qw3y6Pn+Qp5fLe47025S7
|
18
|
+
Lcec18/FbHG0Vbq0qO9cKQw80XyK31N6z556wr2GN2WyixkzVRddXA==
|
19
|
+
-----END CERTIFICATE-----
|