Module: BetterAuth::Cookies

Defined in:
lib/better_auth/cookies.rb

Defined Under Namespace

Classes: Cookie

Constant Summary collapse

"__Secure-"
"__Host-"

Class Method Summary collapse

Class Method Details

.chunked_value(cookies, name) ⇒ Object



238
239
240
241
242
243
244
245
246
247
# File 'lib/better_auth/cookies.rb', line 238

def chunked_value(cookies, name)
  chunks = cookies.each_with_object([]) do |(cookie_name, value), result|
    next unless cookie_name.start_with?("#{name}.")

    result << [SessionStore.chunk_index(cookie_name), value]
  end
  return nil if chunks.empty?

  chunks.sort_by(&:first).map(&:last).join
end


256
257
258
259
260
261
# File 'lib/better_auth/cookies.rb', line 256

def cookie_cache_version(config, session, user)
  return "1" unless config
  return config.to_s unless config.respond_to?(:call)

  config.call(session, user).to_s
end

Raises:



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/better_auth/cookies.rb', line 27

def create_cookie(options, cookie_name, override_attributes = {})
  advanced = options.advanced || {}
  secure = if advanced.key?(:use_secure_cookies)
    advanced[:use_secure_cookies]
  elsif options.base_url.to_s.start_with?("https://")
    true
  else
    production_environment?
  end
  cross_subdomain = advanced.dig(:cross_subdomain_cookies, :enabled)
  domain = if cross_subdomain
    advanced.dig(:cross_subdomain_cookies, :domain) || begin
      uri = URI.parse(options.base_url.to_s)
      uri.host unless uri.host.to_s.empty?
    end
  end
  raise Error, "base_url is required when cross_subdomain_cookies are enabled" if cross_subdomain && domain.to_s.empty?

  custom = advanced.dig(:cookies, cookie_name.to_sym) || {}
  prefix = advanced[:cookie_prefix] || "better-auth"
  name = custom[:name] || "#{prefix}.#{cookie_name}"
  attributes = {
    secure: !!secure,
    same_site: "lax",
    path: "/",
    http_only: true
  }
  attributes[:domain] = domain if domain
  attributes = attributes
    .merge(advanced[:default_cookie_attributes] || {})
    .merge(override_attributes || {})
    .merge(custom[:attributes] || {})
    .compact

  cookie_prefix = secure ? SECURE_COOKIE_PREFIX : ""
  Cookie.new(name: "#{cookie_prefix}#{name}", attributes: attributes)
end

.current_millisObject



263
264
265
# File 'lib/better_auth/cookies.rb', line 263

def current_millis
  (Time.now.to_f * 1000).to_i
end


207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/better_auth/cookies.rb', line 207

def decode_cookie_cache(value, secret, strategy:)
  case strategy.to_s
  when "jwt"
    Crypto.verify_jwt(value, secret)
  when "jwe"
    Crypto.symmetric_decode_jwt(value, secret, "better-auth-session")
  else
    payload = JSON.parse(Crypto.base64url_decode(value))
    return nil if payload["expiresAt"].to_i <= current_millis

    signed = payload.fetch("session").merge("expiresAt" => payload.fetch("expiresAt"))
    valid = Crypto.verify_hmac_signature(JSON.generate(signed), payload["signature"], secret, encoding: :base64url)
    valid ? payload["session"] : nil
  end
rescue JSON::ParserError, KeyError, ArgumentError, JWT::DecodeError
  nil
end


178
179
180
181
182
183
184
185
186
# File 'lib/better_auth/cookies.rb', line 178

def delete_session_cookie(ctx, skip_dont_remember_me: false)
  expire_cookie(ctx, ctx.context.auth_cookies[:session_token])
  expire_cookie(ctx, ctx.context.auth_cookies[:session_data])
  expire_cookie(ctx, ctx.context.auth_cookies[:account_data]) if ctx.context.options.[:store_account_cookie]

  store = SessionStore.new(ctx.context.auth_cookies[:session_data].name, ctx.context.auth_cookies[:session_data].attributes, ctx)
  store.set_cookies(store.clean)
  expire_cookie(ctx, ctx.context.auth_cookies[:dont_remember]) unless skip_dont_remember_me
end

.dont_remember?(ctx) ⇒ Boolean

Returns:

  • (Boolean)


188
189
190
191
# File 'lib/better_auth/cookies.rb', line 188

def dont_remember?(ctx)
  cookie = ctx.context.auth_cookies[:dont_remember]
  ctx.get_signed_cookie(cookie.name, ctx.context.secret) == "true"
end


193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/better_auth/cookies.rb', line 193

def encode_cookie_cache(data, secret, strategy:, max_age:)
  case strategy.to_s
  when "jwt"
    Crypto.sign_jwt(data, secret, expires_in: max_age)
  when "jwe"
    Crypto.symmetric_encode_jwt(data, secret, "better-auth-session", expires_in: max_age)
  else
    expires_at = current_millis + (max_age.to_i * 1000)
    signed = data.merge("expiresAt" => expires_at)
    signature = Crypto.hmac_signature(JSON.generate(signed), secret, encoding: :base64url)
    Crypto.base64url_encode(JSON.generate({"session" => data, "expiresAt" => expires_at, "signature" => signature}))
  end
end


174
175
176
# File 'lib/better_auth/cookies.rb', line 174

def expire_cookie(ctx, cookie)
  ctx.set_cookie(cookie.name, "", cookie.attributes.merge(max_age: 0))
end

.filtered_cache_data(ctx, session) ⇒ Object



225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/better_auth/cookies.rb', line 225

def filtered_cache_data(ctx, session)
  {
    "session" => stringify_keys(Schema.parse_output(ctx.context.options, "session", stringify_keys(session.fetch(:session)))),
    "user" => stringify_keys(Schema.parse_output(ctx.context.options, "user", stringify_keys(session.fetch(:user)))),
    "updatedAt" => current_millis,
    "version" => cookie_cache_version(
      ctx.context.session_config.dig(:cookie_cache, :version),
      session.fetch(:session),
      session.fetch(:user)
    )
  }
end


143
144
145
146
147
148
149
# File 'lib/better_auth/cookies.rb', line 143

def (ctx)
  cookie = ctx.context.auth_cookies[:account_data]
  value = SessionStore.get_chunked_cookie(ctx, cookie.name)
  return nil unless value

  Crypto.symmetric_decode_jwt(value, ctx.context.secret, "better-auth-account")
end


151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/better_auth/cookies.rb', line 151

def get_cookie_cache(request_or_cookie_header, secret:, strategy: "compact", version: nil, cookie_prefix: "better-auth", cookie_name: "session_data", is_secure: nil)
  cookie_header = header_value(request_or_cookie_header)
  return nil if cookie_header.to_s.empty?

  parsed = parse_cookies(cookie_header)
  name = if is_secure.nil?
    production_environment? ? "#{SECURE_COOKIE_PREFIX}#{cookie_prefix}.#{cookie_name}" : "#{cookie_prefix}.#{cookie_name}"
  else
    secure_prefix = is_secure ? SECURE_COOKIE_PREFIX : ""
    "#{secure_prefix}#{cookie_prefix}.#{cookie_name}"
  end
  raw = parsed[name] || chunked_value(parsed, name)
  return nil unless raw

  payload = decode_cookie_cache(raw, secret, strategy: strategy)
  return nil unless payload && payload["session"] && payload["user"]

  expected_version = cookie_cache_version(version, payload["session"], payload["user"])
  return nil if version && (payload["version"] || "1") != expected_version

  payload
end

.get_cookies(options) ⇒ Object



18
19
20
21
22
23
24
25
# File 'lib/better_auth/cookies.rb', line 18

def get_cookies(options)
  {
    session_token: create_cookie(options, "session_token", max_age: options.session[:expires_in] || 60 * 60 * 24 * 7),
    session_data: create_cookie(options, "session_data", max_age: options.session.dig(:cookie_cache, :max_age) || 60 * 5),
    account_data: create_cookie(options, "account_data", max_age: options.session.dig(:cookie_cache, :max_age) || 60 * 5),
    dont_remember: create_cookie(options, "dont_remember")
  }
end


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/better_auth/cookies.rb', line 78

def get_session_cookie(request_or_cookie_header, config = {})
  cookie_header = header_value(request_or_cookie_header)
  return nil if cookie_header.to_s.empty?

  parsed = parse_cookies(cookie_header)
  cookie_name = config[:cookie_name] || "session_token"
  cookie_prefix = config[:cookie_prefix] || "better-auth"
  candidates = [
    "#{cookie_prefix}.#{cookie_name}",
    "#{SECURE_COOKIE_PREFIX}#{cookie_prefix}.#{cookie_name}",
    "#{cookie_prefix}-#{cookie_name}",
    "#{SECURE_COOKIE_PREFIX}#{cookie_prefix}-#{cookie_name}"
  ]
  candidates.lazy.filter_map { |candidate| parsed[candidate] }.first
end

.header_value(request_or_cookie_header) ⇒ Object



249
250
251
252
253
254
# File 'lib/better_auth/cookies.rb', line 249

def header_value(request_or_cookie_header)
  return request_or_cookie_header.headers["cookie"] if request_or_cookie_header.respond_to?(:headers)
  return request_or_cookie_header.get_header("HTTP_COOKIE") if request_or_cookie_header.respond_to?(:get_header)

  request_or_cookie_header.to_s
end

.parse_cookies(cookie_header) ⇒ Object



65
66
67
68
69
70
71
72
# File 'lib/better_auth/cookies.rb', line 65

def parse_cookies(cookie_header)
  cookie_header.to_s.split(/;\s*/).each_with_object({}) do |pair, result|
    name, value = pair.split("=", 2)
    next if name.to_s.empty? || value.nil?

    result[name.strip] = value.strip
  end
end

.production_environment?Boolean

Returns:

  • (Boolean)


274
275
276
# File 'lib/better_auth/cookies.rb', line 274

def production_environment?
  ENV["RACK_ENV"] == "production" || ENV["RAILS_ENV"] == "production" || ENV["APP_ENV"] == "production"
end


127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/better_auth/cookies.rb', line 127

def (ctx, )
  return unless ctx.context.options.[:store_account_cookie]

  cookie = ctx.context.auth_cookies[:account_data]
  attributes = cookie.attributes.merge(max_age: cookie.attributes[:max_age] || 60 * 5)
  value = Crypto.symmetric_encode_jwt(stringify_keys(), ctx.context.secret, "better-auth-account", expires_in: attributes[:max_age])
  store = SessionStore.new(cookie.name, attributes, ctx)

  if value.length > SessionStore::CHUNK_SIZE
    store.set_cookies(store.chunk(value, attributes))
  else
    store.set_cookies(store.clean) if store.chunks?
    ctx.set_cookie(cookie.name, value, attributes)
  end
end


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/better_auth/cookies.rb', line 108

def set_cookie_cache(ctx, session, dont_remember_me)
  config = ctx.context.session_config[:cookie_cache] || {}
  return unless config[:enabled]

  cookie = ctx.context.auth_cookies[:session_data]
  max_age = dont_remember_me ? nil : cookie.attributes[:max_age]
  data = filtered_cache_data(ctx, session)
  value = encode_cookie_cache(data, ctx.context.secret, strategy: config[:strategy] || "compact", max_age: max_age || 60 * 5)
  attributes = cookie.attributes.merge(max_age: max_age)
  store = SessionStore.new(cookie.name, attributes, ctx)

  if value.length > SessionStore::CHUNK_SIZE
    store.set_cookies(store.chunk(value, attributes))
  else
    store.set_cookies(store.clean) if store.chunks?
    ctx.set_cookie(cookie.name, value, attributes)
  end
end


94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/better_auth/cookies.rb', line 94

def set_session_cookie(ctx, session, dont_remember_me = false, overrides = {})
  token_cookie = ctx.context.auth_cookies[:session_token]
  max_age = dont_remember_me ? nil : ctx.context.session_config[:expires_in]
  ctx.set_signed_cookie(token_cookie.name, session.fetch(:session).fetch("token"), ctx.context.secret, token_cookie.attributes.merge(max_age: max_age).merge(overrides || {}))

  if dont_remember_me
    dont_remember_cookie = ctx.context.auth_cookies[:dont_remember]
    ctx.set_signed_cookie(dont_remember_cookie.name, "true", ctx.context.secret, dont_remember_cookie.attributes)
  end

  set_cookie_cache(ctx, session, dont_remember_me)
  ctx.context.set_new_session(session) if ctx.context.respond_to?(:set_new_session)
end

.stringify_keys(value) ⇒ Object



267
268
269
270
271
272
# File 'lib/better_auth/cookies.rb', line 267

def stringify_keys(value)
  return value.each_with_object({}) { |(key, object_value), result| result[key.to_s] = stringify_keys(object_value) } if value.is_a?(Hash)
  return value.map { |entry| stringify_keys(entry) } if value.is_a?(Array)

  value
end


74
75
76
# File 'lib/better_auth/cookies.rb', line 74

def strip_secure_cookie_prefix(name)
  name.to_s.delete_prefix(SECURE_COOKIE_PREFIX).delete_prefix(HOST_COOKIE_PREFIX)
end