otp-jwt 0.2.5 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7d17e5145dd260643ff8c5af4e8bdd866282b126d9873d0eb7db432fa289e97
4
- data.tar.gz: bdedfcdb0ad970bc380f18aa4e518030b15b08f439d0854043ac45d22ba55c81
3
+ metadata.gz: ec8cd90193a14ccefd5ee90e2893186ca468083c354b12f671f1b53436fd6b50
4
+ data.tar.gz: 319db7a5d6dbdca708c0d161a224bbc722ef1fe7fe9209d329a0d5cdd657d2d8
5
5
  SHA512:
6
- metadata.gz: b287bc6b7d738be7ef9028d224a77794a27e1da869115de54b693ee9b6c7f5ce094672bf021e4b79956171bae351fd36c39b2506ce4d0c58f5081a43679294d5
7
- data.tar.gz: bdae8f51b91ef4e488d867b9281a604437524dcf0c40486901402393341ca563ba465c3cc2eb554f76ebda928124a2ae382302acd418852311368c98f39c08a5
6
+ metadata.gz: 96eccb938f849cb05ecad34fa92cd3df5a3c80a784091f5c318b7006908b5bf06f4d2ca8659fa14585eb9ad2b311fccd69b6a7dee30b15152ea019ed205ee7ea
7
+ data.tar.gz: 04abfadfe129775bb7fadaff00f8d20ff23b490fa8bbdc25a505cba85b2ff693c0947e7c6ac725cef34e4a0ad66531ce8d12ce445eeae6e3a504e37e6cfd7c4e
data/README.md CHANGED
@@ -10,7 +10,7 @@ One time password (email, SMS) authentication support for HTTP APIs.
10
10
  This project provides a couple of mixins to help you build
11
11
  applications/HTTP APIs without asking your users to provide passwords.
12
12
 
13
- [Your browser probably can work seamlessly with OTPs](https://web.dev/web-otp/)!!! :heart_eyes:
13
+ [Your browser probably can work seamlessly with OTPs](https://web.dev/web-otp/)!!! :heart_eyes:
14
14
 
15
15
  ## About
16
16
 
@@ -40,6 +40,16 @@ and [JWT](https://github.com/jwt/ruby-jwt/).
40
40
 
41
41
  Thanks to everyone who worked on these amazing projects!
42
42
 
43
+
44
+ ## Sponsors
45
+
46
+ I'm grateful for the following companies for supporting this project!
47
+
48
+ <p align="center">
49
+ <a href="https://www.luneteyewear.com"><img src="https://user-images.githubusercontent.com/112147/136836142-2bfba96e-447f-4eb6-b137-2445aee81b37.png"/></a>
50
+ <a href="https://www.startuplandia.io"><img src="https://user-images.githubusercontent.com/112147/136836147-93f8ab17-2465-4477-a7ab-e38255483c66.png"/></a>
51
+ </p>
52
+
43
53
  ## Installation
44
54
 
45
55
  Add this line to your application's Gemfile:
@@ -77,6 +87,12 @@ require 'otp'
77
87
  # To load the JWT related support.
78
88
  require 'otp/jwt'
79
89
 
90
+ # Set to 'none' to disable verification at all.
91
+ # OTP::JWT::Token.jwt_algorithm = 'HS256'
92
+
93
+ # How long the token will be valid.
94
+ # OTP::JWT::Token.jwt_lifetime = 60 * 60 * 24
95
+
80
96
  OTP::JWT::Token.jwt_signature_key = ENV['YOUR-SIGN-KEY']
81
97
  ```
82
98
  ### OTP for Active Record models
@@ -99,6 +115,7 @@ one time passwords:
99
115
  This concern expects two attributes to be provided by the model, the:
100
116
  * `otp_secret`: of type string, used to store the OTP signature key
101
117
  * `otp_counter`: of type integer, used to store the OTP counter
118
+ * `expire_jwt_at`: of type datetime, **optional** and used to force a token to expire
102
119
 
103
120
  A migration to add these two looks like this:
104
121
  ```
@@ -111,6 +128,13 @@ User.all.each do |u|
111
128
  u.save()
112
129
  end
113
130
  ```
131
+
132
+ ##### Force a token to expire
133
+
134
+ If there's an `expire_jwt_at` value that is in the past, the user token will
135
+ be reset and it will require a new authentication to receive a working token.
136
+
137
+ This is handy if the user access needs to be scheduled and/or removed.
114
138
  #### Mailer support
115
139
 
116
140
  You can use the built-in mailer to deliver the OTP, just require it and
@@ -304,4 +328,5 @@ contributors are expected to adhere to the
304
328
 
305
329
  ## License
306
330
 
307
- TBD
331
+ The gem is available as open source under the terms of the
332
+ [MIT License](https://opensource.org/licenses/MIT).
@@ -36,7 +36,8 @@ module OTP
36
36
  def verify_otp(otp)
37
37
  return nil if !valid? || !persisted? || otp_secret.blank?
38
38
 
39
- hotp = ROTP::HOTP.new(otp_secret, digits: OTP_DIGITS)
39
+ otp_digits = self.class.const_get(:OTP_DIGITS)
40
+ hotp = ROTP::HOTP.new(otp_secret, digits: otp_digits)
40
41
  transaction do
41
42
  otp_status = hotp.verify(otp.to_s, otp_counter)
42
43
  increment!(:otp_counter)
@@ -17,17 +17,19 @@ module OTP
17
17
  val = payload[claim_name]
18
18
  pk_col = self.column_for_attribute(self.primary_key)
19
19
 
20
+
20
21
  # Arel casts the values to the primary key type,
21
22
  # which means that an UUID becomes an integer by default...
22
- if self.connection.respond_to?(:type_cast_from_column)
23
- casted_val = self.connection.type_cast_from_column(pk_col, val)
23
+ if self.connection.respond_to?(:lookup_cast_type_from_column)
24
+ pk_type = self.connection.lookup_cast_type_from_column(pk_col)
25
+ casted_val = pk_type.serialize(val)
24
26
  else
25
27
  casted_val = self.connection.type_cast(val, pk_col)
26
28
  end
27
29
 
28
30
  return if casted_val.to_s != val.to_s.strip
29
31
 
30
- self.find_by(self.primary_key => val)
32
+ self.find_by(self.primary_key => val)&.expire_jwt?
31
33
  end
32
34
  end
33
35
  end
@@ -42,6 +44,17 @@ module OTP
42
44
  **(claims || {})
43
45
  )
44
46
  end
47
+
48
+ # Reset the [JWT] token if it is set to expire
49
+ #
50
+ # This method allows you to expire any token, independently from
51
+ # the JWT flags/payload.
52
+ #
53
+ # @return nil if the expiration worked, otherwise returns the model
54
+ def expire_jwt?
55
+ return self unless self.respond_to?(:expire_jwt_at)
56
+ return self unless expire_jwt_at? && expire_jwt_at.past?
57
+ end
45
58
  end
46
59
  end
47
60
  end
data/lib/otp/jwt/token.rb CHANGED
@@ -37,7 +37,10 @@ module OTP
37
37
  #
38
38
  # @return [Hash], JWT token payload
39
39
  def self.verify(token, opts = nil)
40
- ::JWT.decode(token.to_s, self.jwt_signature_key, true, opts || {})
40
+ verify = self.jwt_algorithm != 'none'
41
+ opts ||= { algorithm: self.jwt_algorithm }
42
+
43
+ ::JWT.decode(token.to_s, self.jwt_signature_key, verify, opts)
41
44
  end
42
45
 
43
46
  # Decodes a valid token into [Hash]
@@ -1,5 +1,5 @@
1
1
  module OTP
2
2
  module JWT
3
- VERSION = '0.2.5'
3
+ VERSION = '0.3.1'
4
4
  end
5
5
  end
data/spec/dummy.rb CHANGED
@@ -32,6 +32,7 @@ ActiveRecord::Schema.define do
32
32
  t.string :otp_secret
33
33
  t.integer :otp_counter
34
34
  t.timestamp :last_login_at
35
+ t.timestamp :expire_jwt_at
35
36
  t.timestamps
36
37
  end
37
38
  end
@@ -12,6 +12,18 @@ RSpec.describe OTP::JWT::Token, type: :model do
12
12
 
13
13
  describe '#sign' do
14
14
  it { expect(described_class.sign(payload)).to eq(token) }
15
+
16
+ context 'with the none algorithm' do
17
+ before do
18
+ OTP::JWT::Token.jwt_algorithm = 'none'
19
+ end
20
+
21
+ after do
22
+ OTP::JWT::Token.jwt_algorithm = 'HS256'
23
+ end
24
+
25
+ it { expect(described_class.sign(payload)).to eq(token) }
26
+ end
15
27
  end
16
28
 
17
29
  describe '#verify' do
@@ -31,6 +43,22 @@ RSpec.describe OTP::JWT::Token, type: :model do
31
43
  expect { described_class.verify(token) }
32
44
  .to raise_error(JWT::ExpiredSignature)
33
45
  end
46
+
47
+ context 'with an RSA key' do
48
+ before do
49
+ OTP::JWT::Token.jwt_signature_key = OpenSSL::PKey::RSA.new(2048)
50
+ OTP::JWT::Token.jwt_algorithm = 'RS256'
51
+ end
52
+
53
+ after do
54
+ OTP::JWT::Token.jwt_signature_key = '_'
55
+ OTP::JWT::Token.jwt_algorithm = 'HS256'
56
+ end
57
+
58
+ it do
59
+ expect(described_class.verify(token).first).to include(payload)
60
+ end
61
+ end
34
62
  end
35
63
 
36
64
  describe '#decode' do
@@ -50,5 +78,21 @@ RSpec.describe OTP::JWT::Token, type: :model do
50
78
  expect(described_class.decode(token)).to eq(nil)
51
79
  end
52
80
  end
81
+
82
+ context 'with the none algorithm' do
83
+ before do
84
+ OTP::JWT::Token.jwt_algorithm = 'none'
85
+ end
86
+
87
+ after do
88
+ OTP::JWT::Token.jwt_algorithm = 'HS256'
89
+ end
90
+
91
+ it do
92
+ expect(
93
+ described_class.decode(token) { |p| User.find(p['sub']) }
94
+ ).to eq(user)
95
+ end
96
+ end
53
97
  end
54
98
  end
data/spec/spec_helper.rb CHANGED
@@ -23,11 +23,12 @@ module OTP::JWT::FactoryHelpers
23
23
  # Creates an user
24
24
  #
25
25
  # @return [User]
26
- def create_user
26
+ def create_user(attrs = {})
27
27
  User.create!(
28
28
  full_name: FFaker::Name.name,
29
29
  email: FFaker::Internet.email,
30
- phone_number: FFaker::PhoneNumber.phone_number
30
+ phone_number: FFaker::PhoneNumber.phone_number,
31
+ **attrs
31
32
  )
32
33
  end
33
34
  end
@@ -13,6 +13,24 @@ RSpec.describe UsersController, type: :request do
13
13
  let(:user) { create_user }
14
14
 
15
15
  it { expect(response).to have_http_status(:ok) }
16
+
17
+ context 'with the expiration for JWT in future' do
18
+ let(:user) { create_user(expire_jwt_at: DateTime.tomorrow) }
19
+
20
+ it do
21
+ expect(response).to have_http_status(:ok)
22
+ expect(user.reload.expire_jwt_at).not_to be_nil
23
+ end
24
+ end
25
+
26
+ context 'with the expiration past for JWT' do
27
+ let(:user) { create_user(expire_jwt_at: DateTime.yesterday) }
28
+
29
+ it do
30
+ expect(response).to have_http_status(:unauthorized)
31
+ expect(user.reload.expire_jwt_at).not_to be_nil
32
+ end
33
+ end
16
34
  end
17
35
 
18
36
  context 'with bad subject' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: otp-jwt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stas Suscov
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-09 00:00:00.000000000 Z
11
+ date: 2021-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -84,16 +84,16 @@ dependencies:
84
84
  name: rails
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '4'
89
+ version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: '4'
96
+ version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: rspec-rails
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -112,16 +112,16 @@ dependencies:
112
112
  name: rubocop
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - '='
115
+ - - ">="
116
116
  - !ruby/object:Gem::Version
117
- version: '0.81'
117
+ version: '0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - '='
122
+ - - ">="
123
123
  - !ruby/object:Gem::Version
124
- version: '0.81'
124
+ version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: rubocop-performance
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -182,16 +182,16 @@ dependencies:
182
182
  name: sqlite3
183
183
  requirement: !ruby/object:Gem::Requirement
184
184
  requirements:
185
- - - "~>"
185
+ - - ">="
186
186
  - !ruby/object:Gem::Version
187
- version: 1.3.6
187
+ version: '0'
188
188
  type: :development
189
189
  prerelease: false
190
190
  version_requirements: !ruby/object:Gem::Requirement
191
191
  requirements:
192
- - - "~>"
192
+ - - ">="
193
193
  - !ruby/object:Gem::Version
194
- version: 1.3.6
194
+ version: '0'
195
195
  - !ruby/object:Gem::Dependency
196
196
  name: tzinfo-data
197
197
  requirement: !ruby/object:Gem::Requirement
@@ -250,7 +250,7 @@ homepage: https://github.com/stas/otp-jwt
250
250
  licenses:
251
251
  - MIT
252
252
  metadata: {}
253
- post_install_message:
253
+ post_install_message:
254
254
  rdoc_options: []
255
255
  require_paths:
256
256
  - lib
@@ -265,8 +265,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
265
265
  - !ruby/object:Gem::Version
266
266
  version: '0'
267
267
  requirements: []
268
- rubygems_version: 3.0.3
269
- signing_key:
268
+ rubygems_version: 3.2.22
269
+ signing_key:
270
270
  specification_version: 4
271
271
  summary: Passwordless HTTP APIs
272
272
  test_files: []