foobara-auth 0.0.1 → 0.0.3
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/CHANGELOG.md +9 -0
- data/README.md +7 -0
- data/src/approve_token.rb +21 -0
- data/src/build_secret.rb +46 -0
- data/src/create_api_key.rb +18 -56
- data/src/create_token.rb +111 -0
- data/src/create_user.rb +19 -1
- data/src/find_user.rb +41 -0
- data/src/login.rb +130 -0
- data/src/refresh_login.rb +129 -0
- data/src/register.rb +41 -0
- data/src/set_password.rb +5 -7
- data/src/types/{password.rb → secret.rb} +2 -2
- data/src/types/token/state.rb +11 -0
- data/src/types/{api_key → token}/state_machine.rb +6 -4
- data/src/types/token.rb +43 -0
- data/src/types/user.rb +5 -4
- data/src/verify_access_token.rb +61 -0
- data/src/verify_password.rb +7 -6
- data/src/verify_secret.rb +30 -0
- data/src/verify_token.rb +104 -0
- metadata +32 -11
- data/src/approve_api_key.rb +0 -21
- data/src/build_password.rb +0 -46
- data/src/types/api_key/state.rb +0 -11
- data/src/types/api_key.rb +0 -28
- data/src/verify_api_key.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 451c9723c4313a390e97e6fe35bdd3bc206884d56ce2e79be20fc0f79bf3009a
|
4
|
+
data.tar.gz: b22db603ba6809fc5dca30a2f8e399f9566dbad2d01efbc79d6fe7e9e703176b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26d1575261570eac9162959e3724043a018961ecaa6fcde48739722800ee18a7e4c584756d7d3b83e81b3cc6cb96b60785d084c7726d93636464d7356e3d2c01
|
7
|
+
data.tar.gz: a641df324a05554c870d812ead5a7d632b8b135419aede674e785c9f5dd7ecfb3fe2482dda39c96feb919da0e9cc5f7beb9b37f803c4a0e055b81122c58ffe5e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## [0.0.3] - 2025-03-28
|
2
|
+
|
3
|
+
- Allow not having an email
|
4
|
+
|
5
|
+
## [0.0.2] - 2025-03-21
|
6
|
+
|
7
|
+
- Implement/test lots of basic auth behavior such as
|
8
|
+
registering/login access tokens/login refresh tokens/api keys/passwords
|
9
|
+
|
1
10
|
## [0.0.1] - 2025-03-08
|
2
11
|
|
3
12
|
- Release as a gem
|
data/README.md
CHANGED
@@ -22,3 +22,10 @@ at https://github.com/foobara/auth
|
|
22
22
|
|
23
23
|
This project is dual licensed under your choice of the Apache-2.0 license and the MIT license.
|
24
24
|
Please see LICENSE.txt for more info.
|
25
|
+
|
26
|
+
## Concepts
|
27
|
+
|
28
|
+
1. token: a string of the form <token_id>-<token_secret>
|
29
|
+
2. password: a string
|
30
|
+
3. secret: either a password or a token_secret. Never stored in the database
|
31
|
+
4. hashed_secret: value stored in the database that can be checked against the secret passed in.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Foobara
|
2
|
+
module Auth
|
3
|
+
class ApproveToken < Foobara::Command
|
4
|
+
inputs do
|
5
|
+
token_record Types::Token, :required
|
6
|
+
end
|
7
|
+
|
8
|
+
result Types::Token
|
9
|
+
|
10
|
+
def execute
|
11
|
+
approve_token
|
12
|
+
|
13
|
+
token_record
|
14
|
+
end
|
15
|
+
|
16
|
+
def approve_token
|
17
|
+
token_record.approve!
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/src/build_secret.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require "argon2"
|
2
|
+
|
3
|
+
module Foobara
|
4
|
+
module Auth
|
5
|
+
class BuildSecret < Foobara::Command
|
6
|
+
inputs do
|
7
|
+
secret :string, :required, :sensitive_exposed
|
8
|
+
end
|
9
|
+
result Types::Secret, :sensitive
|
10
|
+
|
11
|
+
def execute
|
12
|
+
generate_hashed_secret
|
13
|
+
build_secret
|
14
|
+
|
15
|
+
secret_model
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_accessor :hashed_secret, :secret_model
|
19
|
+
|
20
|
+
def generate_hashed_secret
|
21
|
+
self.hashed_secret = Argon2::Password.create(secret, **argon_params)
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_secret
|
25
|
+
self.secret_model = Types::Secret.new(
|
26
|
+
hashed_secret:,
|
27
|
+
parameters: argon_params.merge(other_params),
|
28
|
+
created_at: Time.now
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def argon_params
|
33
|
+
{
|
34
|
+
t_cost: 2,
|
35
|
+
m_cost: 16,
|
36
|
+
parallelism: 1,
|
37
|
+
type: :argon2id
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def other_params
|
42
|
+
{ method: :argon2id }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/src/create_api_key.rb
CHANGED
@@ -1,77 +1,39 @@
|
|
1
1
|
require "securerandom"
|
2
2
|
require "base64"
|
3
3
|
|
4
|
-
require_relative "
|
4
|
+
require_relative "build_secret"
|
5
|
+
require_relative "create_token"
|
5
6
|
|
6
7
|
module Foobara
|
7
8
|
module Auth
|
8
9
|
class CreateApiKey < Foobara::Command
|
9
|
-
|
10
|
+
inputs do
|
11
|
+
user Types::User, :required
|
12
|
+
needs_approval :boolean, default: false
|
13
|
+
end
|
14
|
+
result :string, :sensitive_exposed
|
10
15
|
|
11
|
-
depends_on
|
12
|
-
depends_on_entity Types::
|
16
|
+
depends_on CreateToken
|
17
|
+
depends_on_entity Types::Token
|
13
18
|
|
14
19
|
def execute
|
15
|
-
|
16
|
-
|
17
|
-
generate_key_for_user
|
18
|
-
generate_hashed_key
|
19
|
-
create_api_key
|
20
|
-
prepend_prefix
|
20
|
+
create_token
|
21
|
+
associate_token_with_user
|
21
22
|
|
22
23
|
key_for_user
|
23
24
|
end
|
24
25
|
|
25
|
-
attr_accessor :
|
26
|
-
|
27
|
-
def generate_prefix
|
28
|
-
bytes = SecureRandom.hex(prefix_bytes)
|
29
|
-
self.prefix = Base64.strict_encode64(bytes)[0..prefix_length - 1]
|
30
|
-
end
|
31
|
-
|
32
|
-
def generate_raw_key
|
33
|
-
self.raw_key = SecureRandom.hex(bytes)
|
34
|
-
end
|
35
|
-
|
36
|
-
def bytes
|
37
|
-
24
|
38
|
-
end
|
39
|
-
|
40
|
-
def generate_key_for_user
|
41
|
-
self.key_for_user = Base64.strict_encode64(raw_key)
|
42
|
-
end
|
43
|
-
|
44
|
-
def generate_hashed_key
|
45
|
-
password = run_subcommand!(BuildPassword, plaintext_password: key_for_user)
|
46
|
-
self.hashed_key = password.hashed_password
|
47
|
-
self.build_password_params = password.parameters
|
48
|
-
end
|
49
|
-
|
50
|
-
def create_api_key
|
51
|
-
Types::ApiKey.create(
|
52
|
-
hashed_token: hashed_key,
|
53
|
-
prefix: prefix,
|
54
|
-
token_length: key_for_user.length,
|
55
|
-
token_parameters: build_password_params.merge(other_params),
|
56
|
-
expires_at: nil,
|
57
|
-
created_at: Time.now
|
58
|
-
)
|
59
|
-
end
|
60
|
-
|
61
|
-
def prepend_prefix
|
62
|
-
self.key_for_user = "#{prefix}#{key_for_user}"
|
63
|
-
end
|
26
|
+
attr_accessor :token, :key_for_user
|
64
27
|
|
65
|
-
def
|
66
|
-
|
67
|
-
end
|
28
|
+
def create_token
|
29
|
+
result = run_subcommand!(CreateToken, needs_approval:)
|
68
30
|
|
69
|
-
|
70
|
-
|
31
|
+
self.token = result[:token_record]
|
32
|
+
self.key_for_user = result[:token_string]
|
71
33
|
end
|
72
34
|
|
73
|
-
def
|
74
|
-
|
35
|
+
def associate_token_with_user
|
36
|
+
user.api_keys = [*user.api_keys, token]
|
75
37
|
end
|
76
38
|
end
|
77
39
|
end
|
data/src/create_token.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require "securerandom"
|
2
|
+
require "base64"
|
3
|
+
|
4
|
+
require_relative "build_secret"
|
5
|
+
|
6
|
+
module Foobara
|
7
|
+
module Auth
|
8
|
+
class CreateToken < Foobara::Command
|
9
|
+
inputs do
|
10
|
+
expires_at :datetime
|
11
|
+
token_group :string
|
12
|
+
needs_approval :boolean, default: false
|
13
|
+
end
|
14
|
+
|
15
|
+
result do
|
16
|
+
token_string :string, :required, :sensitive
|
17
|
+
token_record Types::Token, :required, :sensitive
|
18
|
+
end
|
19
|
+
|
20
|
+
depends_on BuildSecret
|
21
|
+
depends_on_entity Types::Token
|
22
|
+
|
23
|
+
def execute
|
24
|
+
generate_token_secret
|
25
|
+
generate_hashed_secret
|
26
|
+
generate_token_id
|
27
|
+
construct_token_string
|
28
|
+
create_token_record
|
29
|
+
|
30
|
+
unless needs_approval?
|
31
|
+
activate_token
|
32
|
+
end
|
33
|
+
|
34
|
+
token_string_and_record
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_accessor :token_id, :token_secret, :token_string, :hashed_secret, :build_password_params, :token_record
|
38
|
+
|
39
|
+
def generate_token_id
|
40
|
+
bytes = SecureRandom.random_bytes(token_id_bytes)
|
41
|
+
|
42
|
+
begin
|
43
|
+
token_id = Base64.strict_encode64(bytes)
|
44
|
+
end while Types::Token.exists?(token_id)
|
45
|
+
|
46
|
+
self.token_id = token_id
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate_token_secret
|
50
|
+
bytes = SecureRandom.random_bytes(secret_bytes)
|
51
|
+
self.token_secret = Base64.strict_encode64(bytes)
|
52
|
+
end
|
53
|
+
|
54
|
+
def secret_bytes
|
55
|
+
32
|
56
|
+
end
|
57
|
+
|
58
|
+
def generate_hashed_secret
|
59
|
+
secret = run_subcommand!(BuildSecret, secret: token_secret)
|
60
|
+
self.hashed_secret = secret.hashed_secret
|
61
|
+
self.build_password_params = secret.parameters
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_token_record
|
65
|
+
attributes = {
|
66
|
+
hashed_secret:,
|
67
|
+
id: token_id,
|
68
|
+
token_parameters: build_password_params.merge(other_params),
|
69
|
+
created_at: Time.now
|
70
|
+
}
|
71
|
+
|
72
|
+
if expires_at
|
73
|
+
attributes[:expires_at] = expires_at
|
74
|
+
end
|
75
|
+
|
76
|
+
if token_group
|
77
|
+
attributes[:token_group] = token_group
|
78
|
+
end
|
79
|
+
|
80
|
+
self.token_record = Types::Token.create(attributes)
|
81
|
+
end
|
82
|
+
|
83
|
+
def needs_approval?
|
84
|
+
needs_approval
|
85
|
+
end
|
86
|
+
|
87
|
+
def activate_token
|
88
|
+
token_record.approve!
|
89
|
+
end
|
90
|
+
|
91
|
+
def construct_token_string
|
92
|
+
self.token_string = "#{token_id}_#{token_secret}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def token_string_and_record
|
96
|
+
{
|
97
|
+
token_string:,
|
98
|
+
token_record:
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def other_params
|
103
|
+
{ secret_bytes:, token_id_bytes: }
|
104
|
+
end
|
105
|
+
|
106
|
+
def token_id_bytes
|
107
|
+
6
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/src/create_user.rb
CHANGED
@@ -3,18 +3,36 @@ module Foobara
|
|
3
3
|
class CreateUser < Foobara::Command
|
4
4
|
inputs Types::User.attributes_for_create
|
5
5
|
|
6
|
+
add_inputs do
|
7
|
+
plaintext_password :string, :sensitive_exposed
|
8
|
+
end
|
9
|
+
|
6
10
|
result Types::User
|
7
11
|
|
12
|
+
depends_on SetPassword
|
13
|
+
|
8
14
|
def execute
|
9
15
|
create_user
|
10
16
|
|
17
|
+
if password_present?
|
18
|
+
set_password
|
19
|
+
end
|
20
|
+
|
11
21
|
user
|
12
22
|
end
|
13
23
|
|
14
24
|
attr_accessor :user
|
15
25
|
|
16
26
|
def create_user
|
17
|
-
self.user = Types::User.create(inputs)
|
27
|
+
self.user = Types::User.create(inputs.except(:plaintext_password))
|
28
|
+
end
|
29
|
+
|
30
|
+
def password_present?
|
31
|
+
plaintext_password && !plaintext_password.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_password
|
35
|
+
run_subcommand!(SetPassword, user:, plaintext_password:)
|
18
36
|
end
|
19
37
|
end
|
20
38
|
end
|
data/src/find_user.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Foobara
|
2
|
+
module Auth
|
3
|
+
class FindUser < Foobara::Command
|
4
|
+
class UserNotFoundError < Foobara::RuntimeError
|
5
|
+
context do
|
6
|
+
id :integer
|
7
|
+
username :string
|
8
|
+
email :email
|
9
|
+
end
|
10
|
+
|
11
|
+
def message
|
12
|
+
"No user found for #{context}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
inputs do
|
17
|
+
id :integer
|
18
|
+
username :string
|
19
|
+
email :email
|
20
|
+
end
|
21
|
+
|
22
|
+
result Types::User
|
23
|
+
|
24
|
+
def execute
|
25
|
+
load_user
|
26
|
+
|
27
|
+
user
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_accessor :user
|
31
|
+
|
32
|
+
def load_user
|
33
|
+
self.user = Types::User.find_by(inputs)
|
34
|
+
|
35
|
+
unless user
|
36
|
+
add_runtime_error(UserNotFoundError.new(context: inputs))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/src/login.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require "jwt"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
require_relative "create_token"
|
5
|
+
require_relative "verify_password"
|
6
|
+
require_relative "verify_token"
|
7
|
+
|
8
|
+
module Foobara
|
9
|
+
module Auth
|
10
|
+
class Login < Foobara::Command
|
11
|
+
class InvalidPasswordError < Foobara::RuntimeError
|
12
|
+
context({})
|
13
|
+
message "Invalid password"
|
14
|
+
end
|
15
|
+
|
16
|
+
class NoUserIdEmailOrUsernameGivenError < Foobara::RuntimeError
|
17
|
+
context({})
|
18
|
+
message "No user id, email, or username given"
|
19
|
+
end
|
20
|
+
|
21
|
+
depends_on CreateToken, VerifyPassword, FindUser
|
22
|
+
|
23
|
+
inputs do
|
24
|
+
user Types::User, :allow_nil
|
25
|
+
username :string, :allow_nil
|
26
|
+
email :string, :allow_nil
|
27
|
+
username_or_email :string, :allow_nil
|
28
|
+
plaintext_password :string, :required, :sensitive_exposed
|
29
|
+
# Configure these instead of defaulting them here?
|
30
|
+
token_ttl :integer, default: 30 * 60
|
31
|
+
refresh_token_ttl :integer, default: 7 * 24 * 60 * 60
|
32
|
+
end
|
33
|
+
|
34
|
+
result do
|
35
|
+
access_token :string, :required, :sensitive_exposed
|
36
|
+
refresh_token :string, :required, :sensitive_exposed
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute
|
40
|
+
find_user_to_login
|
41
|
+
verify_password
|
42
|
+
# TODO: DRY these 5 up
|
43
|
+
determine_timestamps
|
44
|
+
generate_access_token
|
45
|
+
determine_token_group
|
46
|
+
generate_new_refresh_token
|
47
|
+
save_new_refresh_token_on_user
|
48
|
+
|
49
|
+
tokens
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_accessor :access_token, :new_refresh_token, :now, :expires_at, :token_group, :user_to_login
|
53
|
+
|
54
|
+
def find_user_to_login
|
55
|
+
if user
|
56
|
+
self.user_to_login = user
|
57
|
+
elsif username
|
58
|
+
self.user_to_login = run_subcommand!(FindUser, username:)
|
59
|
+
elsif email
|
60
|
+
self.user_to_login = run_subcommand!(FindUser, email:)
|
61
|
+
elsif username_or_email
|
62
|
+
begin
|
63
|
+
self.user_to_login = run_subcommand!(FindUser, username: username_or_email)
|
64
|
+
rescue Halt
|
65
|
+
# I'm a bit nervous about rescuing Halt and clearing the errors, but I'm more nervous bout
|
66
|
+
# introducing a #run_subcommand method.
|
67
|
+
if error_collection.size == 1 && error_collection.errors.first.is_a?(FindUser::UserNotFoundError) &&
|
68
|
+
username_or_email.include?("@")
|
69
|
+
error_collection.clear
|
70
|
+
self.user_to_login = run_subcommand!(FindUser, email: username_or_email)
|
71
|
+
else
|
72
|
+
raise
|
73
|
+
end
|
74
|
+
end
|
75
|
+
else
|
76
|
+
add_runtime_error(NoUserIdEmailOrUsernameGivenError)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def verify_password
|
81
|
+
unless run_subcommand!(VerifyPassword, user: user_to_login, plaintext_password:)
|
82
|
+
add_runtime_error(InvalidPasswordError)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def determine_timestamps
|
87
|
+
self.now = Time.now
|
88
|
+
self.expires_at = now + token_ttl
|
89
|
+
end
|
90
|
+
|
91
|
+
def generate_access_token
|
92
|
+
payload = { sub: user_to_login.id, exp: expires_at.to_i }
|
93
|
+
|
94
|
+
self.access_token = JWT.encode(payload, jwt_secret, "HS256")
|
95
|
+
end
|
96
|
+
|
97
|
+
def jwt_secret
|
98
|
+
jwt_secret_text = ENV.fetch("JWT_SECRET", nil)
|
99
|
+
|
100
|
+
unless jwt_secret_text
|
101
|
+
# :nocov:
|
102
|
+
raise "You must set the JWT_SECRET environment variable"
|
103
|
+
# :nocov:
|
104
|
+
end
|
105
|
+
|
106
|
+
jwt_secret_text
|
107
|
+
end
|
108
|
+
|
109
|
+
def determine_token_group
|
110
|
+
self.token_group = SecureRandom.uuid
|
111
|
+
end
|
112
|
+
|
113
|
+
def generate_new_refresh_token
|
114
|
+
self.new_refresh_token = run_subcommand!(CreateToken, expires_at:, token_group:)
|
115
|
+
end
|
116
|
+
|
117
|
+
def save_new_refresh_token_on_user
|
118
|
+
# TODO: maybe override #<< on these objects to dirty the entity??
|
119
|
+
user_to_login.refresh_tokens += [*user_to_login.refresh_tokens, new_refresh_token[:token_record]]
|
120
|
+
end
|
121
|
+
|
122
|
+
def tokens
|
123
|
+
{
|
124
|
+
access_token:,
|
125
|
+
refresh_token: new_refresh_token[:token_string]
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require "jwt"
|
2
|
+
require "securerandom"
|
3
|
+
|
4
|
+
require_relative "create_token"
|
5
|
+
require_relative "verify_token"
|
6
|
+
|
7
|
+
module Foobara
|
8
|
+
module Auth
|
9
|
+
class RefreshLogin < Foobara::Command
|
10
|
+
class InvalidRefreshTokenError < Foobara::RuntimeError
|
11
|
+
context refresh_token_id: :string
|
12
|
+
message "Invalid refresh token"
|
13
|
+
end
|
14
|
+
|
15
|
+
class RefreshTokenNotOwnedByUser < Foobara::RuntimeError
|
16
|
+
context refresh_token_id: :string
|
17
|
+
message "This refresh token is not owned by this user"
|
18
|
+
end
|
19
|
+
|
20
|
+
depends_on CreateToken, VerifyToken
|
21
|
+
|
22
|
+
inputs do
|
23
|
+
user Types::User, :required
|
24
|
+
refresh_token_text :string, :required, :sensitive
|
25
|
+
# Can we get these TTLs off of the refresh token?
|
26
|
+
token_ttl :integer, default: 30 * 60
|
27
|
+
refresh_token_ttl :integer, default: 7 * 24 * 60 * 60
|
28
|
+
end
|
29
|
+
|
30
|
+
result do
|
31
|
+
access_token :string, :required, :sensitive_exposed
|
32
|
+
refresh_token :string, :required, :sensitive_exposed
|
33
|
+
end
|
34
|
+
|
35
|
+
def execute
|
36
|
+
determine_refresh_token_id_and_secret
|
37
|
+
load_refresh_token
|
38
|
+
validate_refresh_token_belongs_to_user
|
39
|
+
verify_refresh_token
|
40
|
+
# Delete it instead maybe?
|
41
|
+
mark_refresh_token_as_used
|
42
|
+
determine_timestamps
|
43
|
+
determine_token_group
|
44
|
+
|
45
|
+
generate_access_token
|
46
|
+
generate_new_refresh_token
|
47
|
+
save_new_refresh_token_on_user
|
48
|
+
|
49
|
+
tokens
|
50
|
+
end
|
51
|
+
|
52
|
+
attr_accessor :access_token, :new_refresh_token, :now, :expires_at, :refresh_token,
|
53
|
+
:refresh_token_id, :refresh_token_secret, :token_group
|
54
|
+
|
55
|
+
def determine_refresh_token_id_and_secret
|
56
|
+
self.refresh_token_id, self.refresh_token_secret = refresh_token_text.split("_")
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_refresh_token
|
60
|
+
self.refresh_token = Types::Token.load(refresh_token_id)
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_refresh_token_belongs_to_user
|
64
|
+
unless user.refresh_tokens.any? { |token| token.id == refresh_token_id }
|
65
|
+
add_runtime_error(RefreshTokenNotOwnedByUser.new(context: { refresh_token_id: }))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def verify_refresh_token
|
70
|
+
valid = run_subcommand!(VerifyToken, token_string: refresh_token_text)
|
71
|
+
|
72
|
+
unless valid[:verified]
|
73
|
+
add_runtime_error(InvalidRefreshTokenError.new(context: { refresh_token_id: }))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def mark_refresh_token_as_used
|
78
|
+
refresh_token.use_up!
|
79
|
+
end
|
80
|
+
|
81
|
+
def determine_timestamps
|
82
|
+
self.now = Time.now
|
83
|
+
self.expires_at = now + token_ttl
|
84
|
+
end
|
85
|
+
|
86
|
+
def generate_access_token
|
87
|
+
payload = {
|
88
|
+
user_id: user.id,
|
89
|
+
username: user.username,
|
90
|
+
exp: expires_at.to_i
|
91
|
+
}
|
92
|
+
|
93
|
+
self.access_token = JWT.encode(payload, jwt_secret, "HS256")
|
94
|
+
end
|
95
|
+
|
96
|
+
def jwt_secret
|
97
|
+
jwt_secret_text = ENV.fetch("JWT_SECRET", nil)
|
98
|
+
|
99
|
+
unless jwt_secret_text
|
100
|
+
# :nocov:
|
101
|
+
raise "You must set the JWT_SECRET environment variable"
|
102
|
+
# :nocov:
|
103
|
+
end
|
104
|
+
|
105
|
+
jwt_secret_text
|
106
|
+
end
|
107
|
+
|
108
|
+
def determine_token_group
|
109
|
+
self.token_group = refresh_token&.token_group || SecureRandom.uuid
|
110
|
+
end
|
111
|
+
|
112
|
+
def generate_new_refresh_token
|
113
|
+
self.new_refresh_token = run_subcommand!(CreateToken, expires_at:, token_group:)
|
114
|
+
end
|
115
|
+
|
116
|
+
def save_new_refresh_token_on_user
|
117
|
+
# TODO: maybe override #<< on these objects to dirty the entity??
|
118
|
+
user.refresh_tokens += [*user.refresh_tokens, new_refresh_token[:token_record]]
|
119
|
+
end
|
120
|
+
|
121
|
+
def tokens
|
122
|
+
{
|
123
|
+
access_token:,
|
124
|
+
refresh_token: new_refresh_token[:token_string]
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/src/register.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative "types/user"
|
2
|
+
|
3
|
+
module Foobara
|
4
|
+
module Auth
|
5
|
+
class Register < Foobara::Command
|
6
|
+
depends_on CreateUser, SetPassword
|
7
|
+
|
8
|
+
inputs do
|
9
|
+
username :string, :required
|
10
|
+
email :email, :allow_nil
|
11
|
+
plaintext_password :string, :allow_nil, :sensitive_exposed
|
12
|
+
end
|
13
|
+
|
14
|
+
result Types::User
|
15
|
+
|
16
|
+
def execute
|
17
|
+
create_user
|
18
|
+
|
19
|
+
if password?
|
20
|
+
set_password
|
21
|
+
end
|
22
|
+
|
23
|
+
user
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_accessor :user
|
27
|
+
|
28
|
+
def create_user
|
29
|
+
self.user = run_subcommand!(CreateUser, username:, email:)
|
30
|
+
end
|
31
|
+
|
32
|
+
def password?
|
33
|
+
plaintext_password && !plaintext_password.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_password
|
37
|
+
run_subcommand!(SetPassword, user:, plaintext_password:)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/src/set_password.rb
CHANGED
@@ -1,15 +1,13 @@
|
|
1
|
-
require "argon2"
|
2
|
-
|
3
1
|
module Foobara
|
4
2
|
module Auth
|
5
3
|
class SetPassword < Foobara::Command
|
6
4
|
inputs do
|
7
5
|
user Types::User
|
8
|
-
plaintext_password :string, :required
|
6
|
+
plaintext_password :string, :required, :sensitive_exposed
|
9
7
|
end
|
10
8
|
result Types::User
|
11
9
|
|
12
|
-
depends_on
|
10
|
+
depends_on BuildSecret
|
13
11
|
|
14
12
|
# TODO: should we enforce certain password requirements?
|
15
13
|
def execute
|
@@ -19,14 +17,14 @@ module Foobara
|
|
19
17
|
user
|
20
18
|
end
|
21
19
|
|
22
|
-
attr_accessor :
|
20
|
+
attr_accessor :password_secret
|
23
21
|
|
24
22
|
def build_password
|
25
|
-
self.
|
23
|
+
self.password_secret = run_subcommand!(BuildSecret, secret: plaintext_password)
|
26
24
|
end
|
27
25
|
|
28
26
|
def set_password_on_user
|
29
|
-
user.
|
27
|
+
user.password_secret = password_secret
|
30
28
|
end
|
31
29
|
end
|
32
30
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module Foobara
|
2
2
|
module Auth
|
3
3
|
module Types
|
4
|
-
class
|
4
|
+
class Secret < Foobara::Model
|
5
5
|
attributes do
|
6
|
-
|
6
|
+
hashed_secret :string, :required, :sensitive
|
7
7
|
parameters :duck, :required
|
8
8
|
created_at :datetime, :required
|
9
9
|
end
|
@@ -1,15 +1,17 @@
|
|
1
1
|
module Foobara
|
2
2
|
module Auth
|
3
3
|
module Types
|
4
|
-
class
|
4
|
+
class Token < Foobara::Entity
|
5
5
|
class StateMachine < Foobara::StateMachine
|
6
6
|
set_transition_map({
|
7
7
|
needs_approval: {
|
8
|
-
approve: :
|
8
|
+
approve: :active,
|
9
9
|
reject: :rejected
|
10
10
|
},
|
11
|
-
|
12
|
-
revoke: :revoked
|
11
|
+
active: {
|
12
|
+
revoke: :revoked,
|
13
|
+
use_up: :inactive,
|
14
|
+
expire: :expired
|
13
15
|
}
|
14
16
|
})
|
15
17
|
end
|
data/src/types/token.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Foobara
|
2
|
+
module Auth
|
3
|
+
module Types
|
4
|
+
class Token < Foobara::Entity
|
5
|
+
attributes do
|
6
|
+
id :string, :required
|
7
|
+
hashed_secret :string, :required, :sensitive
|
8
|
+
state :token_state, :required, default: :needs_approval
|
9
|
+
token_parameters :duck, :required
|
10
|
+
token_group :string, :allow_nil
|
11
|
+
expires_at :datetime, :allow_nil
|
12
|
+
created_at :datetime, :required
|
13
|
+
end
|
14
|
+
|
15
|
+
primary_key :id
|
16
|
+
|
17
|
+
def state_machine
|
18
|
+
@state_machine ||= StateMachine.new(owner: self, target_attribute: :state)
|
19
|
+
end
|
20
|
+
|
21
|
+
def approve!
|
22
|
+
state_machine.approve!
|
23
|
+
end
|
24
|
+
|
25
|
+
def use_up!
|
26
|
+
state_machine.use_up!
|
27
|
+
end
|
28
|
+
|
29
|
+
def expire!
|
30
|
+
state_machine.expire!
|
31
|
+
end
|
32
|
+
|
33
|
+
def inactive?
|
34
|
+
state_machine.current_state == State::INACTIVE
|
35
|
+
end
|
36
|
+
|
37
|
+
def active?
|
38
|
+
state_machine.current_state == State::ACTIVE
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/src/types/user.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require_relative "role"
|
2
|
-
require_relative "
|
2
|
+
require_relative "secret"
|
3
3
|
|
4
4
|
module Foobara
|
5
5
|
module Auth
|
@@ -8,10 +8,11 @@ module Foobara
|
|
8
8
|
attributes do
|
9
9
|
id :integer
|
10
10
|
username :string, :required
|
11
|
-
email :email, :
|
11
|
+
email :email, :allow_nil
|
12
12
|
roles [Types::Role], default: []
|
13
|
-
|
14
|
-
|
13
|
+
api_keys [Types::Token], :sensitive, default: []
|
14
|
+
refresh_tokens [Types::Token], :sensitive, default: []
|
15
|
+
password_secret Types::Secret, :sensitive, :allow_nil
|
15
16
|
end
|
16
17
|
|
17
18
|
primary_key :id
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "jwt"
|
2
|
+
|
3
|
+
module Foobara
|
4
|
+
module Auth
|
5
|
+
class VerifyAccessToken < Foobara::Command
|
6
|
+
inputs do
|
7
|
+
# TODO: we should add a processor that flags an attribute as sensitive so we can scrub
|
8
|
+
access_token :string, :required, :sensitive
|
9
|
+
end
|
10
|
+
|
11
|
+
result do
|
12
|
+
verified :boolean, :required
|
13
|
+
failure_reason :string, :allow_nil, one_of: %w[invalid expired]
|
14
|
+
payload :associative_array, :allow_nil
|
15
|
+
headers :associative_array, :allow_nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def execute
|
19
|
+
decode_access_token
|
20
|
+
set_verified_flag
|
21
|
+
|
22
|
+
verified_flag_payload_and_headers
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_accessor :verified, :payload, :headers, :failure_reason
|
26
|
+
|
27
|
+
def decode_access_token
|
28
|
+
self.payload, self.headers = JWT.decode(access_token, jwt_secret)
|
29
|
+
rescue JWT::VerificationError
|
30
|
+
self.failure_reason = "invalid"
|
31
|
+
rescue JWT::ExpiredSignature
|
32
|
+
self.failure_reason = "expired"
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_verified_flag
|
36
|
+
self.verified = !failure_reason
|
37
|
+
end
|
38
|
+
|
39
|
+
def verified_flag_payload_and_headers
|
40
|
+
{
|
41
|
+
verified:,
|
42
|
+
failure_reason:,
|
43
|
+
payload:,
|
44
|
+
headers:
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def jwt_secret
|
49
|
+
jwt_secret_text = ENV.fetch("JWT_SECRET", nil)
|
50
|
+
|
51
|
+
unless jwt_secret_text
|
52
|
+
# :nocov:
|
53
|
+
raise "You must set the JWT_SECRET environment variable"
|
54
|
+
# :nocov:
|
55
|
+
end
|
56
|
+
|
57
|
+
jwt_secret_text
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/src/verify_password.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
require_relative "verify_token"
|
2
2
|
|
3
3
|
module Foobara
|
4
4
|
module Auth
|
@@ -6,11 +6,11 @@ module Foobara
|
|
6
6
|
inputs do
|
7
7
|
user Types::User, :required
|
8
8
|
# TODO: we should add a processor that flags an attribute as sensitive so we can scrub
|
9
|
-
plaintext_password :string, :required
|
9
|
+
plaintext_password :string, :required, :sensitive
|
10
10
|
end
|
11
11
|
result :boolean
|
12
12
|
|
13
|
-
|
13
|
+
depends_on VerifySecret
|
14
14
|
|
15
15
|
def execute
|
16
16
|
# TODO: result in error if no password set yet?
|
@@ -26,9 +26,10 @@ module Foobara
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def check_for_valid_password
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
hashed_secret = user.password_secret.hashed_secret
|
30
|
+
|
31
|
+
self.valid_password = if hashed_secret
|
32
|
+
run_subcommand!(VerifySecret, secret: plaintext_password, hashed_secret:)
|
32
33
|
end
|
33
34
|
end
|
34
35
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "argon2"
|
2
|
+
|
3
|
+
module Foobara
|
4
|
+
module Auth
|
5
|
+
class VerifySecret < Foobara::Command
|
6
|
+
inputs do
|
7
|
+
secret :string, :required # TODO: we should add a processor that flags an attribute as sensitive so we can scrub
|
8
|
+
hashed_secret :string, :required, :sensitive
|
9
|
+
end
|
10
|
+
|
11
|
+
result :boolean
|
12
|
+
|
13
|
+
def execute
|
14
|
+
verify_secret_against_hashed_secret
|
15
|
+
|
16
|
+
verified?
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_accessor :verified
|
20
|
+
|
21
|
+
def verified?
|
22
|
+
!!verified
|
23
|
+
end
|
24
|
+
|
25
|
+
def verify_secret_against_hashed_secret
|
26
|
+
self.verified = Argon2::Password.verify_password(secret, hashed_secret)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/src/verify_token.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require_relative "verify_secret"
|
2
|
+
|
3
|
+
module Foobara
|
4
|
+
module Auth
|
5
|
+
class VerifyToken < Foobara::Command
|
6
|
+
class InactiveTokenError < Foobara::RuntimeError
|
7
|
+
context do
|
8
|
+
state :token_state, :required
|
9
|
+
end
|
10
|
+
|
11
|
+
def message
|
12
|
+
"Expected token to be active but it is #{context[:state]}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ExpiredTokenError < Foobara::RuntimeError
|
17
|
+
context({})
|
18
|
+
message "Token is expired"
|
19
|
+
end
|
20
|
+
|
21
|
+
inputs do
|
22
|
+
# TODO: we should add a processor that flags an attribute as sensitive so we can scrub
|
23
|
+
token_string :string, :required, :sensitive
|
24
|
+
token_record Types::Token, "Instead of finding a persisted token, check against a specific token record"
|
25
|
+
end
|
26
|
+
|
27
|
+
result do
|
28
|
+
verified :boolean, :required
|
29
|
+
token_record Types::Token, :required
|
30
|
+
end
|
31
|
+
|
32
|
+
depends_on VerifySecret
|
33
|
+
|
34
|
+
def execute
|
35
|
+
determine_token_id_and_hashed_secret
|
36
|
+
|
37
|
+
unless token_record?
|
38
|
+
load_token_record
|
39
|
+
end
|
40
|
+
|
41
|
+
validate_token_is_active
|
42
|
+
validate_token_is_not_expired
|
43
|
+
verify_hashed_secret_against_token_record
|
44
|
+
|
45
|
+
verified_and_token_record
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_accessor :verified, :token_id, :secret
|
49
|
+
attr_writer :token_record_to_verify_against
|
50
|
+
|
51
|
+
def token_record_to_verify_against
|
52
|
+
@token_record_to_verify_against || token_record
|
53
|
+
end
|
54
|
+
|
55
|
+
def token_record?
|
56
|
+
!!token_record
|
57
|
+
end
|
58
|
+
|
59
|
+
def determine_token_id_and_hashed_secret
|
60
|
+
self.token_id, self.secret = token_string.split("_")
|
61
|
+
end
|
62
|
+
|
63
|
+
def load_token_record
|
64
|
+
self.token_record_to_verify_against = Types::Token.load(token_id)
|
65
|
+
# TODO: handle no record found...
|
66
|
+
end
|
67
|
+
|
68
|
+
def verify_hashed_secret_against_token_record
|
69
|
+
hashed_secret = token_record_to_verify_against.hashed_secret
|
70
|
+
|
71
|
+
self.verified = run_subcommand!(VerifySecret, secret:, hashed_secret:)
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_token_is_not_expired
|
75
|
+
if token_record_to_verify_against.expires_at&.<(Time.now)
|
76
|
+
Types::Token.transaction(mode: :open_nested) do
|
77
|
+
token = Types::Token.load(token_record_to_verify_against.id)
|
78
|
+
token.expire!
|
79
|
+
end
|
80
|
+
add_runtime_error(ExpiredTokenError)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def validate_token_is_active
|
85
|
+
unless token_record_to_verify_against.active?
|
86
|
+
add_runtime_error(
|
87
|
+
InactiveTokenError.new(
|
88
|
+
context: {
|
89
|
+
state: token_record_to_verify_against.state_machine.current_state
|
90
|
+
}
|
91
|
+
)
|
92
|
+
)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def verified_and_token_record
|
97
|
+
{
|
98
|
+
verified:,
|
99
|
+
token_record: token_record_to_verify_against
|
100
|
+
}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foobara-auth
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miles Georgi
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-03-
|
10
|
+
date: 2025-03-29 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: argon2
|
@@ -24,7 +24,7 @@ dependencies:
|
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: '0'
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
27
|
+
name: jwt
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
30
|
- - ">="
|
@@ -37,6 +37,20 @@ dependencies:
|
|
37
37
|
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: foobara
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.0.1
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.0.1
|
40
54
|
email:
|
41
55
|
- azimux@gmail.com
|
42
56
|
executables: []
|
@@ -49,20 +63,27 @@ files:
|
|
49
63
|
- LICENSE.txt
|
50
64
|
- README.md
|
51
65
|
- lib/foobara/auth.rb
|
52
|
-
- src/
|
53
|
-
- src/
|
66
|
+
- src/approve_token.rb
|
67
|
+
- src/build_secret.rb
|
54
68
|
- src/create_api_key.rb
|
55
69
|
- src/create_role.rb
|
70
|
+
- src/create_token.rb
|
56
71
|
- src/create_user.rb
|
72
|
+
- src/find_user.rb
|
73
|
+
- src/login.rb
|
74
|
+
- src/refresh_login.rb
|
75
|
+
- src/register.rb
|
57
76
|
- src/set_password.rb
|
58
|
-
- src/types/api_key.rb
|
59
|
-
- src/types/api_key/state.rb
|
60
|
-
- src/types/api_key/state_machine.rb
|
61
|
-
- src/types/password.rb
|
62
77
|
- src/types/role.rb
|
78
|
+
- src/types/secret.rb
|
79
|
+
- src/types/token.rb
|
80
|
+
- src/types/token/state.rb
|
81
|
+
- src/types/token/state_machine.rb
|
63
82
|
- src/types/user.rb
|
64
|
-
- src/
|
83
|
+
- src/verify_access_token.rb
|
65
84
|
- src/verify_password.rb
|
85
|
+
- src/verify_secret.rb
|
86
|
+
- src/verify_token.rb
|
66
87
|
homepage: https://github.com/foobara/auth
|
67
88
|
licenses:
|
68
89
|
- Apache-2.0 OR MIT
|
@@ -85,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
106
|
- !ruby/object:Gem::Version
|
86
107
|
version: '0'
|
87
108
|
requirements: []
|
88
|
-
rubygems_version: 3.6.
|
109
|
+
rubygems_version: 3.6.6
|
89
110
|
specification_version: 4
|
90
111
|
summary: Provides various auth domain commands and models
|
91
112
|
test_files: []
|
data/src/approve_api_key.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
module Foobara
|
2
|
-
module Auth
|
3
|
-
class ApproveApiKey < Foobara::Command
|
4
|
-
inputs do
|
5
|
-
api_key Types::ApiKey, :required
|
6
|
-
end
|
7
|
-
|
8
|
-
result Types::ApiKey
|
9
|
-
|
10
|
-
def execute
|
11
|
-
approve_api_key
|
12
|
-
|
13
|
-
api_key
|
14
|
-
end
|
15
|
-
|
16
|
-
def approve_api_key
|
17
|
-
api_key.approve!
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
data/src/build_password.rb
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
require "argon2"
|
2
|
-
|
3
|
-
module Foobara
|
4
|
-
module Auth
|
5
|
-
class BuildPassword < Foobara::Command
|
6
|
-
inputs do
|
7
|
-
plaintext_password :string, :required
|
8
|
-
end
|
9
|
-
result Types::Password
|
10
|
-
|
11
|
-
def execute
|
12
|
-
generate_hashed_password
|
13
|
-
build_password
|
14
|
-
|
15
|
-
password
|
16
|
-
end
|
17
|
-
|
18
|
-
attr_accessor :hashed_password, :password
|
19
|
-
|
20
|
-
def generate_hashed_password
|
21
|
-
self.hashed_password = Argon2::Password.create(plaintext_password, **argon_params)
|
22
|
-
end
|
23
|
-
|
24
|
-
def build_password
|
25
|
-
self.password = Types::Password.new(
|
26
|
-
hashed_password:,
|
27
|
-
parameters: argon_params.merge(other_params),
|
28
|
-
created_at: Time.now
|
29
|
-
)
|
30
|
-
end
|
31
|
-
|
32
|
-
def argon_params
|
33
|
-
{
|
34
|
-
t_cost: 2,
|
35
|
-
m_cost: 16,
|
36
|
-
parallelism: 1,
|
37
|
-
type: :argon2id
|
38
|
-
}
|
39
|
-
end
|
40
|
-
|
41
|
-
def other_params
|
42
|
-
{ method: :argon2id }
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
data/src/types/api_key/state.rb
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
module Foobara
|
2
|
-
module Auth
|
3
|
-
module Types
|
4
|
-
class ApiKey < Foobara::Entity
|
5
|
-
State = Foobara::Enumerated.make_module(%i[needs_approval approved rejected revoked])
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
foobara_register_type(:state, :symbol, one_of: Types::ApiKey::State)
|
10
|
-
end
|
11
|
-
end
|
data/src/types/api_key.rb
DELETED
@@ -1,28 +0,0 @@
|
|
1
|
-
module Foobara
|
2
|
-
module Auth
|
3
|
-
module Types
|
4
|
-
class ApiKey < Foobara::Entity
|
5
|
-
attributes do
|
6
|
-
id :integer
|
7
|
-
hashed_token :string, :required
|
8
|
-
prefix :string, :required
|
9
|
-
token_length :integer, :required
|
10
|
-
state :state, :required, default: :needs_approval
|
11
|
-
token_parameters :duck, :required
|
12
|
-
expires_at :datetime, :allow_nil
|
13
|
-
created_at :datetime, :required
|
14
|
-
end
|
15
|
-
|
16
|
-
primary_key :id
|
17
|
-
|
18
|
-
def state_machine
|
19
|
-
@state_machine ||= ApiKey::StateMachine.new(owner: self, target_attribute: :state)
|
20
|
-
end
|
21
|
-
|
22
|
-
def approve!
|
23
|
-
state_machine.approve!
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
data/src/verify_api_key.rb
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
require "argon2"
|
2
|
-
|
3
|
-
module Foobara
|
4
|
-
module Auth
|
5
|
-
class VerifyApiKey < Foobara::Command
|
6
|
-
inputs do
|
7
|
-
token :string, :required # TODO: we should add a processor that flags an attribute as sensitive so we can scrub
|
8
|
-
end
|
9
|
-
result :boolean
|
10
|
-
|
11
|
-
depends_on_entity Types::ApiKey
|
12
|
-
|
13
|
-
def execute
|
14
|
-
check_for_valid_key
|
15
|
-
|
16
|
-
valid_key?
|
17
|
-
end
|
18
|
-
|
19
|
-
attr_accessor :valid_key
|
20
|
-
|
21
|
-
def valid_key?
|
22
|
-
!!valid_key
|
23
|
-
end
|
24
|
-
|
25
|
-
def check_for_valid_key
|
26
|
-
prefix = token[..4]
|
27
|
-
hash_for_user = token[5..]
|
28
|
-
|
29
|
-
Types::ApiKey.find_all_by_attribute(:prefix, prefix).each do |key|
|
30
|
-
if Argon2::Password.verify_password(hash_for_user, key.hashed_token)
|
31
|
-
self.valid_key = true
|
32
|
-
break
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|