warden-cognito 0.1.0 → 0.3.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/.rubocop.yml +1 -0
- data/.travis.yml +13 -2
- data/CHANGELOG.md +28 -0
- data/README.md +90 -8
- data/docker-compose.yml +4 -0
- data/lib/warden/cognito.rb +37 -1
- data/lib/warden/cognito/authenticatable_strategy.rb +20 -11
- data/lib/warden/cognito/cognito_client.rb +31 -16
- data/lib/warden/cognito/has_user_pool_identifier.rb +34 -0
- data/lib/warden/cognito/jwk_loader.rb +34 -0
- data/lib/warden/cognito/local_user_mapper.rb +34 -0
- data/lib/warden/cognito/pool_related_iterator.rb +15 -0
- data/lib/warden/cognito/test_helpers.rb +39 -0
- data/lib/warden/cognito/token_authenticatable_strategy.rb +12 -47
- data/lib/warden/cognito/token_decoder.rb +60 -0
- data/lib/warden/cognito/user_helper.rb +7 -13
- data/lib/warden/cognito/user_not_found_callback.rb +17 -0
- data/lib/warden/cognito/version.rb +1 -1
- data/warden-cognito.gemspec +3 -2
- metadata +28 -7
- data/.ruby-version +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67b38f626935f1b428eade990cec7e0aa79be73de6ecdf12f747a0cf9fca8b9d
|
4
|
+
data.tar.gz: f9e1ae36d5bc9508e02e57c479848d195ad5d47baa01b597f819479dd285d954
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 185dc40763cc5a39964b9f088cb122597abc78bef86b6937a8a630bb5813bb3cfbaa7bc7a7e32732d8daff9892f1033e30a408b8bf2121376b16b220f6c4afae
|
7
|
+
data.tar.gz: 49917c80e5c75c2a29fc62597e697438e8e21bffe15f53ec4c79ec701f50dc57de8e836f87b16cf39ff5fb6390f0d4a10016167ca7140622d5a97b42b6b336f1
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
@@ -3,5 +3,16 @@ sudo: false
|
|
3
3
|
language: ruby
|
4
4
|
cache: bundler
|
5
5
|
rvm:
|
6
|
-
- 2.6
|
7
|
-
|
6
|
+
- 2.6
|
7
|
+
- 2.7
|
8
|
+
- ruby-head
|
9
|
+
before_install:
|
10
|
+
- gem update --system --no-doc
|
11
|
+
- gem install bundler
|
12
|
+
script:
|
13
|
+
- bundle exec rspec
|
14
|
+
- bundle exec rubocop
|
15
|
+
jobs:
|
16
|
+
allow_failures:
|
17
|
+
- rvm: ruby-head
|
18
|
+
|
data/CHANGELOG.md
CHANGED
@@ -5,4 +5,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
7
|
## [Unreleased]
|
8
|
+
|
9
|
+
## [0.3.0]
|
10
|
+
- **Breaking Changes**: Configuration explicitly moved to `user_pools` object
|
11
|
+
|
12
|
+
## [0.2.3]
|
13
|
+
- Require the HTTP dependency
|
14
|
+
|
15
|
+
## [0.2.2]
|
16
|
+
- Fix missing HTTP dependency
|
17
|
+
|
18
|
+
## [0.2.1]
|
19
|
+
- Fix rspec dependency in implementation
|
20
|
+
|
21
|
+
## [0.2.0]
|
22
|
+
- Extended exposed API
|
23
|
+
- TestHelpers utils
|
24
|
+
- Add Travis setup
|
25
|
+
|
26
|
+
## [0.1.0]
|
27
|
+
|
8
28
|
- Scratching the gem
|
29
|
+
|
30
|
+
[Unreleased]: https://github.com/barkibu/warden-cognito/compare/v0.3.0...HEAD
|
31
|
+
[0.3.0]: https://github.com/barkibu/warden-cognito/compare/v0.2.3...v0.3.0
|
32
|
+
[0.2.3]: https://github.com/barkibu/warden-cognito/compare/v0.2.2...v0.2.3
|
33
|
+
[0.2.2]: https://github.com/barkibu/warden-cognito/compare/v0.2.1...v0.2.2
|
34
|
+
[0.2.1]: https://github.com/barkibu/warden-cognito/compare/v0.2.0...v0.2.1
|
35
|
+
[0.2.0]: https://github.com/barkibu/warden-cognito/compare/v0.1.0...v0.2.0
|
36
|
+
[0.1.0]: https://github.com/barkibu/warden-cognito/releases/tag/v0.1.0
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Warden::Cognito
|
2
2
|
|
3
|
-
[](https://travis-ci.com/barkibu/warden-cognito)
|
4
4
|
|
5
5
|
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/warden/cognito`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
6
|
|
@@ -24,31 +24,113 @@ Or install it yourself as:
|
|
24
24
|
|
25
25
|
## Usage
|
26
26
|
|
27
|
-
|
28
|
-
Add to your initializers the following:
|
27
|
+
Configure how the gem maps Cognito users to local ones adding to your initializers the following:
|
29
28
|
```ruby
|
30
29
|
Warden::Cognito.configure do |config|
|
31
30
|
config.user_repository = User
|
32
31
|
config.identifying_attribute = 'sub'
|
33
|
-
config.after_local_user_not_found = ->(cognito_user) { User.create(username: cognito_user.username) }
|
32
|
+
config.after_local_user_not_found = ->(cognito_user, pool_identifier) { User.create(username: cognito_user.username) }
|
34
33
|
config.cache = ActiveSupport::Cache::MemCacheStore.new
|
34
|
+
config.user_pools = { default: {region: 'AWS_REGION', pool_id: 'AWS Cognito UserPool Id', client_id: 'AWS Cognito Client Id'} }
|
35
35
|
end
|
36
36
|
```
|
37
37
|
|
38
|
+
### With Devise
|
39
|
+
|
40
|
+
You can know protects endpoints by settings the available strategies in the Warden section of your Device's configuration:
|
41
|
+
```ruby
|
42
|
+
# config/initializers/devise.rb
|
43
|
+
#
|
44
|
+
# /***/
|
45
|
+
config.warden do |manager|
|
46
|
+
manager.default_strategies(scope: :user).unshift :cognito_auth
|
47
|
+
manager.default_strategies(scope: :user).unshift :cognito_jwt
|
48
|
+
# /***/
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
38
52
|
### User Repository
|
39
53
|
|
40
54
|
The user repository will be used to look for an entity to mark as authenticated, it must implement the following:
|
41
|
-
- `find_by_cognito_username` that should return the user identified by the given username or nil
|
42
|
-
- `find_by_cognito_attribute` that should return the user identified by the given Cognito User attribute (`config.identifying_attribute`) or nil
|
55
|
+
- `find_by_cognito_username` that should return the user identified by the given username or nil (receives as second param the pool_identifier)
|
56
|
+
- `find_by_cognito_attribute` that should return the user identified by the given Cognito User attribute (`config.identifying_attribute`) or nil (receives as second param the pool_identifier)
|
57
|
+
|
58
|
+
### User Model
|
59
|
+
|
60
|
+
The user model must expose a message `cognito_id` that returns the `identifying_attribute` for the given user.
|
43
61
|
|
44
62
|
### `after_local_user_not_found` Callback
|
45
63
|
|
46
|
-
A callback triggered whenever the user correctly authenticated on Cognito but no local user exists (receives the full [cognito user](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CognitoIdentityProvider/Types/GetUserResponse.html))
|
64
|
+
A callback triggered whenever the user correctly authenticated on Cognito but no local user exists (receives the full [cognito user](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CognitoIdentityProvider/Types/GetUserResponse.html), and the pool_identifier as second parameter).
|
47
65
|
|
48
66
|
### Cache
|
49
67
|
The cache used to store the AWS Json Web Keys as well as the mapping between local and remote identifiers.
|
50
68
|
Defaults to `ActiveSupport::Cache::NullStore`
|
51
69
|
|
70
|
+
### Testing
|
71
|
+
|
72
|
+
The TestHelpers module is here to help testing code using this gem to validate tokens and authenticate users:
|
73
|
+
|
74
|
+
Create a module and make sure it is loaded as part of the support files of your rspec configuration:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
module Helpers
|
78
|
+
module JWT
|
79
|
+
def self.included(base)
|
80
|
+
base.class_eval do
|
81
|
+
Warden::Cognito::TestHelpers.setup
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def auth_headers_for_user(user, headers = {})
|
86
|
+
Warden::Cognito::TestHelpers.auth_headers(headers, user)
|
87
|
+
end
|
88
|
+
|
89
|
+
def jwt_for_user(user)
|
90
|
+
auth_headers_for_user(user)[:Authorization].split[1]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
Include this module in the relevant test types:
|
97
|
+
```ruby
|
98
|
+
RSpec.configure do |config|
|
99
|
+
# /***/
|
100
|
+
config.include Helpers::JWT, type: :request
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
You can now generate tokens for your users in your tests, for instance:
|
105
|
+
```ruby
|
106
|
+
let(:user) { create(:user) } # Your users needs to be available through the UserRepository you defined
|
107
|
+
let(:headers) { auth_headers_for_user(user) }
|
108
|
+
let(:token) { jwt_for_user(user) }
|
109
|
+
```
|
110
|
+
|
111
|
+
### API
|
112
|
+
|
113
|
+
This gem also exposes classes that you can use to validate tokens and/or fetch a user from a given token:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
token = 'The token a user passed along in a request'
|
117
|
+
token_decoder = TokenDecoder.new(token, nil) # Pass nil as pool_identifier to loop over all the configured pools and automatically bind the right one [Based on the issuer]
|
118
|
+
|
119
|
+
# Is the token valid ?
|
120
|
+
token_decoder.validate!
|
121
|
+
|
122
|
+
# What's in this token ?
|
123
|
+
token_decoder.decoded_token
|
124
|
+
|
125
|
+
# What's the phone_number attribute of the user identified by this token ?
|
126
|
+
token_decoder.user_attribute('phone_number')
|
127
|
+
|
128
|
+
# Who is the local user associated with this token
|
129
|
+
user = LocalUserMapper.find(token_decoder)
|
130
|
+
# or
|
131
|
+
user = LocalUserMapper.find_by_token(token)
|
132
|
+
```
|
133
|
+
|
52
134
|
## Development
|
53
135
|
|
54
136
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -67,7 +149,7 @@ An then, for example:
|
|
67
149
|
|
68
150
|
## Contributing
|
69
151
|
|
70
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
152
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/barkibu/warden-cognito.
|
71
153
|
|
72
154
|
## License
|
73
155
|
|
data/docker-compose.yml
CHANGED
data/lib/warden/cognito.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'http'
|
1
2
|
require 'jwt'
|
2
3
|
require 'warden'
|
3
4
|
require 'dry/configurable'
|
@@ -8,19 +9,54 @@ require 'active_support/core_ext'
|
|
8
9
|
|
9
10
|
module Warden
|
10
11
|
module Cognito
|
12
|
+
class CognitoError < StandardError; end
|
13
|
+
|
11
14
|
extend Dry::Configurable
|
12
15
|
|
16
|
+
def jwk_config_keys
|
17
|
+
%i[key issuer]
|
18
|
+
end
|
19
|
+
|
20
|
+
def jwk_instance(value)
|
21
|
+
attributes = value&.symbolize_keys&.slice(*jwk_config_keys) || {}
|
22
|
+
Struct.new(*jwk_config_keys, keyword_init: true).new(attributes)
|
23
|
+
end
|
24
|
+
|
25
|
+
def user_pool_configuration_keys
|
26
|
+
%i[identifier region pool_id client_id]
|
27
|
+
end
|
28
|
+
|
29
|
+
def user_pool_configurations(value)
|
30
|
+
value.map do |key, conf|
|
31
|
+
attributes = conf.symbolize_keys.slice(*user_pool_configuration_keys).merge(identifier: key)
|
32
|
+
Struct.new(*user_pool_configuration_keys, keyword_init: true).new(attributes)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module_function :jwk_config_keys, :jwk_instance, :user_pool_configuration_keys, :user_pool_configurations
|
37
|
+
|
13
38
|
setting :user_repository
|
14
|
-
setting
|
39
|
+
setting(:identifying_attribute, 'sub', &:to_s)
|
15
40
|
setting :after_local_user_not_found
|
16
41
|
setting :cache, ActiveSupport::Cache::NullStore.new
|
17
42
|
|
43
|
+
setting(:jwk, nil) { |value| jwk_instance(value) }
|
44
|
+
|
45
|
+
setting(:user_pools, []) { |value| user_pool_configurations(value) }
|
46
|
+
|
18
47
|
Import = Dry::AutoInject(config)
|
19
48
|
end
|
20
49
|
end
|
21
50
|
|
51
|
+
require 'warden/cognito/pool_related_iterator'
|
52
|
+
require 'warden/cognito/has_user_pool_identifier'
|
53
|
+
require 'warden/cognito/jwk_loader'
|
22
54
|
require 'warden/cognito/version'
|
55
|
+
require 'warden/cognito/user_not_found_callback'
|
56
|
+
require 'warden/cognito/local_user_mapper'
|
23
57
|
require 'warden/cognito/authenticatable_strategy'
|
24
58
|
require 'warden/cognito/token_authenticatable_strategy'
|
59
|
+
require 'warden/cognito/token_decoder'
|
25
60
|
require 'warden/cognito/user_helper'
|
26
61
|
require 'warden/cognito/cognito_client'
|
62
|
+
require 'warden/cognito/test_helpers'
|
@@ -4,11 +4,12 @@ require 'aws-sdk-cognitoidentityprovider'
|
|
4
4
|
module Warden
|
5
5
|
module Cognito
|
6
6
|
class AuthenticatableStrategy < Warden::Strategies::Base
|
7
|
-
attr_reader :helper
|
7
|
+
attr_reader :helper, :user_not_found_callback
|
8
8
|
|
9
9
|
def initialize(env, scope = nil)
|
10
10
|
super
|
11
|
-
@
|
11
|
+
@user_not_found_callback = UserNotFoundCallback.new
|
12
|
+
@helper = UserHelper.new
|
12
13
|
end
|
13
14
|
|
14
15
|
def valid?
|
@@ -16,11 +17,11 @@ module Warden
|
|
16
17
|
end
|
17
18
|
|
18
19
|
def authenticate!
|
19
|
-
|
20
|
+
attempt = cognito_client.initiate_auth(email, password)
|
20
21
|
|
21
|
-
return fail(:unknow_cognito_response) unless
|
22
|
+
return fail(:unknow_cognito_response) unless attempt
|
22
23
|
|
23
|
-
user = local_user ||
|
24
|
+
user = local_user || trigger_callback(attempt.authentication_result)
|
24
25
|
|
25
26
|
fail!(:unknown_user) unless user.present?
|
26
27
|
success!(user)
|
@@ -32,13 +33,17 @@ module Warden
|
|
32
33
|
|
33
34
|
private
|
34
35
|
|
35
|
-
def
|
36
|
-
|
36
|
+
def cognito_client
|
37
|
+
CognitoClient.scope pool_identifier
|
38
|
+
end
|
39
|
+
|
40
|
+
def trigger_callback(authentication_result)
|
41
|
+
cognito_user = cognito_client.fetch(authentication_result.access_token)
|
42
|
+
user_not_found_callback.call(cognito_user, cognito_client.pool_identifier)
|
37
43
|
end
|
38
44
|
|
39
|
-
def
|
40
|
-
|
41
|
-
Cognito.config.after_local_user_not_found&.call(user_response)
|
45
|
+
def local_user
|
46
|
+
helper.find_by_cognito_username(email, cognito_client.pool_identifier)
|
42
47
|
end
|
43
48
|
|
44
49
|
def cognito_authenticable?
|
@@ -53,8 +58,12 @@ module Warden
|
|
53
58
|
auth_params[:password]
|
54
59
|
end
|
55
60
|
|
61
|
+
def pool_identifier
|
62
|
+
auth_params[:pool_identifier]&.to_sym
|
63
|
+
end
|
64
|
+
|
56
65
|
def auth_params
|
57
|
-
params[scope.to_s].symbolize_keys.slice(:password, :email)
|
66
|
+
params[scope.to_s].symbolize_keys.slice(:password, :email, :pool_identifier)
|
58
67
|
end
|
59
68
|
end
|
60
69
|
end
|
@@ -1,27 +1,42 @@
|
|
1
1
|
module Warden
|
2
2
|
module Cognito
|
3
3
|
class CognitoClient
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
include Cognito::Import['user_pools']
|
5
|
+
include HasUserPoolIdentifier
|
6
|
+
|
7
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CognitoIdentityProvider/Types/GetUserResponse.html
|
8
|
+
def fetch(access_token)
|
9
|
+
client.get_user(access_token: access_token)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initiate_auth(email, password)
|
13
|
+
client.initiate_auth(
|
14
|
+
client_id: user_pool.client_id,
|
15
|
+
auth_flow: 'USER_PASSWORD_AUTH',
|
16
|
+
auth_parameters: {
|
17
|
+
'USERNAME' => email,
|
18
|
+
'PASSWORD' => password
|
19
|
+
}
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
9
24
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
25
|
+
def client
|
26
|
+
Aws::CognitoIdentityProvider::Client.new
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
def scope(pool_identifier)
|
31
|
+
new.tap do |client|
|
32
|
+
client.user_pool = pool_identifier || default_pool_identifier
|
33
|
+
end
|
19
34
|
end
|
20
35
|
|
21
36
|
private
|
22
37
|
|
23
|
-
def
|
24
|
-
|
38
|
+
def default_pool_identifier
|
39
|
+
Warden::Cognito.config.user_pools.first.identifier
|
25
40
|
end
|
26
41
|
end
|
27
42
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Warden
|
2
|
+
module Cognito
|
3
|
+
module HasUserPoolIdentifier
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
base.class_eval do
|
7
|
+
attr_reader :user_pool
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def user_pool=(pool_identifier)
|
12
|
+
@user_pool = user_pools.detect(self.class.invalid_issuer_error) { |pool| pool.identifier == pool_identifier }
|
13
|
+
end
|
14
|
+
|
15
|
+
def pool_identifier
|
16
|
+
user_pool.identifier
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
def pool_iterator
|
21
|
+
PoolRelatedIterator.new do |pool|
|
22
|
+
new.tap do |pool_related|
|
23
|
+
pool_related.user_pool = pool.identifier
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def invalid_issuer_error
|
29
|
+
-> { raise ::JWT::InvalidIssuerError, 'The token was not generated by any of the configured User Pools' }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Warden
|
2
|
+
module Cognito
|
3
|
+
class JwkLoader
|
4
|
+
include Cognito::Import['cache', 'jwk', 'user_pools']
|
5
|
+
include HasUserPoolIdentifier
|
6
|
+
|
7
|
+
def jwt_issuer
|
8
|
+
jwk.issuer || "https://cognito-idp.#{user_pool.region}.amazonaws.com/#{user_pool.pool_id}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def issued?(token)
|
12
|
+
::JWT.decode(token, nil, false).first['iss'] == jwt_issuer
|
13
|
+
rescue StandardError
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(options)
|
18
|
+
return { keys: [jwk.key.export] } if jwk.key.present?
|
19
|
+
|
20
|
+
cache.delete(jwk_url) if options[:invalidate]
|
21
|
+
|
22
|
+
cache.fetch(jwk_url, expires_in: 1.hour) do
|
23
|
+
JSON.parse(HTTP.get(jwk_url).body.to_s).deep_symbolize_keys
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def jwk_url
|
30
|
+
"#{jwt_issuer}/.well-known/jwks.json"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Warden
|
2
|
+
module Cognito
|
3
|
+
class LocalUserMapper
|
4
|
+
include Cognito::Import['cache', 'identifying_attribute']
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def find(token_decoder)
|
8
|
+
new.call(token_decoder)
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_by_token(token)
|
12
|
+
find(TokenDecoder.new(token))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(token_decoder)
|
17
|
+
helper.find_by_cognito_attribute local_identifier(token_decoder), token_decoder.pool_identifier
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def local_identifier(token_decoder)
|
23
|
+
cache_key = "COGNITO_POOL_#{token_decoder.pool_identifier}LOCAL_IDENTIFIER_#{token_decoder.sub}"
|
24
|
+
cache.fetch(cache_key, skip_nil: true) do
|
25
|
+
token_decoder.user_attribute(identifying_attribute)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def helper
|
30
|
+
UserHelper.new
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class PoolRelatedIterator
|
2
|
+
include Enumerable
|
3
|
+
|
4
|
+
attr_reader :factory
|
5
|
+
|
6
|
+
def initialize(&factory)
|
7
|
+
@factory = factory
|
8
|
+
end
|
9
|
+
|
10
|
+
def each(&block)
|
11
|
+
Warden::Cognito.config.user_pools.each do |pool|
|
12
|
+
block.call factory.call(pool)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Warden
|
2
|
+
module Cognito
|
3
|
+
class TestHelpers
|
4
|
+
class EnvironmentError < StandardError; end
|
5
|
+
|
6
|
+
@jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_reader :jwk
|
10
|
+
|
11
|
+
def setup
|
12
|
+
Warden::Cognito.config.jwk = { key: jwk, issuer: local_issuer }
|
13
|
+
end
|
14
|
+
|
15
|
+
def auth_headers(headers, user)
|
16
|
+
headers.merge(Authorization: "Bearer #{generate_token(user)}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def local_issuer
|
20
|
+
'local_issuer'
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def generate_token(user)
|
26
|
+
payload = { sub: user.object_id,
|
27
|
+
"#{identifying_attribute}": user.cognito_id,
|
28
|
+
iss: local_issuer }
|
29
|
+
headers = { kid: jwk.kid }
|
30
|
+
JWT.encode(payload, jwk.keypair, 'RS256', headers)
|
31
|
+
end
|
32
|
+
|
33
|
+
def identifying_attribute
|
34
|
+
Warden::Cognito.config.identifying_attribute
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -6,22 +6,15 @@ module Warden
|
|
6
6
|
class TokenAuthenticatableStrategy < Warden::Strategies::Base
|
7
7
|
METHOD = 'Bearer'.freeze
|
8
8
|
|
9
|
-
attr_reader :helper
|
9
|
+
attr_reader :helper
|
10
10
|
|
11
11
|
def initialize(env, scope = nil)
|
12
12
|
super
|
13
|
-
@
|
14
|
-
@helper = UserHelper
|
15
|
-
end
|
16
|
-
|
17
|
-
def jwks
|
18
|
-
config.cache.fetch(jwk_url, expires_in: 1.hour) do
|
19
|
-
JSON.parse(HTTP.get(jwk_url).body.to_s).deep_symbolize_keys
|
20
|
-
end
|
13
|
+
@helper = UserHelper.new
|
21
14
|
end
|
22
15
|
|
23
16
|
def valid?
|
24
|
-
|
17
|
+
token_decoder.validate!
|
25
18
|
rescue ::JWT::ExpiredSignature
|
26
19
|
true
|
27
20
|
rescue StandardError
|
@@ -29,7 +22,8 @@ module Warden
|
|
29
22
|
end
|
30
23
|
|
31
24
|
def authenticate!
|
32
|
-
user = local_user ||
|
25
|
+
user = local_user || UserNotFoundCallback.call(cognito_user, token_decoder.pool_identifier)
|
26
|
+
|
33
27
|
fail!(:unknown_user) unless user.present?
|
34
28
|
success!(user)
|
35
29
|
rescue ::JWT::ExpiredSignature
|
@@ -40,49 +34,20 @@ module Warden
|
|
40
34
|
|
41
35
|
private
|
42
36
|
|
43
|
-
def jwt_issuer
|
44
|
-
"https://cognito-idp.#{ENV['AWS_REGION']}.amazonaws.com/#{ENV['AWS_COGNITO_USER_POOL_ID']}"
|
45
|
-
end
|
46
|
-
|
47
|
-
def jwk_url
|
48
|
-
"#{jwt_issuer}/.well-known/jwks.json"
|
49
|
-
end
|
50
|
-
|
51
|
-
def local_user
|
52
|
-
helper.find_by_cognito_attribute(local_identifier)
|
53
|
-
end
|
54
|
-
|
55
|
-
def cognito_user_cache_key
|
56
|
-
"COGNITO_LOCAL_IDENTIFIER_#{cognito_user_identifier}"
|
57
|
-
end
|
58
|
-
|
59
|
-
def cognito_user_identifier
|
60
|
-
decoded_token.first['sub']
|
61
|
-
end
|
62
|
-
|
63
|
-
def local_identifier
|
64
|
-
config.cache.fetch(cognito_user_cache_key, skip_nil: true) do
|
65
|
-
user_attribute identifying_attribute
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
37
|
def cognito_user
|
70
|
-
|
38
|
+
token_decoder.cognito_user
|
71
39
|
end
|
72
40
|
|
73
|
-
def
|
74
|
-
|
75
|
-
attribute.name == attribute_name
|
76
|
-
end&.value
|
41
|
+
def local_user
|
42
|
+
LocalUserMapper.find token_decoder
|
77
43
|
end
|
78
44
|
|
79
|
-
def
|
80
|
-
|
45
|
+
def token_decoder
|
46
|
+
@token_decoder ||= TokenDecoder.new(token, pool_identifier)
|
81
47
|
end
|
82
48
|
|
83
|
-
def
|
84
|
-
|
85
|
-
algorithms: ['RS256'], jwks: jwks)
|
49
|
+
def pool_identifier
|
50
|
+
env['HTTP_X_AUTHORIZATION_POOL_IDENTIFIER']
|
86
51
|
end
|
87
52
|
|
88
53
|
def token
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Warden
|
2
|
+
module Cognito
|
3
|
+
class TokenDecoder
|
4
|
+
attr_reader :jwk_loader, :token
|
5
|
+
|
6
|
+
def initialize(token, pool_identifier = nil)
|
7
|
+
@token = token
|
8
|
+
@jwk_loader = find_loader(pool_identifier)
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate!
|
12
|
+
decoded_token.present?
|
13
|
+
end
|
14
|
+
|
15
|
+
def sub
|
16
|
+
decoded_token.first['sub']
|
17
|
+
end
|
18
|
+
|
19
|
+
def decoded_token
|
20
|
+
@decoded_token ||= ::JWT.decode(token, nil, true, iss: jwk_loader.jwt_issuer, verify_iss: true,
|
21
|
+
algorithms: ['RS256'], jwks: jwk_loader)
|
22
|
+
end
|
23
|
+
|
24
|
+
def cognito_user
|
25
|
+
@cognito_user ||= CognitoClient.scope(pool_identifier).fetch(token)
|
26
|
+
end
|
27
|
+
|
28
|
+
def user_attribute(attribute_name)
|
29
|
+
token_attribute(attribute_name).presence || cognito_user_attribute(attribute_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def pool_identifier
|
33
|
+
jwk_loader.pool_identifier
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def token_attribute(attribute_name)
|
39
|
+
decoded_token.first[attribute_name] if decoded_token.first.key? attribute_name
|
40
|
+
end
|
41
|
+
|
42
|
+
def cognito_user_attribute(attribute_name)
|
43
|
+
cognito_user.user_attributes.detect do |attribute|
|
44
|
+
attribute.name == attribute_name
|
45
|
+
end&.value
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_loader(pool_identifier)
|
49
|
+
if pool_identifier.present?
|
50
|
+
return JwkLoader.new.tap do |loader|
|
51
|
+
loader.user_pool = pool_identifier
|
52
|
+
end
|
53
|
+
end
|
54
|
+
JwkLoader.pool_iterator.detect(JwkLoader.invalid_issuer_error) do |loader|
|
55
|
+
loader.issued? token
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -1,20 +1,14 @@
|
|
1
1
|
module Warden
|
2
2
|
module Cognito
|
3
|
-
|
4
|
-
|
5
|
-
def find_by_cognito_username(username)
|
6
|
-
user_repository.find_by_cognito_username(username)
|
7
|
-
end
|
3
|
+
class UserHelper
|
4
|
+
include Cognito::Import['user_repository']
|
8
5
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
private
|
6
|
+
def find_by_cognito_username(username, pool_identifier)
|
7
|
+
user_repository.find_by_cognito_username(username, pool_identifier)
|
8
|
+
end
|
14
9
|
|
15
|
-
|
16
|
-
|
17
|
-
end
|
10
|
+
def find_by_cognito_attribute(arg, pool_identifier)
|
11
|
+
user_repository.find_by_cognito_attribute(arg, pool_identifier)
|
18
12
|
end
|
19
13
|
end
|
20
14
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Warden
|
2
|
+
module Cognito
|
3
|
+
class UserNotFoundCallback
|
4
|
+
include Cognito::Import['after_local_user_not_found']
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def call(cognito_user, pool_identifier)
|
8
|
+
new.call(cognito_user, pool_identifier)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(cognito_user, pool_identifier)
|
13
|
+
after_local_user_not_found&.call(cognito_user, pool_identifier)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/warden-cognito.gemspec
CHANGED
@@ -5,7 +5,7 @@ require 'warden/cognito/version'
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = 'warden-cognito'
|
7
7
|
spec.version = Warden::Cognito::VERSION
|
8
|
-
spec.authors = ['Juan F. Pérez']
|
8
|
+
spec.authors = ['Juan F. Pérez', 'Léo Figea']
|
9
9
|
spec.email = ['761794+jguitar@users.noreply.github.com']
|
10
10
|
|
11
11
|
spec.summary = 'Amazon Cognito authentication for Warden'
|
@@ -36,10 +36,11 @@ Gem::Specification.new do |spec|
|
|
36
36
|
spec.add_dependency 'aws-sdk-cognitoidentityprovider', '~> 1.47'
|
37
37
|
spec.add_dependency 'dry-auto_inject', '~> 0.6'
|
38
38
|
spec.add_dependency 'dry-configurable', '~> 0.9'
|
39
|
+
spec.add_dependency 'http'
|
39
40
|
spec.add_dependency 'jwt', '~> 2.1'
|
40
41
|
spec.add_dependency 'warden', '~> 1.2'
|
41
42
|
|
42
|
-
spec.add_development_dependency 'bundler'
|
43
|
+
spec.add_development_dependency 'bundler'
|
43
44
|
spec.add_development_dependency 'pry-byebug', '~> 3.7'
|
44
45
|
spec.add_development_dependency 'rack-test', '~> 1.1'
|
45
46
|
spec.add_development_dependency 'rake', '>= 12.3.3'
|
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: warden-cognito
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Juan F. Pérez
|
8
|
+
- Léo Figea
|
8
9
|
autorequire:
|
9
10
|
bindir: exe
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2021-01-20 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: activesupport
|
@@ -66,6 +67,20 @@ dependencies:
|
|
66
67
|
- - "~>"
|
67
68
|
- !ruby/object:Gem::Version
|
68
69
|
version: '0.9'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: http
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
69
84
|
- !ruby/object:Gem::Dependency
|
70
85
|
name: jwt
|
71
86
|
requirement: !ruby/object:Gem::Requirement
|
@@ -98,16 +113,16 @@ dependencies:
|
|
98
113
|
name: bundler
|
99
114
|
requirement: !ruby/object:Gem::Requirement
|
100
115
|
requirements:
|
101
|
-
- - "
|
116
|
+
- - ">="
|
102
117
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
118
|
+
version: '0'
|
104
119
|
type: :development
|
105
120
|
prerelease: false
|
106
121
|
version_requirements: !ruby/object:Gem::Requirement
|
107
122
|
requirements:
|
108
|
-
- - "
|
123
|
+
- - ">="
|
109
124
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
125
|
+
version: '0'
|
111
126
|
- !ruby/object:Gem::Dependency
|
112
127
|
name: pry-byebug
|
113
128
|
requirement: !ruby/object:Gem::Requirement
|
@@ -189,7 +204,6 @@ files:
|
|
189
204
|
- ".gitignore"
|
190
205
|
- ".rspec"
|
191
206
|
- ".rubocop.yml"
|
192
|
-
- ".ruby-version"
|
193
207
|
- ".travis.yml"
|
194
208
|
- CHANGELOG.md
|
195
209
|
- Dockerfile
|
@@ -203,8 +217,15 @@ files:
|
|
203
217
|
- lib/warden/cognito.rb
|
204
218
|
- lib/warden/cognito/authenticatable_strategy.rb
|
205
219
|
- lib/warden/cognito/cognito_client.rb
|
220
|
+
- lib/warden/cognito/has_user_pool_identifier.rb
|
221
|
+
- lib/warden/cognito/jwk_loader.rb
|
222
|
+
- lib/warden/cognito/local_user_mapper.rb
|
223
|
+
- lib/warden/cognito/pool_related_iterator.rb
|
224
|
+
- lib/warden/cognito/test_helpers.rb
|
206
225
|
- lib/warden/cognito/token_authenticatable_strategy.rb
|
226
|
+
- lib/warden/cognito/token_decoder.rb
|
207
227
|
- lib/warden/cognito/user_helper.rb
|
228
|
+
- lib/warden/cognito/user_not_found_callback.rb
|
208
229
|
- lib/warden/cognito/version.rb
|
209
230
|
- warden-cognito.gemspec
|
210
231
|
homepage: https://github.com/barkibu/warden-cognito
|
data/.ruby-version
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
2.6.5
|