better_auth-api-key 0.2.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/lib/better_auth/api_key/adapter.rb +245 -0
- data/lib/better_auth/api_key/configuration.rb +78 -0
- data/lib/better_auth/api_key/error_codes.rb +39 -0
- data/lib/better_auth/api_key/keys.rb +55 -0
- data/lib/better_auth/api_key/org_authorization.rb +80 -0
- data/lib/better_auth/api_key/plugin_factory.rb +37 -0
- data/lib/better_auth/api_key/rate_limit.rb +41 -0
- data/lib/better_auth/api_key/routes/create_api_key.rb +53 -0
- data/lib/better_auth/api_key/routes/delete_all_expired_api_keys.rb +23 -0
- data/lib/better_auth/api_key/routes/delete_api_key.rb +35 -0
- data/lib/better_auth/api_key/routes/get_api_key.rb +33 -0
- data/lib/better_auth/api_key/routes/index.rb +72 -0
- data/lib/better_auth/api_key/routes/list_api_keys.rb +44 -0
- data/lib/better_auth/api_key/routes/update_api_key.rb +45 -0
- data/lib/better_auth/api_key/routes/verify_api_key.rb +43 -0
- data/lib/better_auth/api_key/schema.rb +40 -0
- data/lib/better_auth/api_key/session.rb +63 -0
- data/lib/better_auth/api_key/types.rb +30 -0
- data/lib/better_auth/api_key/utils.rb +93 -0
- data/lib/better_auth/api_key/validation.rb +137 -0
- data/lib/better_auth/api_key/version.rb +1 -1
- data/lib/better_auth/api_key.rb +20 -0
- data/lib/better_auth/plugins/api_key.rb +94 -802
- metadata +21 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module APIKey
|
|
5
|
+
module Validation
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def validate_create_update!(body, config, create:, client:)
|
|
9
|
+
name = body[:name]
|
|
10
|
+
if create && config[:require_name] && name.to_s.empty?
|
|
11
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["NAME_REQUIRED"])
|
|
12
|
+
end
|
|
13
|
+
if name && !name.to_s.length.between?(config[:minimum_name_length].to_i, config[:maximum_name_length].to_i)
|
|
14
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_NAME_LENGTH"])
|
|
15
|
+
end
|
|
16
|
+
prefix = body[:prefix]
|
|
17
|
+
if prefix && !prefix.to_s.length.between?(config[:minimum_prefix_length].to_i, config[:maximum_prefix_length].to_i)
|
|
18
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_PREFIX_LENGTH"])
|
|
19
|
+
end
|
|
20
|
+
if prefix && !prefix.to_s.match?(/\A[a-zA-Z0-9_-]+\z/)
|
|
21
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_PREFIX_LENGTH"])
|
|
22
|
+
end
|
|
23
|
+
if body.key?(:remaining) && !body[:remaining].nil?
|
|
24
|
+
minimum = create ? 0 : 1
|
|
25
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_REMAINING"]) if body[:remaining].to_i < minimum
|
|
26
|
+
end
|
|
27
|
+
if body[:metadata] && (create || config[:enable_metadata])
|
|
28
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["METADATA_DISABLED"]) unless config[:enable_metadata]
|
|
29
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_METADATA_TYPE"]) unless body[:metadata].nil? || body[:metadata].is_a?(Hash)
|
|
30
|
+
end
|
|
31
|
+
server_only_keys = %i[refill_amount refill_interval rate_limit_max rate_limit_time_window rate_limit_enabled remaining permissions]
|
|
32
|
+
if client && server_only_keys.any? { |key| (create && key == :remaining) ? !body[:remaining].nil? : body.key?(key) }
|
|
33
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["SERVER_ONLY_PROPERTY"])
|
|
34
|
+
end
|
|
35
|
+
amount_present = body.key?(:refill_amount)
|
|
36
|
+
interval_present = body.key?(:refill_interval)
|
|
37
|
+
if amount_present && !interval_present
|
|
38
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["REFILL_AMOUNT_AND_INTERVAL_REQUIRED"])
|
|
39
|
+
end
|
|
40
|
+
if interval_present && !amount_present
|
|
41
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["REFILL_INTERVAL_AND_AMOUNT_REQUIRED"])
|
|
42
|
+
end
|
|
43
|
+
if body.key?(:expires_in)
|
|
44
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_DISABLED_EXPIRATION"]) if config[:key_expiration][:disable_custom_expires_time]
|
|
45
|
+
return if body[:expires_in].nil?
|
|
46
|
+
|
|
47
|
+
days = body[:expires_in].to_f / 86_400
|
|
48
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["EXPIRES_IN_IS_TOO_SMALL"]) if days < config[:key_expiration][:min_expires_in].to_f
|
|
49
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["EXPIRES_IN_IS_TOO_LARGE"]) if days > config[:key_expiration][:max_expires_in].to_f
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def update_payload(body, config)
|
|
54
|
+
update = {}
|
|
55
|
+
update[:name] = body[:name] if body.key?(:name)
|
|
56
|
+
update[:enabled] = body[:enabled] unless body[:enabled].nil?
|
|
57
|
+
update[:remaining] = body[:remaining] if body.key?(:remaining)
|
|
58
|
+
update[:refillAmount] = body[:refill_amount] if body.key?(:refill_amount)
|
|
59
|
+
update[:refillInterval] = body[:refill_interval] if body.key?(:refill_interval)
|
|
60
|
+
update[:rateLimitEnabled] = body[:rate_limit_enabled] if body.key?(:rate_limit_enabled)
|
|
61
|
+
update[:rateLimitTimeWindow] = body[:rate_limit_time_window] if body.key?(:rate_limit_time_window)
|
|
62
|
+
update[:rateLimitMax] = body[:rate_limit_max] if body.key?(:rate_limit_max)
|
|
63
|
+
update[:expiresAt] = body[:expires_in].nil? ? nil : Time.now + body[:expires_in].to_i if body.key?(:expires_in)
|
|
64
|
+
update[:metadata] = BetterAuth::APIKey::Utils.encode_json(body[:metadata]) if body.key?(:metadata) && config[:enable_metadata]
|
|
65
|
+
update[:permissions] = BetterAuth::APIKey::Utils.encode_json(body[:permissions]) if body.key?(:permissions)
|
|
66
|
+
update
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def validate_api_key!(ctx, key, config, permissions: nil)
|
|
70
|
+
hashed = BetterAuth::APIKey::Keys.hash(key, config)
|
|
71
|
+
record = BetterAuth::APIKey::Adapter.find_by_hash(ctx, hashed, config)
|
|
72
|
+
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"]) unless record
|
|
73
|
+
unless BetterAuth::APIKey::Routes.config_id_matches?(BetterAuth::APIKey::Types.record_config_id(record), config[:config_id])
|
|
74
|
+
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"])
|
|
75
|
+
end
|
|
76
|
+
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_DISABLED"]) if record["enabled"] == false
|
|
77
|
+
if record["expiresAt"] && record["expiresAt"] <= Time.now
|
|
78
|
+
BetterAuth::APIKey::Adapter.schedule_record_delete(ctx, record, config)
|
|
79
|
+
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_EXPIRED"])
|
|
80
|
+
end
|
|
81
|
+
if record["remaining"].to_i <= 0 && !record["remaining"].nil? && record["refillAmount"].nil?
|
|
82
|
+
BetterAuth::APIKey::Adapter.schedule_record_delete(ctx, record, config)
|
|
83
|
+
raise BetterAuth::APIError.new("TOO_MANY_REQUESTS", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["USAGE_EXCEEDED"])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
check_permissions!(record, permissions)
|
|
87
|
+
update = usage_update(record, config)
|
|
88
|
+
updated = BetterAuth::APIKey::Adapter.update_record(ctx, record, update, config, defer: true)
|
|
89
|
+
BetterAuth::APIKey::Adapter.migrate_legacy_metadata(ctx, updated || record.merge(update.transform_keys { |key_name| BetterAuth::Schema.storage_key(key_name) }), config)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def usage_update(record, config)
|
|
93
|
+
now = Time.now
|
|
94
|
+
update = {lastRequest: now, updatedAt: now}
|
|
95
|
+
|
|
96
|
+
if (try_again_in = BetterAuth::APIKey::RateLimit.try_again_in(record, config, now))
|
|
97
|
+
raise BetterAuth::APIError.new(
|
|
98
|
+
"UNAUTHORIZED",
|
|
99
|
+
message: BetterAuth::Plugins::API_KEY_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
|
|
100
|
+
code: "RATE_LIMITED",
|
|
101
|
+
body: {
|
|
102
|
+
message: BetterAuth::Plugins::API_KEY_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
|
|
103
|
+
code: "RATE_LIMITED",
|
|
104
|
+
details: {tryAgainIn: try_again_in}
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
update[:requestCount] = BetterAuth::APIKey::RateLimit.next_request_count(record, now) if BetterAuth::APIKey::RateLimit.counts_requests?(record, config)
|
|
109
|
+
|
|
110
|
+
remaining = record["remaining"]
|
|
111
|
+
if !remaining.nil?
|
|
112
|
+
if remaining.to_i <= 0 && record["refillAmount"] && record["refillInterval"]
|
|
113
|
+
last_refill = BetterAuth::APIKey::Utils.normalize_time(record["lastRefillAt"] || record["createdAt"])
|
|
114
|
+
if !last_refill || ((now - last_refill) * 1000) > record["refillInterval"].to_i
|
|
115
|
+
remaining = record["refillAmount"].to_i
|
|
116
|
+
update[:lastRefillAt] = now
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
raise BetterAuth::APIError.new("TOO_MANY_REQUESTS", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["USAGE_EXCEEDED"]) if remaining.to_i <= 0
|
|
120
|
+
|
|
121
|
+
update[:remaining] = remaining.to_i - 1
|
|
122
|
+
end
|
|
123
|
+
update
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def check_permissions!(record, required)
|
|
127
|
+
return if required.nil? || required == {}
|
|
128
|
+
|
|
129
|
+
actual = BetterAuth::APIKey::Utils.decode_json(record["permissions"]) || {}
|
|
130
|
+
result = BetterAuth::Plugins::Role.new(actual).authorize(required)
|
|
131
|
+
unless result[:success]
|
|
132
|
+
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_NOT_FOUND"], code: "KEY_NOT_FOUND")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
data/lib/better_auth/api_key.rb
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
require "better_auth"
|
|
4
4
|
require_relative "api_key/version"
|
|
5
|
+
require_relative "api_key/error_codes"
|
|
6
|
+
require_relative "api_key/types"
|
|
7
|
+
require_relative "api_key/utils"
|
|
8
|
+
require_relative "api_key/rate_limit"
|
|
9
|
+
require_relative "api_key/keys"
|
|
10
|
+
require_relative "api_key/adapter"
|
|
11
|
+
require_relative "api_key/schema"
|
|
12
|
+
require_relative "api_key/org_authorization"
|
|
13
|
+
require_relative "api_key/validation"
|
|
14
|
+
require_relative "api_key/configuration"
|
|
15
|
+
require_relative "api_key/session"
|
|
16
|
+
require_relative "api_key/plugin_factory"
|
|
17
|
+
require_relative "api_key/routes/index"
|
|
18
|
+
require_relative "api_key/routes/create_api_key"
|
|
19
|
+
require_relative "api_key/routes/verify_api_key"
|
|
20
|
+
require_relative "api_key/routes/get_api_key"
|
|
21
|
+
require_relative "api_key/routes/update_api_key"
|
|
22
|
+
require_relative "api_key/routes/delete_api_key"
|
|
23
|
+
require_relative "api_key/routes/list_api_keys"
|
|
24
|
+
require_relative "api_key/routes/delete_all_expired_api_keys"
|
|
5
25
|
require_relative "plugins/api_key"
|
|
6
26
|
|
|
7
27
|
module BetterAuth
|