Class: BetterAuth::SecretConfig

Inherits:
Object
  • Object
show all
Defined in:
lib/better_auth/secret_config.rb

Constant Summary collapse

ENVELOPE_PREFIX =
"$ba$"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(keys:, current_version:, legacy_secret: nil) ⇒ SecretConfig

Returns a new instance of SecretConfig.



9
10
11
12
13
14
15
16
# File 'lib/better_auth/secret_config.rb', line 9

def initialize(keys:, current_version:, legacy_secret: nil)
  normalized_keys = keys.each_with_object({}) do |(version, value), result|
    result[normalize_version!(version)] = value.to_s
  end
  @keys = normalized_keys.freeze
  @current_version = normalize_version!(current_version)
  @legacy_secret = legacy_secret unless legacy_secret.to_s.empty?
end

Instance Attribute Details

#current_versionObject (readonly)

Returns the value of attribute current_version.



7
8
9
# File 'lib/better_auth/secret_config.rb', line 7

def current_version
  @current_version
end

#keysObject (readonly)

Returns the value of attribute keys.



7
8
9
# File 'lib/better_auth/secret_config.rb', line 7

def keys
  @keys
end

#legacy_secretObject (readonly)

Returns the value of attribute legacy_secret.



7
8
9
# File 'lib/better_auth/secret_config.rb', line 7

def legacy_secret
  @legacy_secret
end

Class Method Details

.build(secrets, legacy_secret, logger: nil) ⇒ Object



68
69
70
71
72
73
74
75
76
77
# File 'lib/better_auth/secret_config.rb', line 68

def self.build(secrets, legacy_secret, logger: nil)
  validate_secrets!(secrets, logger: logger)
  entries = Array(secrets).map { |entry| normalize_entry(entry) }
  keys = entries.each_with_object({}) do |entry, result|
    result[parse_version!(entry.fetch(:version), source: "`secrets`")] = entry.fetch(:value).to_s
  end
  current_version = parse_version!(entries.first.fetch(:version), source: "`secrets`")
  legacy = (legacy_secret && legacy_secret != Configuration::DEFAULT_SECRET) ? legacy_secret : nil
  new(keys: keys, current_version: current_version, legacy_secret: legacy)
end

.entropy(value) ⇒ Object



96
97
98
99
100
101
# File 'lib/better_auth/secret_config.rb', line 96

def self.entropy(value)
  unique = value.to_s.chars.uniq.length
  return 0 if unique.zero?

  Math.log2(unique**value.to_s.length)
end

.normalize_entry(entry) ⇒ Object

Raises:



79
80
81
82
83
84
85
# File 'lib/better_auth/secret_config.rb', line 79

def self.normalize_entry(entry)
  raise Error, "Invalid `secrets` entry. Expected a hash with `version` and `value`." unless entry.is_a?(Hash)

  entry.each_with_object({}) do |(key, value), result|
    result[key.to_s.tr("-", "_").to_sym] = value
  end
end

.parse_env(value) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/better_auth/secret_config.rb', line 30

def self.parse_env(value)
  return nil if value.to_s.empty?

  value.to_s.split(",").map do |entry|
    entry = entry.strip
    colon_index = entry.index(":")
    raise Error, "Invalid BETTER_AUTH_SECRETS entry: \"#{entry}\". Expected format: \"<version>:<secret>\"" unless colon_index

    version = entry[0...colon_index].strip
    secret = entry[(colon_index + 1)..].to_s.strip
    raise Error, "Empty secret value for version #{version} in BETTER_AUTH_SECRETS." if secret.empty?

    {version: parse_version!(version, source: "BETTER_AUTH_SECRETS"), value: secret}
  end
end

.parse_version!(value, source:) ⇒ Object



87
88
89
90
91
92
93
94
# File 'lib/better_auth/secret_config.rb', line 87

def self.parse_version!(value, source:)
  text = value.to_s.strip
  unless text.match?(/\A(?:0|[1-9]\d*)\z/)
    raise Error, "Invalid version #{value} in #{source}. Version must be a non-negative integer."
  end

  text.to_i
end

.validate_secrets!(secrets, logger: nil) ⇒ Object

Raises:



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/better_auth/secret_config.rb', line 46

def self.validate_secrets!(secrets, logger: nil)
  entries = Array(secrets)
  raise Error, "`secrets` array must contain at least one entry." if entries.empty?

  seen = {}
  entries.each do |entry|
    data = normalize_entry(entry)
    version = parse_version!(data.fetch(:version), source: "`secrets`")
    value = data.fetch(:value, nil).to_s
    raise Error, "Empty secret value for version #{version} in `secrets`." if value.empty?
    raise Error, "Duplicate version #{version} in `secrets`. Each version must be unique." if seen[version]

    seen[version] = true
  end

  current = normalize_entry(entries.first)
  current_version = parse_version!(current.fetch(:version), source: "`secrets`")
  current_value = current.fetch(:value).to_s
  warn(logger, "[better-auth] Warning: the current secret (version #{current_version}) should be at least 32 characters long for adequate security.") if current_value.length < 32
  warn(logger, "[better-auth] Warning: the current secret appears low-entropy. Use a randomly generated secret for production.") if entropy(current_value) < 120
end

.warn(logger, message) ⇒ Object



103
104
105
106
107
108
109
# File 'lib/better_auth/secret_config.rb', line 103

def self.warn(logger, message)
  if logger.respond_to?(:call)
    logger.call(:warn, message)
  elsif logger.respond_to?(:warn)
    logger.warn(message)
  end
end

Instance Method Details

#all_secretsObject



24
25
26
27
28
# File 'lib/better_auth/secret_config.rb', line 24

def all_secrets
  entries = keys.map { |version, value| [version, value] }
  entries << [-1, legacy_secret] if legacy_secret && !keys.value?(legacy_secret)
  entries
end

#current_secretObject



18
19
20
21
22
# File 'lib/better_auth/secret_config.rb', line 18

def current_secret
  keys.fetch(current_version) do
    raise Error, "Secret version #{current_version} not found in keys"
  end
end

#normalize_version!(version) ⇒ Object



111
112
113
# File 'lib/better_auth/secret_config.rb', line 111

def normalize_version!(version)
  self.class.parse_version!(version, source: "`secrets`")
end