warden-cognito 0.2.2 → 0.3.3

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: 5d5790601adacfa6e03722383c258c265230fc7da5e807d20b4dbee07e778445
4
- data.tar.gz: 53e85cbb949c46fff67c737b3f19aaad7a8e8106ac63bd5900dad8788e73c66d
3
+ metadata.gz: 40fa2362bb3ef3268cbecf7b60d3570ed4883e431c1705e8f9c90f8478eaac56
4
+ data.tar.gz: f29115a0f11806c184f78070b468b2e5841f8ab609ff780d1fa94a87ef3d8c16
5
5
  SHA512:
6
- metadata.gz: 9fe02b4d86f39633610658649173db4526091433b8ef609132d4103333555a024cd2d1557609296e598ce49f2c94a89124abf20ca4b1545ee3e28d61f7631649
7
- data.tar.gz: e9ef51005f5e83b8a4ecca6387f6cef0c8b095286e2a4c880438b5e7747438d9f8539584dc92e37099580697f6ee5f254c8766ece27eaf78a8c38d39a5901f34
6
+ metadata.gz: 10d0c0098ef31565f797f73c210a2634d02f641fcff5736bc28a4a44bd749e68edc165c2fbc691f86454d1fa17a8ff21caddcafdf02e17f1dd47b5d9ed05f83c
7
+ data.tar.gz: d3405f6873a37d95d119f88150c6e5e4c3ff03afe440acef16daafcd78368562e770df42e2ed64350ead50854965eae8e8c93ca3b6836ba4c756d6601ab6f175
data/.rubocop.yml CHANGED
@@ -4,6 +4,8 @@ AllCops:
4
4
  - 'db/**/*'
5
5
  - 'vendor/**/*'
6
6
 
7
+ TargetRubyVersion: 2.6
8
+
7
9
  Style/FrozenStringLiteralComment:
8
10
  Enabled: false
9
11
 
data/CHANGELOG.md CHANGED
@@ -6,8 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.3]
10
+ - Improve test helpers to include `jti` and `exp` claims and accept user-supplied claims.
11
+
12
+ ## [0.3.2]
13
+ - Fix - specify region on scoped aws client
14
+
15
+ ## [0.3.1]
16
+ - Allow selection of `user_pool` when generating a jwt through the test helper
17
+
18
+ ## [0.3.0]
19
+ - **Breaking Changes**: Configuration explicitly moved to `user_pools` object
20
+
21
+ ## [0.2.3]
22
+ - Require the HTTP dependency
23
+
9
24
  ## [0.2.2]
10
- - Fix missin HTTP dependency
25
+ - Fix missing HTTP dependency
11
26
 
12
27
  ## [0.2.1]
13
28
  - Fix rspec dependency in implementation
@@ -21,7 +36,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
36
 
22
37
  - Scratching the gem
23
38
 
24
- [Unreleased]: https://github.com/barkibu/warden-cognito/compare/v0.2.2...HEAD
39
+ [Unreleased]: https://github.com/barkibu/warden-cognito/compare/v0.3.3...HEAD
40
+ [0.3.3]: https://github.com/barkibu/warden-cognito/compare/v0.3.2...v0.3.3
41
+ [0.3.2]: https://github.com/barkibu/warden-cognito/compare/v0.3.1...v0.3.2
42
+ [0.3.1]: https://github.com/barkibu/warden-cognito/compare/v0.3.0...v0.3.1
43
+ [0.3.0]: https://github.com/barkibu/warden-cognito/compare/v0.2.3...v0.3.0
44
+ [0.2.3]: https://github.com/barkibu/warden-cognito/compare/v0.2.2...v0.2.3
25
45
  [0.2.2]: https://github.com/barkibu/warden-cognito/compare/v0.2.1...v0.2.2
26
46
  [0.2.1]: https://github.com/barkibu/warden-cognito/compare/v0.2.0...v0.2.1
27
47
  [0.2.0]: https://github.com/barkibu/warden-cognito/compare/v0.1.0...v0.2.0
data/README.md CHANGED
@@ -29,16 +29,12 @@ Configure how the gem maps Cognito users to local ones adding to your initialize
29
29
  Warden::Cognito.configure do |config|
30
30
  config.user_repository = User
31
31
  config.identifying_attribute = 'sub'
32
- 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) }
33
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'} }
34
35
  end
35
36
  ```
36
37
 
37
- This gem will look for the following the env variables:
38
- - **AWS_REGION**
39
- - **AWS_COGNITO_USER_POOL_ID**
40
- - **AWS_COGNITO_CLIENT_ID**
41
-
42
38
  ### With Devise
43
39
 
44
40
  You can know protects endpoints by settings the available strategies in the Warden section of your Device's configuration:
@@ -56,8 +52,8 @@ You can know protects endpoints by settings the available strategies in the Ward
56
52
  ### User Repository
57
53
 
58
54
  The user repository will be used to look for an entity to mark as authenticated, it must implement the following:
59
- - `find_by_cognito_username` that should return the user identified by the given username or nil
60
- - `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)
61
57
 
62
58
  ### User Model
63
59
 
@@ -65,7 +61,7 @@ The user model must expose a message `cognito_id` that returns the `identifying_
65
61
 
66
62
  ### `after_local_user_not_found` Callback
67
63
 
68
- 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).
69
65
 
70
66
  ### Cache
71
67
  The cache used to store the AWS Json Web Keys as well as the mapping between local and remote identifiers.
@@ -86,12 +82,12 @@ module Helpers
86
82
  end
87
83
  end
88
84
 
89
- def auth_headers_for_user(user, headers = {})
90
- Warden::Cognito::TestHelpers.auth_headers(headers, user)
85
+ def auth_headers_for_user(user, pool_identifier, headers = {})
86
+ Warden::Cognito::TestHelpers.auth_headers(headers, user, pool_identifier)
91
87
  end
92
88
 
93
- def jwt_for_user(user)
94
- auth_headers_for_user(user)[:Authorization].split[1]
89
+ def jwt_for_user(user, pool_identifier)
90
+ auth_headers_for_user(user, pool_identifier)[:Authorization].split[1]
95
91
  end
96
92
  end
97
93
  end
@@ -118,7 +114,7 @@ This gem also exposes classes that you can use to validate tokens and/or fetch a
118
114
 
119
115
  ```ruby
120
116
  token = 'The token a user passed along in a request'
121
- token_decoder = TokenDecoder.new(token)
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]
122
118
 
123
119
  # Is the token valid ?
124
120
  token_decoder.validate!
data/docker-compose.yml CHANGED
@@ -5,5 +5,8 @@ services:
5
5
  command: tail -f Gemfile
6
6
  volumes:
7
7
  - .:/app
8
+ - bundle:/usr/local/bundle
8
9
  - ~/.gitconfig:/root/.gitconfig
9
10
 
11
+ volumes:
12
+ bundle:
@@ -1,3 +1,4 @@
1
+ require 'http'
1
2
  require 'jwt'
2
3
  require 'warden'
3
4
  require 'dry/configurable'
@@ -8,6 +9,8 @@ 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
 
13
16
  def jwk_config_keys
@@ -19,7 +22,18 @@ module Warden
19
22
  Struct.new(*jwk_config_keys, keyword_init: true).new(attributes)
20
23
  end
21
24
 
22
- module_function :jwk_config_keys, :jwk_instance
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
23
37
 
24
38
  setting :user_repository
25
39
  setting(:identifying_attribute, 'sub', &:to_s)
@@ -28,10 +42,14 @@ module Warden
28
42
 
29
43
  setting(:jwk, nil) { |value| jwk_instance(value) }
30
44
 
45
+ setting(:user_pools, []) { |value| user_pool_configurations(value) }
46
+
31
47
  Import = Dry::AutoInject(config)
32
48
  end
33
49
  end
34
50
 
51
+ require 'warden/cognito/pool_related_iterator'
52
+ require 'warden/cognito/has_user_pool_identifier'
35
53
  require 'warden/cognito/jwk_loader'
36
54
  require 'warden/cognito/version'
37
55
  require 'warden/cognito/user_not_found_callback'
@@ -17,7 +17,7 @@ module Warden
17
17
  end
18
18
 
19
19
  def authenticate!
20
- attempt = CognitoClient.initiate_auth(email, password)
20
+ attempt = cognito_client.initiate_auth(email, password)
21
21
 
22
22
  return fail(:unknow_cognito_response) unless attempt
23
23
 
@@ -33,13 +33,17 @@ module Warden
33
33
 
34
34
  private
35
35
 
36
+ def cognito_client
37
+ CognitoClient.scope pool_identifier
38
+ end
39
+
36
40
  def trigger_callback(authentication_result)
37
- cognito_user = CognitoClient.fetch(authentication_result.access_token)
38
- user_not_found_callback.call(cognito_user)
41
+ cognito_user = cognito_client.fetch(authentication_result.access_token)
42
+ user_not_found_callback.call(cognito_user, cognito_client.pool_identifier)
39
43
  end
40
44
 
41
45
  def local_user
42
- helper.find_by_cognito_username(email)
46
+ helper.find_by_cognito_username(email, cognito_client.pool_identifier)
43
47
  end
44
48
 
45
49
  def cognito_authenticable?
@@ -54,8 +58,12 @@ module Warden
54
58
  auth_params[:password]
55
59
  end
56
60
 
61
+ def pool_identifier
62
+ auth_params[:pool_identifier]&.to_sym
63
+ end
64
+
57
65
  def auth_params
58
- params[scope.to_s].symbolize_keys.slice(:password, :email)
66
+ params[scope.to_s].symbolize_keys.slice(:password, :email, :pool_identifier)
59
67
  end
60
68
  end
61
69
  end
@@ -1,27 +1,42 @@
1
1
  module Warden
2
2
  module Cognito
3
3
  class CognitoClient
4
- class << self
5
- # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CognitoIdentityProvider/Types/GetUserResponse.html
6
- def fetch(access_token)
7
- client.get_user(access_token: access_token)
8
- end
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
- def initiate_auth(email, password)
11
- client.initiate_auth(
12
- client_id: ENV['AWS_COGNITO_CLIENT_ID'],
13
- auth_flow: 'USER_PASSWORD_AUTH',
14
- auth_parameters: {
15
- 'USERNAME' => email,
16
- 'PASSWORD' => password
17
- }
18
- )
25
+ def client
26
+ Aws::CognitoIdentityProvider::Client.new region: user_pool.region
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 client
24
- Aws::CognitoIdentityProvider::Client.new
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
@@ -1,11 +1,19 @@
1
1
  module Warden
2
2
  module Cognito
3
3
  class JwkLoader
4
- include Cognito::Import['cache']
5
- include Cognito::Import['jwk']
4
+ include Cognito::Import['cache', 'jwk', 'user_pools']
5
+ include HasUserPoolIdentifier
6
6
 
7
7
  def jwt_issuer
8
- jwk.issuer || "https://cognito-idp.#{ENV['AWS_REGION']}.amazonaws.com/#{ENV['AWS_COGNITO_USER_POOL_ID']}"
8
+ return "#{user_pool.identifier}-#{jwk.issuer}" if jwk.issuer.present?
9
+
10
+ "https://cognito-idp.#{user_pool.region}.amazonaws.com/#{user_pool.pool_id}"
11
+ end
12
+
13
+ def issued?(token)
14
+ ::JWT.decode(token, nil, false).first['iss'] == jwt_issuer
15
+ rescue StandardError
16
+ false
9
17
  end
10
18
 
11
19
  def call(options)
@@ -14,13 +14,13 @@ module Warden
14
14
  end
15
15
 
16
16
  def call(token_decoder)
17
- helper.find_by_cognito_attribute local_identifier(token_decoder)
17
+ helper.find_by_cognito_attribute local_identifier(token_decoder), token_decoder.pool_identifier
18
18
  end
19
19
 
20
20
  private
21
21
 
22
22
  def local_identifier(token_decoder)
23
- cache_key = "COGNITO_LOCAL_IDENTIFIER_#{token_decoder.sub}"
23
+ cache_key = "COGNITO_POOL_#{token_decoder.pool_identifier}LOCAL_IDENTIFIER_#{token_decoder.sub}"
24
24
  cache.fetch(cache_key, skip_nil: true) do
25
25
  token_decoder.user_attribute(identifying_attribute)
26
26
  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
@@ -12,8 +12,9 @@ module Warden
12
12
  Warden::Cognito.config.jwk = { key: jwk, issuer: local_issuer }
13
13
  end
14
14
 
15
- def auth_headers(headers, user)
16
- headers.merge(Authorization: "Bearer #{generate_token(user)}")
15
+ def auth_headers(headers, user, pool_identifier = Warden::Cognito.config.user_pools.first.identifier,
16
+ claims = {})
17
+ headers.merge(Authorization: "Bearer #{generate_token(user, pool_identifier, claims)}")
17
18
  end
18
19
 
19
20
  def local_issuer
@@ -22,10 +23,14 @@ module Warden
22
23
 
23
24
  private
24
25
 
25
- def generate_token(user)
26
- payload = { sub: user.object_id,
27
- "#{identifying_attribute}": user.cognito_id,
28
- iss: local_issuer }
26
+ def generate_token(user, pool_identifier, claims = {})
27
+ payload = {
28
+ sub: user.object_id,
29
+ "#{identifying_attribute}": user.cognito_id,
30
+ iss: "#{pool_identifier}-#{local_issuer}",
31
+ jti: SecureRandom.uuid,
32
+ exp: 1.hour.from_now.to_i
33
+ }.merge(claims)
29
34
  headers = { kid: jwk.kid }
30
35
  JWT.encode(payload, jwk.keypair, 'RS256', headers)
31
36
  end
@@ -22,7 +22,7 @@ module Warden
22
22
  end
23
23
 
24
24
  def authenticate!
25
- user = local_user || UserNotFoundCallback.call(cognito_user)
25
+ user = local_user || UserNotFoundCallback.call(cognito_user, token_decoder.pool_identifier)
26
26
 
27
27
  fail!(:unknown_user) unless user.present?
28
28
  success!(user)
@@ -43,7 +43,11 @@ module Warden
43
43
  end
44
44
 
45
45
  def token_decoder
46
- @token_decoder ||= TokenDecoder.new(token)
46
+ @token_decoder ||= TokenDecoder.new(token, pool_identifier)
47
+ end
48
+
49
+ def pool_identifier
50
+ env['HTTP_X_AUTHORIZATION_POOL_IDENTIFIER']
47
51
  end
48
52
 
49
53
  def token
@@ -3,9 +3,9 @@ module Warden
3
3
  class TokenDecoder
4
4
  attr_reader :jwk_loader, :token
5
5
 
6
- def initialize(token)
6
+ def initialize(token, pool_identifier = nil)
7
7
  @token = token
8
- @jwk_loader = JwkLoader.new
8
+ @jwk_loader = find_loader(pool_identifier)
9
9
  end
10
10
 
11
11
  def validate!
@@ -22,13 +22,17 @@ module Warden
22
22
  end
23
23
 
24
24
  def cognito_user
25
- @cognito_user ||= CognitoClient.fetch(token)
25
+ @cognito_user ||= CognitoClient.scope(pool_identifier).fetch(token)
26
26
  end
27
27
 
28
28
  def user_attribute(attribute_name)
29
29
  token_attribute(attribute_name).presence || cognito_user_attribute(attribute_name)
30
30
  end
31
31
 
32
+ def pool_identifier
33
+ jwk_loader.pool_identifier
34
+ end
35
+
32
36
  private
33
37
 
34
38
  def token_attribute(attribute_name)
@@ -40,6 +44,17 @@ module Warden
40
44
  attribute.name == attribute_name
41
45
  end&.value
42
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
43
58
  end
44
59
  end
45
60
  end
@@ -3,12 +3,12 @@ module Warden
3
3
  class UserHelper
4
4
  include Cognito::Import['user_repository']
5
5
 
6
- def find_by_cognito_username(username)
7
- user_repository.find_by_cognito_username(username)
6
+ def find_by_cognito_username(username, pool_identifier)
7
+ user_repository.find_by_cognito_username(username, pool_identifier)
8
8
  end
9
9
 
10
- def find_by_cognito_attribute(arg)
11
- user_repository.find_by_cognito_attribute(arg)
10
+ def find_by_cognito_attribute(arg, pool_identifier)
11
+ user_repository.find_by_cognito_attribute(arg, pool_identifier)
12
12
  end
13
13
  end
14
14
  end
@@ -4,13 +4,13 @@ module Warden
4
4
  include Cognito::Import['after_local_user_not_found']
5
5
 
6
6
  class << self
7
- def call(cognito_user)
8
- new.call(cognito_user)
7
+ def call(cognito_user, pool_identifier)
8
+ new.call(cognito_user, pool_identifier)
9
9
  end
10
10
  end
11
11
 
12
- def call(cognito_user)
13
- after_local_user_not_found&.call(cognito_user)
12
+ def call(cognito_user, pool_identifier)
13
+ after_local_user_not_found&.call(cognito_user, pool_identifier)
14
14
  end
15
15
  end
16
16
  end
@@ -1,5 +1,5 @@
1
1
  module Warden
2
2
  module Cognito
3
- VERSION = '0.2.2'.freeze
3
+ VERSION = '0.3.3'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: warden-cognito
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juan F. Pérez
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-11-27 00:00:00.000000000 Z
12
+ date: 2021-05-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -217,8 +217,10 @@ files:
217
217
  - lib/warden/cognito.rb
218
218
  - lib/warden/cognito/authenticatable_strategy.rb
219
219
  - lib/warden/cognito/cognito_client.rb
220
+ - lib/warden/cognito/has_user_pool_identifier.rb
220
221
  - lib/warden/cognito/jwk_loader.rb
221
222
  - lib/warden/cognito/local_user_mapper.rb
223
+ - lib/warden/cognito/pool_related_iterator.rb
222
224
  - lib/warden/cognito/test_helpers.rb
223
225
  - lib/warden/cognito/token_authenticatable_strategy.rb
224
226
  - lib/warden/cognito/token_decoder.rb