warden-jwt_auth 0.8.0 → 0.12.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 +4 -4
- data/.github/workflows/ci.yml +21 -0
- data/.github/workflows/lint.yml +17 -0
- data/CHANGELOG.md +12 -0
- data/README.md +14 -3
- data/lib/warden/jwt_auth/env_helper.rb +17 -7
- data/lib/warden/jwt_auth/header_parser.rb +3 -3
- data/lib/warden/jwt_auth/hooks.rb +1 -2
- data/lib/warden/jwt_auth/payload_user_helper.rb +8 -0
- data/lib/warden/jwt_auth/strategy.rb +25 -1
- data/lib/warden/jwt_auth/token_encoder.rb +2 -1
- data/lib/warden/jwt_auth/version.rb +1 -1
- data/lib/warden/jwt_auth.rb +12 -2
- data/warden-jwt_auth.gemspec +1 -1
- metadata +16 -9
- data/.travis.yml +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8cdd3cda879ac9ddadab8420efc9be8bd619ab77542fb77c4b2f6e12c3712da8
|
|
4
|
+
data.tar.gz: 3d7d716491fc3884503addf8de20efda5a58a005db6eee3b992c9ccde5b4338c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f90a107cc3cd30099f1505a15c93e1c4bb8b30df2bbcd069bea112a424cf2ca1e3a37b46da76a78fb48c6a4dc2e313ad03f3511981839bb141e4ae43199c622a
|
|
7
|
+
data.tar.gz: 2594477d2bef3bf246f9c5da680278ec7f5f1c178bb4f7e431a28cf4feb352e043fa096132050f4211886bb5ec08e27c5ef88aeea5101304e00a59f58e900a53
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
strategy:
|
|
9
|
+
matrix:
|
|
10
|
+
ruby-version: ['3.1', '3.2', '3.3', '3.4', ruby-head]
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
15
|
+
uses: ruby/setup-ruby@v1
|
|
16
|
+
with:
|
|
17
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
18
|
+
bundler-cache: true # 'bundle install' and cache
|
|
19
|
+
- name: Run specs
|
|
20
|
+
run: |
|
|
21
|
+
bundle exec rspec
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Lint
|
|
2
|
+
|
|
3
|
+
on: [push, pull_request]
|
|
4
|
+
|
|
5
|
+
jobs:
|
|
6
|
+
lint:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
11
|
+
uses: ruby/setup-ruby@v1
|
|
12
|
+
with:
|
|
13
|
+
ruby-version: 2.7
|
|
14
|
+
bundler-cache: true # 'bundle install' and cache
|
|
15
|
+
- name: Run specs
|
|
16
|
+
run: |
|
|
17
|
+
bundle exec rubocop
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.12.0] - 2024-09-14
|
|
8
|
+
- Support jwt v3 ([65](https://github.com/waiting-for-dev/warden-jwt_auth/pull/65))
|
|
9
|
+
|
|
10
|
+
## [0.11.0] - 2024-12-20
|
|
11
|
+
- Prevent strategy from running when the current path matches a dispatch request ([60](https://github.com/waiting-for-dev/warden-jwt_auth/pull/60))
|
|
12
|
+
|
|
13
|
+
## [0.10.1] - 2024-12-15
|
|
14
|
+
- Fix version mismatch
|
|
15
|
+
|
|
16
|
+
## [0.8.0] - 2024-06-28
|
|
17
|
+
- Add support for issue claim ([56](https://github.com/waiting-for-dev/warden-jwt_auth/pull/56))
|
|
18
|
+
|
|
7
19
|
## [0.8.0] - 2023-01-31
|
|
8
20
|
- Add support for secret rotation ([49](https://github.com/waiting-for-dev/warden-jwt_auth/pull/49))
|
|
9
21
|
- Support dry-* v1 ([52](https://github.com/waiting-for-dev/warden-jwt_auth/pull/52))
|
data/README.md
CHANGED
|
@@ -145,7 +145,7 @@ config.dispatch_requests = [
|
|
|
145
145
|
|
|
146
146
|
**Important**: You are encouraged to delimit your regular expression with `^` and `$` to avoid unintentional matches.
|
|
147
147
|
|
|
148
|
-
Tokens will be returned in the `Authorization` response header, with format `Bearer #{token}`.
|
|
148
|
+
Tokens will be returned in the `Authorization` response header (configurable via `config.token_header`), with format `Bearer #{token}`.
|
|
149
149
|
|
|
150
150
|
### Requests authentication
|
|
151
151
|
|
|
@@ -175,14 +175,14 @@ config.revocation_strategies = { user: RevocationStrategy }
|
|
|
175
175
|
|
|
176
176
|
The implementation of the revocation strategy is also on your side. They just need to implement two methods: `jwt_revoked?` and `revoke_jwt`, both of them accepting as parameters the JWT payload and the user record, in this order.
|
|
177
177
|
|
|
178
|
-
You can read about which [JWT recovation strategies](http://waiting-for-dev.github.io/blog/2017/01/24/jwt_revocation_strategies
|
|
178
|
+
You can read about which [JWT recovation strategies](http://waiting-for-dev.github.io/blog/2017/01/24/jwt_revocation_strategies) can be implement with their pros and cons.
|
|
179
179
|
|
|
180
180
|
```ruby
|
|
181
181
|
module RevocationStrategy
|
|
182
182
|
def self.jwt_revoked?(payload, user)
|
|
183
183
|
# Does something to check whether the JWT token is revoked for given user
|
|
184
184
|
end
|
|
185
|
-
|
|
185
|
+
|
|
186
186
|
def self.revoke_jwt(payload, user)
|
|
187
187
|
# Does something to revoke the JWT token for given user
|
|
188
188
|
end
|
|
@@ -208,6 +208,17 @@ end
|
|
|
208
208
|
|
|
209
209
|
You can remove the `rotation_secret` when you are condifent that large enough user base has the fetched the token encrypted with the new secret.
|
|
210
210
|
|
|
211
|
+
### Multiple issuers
|
|
212
|
+
|
|
213
|
+
When your application handles JWT tokens from multiple sources (e.g. webhooks authenticated via provider JTW tokens) you can configure this gem to use the [issuer claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1) to only handle tokens it has issued.
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
Warden::JWTAuth.configure do |config|
|
|
217
|
+
config.secret = ENV['WARDEN_JWT_SECRET_KEY']
|
|
218
|
+
config.issuer = 'http://my-application.com'
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
211
222
|
## Development
|
|
212
223
|
|
|
213
224
|
There are docker and docker-compose files configured to create a development environment for this gem. So, if you use Docker you only need to run:
|
|
@@ -25,16 +25,17 @@ module Warden
|
|
|
25
25
|
env['REQUEST_METHOD']
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Returns
|
|
28
|
+
# Returns header configured through `token_header` option
|
|
29
29
|
#
|
|
30
30
|
# @param env [Hash] Rack env
|
|
31
31
|
# @return [String]
|
|
32
32
|
def self.authorization_header(env)
|
|
33
|
-
|
|
33
|
+
header_env_name = env_name(JWTAuth.config.token_header)
|
|
34
|
+
env[header_env_name]
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
# Returns a copy of `env` with value added to the
|
|
37
|
-
#
|
|
37
|
+
# Returns a copy of `env` with value added to the environment variable
|
|
38
|
+
# configured through `token_header` option
|
|
38
39
|
#
|
|
39
40
|
# Be aware than `env` is not modified in place and still an updated copy
|
|
40
41
|
# is returned.
|
|
@@ -44,7 +45,8 @@ module Warden
|
|
|
44
45
|
# @return [Hash] modified rack env
|
|
45
46
|
def self.set_authorization_header(env, value)
|
|
46
47
|
env = env.dup
|
|
47
|
-
|
|
48
|
+
header_env_name = env_name(JWTAuth.config.token_header)
|
|
49
|
+
env[header_env_name] = value
|
|
48
50
|
env
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -53,8 +55,16 @@ module Warden
|
|
|
53
55
|
# @param env [Hash] Rack env
|
|
54
56
|
# @return [String]
|
|
55
57
|
def self.aud_header(env)
|
|
56
|
-
|
|
57
|
-
env[
|
|
58
|
+
header_env_name = env_name(JWTAuth.config.aud_header)
|
|
59
|
+
env[header_env_name]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the ENV name for a given header
|
|
63
|
+
#
|
|
64
|
+
# @param header [String] Header name
|
|
65
|
+
# @return [String]
|
|
66
|
+
def self.env_name(header)
|
|
67
|
+
('HTTP_' + header.upcase).tr('-', '_')
|
|
58
68
|
end
|
|
59
69
|
end
|
|
60
70
|
end
|
|
@@ -21,8 +21,8 @@ module Warden
|
|
|
21
21
|
method == METHOD ? token : nil
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# Returns a copy of `env` with token added to the
|
|
25
|
-
#
|
|
24
|
+
# Returns a copy of `env` with token added to the header configured through
|
|
25
|
+
# `token_header` option. Be aware than `env` is not modified in place.
|
|
26
26
|
#
|
|
27
27
|
# @param env [Hash] rack env hash
|
|
28
28
|
# @param token [String] JWT token
|
|
@@ -39,7 +39,7 @@ module Warden
|
|
|
39
39
|
# @return [Hash] response headers with the token added
|
|
40
40
|
def self.to_headers(headers, token)
|
|
41
41
|
headers = headers.dup
|
|
42
|
-
headers[
|
|
42
|
+
headers[JWTAuth.config.token_header] = "#{METHOD} #{token}"
|
|
43
43
|
headers
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -47,8 +47,7 @@ module Warden
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def request_matches?(path_info, method)
|
|
50
|
-
dispatch_requests.each do |
|
|
51
|
-
dispatch_method, dispatch_path = tuple
|
|
50
|
+
dispatch_requests.each do |(dispatch_method, dispatch_path)|
|
|
52
51
|
return true if path_info.match(dispatch_path) &&
|
|
53
52
|
method == dispatch_method
|
|
54
53
|
end
|
|
@@ -29,6 +29,14 @@ module Warden
|
|
|
29
29
|
payload['aud'] == aud
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# Returns whether given issuer matches with the one encoded in the payload
|
|
33
|
+
# @param payload [Hash] JWT payload
|
|
34
|
+
# @param issuer [String] The issuer to match
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def self.issuer_matches?(payload, issuer)
|
|
37
|
+
payload['iss'] == issuer.to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
32
40
|
# Returns the payload to encode for a given user in a scope
|
|
33
41
|
#
|
|
34
42
|
# @param user [Interfaces::User] an user, whatever it is
|
|
@@ -7,8 +7,10 @@ module Warden
|
|
|
7
7
|
# Warden strategy to authenticate an user through a JWT token in the
|
|
8
8
|
# `Authorization` request header
|
|
9
9
|
class Strategy < Warden::Strategies::Base
|
|
10
|
+
include JWTAuth::Import['dispatch_requests']
|
|
11
|
+
|
|
10
12
|
def valid?
|
|
11
|
-
!
|
|
13
|
+
token_exists? && issuer_claim_valid? && !path_is_dispatch_request_path?
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def store?
|
|
@@ -25,6 +27,28 @@ module Warden
|
|
|
25
27
|
|
|
26
28
|
private
|
|
27
29
|
|
|
30
|
+
def path_is_dispatch_request_path?
|
|
31
|
+
current_path = EnvHelper.path_info(env)
|
|
32
|
+
request_method = EnvHelper.request_method(env)
|
|
33
|
+
dispatch_requests.any? do |(dispatch_method, dispatch_path)|
|
|
34
|
+
request_method == dispatch_method && current_path.match(dispatch_path)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def issuer_claim_valid?
|
|
39
|
+
configured_issuer = Warden::JWTAuth.config.issuer
|
|
40
|
+
return true if configured_issuer.nil?
|
|
41
|
+
|
|
42
|
+
payload = TokenDecoder.new.call(token)
|
|
43
|
+
PayloadUserHelper.issuer_matches?(payload, configured_issuer)
|
|
44
|
+
rescue JWT::DecodeError
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def token_exists?
|
|
49
|
+
!token.nil?
|
|
50
|
+
end
|
|
51
|
+
|
|
28
52
|
def token
|
|
29
53
|
@token ||= HeaderParser.from_env(env)
|
|
30
54
|
end
|
|
@@ -7,7 +7,7 @@ module Warden
|
|
|
7
7
|
# Encodes a payload into a JWT token, adding some configurable
|
|
8
8
|
# claims
|
|
9
9
|
class TokenEncoder
|
|
10
|
-
include JWTAuth::Import['secret', 'algorithm', 'expiration_time']
|
|
10
|
+
include JWTAuth::Import['secret', 'algorithm', 'expiration_time', 'issuer']
|
|
11
11
|
|
|
12
12
|
# Encodes a payload into a JWT
|
|
13
13
|
#
|
|
@@ -24,6 +24,7 @@ module Warden
|
|
|
24
24
|
now = Time.now.to_i
|
|
25
25
|
payload['iat'] ||= now
|
|
26
26
|
payload['exp'] ||= now + expiration_time
|
|
27
|
+
payload['iss'] ||= issuer if issuer
|
|
27
28
|
payload['jti'] ||= SecureRandom.uuid
|
|
28
29
|
payload
|
|
29
30
|
end
|
data/lib/warden/jwt_auth.rb
CHANGED
|
@@ -19,6 +19,8 @@ module Warden
|
|
|
19
19
|
module JWTAuth
|
|
20
20
|
extend Dry::Configurable
|
|
21
21
|
|
|
22
|
+
module_function
|
|
23
|
+
|
|
22
24
|
def symbolize_keys(hash)
|
|
23
25
|
hash.transform_keys(&:to_sym)
|
|
24
26
|
end
|
|
@@ -36,8 +38,6 @@ module Warden
|
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
module_function :constantize_values, :symbolize_keys, :upcase_first_items
|
|
40
|
-
|
|
41
41
|
# The secret used to encode the token
|
|
42
42
|
setting :secret
|
|
43
43
|
|
|
@@ -53,6 +53,16 @@ module Warden
|
|
|
53
53
|
# Expiration time for tokens
|
|
54
54
|
setting :expiration_time, default: 3600
|
|
55
55
|
|
|
56
|
+
# Request header that will be used for receiving and returning the token.
|
|
57
|
+
setting :token_header, default: 'Authorization'
|
|
58
|
+
|
|
59
|
+
# The issuer claims associated with the tokens
|
|
60
|
+
#
|
|
61
|
+
# Will be used to only apply the warden strategy when the issuer matches.
|
|
62
|
+
# This allows for multiple token issuers being used.
|
|
63
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
|
|
64
|
+
setting :issuer, default: nil
|
|
65
|
+
|
|
56
66
|
# Request header which value will be encoded as `aud` claim in JWT. If
|
|
57
67
|
# the header is not present `aud` will be `nil`.
|
|
58
68
|
setting :aud_header, default: 'JWT_AUD'
|
data/warden-jwt_auth.gemspec
CHANGED
|
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
|
|
|
24
24
|
|
|
25
25
|
spec.add_dependency 'dry-auto_inject', '>= 0.8', '< 2'
|
|
26
26
|
spec.add_dependency 'dry-configurable', '>= 0.13', '< 2'
|
|
27
|
-
spec.add_dependency 'jwt', '
|
|
27
|
+
spec.add_dependency 'jwt', '>= 2.1', '< 4'
|
|
28
28
|
spec.add_dependency 'warden', '~> 1.2'
|
|
29
29
|
|
|
30
30
|
spec.add_development_dependency 'bundler'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: warden-jwt_auth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marc Busqué
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2025-09-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-auto_inject
|
|
@@ -54,16 +54,22 @@ dependencies:
|
|
|
54
54
|
name: jwt
|
|
55
55
|
requirement: !ruby/object:Gem::Requirement
|
|
56
56
|
requirements:
|
|
57
|
-
- - "
|
|
57
|
+
- - ">="
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
59
|
version: '2.1'
|
|
60
|
+
- - "<"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '4'
|
|
60
63
|
type: :runtime
|
|
61
64
|
prerelease: false
|
|
62
65
|
version_requirements: !ruby/object:Gem::Requirement
|
|
63
66
|
requirements:
|
|
64
|
-
- - "
|
|
67
|
+
- - ">="
|
|
65
68
|
- !ruby/object:Gem::Version
|
|
66
69
|
version: '2.1'
|
|
70
|
+
- - "<"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '4'
|
|
67
73
|
- !ruby/object:Gem::Dependency
|
|
68
74
|
name: warden
|
|
69
75
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -214,10 +220,11 @@ extra_rdoc_files: []
|
|
|
214
220
|
files:
|
|
215
221
|
- ".codeclimate.yml"
|
|
216
222
|
- ".github/FUNDING.yml"
|
|
223
|
+
- ".github/workflows/ci.yml"
|
|
224
|
+
- ".github/workflows/lint.yml"
|
|
217
225
|
- ".gitignore"
|
|
218
226
|
- ".rspec"
|
|
219
227
|
- ".rubocop.yml"
|
|
220
|
-
- ".travis.yml"
|
|
221
228
|
- CHANGELOG.md
|
|
222
229
|
- CODE_OF_CONDUCT.md
|
|
223
230
|
- Dockerfile
|
|
@@ -252,7 +259,7 @@ licenses:
|
|
|
252
259
|
- MIT
|
|
253
260
|
metadata:
|
|
254
261
|
rubygems_mfa_required: 'true'
|
|
255
|
-
post_install_message:
|
|
262
|
+
post_install_message:
|
|
256
263
|
rdoc_options: []
|
|
257
264
|
require_paths:
|
|
258
265
|
- lib
|
|
@@ -267,8 +274,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
267
274
|
- !ruby/object:Gem::Version
|
|
268
275
|
version: '0'
|
|
269
276
|
requirements: []
|
|
270
|
-
rubygems_version: 3.
|
|
271
|
-
signing_key:
|
|
277
|
+
rubygems_version: 3.5.9
|
|
278
|
+
signing_key:
|
|
272
279
|
specification_version: 4
|
|
273
280
|
summary: JWT authentication for Warden.
|
|
274
281
|
test_files: []
|
data/.travis.yml
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
language: ruby
|
|
2
|
-
cache: bundler
|
|
3
|
-
rvm:
|
|
4
|
-
- 2.6
|
|
5
|
-
- 2.7
|
|
6
|
-
- 3.0
|
|
7
|
-
- ruby-head
|
|
8
|
-
before_install:
|
|
9
|
-
- gem update --system --no-doc
|
|
10
|
-
- gem install bundler
|
|
11
|
-
script:
|
|
12
|
-
- bundle exec rspec
|
|
13
|
-
- bundle exec rubocop
|
|
14
|
-
- bundle exec codeclimate-test-reporter
|
|
15
|
-
jobs:
|
|
16
|
-
allow_failures:
|
|
17
|
-
- rvm: ruby-head
|
|
18
|
-
addons:
|
|
19
|
-
code_climate:
|
|
20
|
-
repo_token:
|
|
21
|
-
secure: neJ5LVLV6vgeCnerSQjUpLuQDvxEH87iW8swCSWl2hTtPcD/GuwYSeSXnhH72HVHi/9basHHhaYPcE2YeBwBCr39PhiHMNwS5GGGk/RGjKpU/Gt1KXV8KXTbNGT4v+ZMM3cdsdDfe8OnzGguNVsdxseHa3KE2pyuvo2a0swXwKa7BU9VB/3ZoZvvfI3Xr9im4eklWam5yCwVR0FOF7epzmNTKMXcUga2BOc9PV5aVELzLILLCHCJSCupe5Rx8mfcsRoRmZXKduF8Ke3eq8eULvLEo4EGfC107najOqrKt7x8uDVIsuGrP4LUQ4ainmNEb2jIvpjuqAxpusMjhpjCINF1Tn0OXK93OXAp4QKeIYoYEqKtzRxX0TWFNWHB8ombF9HTMF2DmloDZyFRiI40JSMImU0hc4MDxRgiTW5MDWGbohDaJ+9VV6+rIqtlEfLhgj1grFBAroaJce9BB7RQEmfsZPzhC2VXwGxHw/YkJgzBNGq1/9E1DoTY9RPSNTQfSRodhI3XW8LSQSHTBeXZvymVcjeOyYgjzJYviLHR8QS4cXpUALtlFXyaMkPHUBLUn8XsBa5Azfh5y3qPMGiJq1/qaHA4mKj5ls+ngFbzOq82sYGAKgQHj/ZDb+FZMQQanp4jyWADKcpXcmINb9jEQwkU0bjpuhUYtghASxH1Kl8=
|