otp-jwt 0.2.5 → 0.3.1

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: 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: []