aws-cognito-srp 0.4.0 → 0.6.0

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: 6000777b1471345bf83ef454e2147786b46c8960218729c0ef9b33d05bc08634
4
- data.tar.gz: 5b28a08b3614d6dbe6f05a1bc25fd54200a8c90bb6fe8e3a24f97e4afc1bbe80
3
+ metadata.gz: 5b685bf22d6f5a6dd977e1b637f84b30fa5630b1f0e14c9e0dfb46c74273be9d
4
+ data.tar.gz: 1e2db6df2d70acb710906b75c119506df3608b52fd78b34e56233af2e480a6c7
5
5
  SHA512:
6
- metadata.gz: d1e507ab4e5a8ce39647a2699ad1bac52a62352c6b2a04d2eb47b12954ae8c8a1f86f26c457583c13f84200064620cbf938381894d302a457b7a2c0468f5f26e
7
- data.tar.gz: 3714f171b5527ed457d2c05e0ff5aece5f96d572e9f48e4fa6412b9cedad85d69ac54432b7e719f7afe0406aa7d135562b0f7446d705d0a4a494f22c48a45bd4
6
+ metadata.gz: 75e86769e7198362a6e12f05b0e5a137dd62ef3886e122eeb2a7d638169f6dfa856bc12bb13a12616f2663c6b78cf36bf368d615ff617e003446eca7e08372e7
7
+ data.tar.gz: b24ed5925052eae4de68b27ee5fe57dc14bff611576e33748362224b85929ea08a29290919e6d494a2b0ca76cd4ac4db4b073d96de86a6c1a8a51b3496b7187c
@@ -7,14 +7,15 @@ jobs:
7
7
  strategy:
8
8
  fail-fast: false
9
9
  matrix:
10
- ruby: [2.3, 2.4, 2.5, 2.6, 2.7, '3.0']
10
+ os: [ubuntu-latest, macos-latest]
11
+ ruby: [2.4, 2.5, 2.6, 2.7, '3.0', 3.1, 3.2, jruby, truffleruby]
11
12
 
12
- runs-on: ubuntu-latest
13
+ runs-on: ${{ matrix.os }}
13
14
 
14
- name: Test against Ruby ${{ matrix.ruby }}
15
+ name: Test against ${{ matrix.ruby }} on ${{ matrix.os }}
15
16
 
16
17
  steps:
17
- - uses: actions/checkout@v2
18
+ - uses: actions/checkout@v3
18
19
  - name: Set up Ruby
19
20
  uses: ruby/setup-ruby@v1
20
21
  with:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Changelog for aws-cognito-srp-ruby
2
2
 
3
+ ### 0.6.0 (June 20, 2023)
4
+
5
+ * Added support for MFA (@suketa)
6
+
7
+ ### 0.5.0 (February 14❤︎, 2023)
8
+
9
+ * Added support for `client_secret` (@suketa)
10
+
3
11
  ### 0.4.0 (October 1, 2021)
4
12
 
5
13
  * Added `refresh_tokens` method
@@ -12,6 +20,6 @@
12
20
 
13
21
  * Added custom exception classes and better error messages
14
22
 
15
- ### 0.1.0 (Septembre 17, 2021)
23
+ ### 0.1.0 (September 17, 2021)
16
24
 
17
25
  * Initial release
data/README.md CHANGED
@@ -25,11 +25,12 @@ gem 'aws-cognito-srp'
25
25
  require "aws-cognito-srp"
26
26
 
27
27
  aws_srp = Aws::CognitoSrp.new(
28
- username: "username",
29
- password: "password",
30
- pool_id: "pool-id",
31
- client_id: "client-id",
32
- aws_client: Aws::CognitoIdentityProvider::Client.new(region: "aws-region")
28
+ username: "username",
29
+ password: "password",
30
+ pool_id: "pool-id",
31
+ client_id: "client-id",
32
+ client_secret: "client-secret", # Optional
33
+ aws_client: Aws::CognitoIdentityProvider::Client.new(region: "aws-region")
33
34
  )
34
35
 
35
36
  resp = aws_srp.authenticate
@@ -44,9 +45,66 @@ resp.refresh_token
44
45
  new_tokens = aws_srp.refresh_tokens(resp.refresh_token)
45
46
  ```
46
47
 
48
+ ### `USER_ID_FOR_SRP`
49
+
50
+ In case you need access to the `USER_ID_FOR_SRP` value from the auth response,
51
+ you can do so by calling `aws_srp.user_id_for_srp` *after* the initial auth
52
+ (`aws_srp` being the same as in the code example above).
53
+
54
+ If you're using a `client_secret` and calling `#refresh_tokens` in a different
55
+ instance than the one that performed the initial call to `#authenticate` you
56
+ will have to pass the `USER_ID_FOR_SRP` value as a keyword argument:
57
+
58
+ ```ruby
59
+ new_tokens = aws_srp.refresh_token(resp.refresh_token,
60
+ user_id_for_srp: your_user_id_for_srp)
61
+ ```
62
+
63
+ ### MFA (multi-factor authentication)
64
+
65
+ If you're using MFA you should check for the challenge after calling
66
+ `#authenticate` and respond accordingly with `#respond_to_mfa_challenge`.
67
+
68
+ ```ruby
69
+ resp = aws_srp.authenticate
70
+
71
+ if resp.respond_to?(:challenge_name) && resp.mfa_challenge?
72
+ user_code = get.chomp # Get MFA code from user
73
+
74
+ resp = aws_srp.respond_to_mfa_challenge(
75
+ user_code,
76
+ auth_response: resp
77
+ )
78
+ end
79
+
80
+ resp.id_token
81
+ resp.access_token
82
+ resp.refresh_token
83
+ ```
84
+
85
+ Note that when `#authenticate` results in a successful authentication it
86
+ returns a `AuthenticationResultType`
87
+ ([AWS SDK docs](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CognitoIdentityProvider/Types/AuthenticationResultType.html)),
88
+ i.e. an object that responds to `#id_token`, `#access_token`, etc.
89
+
90
+ However, when a MFA challenge step occurs, `#authenticate` instead returns a
91
+ `RespondToAuthChallengeResponse` ([AWS SDK docs](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/CognitoIdentityProvider/Types/RespondToAuthChallengeResponse.html#authentication_result-instance_method)),
92
+ which you can check for with `.respond_to?(:challenge_name)` as in the above
93
+ example. The `RespondToAuthChallengeResponse` object will be extended with the
94
+ convenience methods `#mfa_challenge?`, `#software_token_mfa?` and `#sms_mfa?`.
95
+
96
+ The `#respond_to_mfa_challenge` method can be called with the following
97
+ signatures:
98
+
99
+ ```
100
+ #respond_to_mfa_challenge(user_code, auth_response: [, user_id_for_srp:])
101
+ #respond_to_mfa_challenge(user_code, challenge_name:, session: [, user_id_for_srp:])
102
+ ```
103
+
47
104
  ## Supported rubies
48
105
 
49
- This gem is tested against and supports Ruby 2.3, 2.4, 2.5, 2.6, 2.7 and 3.0.
106
+ This gem is tested against and supports Ruby 2.4 through 3.2, JRuby and
107
+ TruffleRuby.
50
108
 
51
109
  ## Development
52
110
 
@@ -20,11 +20,13 @@ Gem::Specification.new do |spec|
20
20
  end
21
21
  spec.require_paths = ["lib"]
22
22
 
23
+ spec.required_ruby_version = '>= 2.4.0'
24
+
23
25
  spec.add_dependency "aws-sdk-cognitoidentityprovider"
24
26
 
25
- spec.add_development_dependency "bundler", "~> 2.2.0"
27
+ spec.add_development_dependency "bundler", "~> 2.2"
26
28
  spec.add_development_dependency "rake", "~> 13.0"
27
- spec.add_development_dependency "ox", "~> 2.14.0"
29
+ spec.add_development_dependency "nokogiri", "~> 1.9"
28
30
  spec.add_development_dependency "rspec", "~> 3.0"
29
31
  spec.add_development_dependency "pry"
30
32
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aws/cognito_srp/errors"
4
+
5
+ module Aws
6
+ class CognitoSrp
7
+ module ChallengeResponseHelper
8
+ def mfa_challenge?
9
+ software_token_mfa? || sms_mfa?
10
+ end
11
+
12
+ def software_token_mfa?
13
+ challenge_name == SOFTWARE_TOKEN_MFA
14
+ end
15
+
16
+ def sms_mfa?
17
+ challenge_name == SMS_MFA
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Aws
4
4
  class CognitoSrp
5
- VERSION = "0.4.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
@@ -9,6 +9,7 @@ require "base64"
9
9
 
10
10
  require "aws/cognito_srp/version"
11
11
  require "aws/cognito_srp/errors"
12
+ require "aws/cognito_srp/challenge_response_helper"
12
13
 
13
14
  if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.5")
14
15
  module IntegerWithPow
@@ -24,19 +25,6 @@ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.5")
24
25
  using IntegerWithPow
25
26
  end
26
27
 
27
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.4")
28
- module StringWithUnpack1
29
- refine String do
30
- # String#unpack1 was introduced in Ruby 2.4
31
- def unpack1(fmt)
32
- unpack(fmt)[0]
33
- end
34
- end
35
- end
36
-
37
- using StringWithUnpack1
38
- end
39
-
40
28
  module Aws
41
29
  # Client for AWS Cognito Identity Provider using Secure Remote Password (SRP).
42
30
  #
@@ -63,6 +51,8 @@ module Aws
63
51
  PASSWORD_VERIFIER = "PASSWORD_VERIFIER"
64
52
  REFRESH_TOKEN = "REFRESH_TOKEN"
65
53
  USER_SRP_AUTH = "USER_SRP_AUTH"
54
+ SOFTWARE_TOKEN_MFA = "SOFTWARE_TOKEN_MFA"
55
+ SMS_MFA = "SMS_MFA"
66
56
 
67
57
  N_HEX = %w(
68
58
  FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08
@@ -85,12 +75,15 @@ module Aws
85
75
 
86
76
  INFO_BITS = 'Caldera Derived Key'
87
77
 
88
- def initialize(username:, password:, pool_id:, client_id:, aws_client:)
78
+ attr_reader :user_id_for_srp
79
+
80
+ def initialize(username:, password:, pool_id:, client_id:, aws_client:, client_secret: nil)
89
81
  @username = username
90
82
  @password = password
91
83
  @pool_id = pool_id
92
84
  @client_id = client_id
93
85
  @aws_client = aws_client
86
+ @client_secret = client_secret
94
87
 
95
88
  @big_n = hex_to_long(N_HEX)
96
89
  @g = hex_to_long(G_HEX)
@@ -100,13 +93,16 @@ module Aws
100
93
  end
101
94
 
102
95
  def authenticate
96
+ auth_parameters = {
97
+ USERNAME: @username,
98
+ SRP_A: long_to_hex(@large_a_value),
99
+ SECRET_HASH: @client_secret && secret_hash(@username)
100
+ }.compact
101
+
103
102
  init_auth_response = @aws_client.initiate_auth(
104
103
  client_id: @client_id,
105
104
  auth_flow: USER_SRP_AUTH,
106
- auth_parameters: {
107
- USERNAME: @username,
108
- SRP_A: long_to_hex(@large_a_value)
109
- }
105
+ auth_parameters: auth_parameters
110
106
  )
111
107
 
112
108
  unless init_auth_response.challenge_name == PASSWORD_VERIFIER
@@ -114,12 +110,21 @@ module Aws
114
110
  end
115
111
 
116
112
  challenge_response = process_challenge(init_auth_response.challenge_parameters)
113
+ hash = @client_secret && secret_hash(@user_id_for_srp)
117
114
 
118
- auth_response = @aws_client.respond_to_auth_challenge(
115
+ params = {
119
116
  client_id: @client_id,
120
117
  challenge_name: PASSWORD_VERIFIER,
121
- challenge_responses: challenge_response
122
- )
118
+ challenge_responses: challenge_response.merge(SECRET_HASH: hash).compact
119
+ }
120
+
121
+ auth_response = @aws_client.respond_to_auth_challenge(params)
122
+
123
+ if auth_response.challenge_name == SOFTWARE_TOKEN_MFA || auth_response.challenge_name == SMS_MFA
124
+ auth_response.extend(ChallengeResponseHelper)
125
+
126
+ return auth_response
127
+ end
123
128
 
124
129
  if auth_response.challenge_name == NEW_PASSWORD_REQUIRED
125
130
  raise NewPasswordRequired, "Cognito responded to password verifier with a #{NEW_PASSWORD_REQUIRED} challenge"
@@ -128,19 +133,50 @@ module Aws
128
133
  auth_response.authentication_result
129
134
  end
130
135
 
131
- def refresh_tokens(refresh_token)
136
+ def refresh_tokens(refresh_token, user_id_for_srp: @user_id_for_srp)
137
+ auth_parameters = {
138
+ REFRESH_TOKEN: refresh_token,
139
+ SECRET_HASH: @client_secret && secret_hash(user_id_for_srp)
140
+ }.compact
141
+
132
142
  resp = @aws_client.initiate_auth(
133
143
  client_id: @client_id,
134
144
  auth_flow: REFRESH_TOKEN,
135
- auth_parameters: {
136
- REFRESH_TOKEN: refresh_token
137
- }
145
+ auth_parameters: auth_parameters
138
146
  )
139
147
 
140
148
  resp.authentication_result
141
149
  end
142
150
  alias_method :refresh, :refresh_tokens
143
151
 
152
+ def respond_to_mfa_challenge(user_code, auth_response: nil, challenge_name: auth_response&.challenge_name, session: auth_response&.session, user_id_for_srp: @user_id_for_srp)
153
+ unless auth_response || (challenge_name && session)
154
+ raise ArgumentError, "Either `auth_response' or `challenge_name'+`session' keyword arguments should be given"
155
+ end
156
+
157
+ hash = @client_secret && secret_hash(user_id_for_srp)
158
+
159
+ challenge_responses = {
160
+ USERNAME: user_id_for_srp,
161
+ SECRET_HASH: hash
162
+ }
163
+ if challenge_name == SOFTWARE_TOKEN_MFA
164
+ challenge_responses[:SOFTWARE_TOKEN_MFA_CODE] = user_code
165
+ elsif challenge_name == SMS_MFA
166
+ challenge_responses[:SMS_MFA_CODE] = user_code
167
+ end
168
+
169
+ params = {
170
+ challenge_name: challenge_name,
171
+ session: session,
172
+ client_id: @client_id,
173
+ challenge_responses: challenge_responses.compact
174
+ }.compact
175
+
176
+ resp = @aws_client.respond_to_auth_challenge(params)
177
+ resp.authentication_result
178
+ end
179
+
144
180
  private
145
181
 
146
182
  def generate_random_small_a
@@ -170,22 +206,22 @@ module Aws
170
206
  end
171
207
 
172
208
  def process_challenge(challenge_parameters)
173
- user_id_for_srp = challenge_parameters.fetch("USER_ID_FOR_SRP")
209
+ @user_id_for_srp = challenge_parameters.fetch("USER_ID_FOR_SRP")
174
210
  salt_hex = challenge_parameters.fetch("SALT")
175
211
  srp_b_hex = challenge_parameters.fetch("SRP_B")
176
212
  secret_block_b64 = challenge_parameters.fetch("SECRET_BLOCK")
177
213
 
178
214
  timestamp = ::Time.now.utc.strftime("%a %b %-d %H:%M:%S %Z %Y")
179
215
 
180
- hkdf = get_password_authentication_key(user_id_for_srp, @password, srp_b_hex.to_i(16), salt_hex)
216
+ hkdf = get_password_authentication_key(@user_id_for_srp, @password, srp_b_hex.to_i(16), salt_hex)
181
217
  secret_block_bytes = ::Base64.strict_decode64(secret_block_b64)
182
- msg = @pool_id.split("_")[1] + user_id_for_srp + secret_block_bytes + timestamp
218
+ msg = @pool_id.split("_")[1] + @user_id_for_srp + secret_block_bytes + timestamp
183
219
  hmac_digest = ::OpenSSL::HMAC.digest(::OpenSSL::Digest::SHA256.new, hkdf, msg)
184
220
  signature_string = ::Base64.strict_encode64(hmac_digest).force_encoding('utf-8')
185
221
 
186
222
  {
187
223
  TIMESTAMP: timestamp,
188
- USERNAME: user_id_for_srp,
224
+ USERNAME: @user_id_for_srp,
189
225
  PASSWORD_CLAIM_SECRET_BLOCK: secret_block_b64,
190
226
  PASSWORD_CLAIM_SIGNATURE: signature_string
191
227
  }
@@ -242,5 +278,9 @@ module Aws
242
278
  u_hex_hash = hex_hash(pad_hex(big_a) + pad_hex(big_b))
243
279
  hex_to_long(u_hex_hash)
244
280
  end
281
+
282
+ def secret_hash(username)
283
+ Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', @client_secret, username + @client_id))
284
+ end
245
285
  end
246
286
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-cognito-srp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Viney
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2021-10-01 00:00:00.000000000 Z
13
+ date: 2023-06-20 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: aws-sdk-cognitoidentityprovider
@@ -32,14 +32,14 @@ dependencies:
32
32
  requirements:
33
33
  - - "~>"
34
34
  - !ruby/object:Gem::Version
35
- version: 2.2.0
35
+ version: '2.2'
36
36
  type: :development
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
39
39
  requirements:
40
40
  - - "~>"
41
41
  - !ruby/object:Gem::Version
42
- version: 2.2.0
42
+ version: '2.2'
43
43
  - !ruby/object:Gem::Dependency
44
44
  name: rake
45
45
  requirement: !ruby/object:Gem::Requirement
@@ -55,19 +55,19 @@ dependencies:
55
55
  - !ruby/object:Gem::Version
56
56
  version: '13.0'
57
57
  - !ruby/object:Gem::Dependency
58
- name: ox
58
+ name: nokogiri
59
59
  requirement: !ruby/object:Gem::Requirement
60
60
  requirements:
61
61
  - - "~>"
62
62
  - !ruby/object:Gem::Version
63
- version: 2.14.0
63
+ version: '1.9'
64
64
  type: :development
65
65
  prerelease: false
66
66
  version_requirements: !ruby/object:Gem::Requirement
67
67
  requirements:
68
68
  - - "~>"
69
69
  - !ruby/object:Gem::Version
70
- version: 2.14.0
70
+ version: '1.9'
71
71
  - !ruby/object:Gem::Dependency
72
72
  name: rspec
73
73
  requirement: !ruby/object:Gem::Requirement
@@ -117,6 +117,7 @@ files:
117
117
  - bin/setup
118
118
  - lib/aws-cognito-srp.rb
119
119
  - lib/aws/cognito_srp.rb
120
+ - lib/aws/cognito_srp/challenge_response_helper.rb
120
121
  - lib/aws/cognito_srp/errors.rb
121
122
  - lib/aws/cognito_srp/version.rb
122
123
  - lib/aws_cognito_srp.rb
@@ -132,14 +133,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
132
133
  requirements:
133
134
  - - ">="
134
135
  - !ruby/object:Gem::Version
135
- version: '0'
136
+ version: 2.4.0
136
137
  required_rubygems_version: !ruby/object:Gem::Requirement
137
138
  requirements:
138
139
  - - ">="
139
140
  - !ruby/object:Gem::Version
140
141
  version: '0'
141
142
  requirements: []
142
- rubygems_version: 3.0.6
143
+ rubygems_version: 3.3.3
143
144
  signing_key:
144
145
  specification_version: 4
145
146
  summary: AWS Cognito SRP auth for Ruby