authonomy 1.0.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +2 -0
- data/lib/authonomy/authenticator.rb +117 -0
- data/lib/authonomy/encryptor.rb +39 -0
- data/lib/authonomy/token_generator.rb +50 -0
- data/lib/authonomy/version.rb +5 -0
- data/lib/authonomy.rb +136 -0
- metadata +98 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4eb4be2ddf04609cecbcc0529726a09b46ae722b3df4efe8874267f44031dd52
|
4
|
+
data.tar.gz: 9de066f4e9797ae65a60487abfdec9ee310bb76ef2f2e1b54e0ef1dbc23f48c1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b3376fc1684ced643e417534f6b1c48d867800619521cdfdb20e8d4d146c502486a900ae45e3e9103a673a23ab2fdc4981eb6f7e4a73b8893c3d9fe1201aec4d
|
7
|
+
data.tar.gz: 8815a23de14bfd2646b1ff1c34650b436bf55c1c957b37b0cdd5ae0e605b8a85fd6fd9fb7ad4d3cebae01729b57dbb4e650a0919240c357010e32e8f70243dc2
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2022 Ilya Konyukhov
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt'
|
4
|
+
|
5
|
+
module Authonomy
|
6
|
+
class Authenticator
|
7
|
+
ALGO = 'HS256'
|
8
|
+
LEEWAY = 30.seconds
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Returns access_token and refresh_token for user authentication
|
12
|
+
def tokens(subject, refresh_exp = nil)
|
13
|
+
now = Time.now.utc.to_i
|
14
|
+
|
15
|
+
access_payload = {
|
16
|
+
sub: subject,
|
17
|
+
iat: now,
|
18
|
+
exp: now + Authonomy.access_token_ttl.to_i
|
19
|
+
}
|
20
|
+
refresh_payload = {
|
21
|
+
acc: payload_digest(access_payload),
|
22
|
+
iat: now
|
23
|
+
}
|
24
|
+
if refresh_exp
|
25
|
+
refresh_payload[:exp] = refresh_exp.is_a?(Integer) ? refresh_exp : now + Authonomy.refresh_token_ttl.to_i
|
26
|
+
end
|
27
|
+
|
28
|
+
[encode(access_payload), encode(refresh_payload)]
|
29
|
+
end
|
30
|
+
|
31
|
+
# Based on given access and refresh tokens authenticates user or not
|
32
|
+
def authenticate(klass, access_token, refresh_token)
|
33
|
+
now = Time.now.utc.to_i
|
34
|
+
|
35
|
+
generate_response_tokens = false
|
36
|
+
refresh_payload = nil
|
37
|
+
refresh_exp = nil
|
38
|
+
|
39
|
+
access_opts = {
|
40
|
+
verify_sub: true,
|
41
|
+
verify_iat: true,
|
42
|
+
verify_expiration: true
|
43
|
+
}
|
44
|
+
|
45
|
+
if refresh_token
|
46
|
+
refresh_opts = {
|
47
|
+
verify_iat: true,
|
48
|
+
verify_expiration: false
|
49
|
+
}
|
50
|
+
|
51
|
+
refresh_payload = decode(refresh_token, refresh_opts)
|
52
|
+
refresh_exp = refresh_payload['exp']
|
53
|
+
|
54
|
+
if refresh_exp && refresh_exp > now
|
55
|
+
# don't verify access token expiration if refresh token has an exp timestamp and not expired yet
|
56
|
+
access_opts[:verify_expiration] = false
|
57
|
+
else
|
58
|
+
# sliding expiration
|
59
|
+
refresh_exp = nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
access_payload = decode(access_token, access_opts)
|
64
|
+
|
65
|
+
if refresh_payload
|
66
|
+
if refresh_payload['acc'] && refresh_payload['acc'] != payload_digest(access_payload)
|
67
|
+
# puts 'Refresh token mismatch'
|
68
|
+
return nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# to avoid tokens mismatch for multiple simultaneous calls
|
72
|
+
generate_response_tokens = true # if now - access_payload['iat'] > 60
|
73
|
+
end
|
74
|
+
|
75
|
+
user = klass.find_by(id: access_payload['sub'])
|
76
|
+
|
77
|
+
unless user
|
78
|
+
# puts 'User not found'
|
79
|
+
return nil
|
80
|
+
end
|
81
|
+
|
82
|
+
if user.respond_to?(:access_expired_at) && user.access_expired_at.to_i > access_payload['iat']
|
83
|
+
# puts 'User access expired'
|
84
|
+
return nil
|
85
|
+
end
|
86
|
+
|
87
|
+
if generate_response_tokens
|
88
|
+
[user] + tokens(user.id, refresh_exp)
|
89
|
+
else
|
90
|
+
user
|
91
|
+
end
|
92
|
+
|
93
|
+
rescue ::JWT::DecodeError
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def encode(payload)
|
100
|
+
::JWT.encode(payload, hmac_secret, ALGO)
|
101
|
+
end
|
102
|
+
|
103
|
+
def decode(token, opts = {})
|
104
|
+
::JWT.decode(token, hmac_secret, true, opts.merge(leeway: LEEWAY, algorithms: [ALGO])).first
|
105
|
+
# can throw JWT::IncorrectAlgorithm, JWT::VerificationError, JWT::ExpiredSignature, JWT::InvalidIatError, JWT::InvalidSubError
|
106
|
+
end
|
107
|
+
|
108
|
+
def payload_digest(payload)
|
109
|
+
Digest::MD5.hexdigest(payload.to_json)
|
110
|
+
end
|
111
|
+
|
112
|
+
def hmac_secret
|
113
|
+
Authonomy.jwt_secret_key
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bcrypt'
|
4
|
+
|
5
|
+
module Authonomy
|
6
|
+
class Encryptor
|
7
|
+
class << self
|
8
|
+
def digest(password)
|
9
|
+
password = "#{password}#{Authonomy.pepper.presence}"
|
10
|
+
|
11
|
+
::BCrypt::Password.create(password, cost: Authonomy.stretches).to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def compare(hashed_password, password)
|
15
|
+
return false if hashed_password.blank?
|
16
|
+
|
17
|
+
bcrypt = ::BCrypt::Password.new(hashed_password)
|
18
|
+
|
19
|
+
password = "#{password}#{Authonomy.pepper.presence}"
|
20
|
+
password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt)
|
21
|
+
|
22
|
+
secure_compare(password, hashed_password)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Constant-time comparison algorithm to prevent timing attacks
|
28
|
+
def secure_compare(abc, xyz)
|
29
|
+
return false if abc.blank? || xyz.blank? || abc.bytesize != xyz.bytesize
|
30
|
+
|
31
|
+
l = abc.unpack "C#{abc.bytesize}"
|
32
|
+
|
33
|
+
res = 0
|
34
|
+
xyz.each_byte { |byte| res |= byte ^ l.shift }
|
35
|
+
res.zero?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module Authonomy
|
7
|
+
class TokenGenerator
|
8
|
+
ALGO = 'SHA256'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def generator
|
12
|
+
@generator ||= new(
|
13
|
+
ActiveSupport::CachingKeyGenerator.new(
|
14
|
+
ActiveSupport::KeyGenerator.new(Authonomy.secret_key)
|
15
|
+
)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(key_generator)
|
21
|
+
@key_generator = key_generator
|
22
|
+
end
|
23
|
+
|
24
|
+
def digest(column, token)
|
25
|
+
key = key_for(column)
|
26
|
+
token.present? && OpenSSL::HMAC.hexdigest(ALGO, key, token.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate(klass, column, length)
|
30
|
+
key = key_for(column)
|
31
|
+
|
32
|
+
loop do
|
33
|
+
token = friendly_token(length)
|
34
|
+
encoded_token = OpenSSL::HMAC.hexdigest(ALGO, key, token)
|
35
|
+
break [token, encoded_token] unless klass.find_by(column => encoded_token)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def key_for(column)
|
42
|
+
@key_generator.generate_key("Authonomy #{column}")
|
43
|
+
end
|
44
|
+
|
45
|
+
def friendly_token(length)
|
46
|
+
rlength = (length * 3) / 4
|
47
|
+
SecureRandom.urlsafe_base64(rlength).tr('lIO0', 'sxyz')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/authonomy.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/time'
|
5
|
+
require 'active_support/key_generator'
|
6
|
+
|
7
|
+
require_relative 'authonomy/encryptor'
|
8
|
+
require_relative 'authonomy/token_generator'
|
9
|
+
require_relative 'authonomy/authenticator'
|
10
|
+
|
11
|
+
require 'uri/mailto'
|
12
|
+
|
13
|
+
module Authonomy
|
14
|
+
class << self
|
15
|
+
WRITER_METHODS = %i[
|
16
|
+
pepper
|
17
|
+
stretches
|
18
|
+
password_length
|
19
|
+
phone_length
|
20
|
+
access_token_ttl
|
21
|
+
refresh_token_ttl
|
22
|
+
invite_token_ttl
|
23
|
+
invite_token_length
|
24
|
+
confirm_email_token_ttl
|
25
|
+
confirm_email_token_length
|
26
|
+
reset_password_token_ttl
|
27
|
+
reset_password_token_length
|
28
|
+
secret_key
|
29
|
+
jwt_secret_key
|
30
|
+
].freeze
|
31
|
+
attr_writer(*WRITER_METHODS)
|
32
|
+
|
33
|
+
READER_METHODS = %i[
|
34
|
+
pepper
|
35
|
+
secret_key
|
36
|
+
jwt_secret_key
|
37
|
+
].freeze
|
38
|
+
attr_reader(*READER_METHODS)
|
39
|
+
|
40
|
+
def configure
|
41
|
+
yield self if block_given?
|
42
|
+
end
|
43
|
+
|
44
|
+
def stretches
|
45
|
+
@stretches || 11
|
46
|
+
end
|
47
|
+
|
48
|
+
def password_regexp
|
49
|
+
%r{
|
50
|
+
\A
|
51
|
+
(?=.*\d) # Must contain a digit
|
52
|
+
(?=.*[a-z]) # Must contain a lowercase character
|
53
|
+
(?=.*[A-Z]) # Must contain an uppercase character
|
54
|
+
(?=.*[!"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~]) # Must contain a special character
|
55
|
+
}x
|
56
|
+
end
|
57
|
+
|
58
|
+
def password_length
|
59
|
+
@password_length || (8..128)
|
60
|
+
end
|
61
|
+
|
62
|
+
def email_regexp
|
63
|
+
# /\A[^@\s]+@([^@\s]+\.)+[^@\W]+\z/
|
64
|
+
URI::MailTo::EMAIL_REGEXP
|
65
|
+
end
|
66
|
+
|
67
|
+
def phone_regexp
|
68
|
+
/\A[\d\s+\-#*@&.,()]+\z/
|
69
|
+
end
|
70
|
+
|
71
|
+
def phone_length
|
72
|
+
@phone_length || (3..40)
|
73
|
+
end
|
74
|
+
|
75
|
+
def money_regexp
|
76
|
+
/[+\-]?[\d.,]+/
|
77
|
+
end
|
78
|
+
|
79
|
+
def url_regexp
|
80
|
+
%r{
|
81
|
+
\A
|
82
|
+
(?:(?:https?|ftp)://) # scheme
|
83
|
+
(?:\S+(?::\S*)?@)? # user:pass authentication
|
84
|
+
(?:
|
85
|
+
(?!10(?:\.\d{1,3}){3}) # IP address exclusion
|
86
|
+
(?!127(?:\.\d{1,3}){3}) # private & local networks
|
87
|
+
(?!169\.254(?:\.\d{1,3}){2})
|
88
|
+
(?!192\.168(?:\.\d{1,3}){2})
|
89
|
+
(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})
|
90
|
+
(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3]) # IP address dotted notation octets
|
91
|
+
(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2} # excludes loopback network 0.0.0.0, reserved space >= 224.0.0.0, network & broacast addresses
|
92
|
+
(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4])) # (first & last IP address of each class)
|
93
|
+
|
|
94
|
+
(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+) # host name
|
95
|
+
(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)* # domain name
|
96
|
+
(?:\.(?:[a-z\u00a1-\uffff]{2,})) # TLD identifier
|
97
|
+
)
|
98
|
+
(?::\d{2,5})? # port number
|
99
|
+
(?:/\S*)? # resource path
|
100
|
+
\z
|
101
|
+
}xi
|
102
|
+
end
|
103
|
+
|
104
|
+
def access_token_ttl
|
105
|
+
@access_token_ttl || 30.minutes
|
106
|
+
end
|
107
|
+
|
108
|
+
def refresh_token_ttl
|
109
|
+
@refresh_token_ttl || 1.week
|
110
|
+
end
|
111
|
+
|
112
|
+
def invite_token_ttl
|
113
|
+
@invite_token_ttl || 24.hours
|
114
|
+
end
|
115
|
+
|
116
|
+
def invite_token_length
|
117
|
+
@invite_token_length || 48
|
118
|
+
end
|
119
|
+
|
120
|
+
def confirm_email_token_ttl
|
121
|
+
@confirm_email_token_ttl || 24.hours
|
122
|
+
end
|
123
|
+
|
124
|
+
def confirm_email_token_length
|
125
|
+
@confirm_email_token_length || 48
|
126
|
+
end
|
127
|
+
|
128
|
+
def reset_password_token_ttl
|
129
|
+
@reset_password_token_ttl || 60.minutes
|
130
|
+
end
|
131
|
+
|
132
|
+
def reset_password_token_length
|
133
|
+
@reset_password_token_length || 48
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: authonomy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ilya Konyukhov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-09-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bcrypt
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: jwt
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: Flexible authentication solution for Rails
|
56
|
+
email: ilya@konyukhov.com
|
57
|
+
executables: []
|
58
|
+
extensions: []
|
59
|
+
extra_rdoc_files: []
|
60
|
+
files:
|
61
|
+
- LICENSE
|
62
|
+
- README.md
|
63
|
+
- lib/authonomy.rb
|
64
|
+
- lib/authonomy/authenticator.rb
|
65
|
+
- lib/authonomy/encryptor.rb
|
66
|
+
- lib/authonomy/token_generator.rb
|
67
|
+
- lib/authonomy/version.rb
|
68
|
+
homepage: https://github.com/ilkon/authonomy
|
69
|
+
licenses:
|
70
|
+
- MIT
|
71
|
+
metadata:
|
72
|
+
rubygems_mfa_required: 'true'
|
73
|
+
homepage_uri: https://github.com/ilkon/authonomy
|
74
|
+
documentation_uri: https://rubydoc.info/github/ilkon/authonomy
|
75
|
+
changelog_uri: https://github.com/ilkon/authonomy/blob/main/CHANGELOG.md
|
76
|
+
source_code_uri: https://github.com/ilkon/authonomy
|
77
|
+
bug_tracker_uri: https://github.com/ilkon/authonomy/issues
|
78
|
+
wiki_uri: https://github.com/ilkon/authonomy/wiki
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
require_paths:
|
82
|
+
- lib
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: 2.1.0
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
requirements: []
|
94
|
+
rubygems_version: 3.3.7
|
95
|
+
signing_key:
|
96
|
+
specification_version: 4
|
97
|
+
summary: Flexible authentication solution for Rails
|
98
|
+
test_files: []
|