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 +4 -4
- data/README.md +27 -2
- data/lib/otp/active_record.rb +2 -1
- data/lib/otp/jwt/active_record.rb +16 -3
- data/lib/otp/jwt/token.rb +4 -1
- data/lib/otp/jwt/version.rb +1 -1
- data/spec/dummy.rb +1 -0
- data/spec/otp/jwt/token_spec.rb +44 -0
- data/spec/spec_helper.rb +3 -2
- data/spec/users_controller_spec.rb +18 -0
- metadata +18 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ec8cd90193a14ccefd5ee90e2893186ca468083c354b12f671f1b53436fd6b50
|
4
|
+
data.tar.gz: 319db7a5d6dbdca708c0d161a224bbc722ef1fe7fe9209d329a0d5cdd657d2d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
331
|
+
The gem is available as open source under the terms of the
|
332
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/lib/otp/active_record.rb
CHANGED
@@ -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
|
-
|
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?(:
|
23
|
-
|
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
|
-
|
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]
|
data/lib/otp/jwt/version.rb
CHANGED
data/spec/dummy.rb
CHANGED
data/spec/otp/jwt/token_spec.rb
CHANGED
@@ -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.
|
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-
|
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: '
|
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: '
|
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
|
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
|
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:
|
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:
|
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.
|
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: []
|