authonomy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,2 @@
1
+ # authonomy
2
+ The authentication library for Rails framework
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Authonomy
4
+ VERSION = '1.0.0'
5
+ 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: []