authonomy 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|