Module: BetterAuth::Routes

Defined in:
lib/better_auth/routes/ok.rb,
lib/better_auth/routes/user.rb,
lib/better_auth/routes/error.rb,
lib/better_auth/routes/social.rb,
lib/better_auth/routes/account.rb,
lib/better_auth/routes/session.rb,
lib/better_auth/routes/sign_in.rb,
lib/better_auth/routes/sign_up.rb,
lib/better_auth/routes/password.rb,
lib/better_auth/routes/sign_out.rb,
lib/better_auth/routes/email_verification.rb

Constant Summary collapse

EMAIL_PATTERN =
/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
PASSWORD_RESET_MESSAGE =
"If this email exists in our system, check your email for the reset link"

Class Method Summary collapse

Class Method Details

.absolute_callback(context, callback_url, params) ⇒ Object



155
156
157
158
159
160
161
162
163
164
# File 'lib/better_auth/routes/password.rb', line 155

def self.absolute_callback(context, callback_url, params)
  validate_callback_url!(context, callback_url)
  uri = URI.parse(callback_url.to_s)
  origin = Configuration.origin_for(URI.parse(context.base_url))
  url = uri.relative? ? URI.join("#{origin}/", callback_url.to_s.delete_prefix("/")) : uri
  query = URI.decode_www_form(url.query.to_s)
  params.each { |key, value| query << [key.to_s, value] }
  url.query = URI.encode_www_form(query)
  url.to_s
end

.access_token_expired?(account) ⇒ Boolean

Returns:

  • (Boolean)


153
154
155
156
# File 'lib/better_auth/routes/account.rb', line 153

def self.access_token_expired?()
  value = parse_time(["accessTokenExpiresAt"])
  value && value < Time.now + 5
end


142
143
144
145
146
147
148
149
150
151
# File 'lib/better_auth/routes/account.rb', line 142

def self.(ctx, provider_id,  = nil, user_id = nil)
  return nil unless ctx.context.options.[:store_account_cookie]

   = Cookies.(ctx)
  return nil unless  && ["providerId"] == provider_id
  return nil unless .to_s.empty? || ["id"] ==  || ["accountId"] == 
  return nil unless user_id.to_s.empty? || ["userId"].to_s.empty? || ["userId"] == user_id

  
end

.account_infoObject



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/better_auth/routes/account.rb', line 104

def self.
  Endpoint.new(path: "/account-info", method: "GET") do |ctx|
    session = current_session(ctx)
     = fetch_value(ctx.query, "accountId")
     = if 
      ctx.context.internal_adapter.find_accounts(session[:user]["id"]).find do |entry|
        entry["id"] ==  || entry["accountId"] == 
      end
    end
    raise APIError.new("BAD_REQUEST", message: "Account not found") unless  && ["userId"] == session[:user]["id"]

    provider = social_provider(ctx.context, ["providerId"])
    raise APIError.new("INTERNAL_SERVER_ERROR", message: "Provider account provider is #{["providerId"]} but it is not configured") unless provider
    raise APIError.new("BAD_REQUEST", message: "Access token not found") if ["accessToken"].to_s.empty?

    info = call_provider(provider, :get_user_info, {
      accessToken: oauth_token_value(ctx, ["accessToken"]),
      access_token: oauth_token_value(ctx, ["accessToken"]),
      idToken: ["idToken"],
      scopes: ["scope"].to_s.split(",")
    })
    ctx.json(info)
  end
end

.append_query(url, query) ⇒ Object



47
48
49
50
# File 'lib/better_auth/routes/error.rb', line 47

def self.append_query(url, query)
  separator = url.include?("?") ? "&" : "?"
  "#{url}#{separator}#{query}"
end

.call_existing_sign_up_callback(ctx, email_config, existing) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/better_auth/routes/sign_up.rb', line 121

def self.(ctx, email_config, existing)
  callback = email_config[:on_existing_user_sign_up]
  return unless callback.respond_to?(:call)

  user = existing[:user] || existing["user"] || existing
  data = {user: user}
  if callback.arity == 1
    callback.call(data)
  else
    callback.call(data, ctx.request)
  end
end

.call_option(callback, user, request) ⇒ Object



107
108
109
# File 'lib/better_auth/routes/email_verification.rb', line 107

def self.call_option(callback, user, request)
  callback.call(user, request) if callback.respond_to?(:call)
end

.call_provider(provider, key, *arguments) ⇒ Object



204
205
206
207
208
209
# File 'lib/better_auth/routes/account.rb', line 204

def self.call_provider(provider, key, *arguments)
  return provider.public_send(key, *arguments) if provider.respond_to?(key)

  callable = provider[key] || provider[key.to_s]
  callable.respond_to?(:call) ? callable.call(*arguments) : callable
end

.callback_oauthObject



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/better_auth/routes/social.rb', line 67

def self.callback_oauth
  Endpoint.new(
    path: "/callback/:providerId",
    method: ["GET", "POST"],
    metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}
  ) do |ctx|
    if ctx.method == "POST"
      merged = normalize_hash(ctx.query).merge(normalize_hash(ctx.body))
      query = URI.encode_www_form(merged.reject { |_key, value| value.nil? || value.to_s.empty? })
      target = "#{ctx.context.base_url}/callback/#{fetch_value(ctx.params, "providerId")}"
      target = "#{target}?#{query}" unless query.empty?
      raise ctx.redirect(target)
    end

    source = ctx.query
    data = normalize_hash(source)
    provider_id = fetch_value(ctx.params, "providerId").to_s
    provider = social_provider(ctx.context, provider_id)
    state = data["state"].to_s
    state_data = state.empty? ? nil : Crypto.verify_jwt(state, ctx.context.secret)
    error_url = state_data ? (state_data["errorCallbackURL"] || "#{ctx.context.base_url}/error") : "#{ctx.context.base_url}/error"

    raise ctx.redirect(oauth_error_url(error_url, data["error"], data["errorDescription"] || data["error_description"])) if data["error"]
    raise ctx.redirect(oauth_error_url(error_url, "oauth_provider_not_found")) unless provider
    raise ctx.redirect(oauth_error_url(error_url, "state_not_found")) unless state_data
    raise ctx.redirect(oauth_error_url(error_url, "no_code")) if data["code"].to_s.empty?

    tokens = call_provider(provider, :validate_authorization_code, {
      code: data["code"],
      codeVerifier: state_data["codeVerifier"],
      code_verifier: state_data["codeVerifier"],
      redirectURI: "#{ctx.context.base_url}/callback/#{provider_id}",
      redirect_uri: "#{ctx.context.base_url}/callback/#{provider_id}"
    })
    raise ctx.redirect(oauth_error_url(error_url, "invalid_code")) unless tokens

    token_data = token_hash(tokens)
    token_data["user"] = parse_json_hash(data["user"]) if data["user"]
     = call_provider(provider, :get_user_info, token_data)
    user = [:user] || ["user"] if 
    raise ctx.redirect(oauth_error_url(error_url, "unable_to_get_user_info")) unless user
    raise ctx.redirect(oauth_error_url(error_url, "email_not_found")) if fetch_value(user, "email").to_s.empty?

    link = state_data["link"] || state_data[:link]
    if link
      linked = (ctx, provider_id, user, tokens, link)
      raise ctx.redirect(oauth_error_url(error_url, linked[:error])) if linked[:error]

      raise ctx.redirect(state_data["callbackURL"] || "/")
    end

     = token_hash_for_storage(ctx, tokens).merge("accountId" => fetch_value(user, "id").to_s)
    session_data = persist_social_user(
      ctx,
      provider_id,
      user,
      ,
      callback_url: state_data["callbackURL"],
      disable_sign_up: provider_disable_sign_up?(provider) || (provider_disable_implicit_sign_up?(provider) && !state_data["requestSignUp"])
    )
    raise ctx.redirect(oauth_error_url(error_url, session_data[:error].tr(" ", "_"))) if session_data[:error]
    Cookies.set_session_cookie(ctx, session_data)
    callback_url = session_data[:new_user] ? (state_data["newUserCallbackURL"] || state_data["callbackURL"] || "/") : (state_data["callbackURL"] || "/")
    raise ctx.redirect(callback_url)
  end
end

.change_emailObject



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/better_auth/routes/user.rb', line 114

def self.change_email
  Endpoint.new(path: "/change-email", method: "POST") do |ctx|
    enabled = ctx.context.options.user.dig(:change_email, :enabled)
    raise APIError.new("BAD_REQUEST", message: "Change email is disabled") unless enabled
    session = current_session(ctx, sensitive: true)
    body = normalize_hash(ctx.body)
    new_email = (body["newEmail"] || body["new_email"]).to_s.downcase
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_EMAIL"]) unless EMAIL_PATTERN.match?(new_email)
    raise APIError.new("BAD_REQUEST", message: "Email is the same") if new_email == session[:user]["email"]
    raise APIError.new("UNPROCESSABLE_ENTITY", message: BASE_ERROR_CODES["USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"]) if ctx.context.internal_adapter.find_user_by_email(new_email)

    if !session[:user]["emailVerified"] && ctx.context.options.user.dig(:change_email, :update_email_without_verification)
      updated = ctx.context.internal_adapter.update_user_by_email(session[:user]["email"], email: new_email)
      Cookies.set_session_cookie(ctx, {session: session[:session], user: updated})
      next ctx.json({status: true})
    end

    sender = ctx.context.options.email_verification[:send_verification_email]
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["VERIFICATION_EMAIL_NOT_ENABLED"]) unless sender.respond_to?(:call)

    token = create_email_verification_token(ctx, session[:user]["email"], update_to: new_email, extra: {"requestType" => "change-email-verification"})
    sender.call({user: session[:user].merge("email" => new_email), token: token}, ctx.request)
    ctx.json({status: true})
  end
end

.change_passwordObject



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/better_auth/routes/user.rb', line 20

def self.change_password
  Endpoint.new(path: "/change-password", method: "POST") do |ctx|
    session = current_session(ctx, sensitive: true)
    body = normalize_hash(ctx.body)
    new_password = body["newPassword"] || body["new_password"]
    current_password = body["currentPassword"] || body["current_password"]
    validate_password_length!(new_password, ctx.context.options.email_and_password)
     = (ctx, session[:user]["id"])
    unless  && ["password"] && verify_password_value(ctx, current_password.to_s, ["password"])
      raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_PASSWORD"])
    end

    ctx.context.internal_adapter.(["id"], password: hash_password(ctx, new_password))
    token = nil
    if body["revokeOtherSessions"] || body["revoke_other_sessions"]
      ctx.context.internal_adapter.delete_sessions(session[:user]["id"])
      new_session = ctx.context.internal_adapter.create_session(session[:user]["id"])
      Cookies.set_session_cookie(ctx, {session: new_session, user: session[:user]})
      token = new_session["token"]
    end
    ctx.json({token: token, user: Schema.parse_output(ctx.context.options, "user", session[:user])})
  end
end

.coerce_input_value(value, attributes) ⇒ Object



187
188
189
190
191
192
# File 'lib/better_auth/routes/user.rb', line 187

def self.coerce_input_value(value, attributes)
  return value if value.nil?
  return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)

  value
end

.core_model_fields(model) ⇒ Object



194
195
196
197
198
199
200
201
202
203
# File 'lib/better_auth/routes/user.rb', line 194

def self.core_model_fields(model)
  case model.to_s
  when "user"
    %w[id name email emailVerified image createdAt updatedAt]
  when "session"
    %w[id expiresAt token ipAddress userAgent userId createdAt updatedAt]
  else
    %w[id createdAt updatedAt]
  end
end

.create_email_verification_token(ctx, email, update_to: nil, extra: {}) ⇒ Object



74
75
76
77
78
# File 'lib/better_auth/routes/email_verification.rb', line 74

def self.create_email_verification_token(ctx, email, update_to: nil, extra: {})
  payload = {"email" => email.to_s.downcase}.merge(extra)
  payload["updateTo"] = update_to if update_to
  Crypto.sign_jwt(payload, ctx.context.secret, expires_in: ctx.context.options.email_verification[:expires_in] || 3600)
end

.create_sign_up_user(ctx, body, email, name, image) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/better_auth/routes/sign_up.rb', line 104

def self.(ctx, body, email, name, image)
  reserved = %w[email password name image callbackURL callbackUrl callback_url rememberMe remember_me]
  additional = parse_declared_input(ctx, "user", body.except(*reserved), allowed_base: [])
  ctx.context.internal_adapter.create_user(
    additional.merge(
      "email" => email.downcase,
      "name" => name,
      "image" => image,
      "emailVerified" => false
    )
  )
rescue APIError
  raise
rescue
  raise APIError.new("UNPROCESSABLE_ENTITY", message: BASE_ERROR_CODES["FAILED_TO_CREATE_USER"])
end

.credential_account(ctx, user_id) ⇒ Object



133
134
135
# File 'lib/better_auth/routes/password.rb', line 133

def self.(ctx, user_id)
  ctx.context.internal_adapter.find_accounts(user_id).find { |entry| entry["providerId"] == "credential" }
end

.current_session(ctx, allow_nil: false, sensitive: false) ⇒ Object

Raises:



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/better_auth/routes/session.rb', line 84

def self.current_session(ctx, allow_nil: false, sensitive: false)
  data = Session.find_current(
    ctx,
    disable_cookie_cache: truthy_query?(ctx.query, "disableCookieCache"),
    disable_refresh: truthy_query?(ctx.query, "disableRefresh"),
    sensitive: sensitive
  )
  return nil if allow_nil && data.nil?

  raise APIError.new("UNAUTHORIZED") unless data

  session = stringify_keys(data[:session] || data["session"])
  ensure_fresh_session!(ctx, session) if sensitive

  {
    session: session,
    user: stringify_keys(data[:user] || data["user"])
  }
end

.default_error_description(code) ⇒ Object



88
89
90
91
# File 'lib/better_auth/routes/error.rb', line 88

def self.default_error_description(code)
  "We encountered an unexpected error. You can find more information about this error at " \
    "<a href=\"https://better-auth.com/docs/reference/errors/#{URI.encode_www_form_component(code)}\">Better Auth docs</a>."
end

.delete_current_user!(ctx, session) ⇒ Object



148
149
150
151
152
153
154
155
# File 'lib/better_auth/routes/user.rb', line 148

def self.delete_current_user!(ctx, session)
  config = ctx.context.options.user[:delete_user] || {}
  call_option(config[:before_delete], session[:user], ctx.request)
  ctx.context.internal_adapter.delete_user(session[:user]["id"])
  ctx.context.internal_adapter.delete_sessions(session[:user]["id"])
  Cookies.delete_session_cookie(ctx)
  call_option(config[:after_delete], session[:user], ctx.request)
end

.delete_userObject



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/better_auth/routes/user.rb', line 63

def self.delete_user
  Endpoint.new(path: "/delete-user", method: "POST") do |ctx|
    enabled = ctx.context.options.user.dig(:delete_user, :enabled)
    raise APIError.new("NOT_FOUND") unless enabled

    session = current_session(ctx, sensitive: true)
    body = normalize_hash(ctx.body)
    sender = ctx.context.options.user.dig(:delete_user, :send_delete_account_verification)
    if body["password"]
       = (ctx, session[:user]["id"])
      unless  && ["password"] && verify_password_value(ctx, body["password"], ["password"])
        raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_PASSWORD"])
      end
    end

    if body["token"]
      delete_user_by_token!(ctx, session, body["token"])
    elsif sender
      token = SecureRandom.hex(16)
      ctx.context.internal_adapter.create_verification_value(
        identifier: "delete-account-#{token}",
        value: session[:user]["id"],
        expiresAt: Time.now + ctx.context.options.user.dig(:delete_user, :delete_token_expires_in).to_i
      )
      sender.call({user: session[:user], token: token}, ctx.request)
      next ctx.json({success: true, message: "Verification email sent"})
    elsif !body["password"]
      require_fresh_session!(ctx, session)
    end

    delete_current_user!(ctx, session)
    ctx.json({success: true, message: "User deleted"})
  end
end

.delete_user_by_token!(ctx, session, token) ⇒ Object



140
141
142
143
144
145
146
# File 'lib/better_auth/routes/user.rb', line 140

def self.delete_user_by_token!(ctx, session, token)
  verification = ctx.context.internal_adapter.find_verification_value("delete-account-#{token}")
  unless verification && verification["value"] == session[:user]["id"] && !expired_time?(verification["expiresAt"])
    raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES["INVALID_TOKEN"])
  end
  ctx.context.internal_adapter.delete_verification_value(verification["id"])
end

.delete_user_callbackObject



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/better_auth/routes/user.rb', line 98

def self.delete_user_callback
  Endpoint.new(path: "/delete-user/callback", method: "GET") do |ctx|
    enabled = ctx.context.options.user.dig(:delete_user, :enabled)
    raise APIError.new("NOT_FOUND") unless enabled
    session = current_session(ctx)
    token = fetch_value(ctx.query, "token")
    delete_user_by_token!(ctx, session, token)
    callback_url = fetch_value(ctx.query, "callbackURL")
    validate_callback_url!(ctx.context, callback_url)
    delete_current_user!(ctx, session)
    raise ctx.redirect(callback_url) if callback_url

    ctx.json({success: true, message: "User deleted"})
  end
end

.ensure_fresh_session!(ctx, session) ⇒ Object

Raises:



104
105
106
107
108
109
110
111
112
# File 'lib/better_auth/routes/session.rb', line 104

def self.ensure_fresh_session!(ctx, session)
  fresh_age = ctx.context.session_config[:fresh_age].to_i
  return if fresh_age.zero?

  created_at = normalize_time(session["createdAt"])
  return unless created_at && Time.now - created_at >= fresh_age

  raise APIError.new("FORBIDDEN", code: "SESSION_NOT_FRESH", message: BASE_ERROR_CODES.fetch("SESSION_NOT_FRESH"))
end

.errorObject



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/better_auth/routes/error.rb', line 7

def self.error
  Endpoint.new(
    path: "/error",
    method: "GET",
    metadata: {hide: true}
  ) do |ctx|
    query = ctx.query || {}
    raw_code = query["error"] || query[:error] || "UNKNOWN"
    raw_description = query["error_description"] || query[:error_description]
    safe_code = valid_error_code?(raw_code) ? raw_code.to_s : "UNKNOWN"
    query_params = error_query_params(safe_code, raw_description)
    error_url = ctx.context.options.on_api_error[:error_url]

    if error_url
      location = append_query(error_url, query_params)
      next [302, {"location" => location}, [""]]
    end

    if ctx.context.options.production? && !ctx.context.options.on_api_error[:customize_default_error_page]
      next [302, {"location" => "/?#{query_params}"}, [""]]
    end

    [
      200,
      {"content-type" => "text/html"},
      [error_html(safe_code, raw_description)]
    ]
  end
end

.error_html(code, description) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/better_auth/routes/error.rb', line 52

def self.error_html(code, description)
  safe_code = sanitize_html(code)
  safe_description = description ? sanitize_html(description) : default_error_description(safe_code)

  <<~HTML
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Error</title>
        <style>
          body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; min-height: 100vh; display: grid; place-items: center; background: #fff; color: #171717; }
          main { width: min(42rem, calc(100% - 2rem)); border: 2px solid #d4d4d4; padding: 1.5rem; text-align: center; }
          h1 { margin: 0 0 1rem; font-size: 3rem; line-height: 1; }
          code { display: inline-block; border: 1px solid #d4d4d4; padding: 0.375rem 0.75rem; margin-bottom: 1rem; word-break: break-all; }
          p { color: #525252; line-height: 1.5; }
          a { color: inherit; }
          @media (prefers-color-scheme: dark) {
            body { background: #171717; color: #fafafa; }
            main, code { border-color: #404040; }
            p { color: #d4d4d4; }
          }
        </style>
      </head>
      <body>
        <main>
          <h1>ERROR</h1>
          <code>#{safe_code}</code>
          <p>#{safe_description}</p>
        </main>
      </body>
    </html>
  HTML
end

.error_query_params(code, description) ⇒ Object



41
42
43
44
45
# File 'lib/better_auth/routes/error.rb', line 41

def self.error_query_params(code, description)
  params = {error: code}
  params[:error_description] = description if description
  URI.encode_www_form(params)
end

.expired_time?(value) ⇒ Boolean

Returns:

  • (Boolean)


137
138
139
# File 'lib/better_auth/routes/password.rb', line 137

def self.expired_time?(value)
  value && value < Time.now
end

.fetch_value(hash, key) ⇒ Object



141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/better_auth/routes/password.rb', line 141

def self.fetch_value(hash, key)
  snake_key = key.to_s
    .gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
    .tr("-", "_")
    .downcase
  hash[key] ||
    hash[key.to_s] ||
    hash[key.to_sym] ||
    hash[Schema.storage_key(key)] ||
    hash[Schema.storage_key(key).to_sym] ||
    hash[snake_key] ||
    hash[snake_key.to_sym]
end

.find_provider_account(ctx, user_id, provider_id, account_id = nil) ⇒ Object



136
137
138
139
140
# File 'lib/better_auth/routes/account.rb', line 136

def self.(ctx, user_id, provider_id,  = nil)
  ctx.context.internal_adapter.find_accounts(user_id).find do ||
    ["providerId"] == provider_id && (.to_s.empty? || ["id"] ==  || ["accountId"] == )
  end
end

.get_access_tokenObject



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
64
65
66
67
# File 'lib/better_auth/routes/account.rb', line 38

def self.get_access_token
  Endpoint.new(path: "/get-access-token", method: "POST") do |ctx|
    session = current_session(ctx, allow_nil: true)
    body = normalize_hash(ctx.body)
    user_id = session&.dig(:user, "id") || body["userId"] || body["user_id"]
    raise APIError.new("UNAUTHORIZED") if user_id.to_s.empty?

    provider_id = body["providerId"] || body["provider_id"]
    provider = social_provider(ctx.context, provider_id)
    raise APIError.new("BAD_REQUEST", message: "Provider #{provider_id} is not supported.") unless provider

     = body["accountId"] || body["account_id"]
     = (ctx, provider_id, , user_id) || (ctx, user_id, provider_id, )
    raise APIError.new("BAD_REQUEST", message: "Account not found") unless 

    if ["refreshToken"] && access_token_expired?() && provider_callable(provider, :refresh_access_token)
      tokens = call_provider(provider, :refresh_access_token, oauth_token_value(ctx, ["refreshToken"]))
      updated = (ctx, , tokens)
       = .merge(token_hash(tokens))
      Cookies.(ctx, updated || .merge(token_hash_for_storage(ctx, tokens)))
    end

    ctx.json({
      accessToken: oauth_token_value(ctx, ["accessToken"]),
      accessTokenExpiresAt: ["accessTokenExpiresAt"],
      scopes: ["scopes"] || (["scope"].to_s.empty? ? [] : ["scope"].to_s.split(",")),
      idToken: ["idToken"]
    })
  end
end

.get_sessionObject



5
6
7
8
9
10
11
12
13
14
15
16
17
# File 'lib/better_auth/routes/session.rb', line 5

def self.get_session
  Endpoint.new(path: "/get-session", method: "GET") do |ctx|
    session = current_session(ctx, allow_nil: true)
    next ctx.json(nil) unless session

    ctx.json(parsed_session_response(ctx, session))
  rescue APIError
    raise
  rescue => error
    log(ctx.context, :error, "FAILED_TO_GET_SESSION #{error.message}")
    raise APIError.new("INTERNAL_SERVER_ERROR", message: BASE_ERROR_CODES["FAILED_TO_GET_SESSION"])
  end
end

.hash_password(ctx, password) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
# File 'lib/better_auth/routes/password.rb', line 107

def self.hash_password(ctx, password)
  hasher = ctx.context.options.email_and_password.dig(:password, :hash)
  if hasher.respond_to?(:call)
    return hasher_arity_accepts_context?(hasher) ? hasher.call(password, ctx) : hasher.call(password)
  end

  Password.hash(
    password,
    algorithm: ctx.context.options.password_hasher
  )
end

.hasher_arity_accepts_context?(hasher) ⇒ Boolean

Returns:

  • (Boolean)


119
120
121
122
# File 'lib/better_auth/routes/password.rb', line 119

def self.hasher_arity_accepts_context?(hasher)
  arity = hasher.arity
  arity != 1 && arity != -1
end


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/better_auth/routes/social.rb', line 134

def self.link_social
  Endpoint.new(path: "/link-social", method: "POST") do |ctx|
    session = current_session(ctx)
    body = normalize_hash(ctx.body)
    provider_id = body["provider"].to_s
    provider = social_provider(ctx.context, provider_id)
    raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES["PROVIDER_NOT_FOUND"]) unless provider
    validate_social_callback_url!(ctx.context, body["callbackURL"] || body["callbackUrl"] || body["callback_url"], "INVALID_CALLBACK_URL")
    validate_social_callback_url!(ctx.context, body["errorCallbackURL"] || body["errorCallbackUrl"] || body["error_callback_url"], "INVALID_ERROR_CALLBACK_URL")

    id_token = fetch_value(body, "idToken")
    if id_token
      data = social_user_from_id_token!(ctx, provider, id_token)
      email = fetch_value(data[:user], "email").to_s.downcase
      unless linkable_provider?(ctx, provider_id, data[:user])
        raise APIError.new("UNAUTHORIZED", message: "Account not linked - untrusted provider")
      end
      unless email == session[:user]["email"].to_s.downcase || ctx.context.options..dig(:account_linking, :allow_different_emails)
        raise APIError.new("UNAUTHORIZED", message: "Account not linked - different emails not allowed")
      end

       = fetch_value(data[:user], "id").to_s
      existing = ctx.context.internal_adapter.find_accounts(session[:user]["id"]).find do ||
        ["providerId"] == provider_id && ["accountId"] == 
      end
      unless existing
        ctx.context.internal_adapter.(token_hash_for_storage(ctx, data[:account]).merge("userId" => session[:user]["id"]))
      end
      update_verified_email_on_link(ctx, session[:user]["id"], session[:user]["email"], data[:user])
      next ctx.json({url: "", status: true, redirect: false})
    end

    code_verifier = SecureRandom.hex(16)
    state_data = {
      "callbackURL" => body["callbackURL"] || body["callbackUrl"] || body["callback_url"] || ctx.context.base_url,
      "errorCallbackURL" => body["errorCallbackURL"] || body["errorCallbackUrl"] || body["error_callback_url"],
      "requestSignUp" => body["requestSignUp"] || body["request_sign_up"],
      "codeVerifier" => code_verifier,
      "link" => {
        "userId" => session[:user]["id"],
        "email" => session[:user]["email"]
      }
    }.merge(safe_additional_state(body))
    state = Crypto.sign_jwt(state_data, ctx.context.secret, expires_in: 600)
    url = call_provider(provider, :create_authorization_url, {
      state: state,
      codeVerifier: code_verifier,
      code_verifier: code_verifier,
      redirectURI: "#{ctx.context.base_url}/callback/#{provider_id}",
      redirect_uri: "#{ctx.context.base_url}/callback/#{provider_id}",
      scopes: body["scopes"],
      loginHint: body["loginHint"] || body["login_hint"]
    })
    ctx.set_header("location", url.to_s) unless body["disableRedirect"] || body["disable_redirect"]
    ctx.json({url: url.to_s, redirect: !(body["disableRedirect"] || body["disable_redirect"])})
  end
end


285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/better_auth/routes/social.rb', line 285

def self.(ctx, provider_id, , tokens, link)
  return {error: "unable_to_link_account"} unless linkable_provider?(ctx, provider_id, )

  email = fetch_value(, "email").to_s.downcase
  link_email = fetch_value(link, "email").to_s.downcase
  unless email == link_email || ctx.context.options..dig(:account_linking, :allow_different_emails)
    return {error: "email_doesn't_match"}
  end

   = fetch_value(, "id").to_s
  user_id = fetch_value(link, "userId").to_s
   = token_hash_for_storage(ctx, tokens).merge(
    "providerId" => provider_id,
    "accountId" => ,
    "userId" => user_id
  )
  existing = ctx.context.internal_adapter.(, provider_id)
  if existing
    return {error: "account_already_linked_to_different_user"} if existing["userId"].to_s != user_id

    ctx.context.internal_adapter.(existing["id"], )
  else
    ctx.context.internal_adapter.()
  end

  if ctx.context.options..dig(:account_linking, :update_user_info_on_link)
    ctx.context.internal_adapter.update_user(user_id, {
      name: fetch_value(, "name"),
      image: fetch_value(, "image")
    }.compact)
  end
  update_verified_email_on_link(ctx, user_id, link_email, )

  {status: true}
end

.linkable_provider?(ctx, provider_id, user_info, implicit: false) ⇒ Boolean

Returns:

  • (Boolean)


276
277
278
279
280
281
282
283
# File 'lib/better_auth/routes/social.rb', line 276

def self.linkable_provider?(ctx, provider_id, , implicit: false)
  linking = ctx.context.options.[:account_linking] || {}
  return false if linking[:enabled] == false
  return false if implicit && linking[:disable_implicit_linking] == true

  trusted = Array(linking[:trusted_providers]).map(&:to_s).include?(provider_id.to_s)
  trusted || !!fetch_value(, "emailVerified")
end

.list_accountsObject



5
6
7
8
9
10
11
12
13
14
15
# File 'lib/better_auth/routes/account.rb', line 5

def self.list_accounts
  Endpoint.new(path: "/list-accounts", method: "GET") do |ctx|
    session = current_session(ctx)
    accounts = ctx.context.internal_adapter.find_accounts(session[:user]["id"]).map do ||
      parsed = Schema.parse_output(ctx.context.options, "account", )
      scope = parsed.delete("scope")
      parsed.merge("scopes" => scope.to_s.empty? ? [] : scope.to_s.split(","))
    end
    ctx.json(accounts)
  end
end

.list_sessionsObject



19
20
21
22
23
24
25
26
27
28
29
# File 'lib/better_auth/routes/session.rb', line 19

def self.list_sessions
  Endpoint.new(path: "/list-sessions", method: "GET") do |ctx|
    session = current_session(ctx)
    sessions = ctx.context.internal_adapter.list_sessions(session[:user]["id"])
    active = sessions
      .map { |entry| stringify_keys(entry) }
      .select { |entry| !Session.expired?(entry) }
      .map { |entry| Schema.parse_output(ctx.context.options, "session", entry) }
    ctx.json(active)
  end
end

.log(context, level, message) ⇒ Object



151
152
153
154
155
156
157
158
# File 'lib/better_auth/routes/session.rb', line 151

def self.log(context, level, message)
  logger = context.logger
  if logger.respond_to?(:call)
    logger.call(level, message)
  elsif logger.respond_to?(level)
    logger.public_send(level, message)
  end
end

.normalize_hash(value) ⇒ Object



190
191
192
193
194
# File 'lib/better_auth/routes/sign_up.rb', line 190

def self.normalize_hash(value)
  value.each_with_object({}) do |(key, object_value), result|
    result[Schema.storage_key(key)] = object_value
  end
end

.normalize_time(value) ⇒ Object



114
115
116
117
118
119
120
121
# File 'lib/better_auth/routes/session.rb', line 114

def self.normalize_time(value)
  return value if value.is_a?(Time)
  return nil if value.nil? || value.to_s.empty?

  Time.parse(value.to_s)
rescue ArgumentError
  nil
end

.oauth_error_url(base_url, error, description = nil) ⇒ Object



259
260
261
262
263
264
265
266
# File 'lib/better_auth/routes/social.rb', line 259

def self.oauth_error_url(base_url, error, description = nil)
  uri = URI.parse(base_url.to_s)
  query = URI.decode_www_form(uri.query.to_s)
  query << ["error", error.to_s]
  query << ["error_description", description.to_s] if description
  uri.query = URI.encode_www_form(query)
  uri.to_s
end

.oauth_proxy_callback_url?(context, callback_url) ⇒ Boolean

Returns:

  • (Boolean)


338
339
340
341
342
343
344
345
346
347
348
# File 'lib/better_auth/routes/social.rb', line 338

def self.oauth_proxy_callback_url?(context, callback_url)
  uri = URI.parse(callback_url.to_s)
  proxy_path = "#{context.options.base_path}/oauth-proxy-callback"
  return false unless uri.path == proxy_path

  nested = URI.decode_www_form(uri.query.to_s).assoc("callbackURL")&.last
  validate_callback_url!(context, nested)
  true
rescue APIError, URI::InvalidURIError
  false
end

.oauth_token_for_storage(ctx, token) ⇒ Object



186
187
188
189
190
191
# File 'lib/better_auth/routes/account.rb', line 186

def self.oauth_token_for_storage(ctx, token)
  return token if token.to_s.empty?
  return token unless ctx.context.options.[:encrypt_oauth_tokens]

  Crypto.symmetric_encrypt(key: ctx.context.secret, data: token)
end

.oauth_token_value(ctx, token) ⇒ Object



193
194
195
196
197
198
# File 'lib/better_auth/routes/account.rb', line 193

def self.oauth_token_value(ctx, token)
  return token if token.to_s.empty?
  return token unless ctx.context.options.[:encrypt_oauth_tokens]

  Crypto.symmetric_decrypt(key: ctx.context.secret, data: token) || token
end

.okObject



5
6
7
8
9
10
11
12
13
# File 'lib/better_auth/routes/ok.rb', line 5

def self.ok
  Endpoint.new(
    path: "/ok",
    method: "GET",
    metadata: {hide: true}
  ) do |ctx|
    ctx.json({ok: true})
  end
end

.parse_declared_input(ctx, model, data, allowed_base: []) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/better_auth/routes/user.rb', line 165

def self.parse_declared_input(ctx, model, data, allowed_base: [])
  input = normalize_hash(data || {})
  table = Schema.auth_tables(ctx.context.options)[model.to_s]
  fields = table ? table.fetch(:fields) : {}
  additional = ctx.context.options.public_send(model.to_sym)[:additional_fields] || {}
  fields = fields.merge(additional.each_with_object({}) { |(key, value), result| result[Schema.storage_key(key)] = value }) if model.to_s == "session"
  declared_fields = fields.keys - core_model_fields(model)
  allowed = (Array(allowed_base).map { |field| Schema.storage_key(field) } + declared_fields).uniq

  input.each_with_object({}) do |(field, value), result|
    next unless fields.key?(field)
    next unless allowed.include?(field)

    attributes = fields.fetch(field)
    if attributes[:input] == false
      raise APIError.new("BAD_REQUEST", message: "#{field} is not allowed to be set")
    end

    result[field] = coerce_input_value(value, attributes)
  end
end

.parse_json_hash(value) ⇒ Object



357
358
359
360
361
362
363
364
365
# File 'lib/better_auth/routes/social.rb', line 357

def self.parse_json_hash(value)
  return value if value.is_a?(Hash)
  return {} if value.nil? || value.to_s.empty?

  parsed = JSON.parse(value.to_s)
  parsed.is_a?(Hash) ? parsed : {}
rescue JSON::ParserError
  {}
end

.parse_time(value) ⇒ Object



158
159
160
161
162
163
164
165
# File 'lib/better_auth/routes/account.rb', line 158

def self.parse_time(value)
  return value if value.is_a?(Time)
  return nil if value.nil? || value.to_s.empty?

  Time.parse(value.to_s)
rescue ArgumentError
  nil
end

.parsed_session_response(ctx, session) ⇒ Object



123
124
125
126
127
128
# File 'lib/better_auth/routes/session.rb', line 123

def self.parsed_session_response(ctx, session)
  {
    session: Schema.parse_output(ctx.context.options, "session", session[:session]),
    user: Schema.parse_output(ctx.context.options, "user", session[:user])
  }
end

.persist_social_user(ctx, provider_id, user_info, account_info, callback_url: nil, disable_sign_up: false) ⇒ Object



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/better_auth/routes/social.rb', line 224

def self.persist_social_user(ctx, provider_id, , , callback_url: nil, disable_sign_up: false)
  email = fetch_value(, "email").to_s.downcase
   = (["accountId"] || [:accountId] || [:account_id] || fetch_value(, "id")).to_s
  existing = ctx.context.internal_adapter.find_oauth_user(email, , provider_id)

  if existing && existing[:linked_account]
    user = existing[:user]
    new_user = false
  elsif existing
    unless linkable_provider?(ctx, provider_id, , implicit: true)
      return {error: "account not linked"}
    end
    user = existing[:user]
    ctx.context.internal_adapter.(.merge("providerId" => provider_id, "accountId" => , "userId" => user["id"]))
    new_user = false
  else
    return {error: "signup disabled"} if 

    created = ctx.context.internal_adapter.create_oauth_user(
      {
        email: email,
        name: fetch_value(, "name").to_s,
        image: fetch_value(, "image"),
        emailVerified: !!fetch_value(, "emailVerified")
      },
      .merge("providerId" => provider_id, "accountId" => )
    )
    user = created[:user]
    new_user = true
  end

  session = ctx.context.internal_adapter.create_session(user["id"], false, session_overrides(ctx), true, ctx)
  {session: session, user: user, new_user: new_user}
end

.provider_callable(provider, key) ⇒ Object



200
201
202
# File 'lib/better_auth/routes/account.rb', line 200

def self.provider_callable(provider, key)
  provider.respond_to?(key) || (provider.is_a?(Hash) && (provider[key] || provider[key.to_s]))
end

.provider_disable_implicit_sign_up?(provider) ⇒ Boolean

Returns:

  • (Boolean)


268
269
270
# File 'lib/better_auth/routes/social.rb', line 268

def self.provider_disable_implicit_sign_up?(provider)
  !!(fetch_value(provider, "disableImplicitSignUp") || fetch_value(provider, "disableSignUp") || fetch_value(fetch_value(provider, "options") || {}, "disableSignUp"))
end

.provider_disable_sign_up?(provider) ⇒ Boolean

Returns:

  • (Boolean)


272
273
274
# File 'lib/better_auth/routes/social.rb', line 272

def self.provider_disable_sign_up?(provider)
  !!(fetch_value(provider, "disableSignUp") || fetch_value(fetch_value(provider, "options") || {}, "disableSignUp"))
end

.redirect_or_error(ctx, callback_url, error) ⇒ Object

Raises:



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

def self.redirect_or_error(ctx, callback_url, error)
  if callback_url
    separator = callback_url.include?("?") ? "&" : "?"
    raise ctx.redirect("#{callback_url}#{separator}error=#{error}")
  end
  raise APIError.new("UNAUTHORIZED", message: error)
end

.redirect_or_json(ctx, callback_url, data) ⇒ Object



95
96
97
98
99
# File 'lib/better_auth/routes/email_verification.rb', line 95

def self.redirect_or_json(ctx, callback_url, data)
  raise ctx.redirect(callback_url) if callback_url

  ctx.json(data)
end

.refresh_tokenObject



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/better_auth/routes/account.rb', line 69

def self.refresh_token
  Endpoint.new(path: "/refresh-token", method: "POST") do |ctx|
    session = current_session(ctx, allow_nil: true)
    body = normalize_hash(ctx.body)
    user_id = session&.dig(:user, "id") || body["userId"] || body["user_id"]
    raise APIError.new("BAD_REQUEST", message: "Either userId or session is required") if user_id.to_s.empty?

    provider_id = body["providerId"] || body["provider_id"]
    provider = social_provider(ctx.context, provider_id)
    raise APIError.new("BAD_REQUEST", message: "Provider #{provider_id} not found.") unless provider
    raise APIError.new("BAD_REQUEST", message: "Provider #{provider_id} does not support token refreshing.") unless provider_callable(provider, :refresh_access_token)

     = body["accountId"] || body["account_id"]
     = (ctx, provider_id, , user_id) || (ctx, user_id, provider_id, )
    raise APIError.new("BAD_REQUEST", message: "Account not found") unless 
    refresh_token = oauth_token_value(ctx, ["refreshToken"])
    raise APIError.new("BAD_REQUEST", message: "Refresh token not found") if refresh_token.to_s.empty?

    tokens = call_provider(provider, :refresh_access_token, refresh_token)
    updated = (ctx, , tokens)
    values = token_hash(tokens)
    Cookies.(ctx, updated || .merge(token_hash_for_storage(ctx, tokens)))
    ctx.json({
      accessToken: values["accessToken"],
      refreshToken: values["refreshToken"],
      accessTokenExpiresAt: values["accessTokenExpiresAt"],
      refreshTokenExpiresAt: values["refreshTokenExpiresAt"],
      scope: Array(values["scopes"]).join(","),
      idToken: values["idToken"] || ["idToken"],
      providerId: ["providerId"],
      accountId: ["accountId"]
    })
  end
end

.request_password_resetObject



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/better_auth/routes/password.rb', line 10

def self.request_password_reset
  Endpoint.new(path: "/request-password-reset", method: "POST") do |ctx|
    sender = ctx.context.options.email_and_password[:send_reset_password]
    raise APIError.new("BAD_REQUEST", message: "Reset password isn't enabled") unless sender.respond_to?(:call)

    body = normalize_hash(ctx.body)
    email = body["email"].to_s.downcase
    found = ctx.context.internal_adapter.find_user_by_email(email, include_accounts: true)
    unless found
      SecureRandom.hex(12)
      ctx.context.internal_adapter.find_verification_value("dummy-verification-token")
      next ctx.json({status: true, message: PASSWORD_RESET_MESSAGE})
    end

    token = SecureRandom.hex(12)
    expires_in = ctx.context.options.email_and_password[:reset_password_token_expires_in] || 3600
    ctx.context.internal_adapter.create_verification_value(
      identifier: "reset-password:#{token}",
      value: found[:user]["id"],
      expiresAt: Time.now + expires_in.to_i
    )

    redirect_to = body["redirectTo"] || body["redirect_to"]
    callback = redirect_to ? URI.encode_www_form_component(redirect_to) : ""
    url = "#{ctx.context.base_url}/reset-password/#{token}?callbackURL=#{callback}"
    sender.call({user: found[:user], url: url, token: token}, ctx.request)
    ctx.json({status: true, message: PASSWORD_RESET_MESSAGE})
  end
end

.request_password_reset_callbackObject



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/better_auth/routes/password.rb', line 40

def self.request_password_reset_callback
  Endpoint.new(path: "/reset-password/:token", method: "GET") do |ctx|
    token = ctx.params[:token].to_s
    callback_url = fetch_value(ctx.query, "callbackURL") || "/error"
    validate_callback_url!(ctx.context, callback_url)
    verification = ctx.context.internal_adapter.find_verification_value("reset-password:#{token}")

    unless verification && !expired_time?(verification["expiresAt"])
      raise ctx.redirect(absolute_callback(ctx.context, callback_url, error: "INVALID_TOKEN"))
    end

    raise ctx.redirect(absolute_callback(ctx.context, callback_url, token: token))
  end
end

.require_fresh_session!(ctx, session) ⇒ Object

Raises:



157
158
159
160
161
162
163
# File 'lib/better_auth/routes/user.rb', line 157

def self.require_fresh_session!(ctx, session)
  fresh_age = ctx.context.session_config[:fresh_age].to_i
  return if fresh_age <= 0

  updated_at = Session.normalize_time(session[:session]["updatedAt"] || session[:session]["updated_at"] || session[:session]["createdAt"] || session[:session]["created_at"])
  raise APIError.new("UNAUTHORIZED") unless updated_at && updated_at + fresh_age > Time.now
end

.reset_passwordObject



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/better_auth/routes/password.rb', line 55

def self.reset_password
  Endpoint.new(path: "/reset-password", method: "POST") do |ctx|
    body = normalize_hash(ctx.body)
    token = body["token"] || fetch_value(ctx.query, "token")
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_TOKEN"]) if token.to_s.empty?

    password = body["newPassword"] || body["new_password"]
    validate_password_length!(password, ctx.context.options.email_and_password)

    verification = ctx.context.internal_adapter.find_verification_value("reset-password:#{token}")
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_TOKEN"]) unless verification && !expired_time?(verification["expiresAt"])

    user_id = verification["value"]
    hashed = hash_password(ctx, password)
     = ctx.context.internal_adapter.find_accounts(user_id).find { |entry| entry["providerId"] == "credential" }
    if 
      ctx.context.internal_adapter.update_password(user_id, hashed)
    else
      ctx.context.internal_adapter.(userId: user_id, providerId: "credential", accountId: user_id, password: hashed)
    end
    ctx.context.internal_adapter.delete_verification_value(verification["id"])

    if (callback = ctx.context.options.email_and_password[:on_password_reset])
      user = ctx.context.internal_adapter.find_user_by_id(user_id)
      callback.call({user: user}, ctx.request) if user
    end
    ctx.context.internal_adapter.delete_sessions(user_id) if ctx.context.options.email_and_password[:revoke_sessions_on_password_reset]

    ctx.json({status: true})
  end
end

.revoke_other_sessionsObject



69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/better_auth/routes/session.rb', line 69

def self.revoke_other_sessions
  Endpoint.new(path: "/revoke-other-sessions", method: "POST") do |ctx|
    session = current_session(ctx, sensitive: true)
    current_token = session[:session]["token"]
    sessions = ctx.context.internal_adapter.list_sessions(session[:user]["id"])
    sessions.each do |entry|
      data = stringify_keys(entry)
      next if Session.expired?(data) || data["token"] == current_token

      ctx.context.internal_adapter.delete_session(data["token"])
    end
    ctx.json({status: true})
  end
end

.revoke_sessionObject



45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/better_auth/routes/session.rb', line 45

def self.revoke_session
  Endpoint.new(path: "/revoke-session", method: "POST") do |ctx|
    session = current_session(ctx, sensitive: true)
    body = normalize_hash(ctx.body)
    token = body["token"].to_s
    found = ctx.context.internal_adapter.find_session(token)

    if found && stringify_keys(found[:session] || found["session"])["userId"] == session[:user]["id"]
      ctx.context.internal_adapter.delete_session(token)
    end

    ctx.json({status: true})
  end
end

.revoke_sessionsObject



60
61
62
63
64
65
66
67
# File 'lib/better_auth/routes/session.rb', line 60

def self.revoke_sessions
  Endpoint.new(path: "/revoke-sessions", method: "POST") do |ctx|
    session = current_session(ctx, sensitive: true)
    ctx.context.internal_adapter.delete_sessions(session[:user]["id"])
    Cookies.delete_session_cookie(ctx)
    ctx.json({status: true})
  end
end

.safe_additional_state(body) ⇒ Object



321
322
323
324
325
326
327
# File 'lib/better_auth/routes/social.rb', line 321

def self.safe_additional_state(body)
  additional = body["additionalData"] || body["additional_data"]
  return {} unless additional.is_a?(Hash)

  reserved = %w[callbackURL callbackUrl callback_url errorCallbackURL errorCallbackUrl error_callback_url errorURL error_url newUserCallbackURL newUserCallbackUrl new_user_callback_url newUserURL new_user_url requestSignUp request_sign_up codeVerifier code_verifier link expiresAt expires_at]
  normalize_hash(additional).reject { |key, _value| reserved.include?(key.to_s) }
end

.sanitize_html(value) ⇒ Object



93
94
95
96
97
98
99
100
# File 'lib/better_auth/routes/error.rb', line 93

def self.sanitize_html(value)
  value.to_s
    .gsub("<", "&lt;")
    .gsub(">", "&gt;")
    .gsub('"', "&quot;")
    .gsub("'", "&#39;")
    .gsub(/&(?!(?:amp|lt|gt|quot|#39|#x[0-9a-fA-F]+|#[0-9]+);)/, "&amp;")
end

.send_sign_in_verification_email(ctx, user, callback_url) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/better_auth/routes/sign_in.rb', line 75

def self.(ctx, user, callback_url)
  verification = ctx.context.options.email_verification
  sender = verification[:send_verification_email]
  return unless verification[:send_on_sign_in] && sender.respond_to?(:call)

  token = Crypto.sign_jwt(
    {"email" => user["email"].to_s.downcase},
    ctx.context.secret,
    expires_in: verification[:expires_in] || 3600
  )
  callback = URI.encode_www_form_component(callback_url || "/")
  url = "#{ctx.context.base_url}/verify-email?token=#{URI.encode_www_form_component(token)}&callbackURL=#{callback}"
  sender.call({user: user, url: url, token: token}, ctx.request)
end

.send_sign_up_verification_email(ctx, user, callback_url) ⇒ Object



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/better_auth/routes/sign_up.rb', line 164

def self.(ctx, user, callback_url)
  verification = ctx.context.options.email_verification
  password_config = ctx.context.options.email_and_password
   = verification.key?(:send_on_sign_up) ? verification[:send_on_sign_up] : password_config[:require_email_verification]
  return unless 

  sender = verification[:send_verification_email]
  return unless sender.respond_to?(:call)

  token = Crypto.sign_jwt(
    {"email" => user["email"].to_s.downcase},
    ctx.context.secret,
    expires_in: verification[:expires_in] || 3600
  )
  callback = URI.encode_www_form_component(callback_url || "/")
  url = "#{ctx.context.base_url}/verify-email?token=#{URI.encode_www_form_component(token)}&callbackURL=#{callback}"
  sender.call({user: user, url: url, token: token}, ctx.request)
end

.send_verification_emailObject



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/better_auth/routes/email_verification.rb', line 7

def self.send_verification_email
  Endpoint.new(path: "/send-verification-email", method: "POST") do |ctx|
    sender = ctx.context.options.email_verification[:send_verification_email]
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["VERIFICATION_EMAIL_NOT_ENABLED"]) unless sender.respond_to?(:call)

    body = normalize_hash(ctx.body)
    email = body["email"].to_s.downcase
    session = current_session(ctx, allow_nil: true)

    if session
      raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["EMAIL_MISMATCH"]) if session[:user]["email"] != email
      raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["EMAIL_ALREADY_VERIFIED"]) if session[:user]["emailVerified"]

      send_verification_email_payload(ctx, session[:user], body["callbackURL"] || body["callbackUrl"] || body["callback_url"])
      next ctx.json({status: true})
    end

    found = ctx.context.internal_adapter.find_user_by_email(email)
    if found && !found[:user]["emailVerified"]
      send_verification_email_payload(ctx, found[:user], body["callbackURL"] || body["callbackUrl"] || body["callback_url"])
    else
      create_email_verification_token(ctx, email)
    end
    ctx.json({status: true})
  end
end

.send_verification_email_payload(ctx, user, callback_url) ⇒ Object



67
68
69
70
71
72
# File 'lib/better_auth/routes/email_verification.rb', line 67

def self.send_verification_email_payload(ctx, user, callback_url)
  token = create_email_verification_token(ctx, user["email"])
  callback = URI.encode_www_form_component(callback_url || "/")
  url = "#{ctx.context.base_url}/verify-email?token=#{URI.encode_www_form_component(token)}&callbackURL=#{callback}"
  ctx.context.options.email_verification[:send_verification_email].call({user: user, url: url, token: token}, ctx.request)
end

.session_overrides(ctx) ⇒ Object



183
184
185
186
187
188
# File 'lib/better_auth/routes/sign_up.rb', line 183

def self.session_overrides(ctx)
  {
    ipAddress: RequestIP.client_ip(ctx, ctx.context.options).to_s,
    userAgent: ctx.headers["user-agent"].to_s
  }
end

.set_passwordObject



44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/better_auth/routes/user.rb', line 44

def self.set_password
  Endpoint.new(path: "/set-password", method: "POST") do |ctx|
    session = current_session(ctx, sensitive: true)
    body = normalize_hash(ctx.body)
    new_password = body["newPassword"] || body["new_password"]
    validate_password_length!(new_password, ctx.context.options.email_and_password)
     = (ctx, session[:user]["id"])
    raise APIError.new("BAD_REQUEST", message: "user already has a password") if  && ["password"]

    ctx.context.internal_adapter.(
      userId: session[:user]["id"],
      providerId: "credential",
      accountId: session[:user]["id"],
      password: hash_password(ctx, new_password)
    )
    ctx.json({status: true})
  end
end


101
102
103
104
105
# File 'lib/better_auth/routes/email_verification.rb', line 101

def self.set_verified_session_cookie(ctx, user)
  session = current_session(ctx, allow_nil: true)
  session_data = session ? session[:session] : ctx.context.internal_adapter.create_session(user["id"])
  Cookies.set_session_cookie(ctx, {session: session_data, user: user})
end

.sign_in_emailObject



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
64
65
66
67
68
69
70
71
72
73
# File 'lib/better_auth/routes/sign_in.rb', line 7

def self.
  Endpoint.new(
    path: "/sign-in/email",
    method: "POST",
    metadata: {
      allowed_media_types: [
        "application/x-www-form-urlencoded",
        "application/json"
      ]
    }
  ) do |ctx|
    options = ctx.context.options
    email_config = options.email_and_password
    if email_config[:enabled] != true
      raise APIError.new("BAD_REQUEST", message: "Email and password is not enabled")
    end

    body = normalize_hash(ctx.body)
    email = body["email"].to_s
    password = body["password"].to_s
    callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"]
    remember_me = body.key?("rememberMe") ? body["rememberMe"] : body["remember_me"]

    unless EMAIL_PATTERN.match?(email)
      raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_EMAIL"])
    end

    found = ctx.context.internal_adapter.find_user_by_email(email, include_accounts: true)
    unless found
      hash_password(ctx, password)
      raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["INVALID_EMAIL_OR_PASSWORD"])
    end

    user = found[:user] || found["user"]
    accounts = found[:accounts] || found["accounts"] || []
     = accounts.find { || ["providerId"] == "credential" || [:providerId] == "credential" }
    current_password =  && (["password"] || [:password])
    unless current_password && verify_password_value(ctx, password, current_password)
      hash_password(ctx, password) unless current_password
      raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["INVALID_EMAIL_OR_PASSWORD"])
    end

    if email_config[:require_email_verification] && !user["emailVerified"]
      (ctx, user, callback_url)
      raise APIError.new("FORBIDDEN", message: BASE_ERROR_CODES["EMAIL_NOT_VERIFIED"])
    end

    dont_remember_me = remember_me == false || remember_me.to_s == "false"
    session = ctx.context.internal_adapter.create_session(
      user["id"],
      dont_remember_me,
      session_overrides(ctx),
      true,
      ctx
    )
    raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["FAILED_TO_CREATE_SESSION"]) unless session

    Cookies.set_session_cookie(ctx, {session: session, user: user}, dont_remember_me)
    ctx.set_header("location", callback_url) if callback_url
    ctx.json({
      redirect: !!callback_url,
      token: session["token"],
      url: callback_url,
      user: Schema.parse_output(options, "user", user)
    })
  end
end

.sign_in_socialObject



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
64
65
# File 'lib/better_auth/routes/social.rb', line 9

def self.
  Endpoint.new(path: "/sign-in/social", method: "POST") do |ctx|
    body = normalize_hash(ctx.body)
    provider_id = body["provider"].to_s
    provider = social_provider(ctx.context, provider_id)
    raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES["PROVIDER_NOT_FOUND"]) unless provider
    validate_social_callback_url!(ctx.context, body["callbackURL"] || body["callbackUrl"] || body["callback_url"], "INVALID_CALLBACK_URL")
    validate_social_callback_url!(ctx.context, body["errorCallbackURL"] || body["errorCallbackUrl"] || body["error_callback_url"], "INVALID_ERROR_CALLBACK_URL")
    validate_social_callback_url!(ctx.context, body["newUserCallbackURL"] || body["newUserCallbackUrl"] || body["new_user_callback_url"], "INVALID_NEW_USER_CALLBACK_URL")

    id_token = fetch_value(body, "idToken")
    if id_token
      data = social_user_from_id_token!(ctx, provider, id_token)
      session_data = persist_social_user(
        ctx,
        provider_id,
        data[:user],
        token_hash_for_storage(ctx, data[:account]),
        callback_url: body["callbackURL"],
        disable_sign_up: provider_disable_sign_up?(provider) || (provider_disable_implicit_sign_up?(provider) && !body["requestSignUp"])
      )
      raise APIError.new("UNAUTHORIZED", message: session_data[:error], code: "OAUTH_LINK_ERROR") if session_data[:error]

      Cookies.set_session_cookie(ctx, session_data)
      next ctx.json({
        redirect: false,
        token: session_data[:session]["token"],
        url: nil,
        user: Schema.parse_output(ctx.context.options, "user", session_data[:user])
      })
    end

    code_verifier = SecureRandom.hex(16)
    state = Crypto.sign_jwt(
      {
        "callbackURL" => body["callbackURL"] || body["callbackUrl"] || body["callback_url"] || "/",
        "errorCallbackURL" => body["errorCallbackURL"] || body["errorCallbackUrl"] || body["error_callback_url"],
        "newUserCallbackURL" => body["newUserCallbackURL"] || body["newUserCallbackUrl"] || body["new_user_callback_url"],
        "requestSignUp" => body["requestSignUp"] || body["request_sign_up"],
        "codeVerifier" => code_verifier
      }.merge(safe_additional_state(body)),
      ctx.context.secret,
      expires_in: 600
    )
    url = call_provider(provider, :create_authorization_url, {
      state: state,
      codeVerifier: code_verifier,
      code_verifier: code_verifier,
      redirectURI: "#{ctx.context.base_url}/callback/#{provider_id}",
      redirect_uri: "#{ctx.context.base_url}/callback/#{provider_id}",
      scopes: body["scopes"],
      loginHint: body["loginHint"] || body["login_hint"]
    })
    ctx.set_header("location", url.to_s) unless body["disableRedirect"] || body["disable_redirect"]
    ctx.json({url: url.to_s, redirect: !(body["disableRedirect"] || body["disable_redirect"])})
  end
end

.sign_outObject



5
6
7
8
9
10
11
12
13
# File 'lib/better_auth/routes/sign_out.rb', line 5

def self.sign_out
  Endpoint.new(path: "/sign-out", method: "POST") do |ctx|
    token_cookie = ctx.context.auth_cookies[:session_token]
    token = ctx.get_signed_cookie(token_cookie.name, ctx.context.secret)
    ctx.context.internal_adapter.delete_session(token) if token
    Cookies.delete_session_cookie(ctx)
    ctx.json({success: true})
  end
end

.sign_up_emailObject



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/better_auth/routes/sign_up.rb', line 10

def self.
  Endpoint.new(
    path: "/sign-up/email",
    method: "POST",
    metadata: {
      allowed_media_types: [
        "application/x-www-form-urlencoded",
        "application/json"
      ]
    }
  ) do |ctx|
    options = ctx.context.options
    email_config = options.email_and_password
    if email_config[:enabled] != true || email_config[:disable_sign_up]
      raise APIError.new("BAD_REQUEST", message: "Email and password sign up is not enabled")
    end

    body = normalize_hash(ctx.body)
    name = body["name"].to_s
    email = body["email"].to_s
    password = body["password"]
    image = body["image"]
    callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"]
    remember_me = body.key?("rememberMe") ? body["rememberMe"] : body["remember_me"]

    (email, password, email_config)

    ctx.context.adapter.transaction do
      existing = ctx.context.internal_adapter.find_user_by_email(email)
      if existing
        if email_config[:require_email_verification]
          hash_password(ctx, password)
          (ctx, email_config, existing)
          synthetic_user = (ctx, body, email, name, image)
          next ctx.json({token: nil, user: Schema.parse_output(options, "user", synthetic_user)})
        end

        raise APIError.new(
          "UNPROCESSABLE_ENTITY",
          message: BASE_ERROR_CODES["USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"]
        )
      end

      hashed_password = hash_password(ctx, password)
      created_user = (ctx, body, email, name, image)
      raise APIError.new("UNPROCESSABLE_ENTITY", message: BASE_ERROR_CODES["FAILED_TO_CREATE_USER"]) unless created_user

      ctx.context.internal_adapter.(
        userId: created_user["id"],
        providerId: "credential",
        accountId: created_user["id"],
        password: hashed_password
      )

      (ctx, created_user, callback_url)

      if email_config[:auto_sign_in] == false || email_config[:require_email_verification]
        next ctx.json({token: nil, user: Schema.parse_output(options, "user", created_user)})
      end

      dont_remember_me = remember_me == false || remember_me.to_s == "false"
      session = ctx.context.internal_adapter.create_session(
        created_user["id"],
        dont_remember_me,
        session_overrides(ctx),
        true,
        ctx
      )
      raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["FAILED_TO_CREATE_SESSION"]) unless session

      Cookies.set_session_cookie(ctx, {session: session, user: created_user}, dont_remember_me)
      ctx.json({token: session["token"], user: Schema.parse_output(options, "user", created_user)})
    end
  end
end

.social_provider(context, provider_id) ⇒ Object



129
130
131
132
133
134
# File 'lib/better_auth/routes/account.rb', line 129

def self.social_provider(context, provider_id)
  provider = context.social_providers[provider_id.to_sym] || context.social_providers[provider_id.to_s]
  return provider.merge(id: provider_id.to_s) if provider.is_a?(Hash) && !provider.key?(:id) && !provider.key?("id")

  provider
end

.social_user_from_id_token!(ctx, provider, id_token) ⇒ Object

Raises:



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/better_auth/routes/social.rb', line 192

def self.social_user_from_id_token!(ctx, provider, id_token)
  token = fetch_value(id_token, "token").to_s
  valid = call_provider(provider, :verify_id_token, token, fetch_value(id_token, "nonce"))
  raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["INVALID_TOKEN"]) unless valid

  token_user = parse_json_hash(fetch_value(id_token, "user"))
   = call_provider(provider, :get_user_info, {
    "idToken" => token,
    "id_token" => token,
    "accessToken" => fetch_value(id_token, "accessToken"),
    "access_token" => fetch_value(id_token, "accessToken"),
    "refreshToken" => fetch_value(id_token, "refreshToken"),
    "refresh_token" => fetch_value(id_token, "refreshToken"),
    "user" => token_user
  })
  user = [:user] || ["user"] if 
  raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["FAILED_TO_GET_USER_INFO"]) unless user
  raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["USER_EMAIL_NOT_FOUND"]) if fetch_value(user, "email").to_s.empty?

  {
    user: user,
    account: {
      "providerId" => fetch_value(provider, "id").to_s,
      "accountId" => fetch_value(user, "id").to_s,
      "accessToken" => fetch_value(id_token, "accessToken"),
      "refreshToken" => fetch_value(id_token, "refreshToken"),
      "idToken" => token,
      "user" => token_user
    }
  }
end

.stringify_keys(value) ⇒ Object



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

def self.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

.stringify_synthetic_user(value) ⇒ Object



158
159
160
161
162
# File 'lib/better_auth/routes/sign_up.rb', line 158

def self.stringify_synthetic_user(value)
  return value.each_with_object({}) { |(key, object_value), result| result[Schema.storage_key(key)] = object_value } if value.is_a?(Hash)

  {}
end

.synthetic_sign_up_user(ctx, body, email, name, image) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/better_auth/routes/sign_up.rb', line 134

def self.(ctx, body, email, name, image)
  now = Time.now
  core_fields = {
    "id" => SecureRandom.hex(16),
    "name" => name,
    "email" => email.to_s.downcase,
    "emailVerified" => false,
    "image" => image,
    "createdAt" => now,
    "updatedAt" => now
  }
  reserved = %w[email password name image callbackURL callbackUrl callback_url rememberMe remember_me]
  additional = parse_declared_input(ctx, "user", body.except(*reserved), allowed_base: [])
  custom = ctx.context.options.email_and_password[:custom_synthetic_user]
  return core_fields.merge(additional) unless custom.respond_to?(:call)

  value = {
    core_fields: core_fields.except("id"),
    additional_fields: additional,
    id: core_fields["id"]
  }
  stringify_synthetic_user(custom.call(value))
end

.token_hash(tokens) ⇒ Object



173
174
175
176
177
# File 'lib/better_auth/routes/account.rb', line 173

def self.token_hash(tokens)
  data = normalize_hash(tokens || {})
  data["scope"] = Array(data.delete("scopes")).join(",") if data.key?("scopes")
  data
end

.token_hash_for_storage(ctx, tokens) ⇒ Object



179
180
181
182
183
184
# File 'lib/better_auth/routes/account.rb', line 179

def self.token_hash_for_storage(ctx, tokens)
  data = token_hash(tokens)
  data["accessToken"] = oauth_token_for_storage(ctx, data["accessToken"]) if data.key?("accessToken")
  data["refreshToken"] = oauth_token_for_storage(ctx, data["refreshToken"]) if data.key?("refreshToken")
  data
end

.truthy_query?(query, key) ⇒ Boolean

Returns:

  • (Boolean)


130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/better_auth/routes/session.rb', line 130

def self.truthy_query?(query, key)
  snake_key = key.to_s
    .gsub(/([a-z\d])([A-Z])/, "\\1_\\2")
    .tr("-", "_")
    .downcase
  value = query[key] ||
    query[key.to_sym] ||
    query[Schema.storage_key(key)] ||
    query[Schema.storage_key(key).to_sym] ||
    query[snake_key] ||
    query[snake_key.to_sym]
  value == true || value.to_s == "true"
end


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/better_auth/routes/account.rb', line 17

def self.
  Endpoint.new(path: "/unlink-account", method: "POST") do |ctx|
    session = current_session(ctx, sensitive: true)
    body = normalize_hash(ctx.body)
    accounts = ctx.context.internal_adapter.find_accounts(session[:user]["id"])
    if accounts.length == 1 && !ctx.context.options..dig(:account_linking, :allow_unlinking_all)
      raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["FAILED_TO_UNLINK_LAST_ACCOUNT"])
    end

    provider_id = body["providerId"] || body["provider_id"]
     = body["accountId"] || body["account_id"]
     = accounts.find do |entry|
      entry["providerId"] == provider_id && (.to_s.empty? || entry["accountId"] == )
    end
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["ACCOUNT_NOT_FOUND"]) unless 

    ctx.context.internal_adapter.(["id"])
    ctx.json({status: true})
  end
end

.update_account_tokens(ctx, account, tokens) ⇒ Object



167
168
169
170
171
# File 'lib/better_auth/routes/account.rb', line 167

def self.(ctx, , tokens)
  return nil if ["id"].to_s.empty?

  ctx.context.internal_adapter.(["id"], token_hash_for_storage(ctx, tokens))
end

.update_sessionObject



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

def self.update_session
  Endpoint.new(path: "/update-session", method: "POST") do |ctx|
    session = current_session(ctx, sensitive: true)
    body = Routes.parse_declared_input(ctx, "session", ctx.body, allowed_base: [])
    raise APIError.new("BAD_REQUEST", message: "No fields to update") if body.empty?

    update = body.merge("updatedAt" => Time.now)
    updated = ctx.context.internal_adapter.update_session(session[:session]["token"], update)
    merged = session[:session].merge(updated || update)
    Cookies.set_session_cookie(ctx, {session: merged, user: session[:user]}, Cookies.dont_remember?(ctx))
    ctx.json(parsed_session_response(ctx, {session: merged, user: session[:user]}))
  end
end

.update_userObject



5
6
7
8
9
10
11
12
13
14
15
16
17
18
# File 'lib/better_auth/routes/user.rb', line 5

def self.update_user
  Endpoint.new(path: "/update-user", method: "POST") do |ctx|
    session = current_session(ctx)
    body = normalize_hash(ctx.body)
    raise APIError.new("BAD_REQUEST", message: "Body must be an object") unless body.is_a?(Hash)
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["EMAIL_CAN_NOT_BE_UPDATED"]) if body.key?("email")
    update = parse_declared_input(ctx, "user", body, allowed_base: ["name", "image"])
    raise APIError.new("BAD_REQUEST", message: "No fields to update") if update.empty?

    updated = ctx.context.internal_adapter.update_user(session[:user]["id"], update)
    Cookies.set_session_cookie(ctx, {session: session[:session], user: updated}, Cookies.dont_remember?(ctx))
    ctx.json({status: true})
  end
end


350
351
352
353
354
355
# File 'lib/better_auth/routes/social.rb', line 350

def self.update_verified_email_on_link(ctx, user_id, current_email, social_user)
  return unless fetch_value(social_user, "emailVerified")
  return unless fetch_value(social_user, "email").to_s.downcase == current_email.to_s.downcase

  ctx.context.internal_adapter.update_user(user_id, {"emailVerified" => true})
end

.valid_error_code?(value) ⇒ Boolean

Returns:

  • (Boolean)


37
38
39
# File 'lib/better_auth/routes/error.rb', line 37

def self.valid_error_code?(value)
  /\A['A-Za-z0-9_-]+\z/.match?(value.to_s)
end

.validate_callback_url!(context, callback_url) ⇒ Object

Raises:



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/better_auth/routes/password.rb', line 166

def self.validate_callback_url!(context, callback_url)
  return if callback_url.nil? || callback_url.to_s.empty?

  value = callback_url.to_s
  if value.start_with?("/")
    return if Configuration.relative_path_allowed?(value)
  else
    uri = Configuration.parse_uri(value)
    base_uri = Configuration.parse_uri(context.base_url.to_s)
    base_origin = base_uri && Configuration.origin_for(base_uri)
    return if uri && Configuration.origin_for(uri) == base_origin
    return if context.trusted_origin?(value)
  end

  raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_CALLBACK_URL"])
end

.validate_password_length!(password, email_config) ⇒ Object

Raises:



99
100
101
102
103
104
105
# File 'lib/better_auth/routes/password.rb', line 99

def self.validate_password_length!(password, email_config)
  unless password.is_a?(String) && !password.empty?
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_PASSWORD"])
  end
  raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["PASSWORD_TOO_SHORT"]) if password.length < email_config[:min_password_length].to_i
  raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["PASSWORD_TOO_LONG"]) if password.length > email_config[:max_password_length].to_i
end

.validate_sign_up_input!(email, password, email_config) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/better_auth/routes/sign_up.rb', line 86

def self.(email, password, email_config)
  unless EMAIL_PATTERN.match?(email)
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_EMAIL"])
  end

  unless password.is_a?(String) && !password.empty?
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_PASSWORD"])
  end

  if password.length < email_config[:min_password_length].to_i
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["PASSWORD_TOO_SHORT"])
  end

  if password.length > email_config[:max_password_length].to_i
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["PASSWORD_TOO_LONG"])
  end
end

.validate_social_callback_url!(context, callback_url, error_code) ⇒ Object



329
330
331
332
333
334
335
336
# File 'lib/better_auth/routes/social.rb', line 329

def self.validate_social_callback_url!(context, callback_url, error_code)
  validate_callback_url!(context, callback_url)
rescue APIError => error
  return if oauth_proxy_callback_url?(context, callback_url)
  raise error unless error.message == BASE_ERROR_CODES["INVALID_CALLBACK_URL"]

  raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES[error_code])
end

.verify_emailObject



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
64
65
# File 'lib/better_auth/routes/email_verification.rb', line 34

def self.verify_email
  Endpoint.new(path: "/verify-email", method: "GET") do |ctx|
    token = fetch_value(ctx.query, "token").to_s
    callback_url = fetch_value(ctx.query, "callbackURL")
    validate_callback_url!(ctx.context, callback_url)
    payload = verify_email_token(ctx, token, callback_url)
    email = payload["email"].to_s.downcase
    update_to = payload["updateTo"] || payload["update_to"]
    user_data = ctx.context.internal_adapter.find_user_by_email(email)
    return redirect_or_error(ctx, callback_url, "user_not_found") unless user_data

    user = user_data[:user]
    if update_to
      updated = ctx.context.internal_adapter.update_user_by_email(email, email: update_to, emailVerified: false)
      updated_user = updated || user.merge("email" => update_to, "emailVerified" => false)
      send_verification_email_payload(ctx, updated_user, callback_url) if ctx.context.options.email_verification[:send_verification_email].respond_to?(:call)
      set_verified_session_cookie(ctx, updated_user)
      next redirect_or_json(ctx, callback_url, {status: true, user: Schema.parse_output(ctx.context.options, "user", updated)})
    end

    if user["emailVerified"]
      next redirect_or_json(ctx, callback_url, {status: true, user: nil})
    end

    call_option(ctx.context.options.email_verification[:before_email_verification], user, ctx.request)
    call_option(ctx.context.options.email_verification[:on_email_verification], user, ctx.request)
    updated = ctx.context.internal_adapter.update_user_by_email(email, emailVerified: true)
    call_option(ctx.context.options.email_verification[:after_email_verification], updated, ctx.request)
    set_verified_session_cookie(ctx, updated) if ctx.context.options.email_verification[:auto_sign_in_after_verification]
    redirect_or_json(ctx, callback_url, {status: true, user: nil})
  end
end

.verify_email_token(ctx, token, callback_url) ⇒ Object



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

def self.verify_email_token(ctx, token, callback_url)
  payload = Crypto.verify_jwt(token, ctx.context.secret)
  return payload if payload

  redirect_or_error(ctx, callback_url, "invalid_token")
end

.verify_passwordObject



87
88
89
90
91
92
93
94
95
96
97
# File 'lib/better_auth/routes/password.rb', line 87

def self.verify_password
  Endpoint.new(path: "/verify-password", method: "POST") do |ctx|
    session = current_session(ctx, sensitive: true)
    password = normalize_hash(ctx.body)["password"].to_s
     = (ctx, session[:user]["id"])
    valid =  && ["password"] && verify_password_value(ctx, password, ["password"])
    raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_PASSWORD"]) unless valid

    ctx.json({status: true})
  end
end

.verify_password_value(ctx, password, digest) ⇒ Object



124
125
126
127
128
129
130
131
# File 'lib/better_auth/routes/password.rb', line 124

def self.verify_password_value(ctx, password, digest)
  Password.verify(
    password: password,
    hash: digest,
    verifier: ctx.context.options.email_and_password.dig(:password, :verify),
    algorithm: ctx.context.options.password_hasher
  )
end