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/validation.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"- REQUEST_EMAIL_PATTERN =
/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
Class Method Summary collapse
- .absolute_callback(context, callback_url, params) ⇒ Object
- .access_token_expired?(account) ⇒ Boolean
- .access_token_response(ctx, user_id:, provider_id:, account_id: nil, provider: nil) ⇒ Object
- .account_cookie(ctx, provider_id, account_id = nil, user_id = nil) ⇒ Object
- .account_info ⇒ Object
- .account_storage_fields(data) ⇒ Object
- .account_token_update_hash(ctx, tokens) ⇒ Object
- .append_query(url, query) ⇒ Object
- .call_existing_sign_up_callback(ctx, email_config, existing) ⇒ Object
- .call_option(callback, user, request) ⇒ Object
- .call_provider(provider, key, *arguments) ⇒ Object
- .callback_oauth ⇒ Object
- .change_email ⇒ Object
- .change_password ⇒ Object
- .coerce_input_value(value, attributes) ⇒ Object
- .core_model_fields(model) ⇒ Object
- .create_email_verification_token(ctx, email, update_to: nil, extra: {}) ⇒ Object
- .create_sign_up_user(ctx, body, email, name, image) ⇒ Object
- .credential_account(ctx, user_id) ⇒ Object
- .current_session(ctx, allow_nil: false, sensitive: false, fresh: false, disable_refresh: false) ⇒ Object
- .default_error_description(code) ⇒ Object
- .delete_current_user!(ctx, session) ⇒ Object
- .delete_user ⇒ Object
- .delete_user_by_token!(ctx, session, token) ⇒ Object
- .delete_user_callback ⇒ Object
- .email_verification_url(ctx, token, callback_url) ⇒ Object
- .ensure_fresh_session!(ctx, session) ⇒ Object
- .error ⇒ Object
- .error_html(code, description) ⇒ Object
- .error_query_params(code, description) ⇒ Object
- .expired_time?(value) ⇒ Boolean
- .fetch_value(hash, key) ⇒ Object
- .find_provider_account(ctx, user_id, provider_id, account_id = nil) ⇒ Object
- .get_access_token ⇒ Object
- .get_session ⇒ Object
- .hash_password(ctx, password) ⇒ Object
- .hasher_arity_accepts_context?(hasher) ⇒ Boolean
- .link_social ⇒ Object
- .link_social_account_from_callback(ctx, provider_id, user_info, tokens, link) ⇒ Object
- .linkable_provider?(ctx, provider_id, user_info, implicit: false) ⇒ Boolean
- .list_accounts ⇒ Object
- .list_sessions ⇒ Object
- .log(context, level, message) ⇒ Object
- .normalize_hash(value) ⇒ Object
- .normalize_time(value) ⇒ Object
- .oauth_error_url(base_url, error, description = nil) ⇒ Object
- .oauth_proxy_callback_url?(context, callback_url) ⇒ Boolean
- .oauth_token_for_storage(ctx, token) ⇒ Object
- .oauth_token_value(ctx, token) ⇒ Object
- .ok ⇒ Object
- .override_social_user_info(ctx, user, user_info) ⇒ Object
- .parse_declared_input(ctx, model, data, allowed_base: []) ⇒ Object
- .parse_json_hash(value) ⇒ Object
- .parse_time(value) ⇒ Object
- .parsed_session_response(ctx, session) ⇒ Object
- .persist_social_user(ctx, provider_id, user_info, account_info, callback_url: nil, disable_sign_up: false) ⇒ Object
- .provider_callable(provider, key) ⇒ Object
- .provider_disable_implicit_sign_up?(provider) ⇒ Boolean
- .provider_disable_sign_up?(provider) ⇒ Boolean
- .provider_override_user_info_on_sign_in?(provider_id, context) ⇒ Boolean
- .redirect_or_error(ctx, callback_url, error, code: nil) ⇒ Object
- .redirect_or_json(ctx, callback_url, data) ⇒ Object
- .refresh_token ⇒ Object
- .request_body_schema(required_strings: [], required_nonempty_strings: [], email_strings: [], optional_strings: []) ⇒ Object
- .request_only_session(ctx) ⇒ Object
- .request_password_reset ⇒ Object
- .request_password_reset_callback ⇒ Object
- .request_query_schema(required_strings: [], optional_strings: []) ⇒ Object
- .request_storage_key(key) ⇒ Object
- .request_string?(data, key) ⇒ Boolean
- .request_validation_hash(value) ⇒ Object
- .require_fresh_session!(ctx, session) ⇒ Object
- .reset_password ⇒ Object
- .resolve_default(value) ⇒ Object
- .revoke_other_sessions ⇒ Object
- .revoke_session ⇒ Object
- .revoke_sessions ⇒ Object
- .safe_additional_state(body) ⇒ Object
- .sanitize_html(value) ⇒ Object
- .send_change_email_verification(ctx, sender, user, current_email, new_email, callback_url) ⇒ Object
- .send_change_email_verification_payload(ctx, user, update_to, callback_url) ⇒ Object
- .send_sign_in_verification_email(ctx, user, callback_url) ⇒ Object
- .send_sign_up_verification_email(ctx, user, callback_url) ⇒ Object
- .send_verification_email ⇒ Object
- .send_verification_email_payload(ctx, user, callback_url) ⇒ Object
- .session_needs_refresh?(ctx, session) ⇒ Boolean
- .session_overrides(ctx) ⇒ Object
- .set_password ⇒ Object
- .set_verified_session_cookie(ctx, user) ⇒ Object
- .sign_in_email ⇒ Object
- .sign_in_social ⇒ Object
- .sign_out ⇒ Object
- .sign_up_email ⇒ Object
- .social_provider(context, provider_id) ⇒ Object
- .social_user_from_id_token!(ctx, provider, id_token) ⇒ Object
- .stringify_keys(value) ⇒ Object
- .stringify_synthetic_user(value) ⇒ Object
- .synthetic_additional_user_fields(ctx, data) ⇒ Object
- .synthetic_sign_up_user(ctx, body, email, name, image) ⇒ Object
- .token_hash(tokens) ⇒ Object
- .token_hash_for_storage(ctx, tokens) ⇒ Object
- .truthy_query?(query, key) ⇒ Boolean
- .unlink_account ⇒ Object
- .update_account_tokens(ctx, account, tokens) ⇒ Object
- .update_session ⇒ Object
- .update_user ⇒ Object
- .update_verified_email_on_link(ctx, user_id, current_email, social_user) ⇒ Object
- .valid_error_code?(value) ⇒ Boolean
- .validate_auth_callback_url!(context, value, label) ⇒ Object
- .validate_callback_url!(context, callback_url) ⇒ Object
- .validate_password_length!(password, email_config) ⇒ Object
- .validate_redirect_url!(context, redirect_url) ⇒ Object
- .validate_sign_up_input!(email, password, email_config) ⇒ Object
- .validate_social_callback_url!(context, callback_url, error_code) ⇒ Object
- .verify_email ⇒ Object
- .verify_email_token(ctx, token, callback_url) ⇒ Object
- .verify_password ⇒ Object
- .verify_password_value(ctx, password, digest) ⇒ Object
Class Method Details
.absolute_callback(context, callback_url, params) ⇒ Object
265 266 267 268 269 270 271 272 273 274 |
# File 'lib/better_auth/routes/password.rb', line 265 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
319 320 321 322 |
# File 'lib/better_auth/routes/account.rb', line 319 def self.access_token_expired?(account) value = parse_time(account["accessTokenExpiresAt"]) value && value < Time.now + 5 end |
.access_token_response(ctx, user_id:, provider_id:, account_id: nil, provider: nil) ⇒ Object
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/better_auth/routes/account.rb', line 265 def self.access_token_response(ctx, user_id:, provider_id:, account_id: nil, provider: nil) provider ||= (ctx.context, provider_id) raise APIError.new("BAD_REQUEST", message: "Provider #{provider_id} is not supported.") unless provider account = (ctx, provider_id, account_id, user_id) || find_provider_account(ctx, user_id, provider_id, account_id) raise APIError.new("BAD_REQUEST", message: "Account not found") unless account if account["refreshToken"] && access_token_expired?(account) && provider_callable(provider, :refresh_access_token) begin tokens = call_provider(provider, :refresh_access_token, oauth_token_value(ctx, account["refreshToken"])) updated = update_account_tokens(ctx, account, tokens) account = account.merge(token_hash(tokens)) Cookies.(ctx, updated || account.merge(token_hash_for_storage(ctx, tokens))) rescue => error log(ctx.context, :error, "FAILED_TO_GET_ACCESS_TOKEN #{error.}") raise APIError.new("BAD_REQUEST", code: "FAILED_TO_GET_ACCESS_TOKEN", message: "Failed to get a valid access token") end end { accessToken: oauth_token_value(ctx, account["accessToken"]), accessTokenExpiresAt: account["accessTokenExpiresAt"], scopes: account["scopes"] || (account["scope"].to_s.empty? ? [] : account["scope"].to_s.split(",")), idToken: account["idToken"] } end |
.account_cookie(ctx, provider_id, account_id = nil, user_id = nil) ⇒ Object
307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/better_auth/routes/account.rb', line 307 def self.(ctx, provider_id, account_id = nil, user_id = nil) return nil unless ctx.context..account[:store_account_cookie] account = Cookies.(ctx) return nil unless account return nil if provider_id && account["providerId"] != provider_id return nil unless account_id.to_s.empty? || account["id"] == account_id || account["accountId"] == account_id return nil unless user_id.to_s.empty? || account["userId"].to_s.empty? || account["userId"] == user_id account end |
.account_info ⇒ Object
215 216 217 218 219 220 221 222 223 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 258 259 260 261 262 263 |
# File 'lib/better_auth/routes/account.rb', line 215 def self.account_info Endpoint.new( path: "/account-info", method: "GET", metadata: { openapi: { operationId: "accountInfo", description: "Get user info from a linked provider account", parameters: [ { name: "accountId", in: "query", required: false, schema: {type: "string"} } ], responses: { "200" => OpenAPI.json_response("Provider user info", {type: "object"}) } } } ) do |ctx| session = current_session(ctx) account_id = fetch_value(ctx.query, "accountId") account = if account_id ctx.context.internal_adapter.find_accounts(session[:user]["id"]).find do |entry| entry["id"] == account_id || entry["accountId"] == account_id end else (ctx, nil, nil, session[:user]["id"]) end raise APIError.new("BAD_REQUEST", message: "Account not found") unless account && account["userId"] == session[:user]["id"] provider = (ctx.context, account["providerId"]) raise APIError.new("INTERNAL_SERVER_ERROR", message: "Provider account provider is #{account["providerId"]} but it is not configured") unless provider tokens = access_token_response( ctx, user_id: session[:user]["id"], provider_id: account["providerId"], account_id: account["accountId"], provider: provider ) raise APIError.new("BAD_REQUEST", message: "Access token not found") if tokens[:accessToken].to_s.empty? info = call_provider(provider, :get_user_info, tokens.merge(access_token: tokens[:accessToken])) ctx.json(info) end end |
.account_storage_fields(data) ⇒ Object
359 360 361 362 |
# File 'lib/better_auth/routes/account.rb', line 359 def self.account_storage_fields(data) allowed = %w[accessToken refreshToken idToken accessTokenExpiresAt refreshTokenExpiresAt scope] token_hash(data).select { |key, value| allowed.include?(key) && !value.nil? } end |
.account_token_update_hash(ctx, tokens) ⇒ Object
355 356 357 |
# File 'lib/better_auth/routes/account.rb', line 355 def self.account_token_update_hash(ctx, tokens) account_storage_fields(token_hash_for_storage(ctx, tokens)) 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
165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/better_auth/routes/sign_up.rb', line 165 def self.call_existing_sign_up_callback(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
191 192 193 |
# File 'lib/better_auth/routes/email_verification.rb', line 191 def self.call_option(callback, user, request) callback.call(user, request) if callback.respond_to?(:call) end |
.call_provider(provider, key, *arguments) ⇒ Object
382 383 384 385 386 387 |
# File 'lib/better_auth/routes/account.rb', line 382 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_oauth ⇒ Object
109 110 111 112 113 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 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 191 |
# File 'lib/better_auth/routes/social.rb', line 109 def self.callback_oauth Endpoint.new( path: "/callback/:id", method: ["GET", "POST"], metadata: { allowed_media_types: ["application/x-www-form-urlencoded", "application/json"], openapi: { operationId: "callbackOAuth", description: "Handle an OAuth provider callback", parameters: [ { name: "id", in: "path", required: true, schema: {type: "string"} } ], responses: { "302" => {description: "Redirects to the configured callback URL"} } } } ) do |ctx| provider_id = (fetch_value(ctx.params, "id") || fetch_value(ctx.params, "providerId")).to_s 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/#{provider_id}" target = "#{target}?#{query}" unless query.empty? raise ctx.redirect(target) end source = ctx.query data = normalize_hash(source) 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"] user_info = call_provider(provider, :get_user_info, token_data) user = user_info[:user] || user_info["user"] if user_info 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 account_info = token_hash_for_storage(ctx, tokens).merge("accountId" => fetch_value(user, "id").to_s) session_data = ( ctx, provider_id, user, account_info, 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.(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_email ⇒ Object
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 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 320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'lib/better_auth/routes/user.rb', line 257 def self.change_email Endpoint.new( path: "/change-email", method: "POST", metadata: { openapi: { operationId: "changeEmail", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { callbackURL: {type: ["string", "null"], description: "The URL to redirect to after email verification"}, newEmail: {type: "string", description: "The new email address to set must be a valid email address"} }, required: ["newEmail"] ) ), responses: { "200" => OpenAPI.json_response( "Email change request processed successfully", OpenAPI.object_schema( { message: { type: "string", nullable: true, enum: ["Email updated", "Verification email sent"], description: "Status message of the email change process" }, status: {type: "boolean", description: "Indicates if the request was successful"}, user: {type: "object", "$ref": "#/components/schemas/User"} }, required: ["status"] ) ), "422" => OpenAPI.error_response("Unprocessable Entity. Email already exists") } } } ) do |ctx| enabled = ctx.context..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"] sender = ctx.context..email_verification[:send_verification_email] confirmation_sender = ctx.context..user.dig(:change_email, :send_change_email_confirmation) can_update_without_verification = !session[:user]["emailVerified"] && ctx.context..user.dig(:change_email, :update_email_without_verification) can_send_confirmation = session[:user]["emailVerified"] && confirmation_sender.respond_to?(:call) can_send_verification = sender.respond_to?(:call) unless can_update_without_verification || can_send_confirmation || can_send_verification raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["VERIFICATION_EMAIL_NOT_ENABLED"]) end existing_target = ctx.context.internal_adapter.find_user_by_email(new_email) next ctx.json({status: true}) if existing_target if can_update_without_verification updated = ctx.context.internal_adapter.update_user_by_email(session[:user]["email"], email: new_email) Cookies.(ctx, {session: session[:session], user: updated}) send_verification_email_payload(ctx, updated, body["callbackURL"] || body["callbackUrl"] || body["callback_url"]) if can_send_verification next ctx.json({status: true}) end if can_send_confirmation callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"] token = create_email_verification_token(ctx, session[:user]["email"], update_to: new_email, extra: {"requestType" => "change-email-confirmation"}) url = email_verification_url(ctx, token, callback_url) confirmation_sender.call({user: session[:user], new_email: new_email, url: url, token: token}, ctx.request) next ctx.json({status: true}) end send_change_email_verification(ctx, sender, session[:user], session[:user]["email"], new_email, body["callbackURL"] || body["callbackUrl"] || body["callback_url"]) ctx.json({status: true}) end end |
.change_password ⇒ Object
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 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/better_auth/routes/user.rb', line 44 def self.change_password Endpoint.new( path: "/change-password", method: "POST", metadata: { openapi: { description: "Change the password of the user", operationId: "changePassword", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { newPassword: {type: "string", description: "The new password to set"}, currentPassword: {type: "string", description: "The current password is required"}, revokeOtherSessions: {type: ["boolean", "null"], description: "Must be a boolean value"} }, required: ["newPassword", "currentPassword"] ) ), responses: { "200" => OpenAPI.json_response( "Password successfully changed", OpenAPI.object_schema( { token: {type: "string", nullable: true, description: "New session token if other sessions were revoked"}, user: OpenAPI.user_response_schema }, required: ["user"] ) ) } } } ) 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..email_and_password) account = credential_account(ctx, session[:user]["id"]) unless account && account["password"] && verify_password_value(ctx, current_password.to_s, account["password"]) raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_PASSWORD"]) end ctx.context.internal_adapter.update_account(account["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.(ctx, {session: new_session, user: session[:user]}) token = new_session["token"] end ctx.json({token: token, user: Schema.parse_output(ctx.context., "user", session[:user])}) end end |
.coerce_input_value(value, attributes) ⇒ Object
395 396 397 398 399 400 |
# File 'lib/better_auth/routes/user.rb', line 395 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
402 403 404 405 406 407 408 409 410 411 |
# File 'lib/better_auth/routes/user.rb', line 402 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
156 157 158 159 160 |
# File 'lib/better_auth/routes/email_verification.rb', line 156 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..email_verification[:expires_in] || 3600) end |
.create_sign_up_user(ctx, body, email, name, image) ⇒ Object
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/better_auth/routes/sign_up.rb', line 147 def self.create_sign_up_user(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 ), context: ctx ) rescue APIError raise rescue raise APIError.new("UNPROCESSABLE_ENTITY", message: BASE_ERROR_CODES["FAILED_TO_CREATE_USER"]) end |
.credential_account(ctx, user_id) ⇒ Object
243 244 245 |
# File 'lib/better_auth/routes/password.rb', line 243 def self.credential_account(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, fresh: false, disable_refresh: false) ⇒ Object
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/better_auth/routes/session.rb', line 180 def self.current_session(ctx, allow_nil: false, sensitive: false, fresh: false, disable_refresh: false) data = Session.find_current( ctx, disable_cookie_cache: truthy_query?(ctx.query, "disableCookieCache"), disable_refresh: 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 fresh { 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
352 353 354 355 356 357 358 359 360 361 |
# File 'lib/better_auth/routes/user.rb', line 352 def self.delete_current_user!(ctx, session) config = ctx.context..user[:delete_user] || {} call_option(config[:before_delete], session[:user], ctx.request) deleted = ctx.context.internal_adapter.delete_user(session[:user]["id"]) raise APIError.new("BAD_REQUEST", message: "User delete aborted") if deleted == false ctx.context.internal_adapter.delete_sessions(session[:user]["id"]) Cookies.(ctx) call_option(config[:after_delete], session[:user], ctx.request) end |
.delete_user ⇒ Object
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 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/better_auth/routes/user.rb', line 138 def self.delete_user Endpoint.new( path: "/delete-user", method: "POST", metadata: { openapi: { operationId: "deleteUser", description: "Delete the current user", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { password: {type: ["string", "null"], description: "The user's password"}, token: {type: ["string", "null"], description: "Delete account verification token"}, callbackURL: {type: ["string", "null"], description: "The URL to redirect to after deletion"} } ) ), responses: { "200" => OpenAPI.json_response( "User deleted or verification email sent", OpenAPI.object_schema( { success: {type: "boolean"}, message: {type: "string"} }, required: ["success", "message"] ) ) } } } ) do |ctx| enabled = ctx.context..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..user.dig(:delete_user, :send_delete_account_verification) if body["password"] account = credential_account(ctx, session[:user]["id"]) unless account && account["password"] && verify_password_value(ctx, body["password"], account["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) expires_in = ctx.context..user.dig(:delete_user, :delete_token_expires_in) || 3600 callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"] || "/" url = "#{ctx.context.base_url}/delete-user/callback?token=#{URI.encode_www_form_component(token)}&callbackURL=#{URI.encode_www_form_component(callback_url)}" ctx.context.internal_adapter.create_verification_value( identifier: "delete-account-#{token}", value: session[:user]["id"], expiresAt: Time.now + expires_in.to_i ) sender.call({user: session[:user], url: url, 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
344 345 346 347 348 349 350 |
# File 'lib/better_auth/routes/user.rb', line 344 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_callback ⇒ Object
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 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 |
# File 'lib/better_auth/routes/user.rb', line 206 def self.delete_user_callback Endpoint.new( path: "/delete-user/callback", method: "GET", metadata: { openapi: { operationId: "deleteUserCallback", description: "Delete the current user using a verification token", parameters: [ { name: "token", in: "query", required: true, schema: {type: "string"} }, { name: "callbackURL", in: "query", required: false, schema: {type: "string"} } ], responses: { "200" => OpenAPI.json_response( "User deleted", OpenAPI.object_schema( { success: {type: "boolean"}, message: {type: "string"} }, required: ["success", "message"] ) ) } } } ) do |ctx| enabled = ctx.context..user.dig(:delete_user, :enabled) raise APIError.new("NOT_FOUND") unless enabled session = current_session(ctx) token = fetch_value(ctx.query, "token") callback_url = fetch_value(ctx.query, "callbackURL") validate_callback_url!(ctx.context, callback_url) delete_user_by_token!(ctx, session, token) delete_current_user!(ctx, session) raise ctx.redirect(callback_url) if callback_url ctx.json({success: true, message: "User deleted"}) end end |
.email_verification_url(ctx, token, callback_url) ⇒ Object
339 340 341 342 |
# File 'lib/better_auth/routes/user.rb', line 339 def self.email_verification_url(ctx, token, callback_url) callback = URI.encode_www_form_component(callback_url || "/") "#{ctx.context.base_url}/verify-email?token=#{URI.encode_www_form_component(token)}&callbackURL=#{callback}" end |
.ensure_fresh_session!(ctx, session) ⇒ Object
209 210 211 212 213 214 215 216 217 |
# File 'lib/better_auth/routes/session.rb', line 209 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 |
.error ⇒ Object
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..on_api_error[:error_url] if error_url location = append_query(error_url, query_params) next [302, {"location" => location}, [""]] end if ctx.context..production? && !ctx.context..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
247 248 249 |
# File 'lib/better_auth/routes/password.rb', line 247 def self.expired_time?(value) value && value < Time.now end |
.fetch_value(hash, key) ⇒ Object
251 252 253 254 255 256 257 258 259 260 261 262 263 |
# File 'lib/better_auth/routes/password.rb', line 251 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
301 302 303 304 305 |
# File 'lib/better_auth/routes/account.rb', line 301 def self.find_provider_account(ctx, user_id, provider_id, account_id = nil) ctx.context.internal_adapter.find_accounts(user_id).find do |account| account["providerId"] == provider_id && (account_id.to_s.empty? || account["id"] == account_id || account["accountId"] == account_id) end end |
.get_access_token ⇒ Object
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 |
# File 'lib/better_auth/routes/account.rb', line 80 def self.get_access_token Endpoint.new( path: "/get-access-token", method: "POST", body_schema: request_body_schema( required_strings: %w[providerId], optional_strings: %w[accountId userId] ), metadata: { openapi: { operationId: "getAccessToken", description: "Get an access token for a linked provider account", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { providerId: {type: "string"}, accountId: {type: ["string", "null"]}, userId: {type: ["string", "null"]} }, required: ["providerId"] ) ), responses: { "200" => OpenAPI.json_response( "Provider access token", OpenAPI.object_schema( { accessToken: {type: ["string", "null"]}, accessTokenExpiresAt: {type: ["string", "null"], format: "date-time"}, scopes: {type: "array", items: {type: "string"}}, idToken: {type: ["string", "null"]} }, required: ["scopes"] ) ) } } } ) 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"] raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["VALIDATION_ERROR"]) if provider_id.to_s.empty? account_id = body["accountId"] || body["account_id"] ctx.json(access_token_response(ctx, user_id: user_id, provider_id: provider_id, account_id: account_id)) end end |
.get_session ⇒ Object
5 6 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 |
# File 'lib/better_auth/routes/session.rb', line 5 def self.get_session Endpoint.new( path: "/get-session", method: ["GET", "POST"], metadata: { openapi: { operationId: "getSession", description: "Get the current session", responses: { "200" => OpenAPI.json_response("Current session or null", OpenAPI.session_response_schema_pair.merge(nullable: true)) } } } ) do |ctx| defer_refresh = ctx.context.session_config[:defer_session_refresh] if ctx.method == "POST" && !defer_refresh raise APIError.new( "METHOD_NOT_ALLOWED", code: "METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED", message: BASE_ERROR_CODES["METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED"] ) end session = current_session(ctx, allow_nil: true, disable_refresh: defer_refresh && ctx.method == "GET") next ctx.json(nil) unless session response = parsed_session_response(ctx, session) response[:needsRefresh] = session_needs_refresh?(ctx, session[:session]) if defer_refresh && ctx.method == "GET" ctx.json(response) rescue APIError raise rescue => error log(ctx.context, :error, "FAILED_TO_GET_SESSION #{error.}") raise APIError.new("INTERNAL_SERVER_ERROR", message: BASE_ERROR_CODES["FAILED_TO_GET_SESSION"]) end end |
.hash_password(ctx, password) ⇒ Object
217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/better_auth/routes/password.rb', line 217 def self.hash_password(ctx, password) hasher = ctx.context..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..password_hasher ) end |
.hasher_arity_accepts_context?(hasher) ⇒ Boolean
229 230 231 232 |
# File 'lib/better_auth/routes/password.rb', line 229 def self.hasher_arity_accepts_context?(hasher) arity = hasher.arity arity != 1 && arity != -1 end |
.link_social ⇒ Object
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 223 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 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/better_auth/routes/social.rb', line 193 def self. Endpoint.new( path: "/link-social", method: "POST", metadata: { openapi: { operationId: "linkSocialAccount", description: "Link a social account to the current user", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { provider: {type: "string"}, callbackURL: {type: ["string", "null"]}, errorCallbackURL: {type: ["string", "null"]}, disableRedirect: {type: ["boolean", "null"]}, scopes: {type: ["array", "null"], items: {type: "string"}}, idToken: {type: ["object", "null"]} }, required: ["provider"] ) ), responses: { "200" => OpenAPI.json_response( "Social account link started or completed", OpenAPI.object_schema( { url: {type: "string"}, redirect: {type: "boolean"}, status: {type: ["boolean", "null"]} }, required: ["url", "redirect"] ) ) } } } ) do |ctx| session = current_session(ctx) body = normalize_hash(ctx.body) provider_id = body["provider"].to_s provider = (ctx.context, provider_id) raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES["PROVIDER_NOT_FOUND"]) unless provider (ctx.context, body["callbackURL"] || body["callbackUrl"] || body["callback_url"], "INVALID_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 = (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..account.dig(:account_linking, :allow_different_emails) raise APIError.new("UNAUTHORIZED", message: "Account not linked - different emails not allowed") end account_id = fetch_value(data[:user], "id").to_s existing = ctx.context.internal_adapter.find_accounts(session[:user]["id"]).find do |account| account["providerId"] == provider_id && account["accountId"] == account_id end unless existing ctx.context.internal_adapter.create_account(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 |
.link_social_account_from_callback(ctx, provider_id, user_info, tokens, link) ⇒ Object
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 |
# File 'lib/better_auth/routes/social.rb', line 389 def self.(ctx, provider_id, user_info, tokens, link) return {error: "unable_to_link_account"} unless linkable_provider?(ctx, provider_id, user_info) email = fetch_value(user_info, "email").to_s.downcase link_email = fetch_value(link, "email").to_s.downcase unless email == link_email || ctx.context..account.dig(:account_linking, :allow_different_emails) return {error: "email_doesn't_match"} end account_id = fetch_value(user_info, "id").to_s user_id = fetch_value(link, "userId").to_s account_info = token_hash_for_storage(ctx, tokens).merge( "providerId" => provider_id, "accountId" => account_id, "userId" => user_id ) existing = ctx.context.internal_adapter.find_account_by_provider_id(account_id, provider_id) if existing return {error: "account_already_linked_to_different_user"} if existing["userId"].to_s != user_id ctx.context.internal_adapter.update_account(existing["id"], account_info) else ctx.context.internal_adapter.create_account(account_info) end if ctx.context..account.dig(:account_linking, :update_user_info_on_link) ctx.context.internal_adapter.update_user(user_id, { name: fetch_value(user_info, "name"), image: fetch_value(user_info, "image") }.compact) end update_verified_email_on_link(ctx, user_id, link_email, user_info) {status: true} end |
.linkable_provider?(ctx, provider_id, user_info, implicit: false) ⇒ Boolean
380 381 382 383 384 385 386 387 |
# File 'lib/better_auth/routes/social.rb', line 380 def self.linkable_provider?(ctx, provider_id, user_info, implicit: false) linking = ctx.context..account[: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(user_info, "emailVerified") end |
.list_accounts ⇒ Object
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# File 'lib/better_auth/routes/account.rb', line 5 def self.list_accounts Endpoint.new( path: "/list-accounts", method: "GET", metadata: { openapi: { operationId: "listUserAccounts", description: "List linked accounts for the current user", responses: { "200" => OpenAPI.json_response( "Linked accounts", {type: "array", items: {type: "object", "$ref": "#/components/schemas/Account"}} ) } } } ) do |ctx| session = current_session(ctx) accounts = ctx.context.internal_adapter.find_accounts(session[:user]["id"]).map do |account| parsed = Schema.parse_output(ctx.context., "account", account) scope = parsed.delete("scope") parsed.merge("scopes" => scope.to_s.empty? ? [] : scope.to_s.split(",")) end ctx.json(accounts) end end |
.list_sessions ⇒ Object
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/session.rb', line 42 def self.list_sessions Endpoint.new( path: "/list-sessions", method: "GET", metadata: { openapi: { operationId: "listSessions", description: "List active sessions for the current user", responses: { "200" => OpenAPI.json_response( "Active sessions", {type: "array", items: {type: "object", "$ref": "#/components/schemas/Session"}} ) } } } ) 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., "session", entry) } ctx.json(active) end end |
.log(context, level, message) ⇒ Object
266 267 268 269 270 271 272 273 |
# File 'lib/better_auth/routes/session.rb', line 266 def self.log(context, level, ) logger = context.logger if logger.respond_to?(:call) logger.call(level, ) elsif logger.respond_to?(level) logger.public_send(level, ) end end |
.normalize_hash(value) ⇒ Object
256 257 258 259 260 |
# File 'lib/better_auth/routes/sign_up.rb', line 256 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
229 230 231 232 233 234 235 236 |
# File 'lib/better_auth/routes/session.rb', line 229 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
363 364 365 366 367 368 369 370 |
# File 'lib/better_auth/routes/social.rb', line 363 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
463 464 465 466 467 468 469 470 471 472 473 |
# File 'lib/better_auth/routes/social.rb', line 463 def self.oauth_proxy_callback_url?(context, callback_url) uri = URI.parse(callback_url.to_s) proxy_path = "#{context..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
364 365 366 367 368 369 |
# File 'lib/better_auth/routes/account.rb', line 364 def self.oauth_token_for_storage(ctx, token) return token if token.to_s.empty? return token unless ctx.context..account[:encrypt_oauth_tokens] Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: token) end |
.oauth_token_value(ctx, token) ⇒ Object
371 372 373 374 375 376 |
# File 'lib/better_auth/routes/account.rb', line 371 def self.oauth_token_value(ctx, token) return token if token.to_s.empty? return token unless ctx.context..account[:encrypt_oauth_tokens] Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: token) || token end |
.ok ⇒ Object
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 |
.override_social_user_info(ctx, user, user_info) ⇒ Object
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 |
# File 'lib/better_auth/routes/social.rb', line 430 def self.(ctx, user, user_info) email = fetch_value(user_info, "email").to_s.downcase email_verified = if email == user["email"].to_s.downcase !!(user["emailVerified"] || fetch_value(user_info, "emailVerified")) else !!fetch_value(user_info, "emailVerified") end update = { "email" => email, "name" => fetch_value(user_info, "name").to_s, "image" => fetch_value(user_info, "image"), "emailVerified" => email_verified }.reject { |_key, value| value.nil? } ctx.context.internal_adapter.update_user(user["id"], update) || user end |
.parse_declared_input(ctx, model, data, allowed_base: []) ⇒ Object
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/better_auth/routes/user.rb', line 373 def self.parse_declared_input(ctx, model, data, allowed_base: []) input = normalize_hash(data || {}) table = Schema.auth_tables(ctx.context.)[model.to_s] fields = table ? table.fetch(:fields) : {} additional = ctx.context..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
482 483 484 485 486 487 488 489 490 |
# File 'lib/better_auth/routes/social.rb', line 482 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
324 325 326 327 328 329 330 331 |
# File 'lib/better_auth/routes/account.rb', line 324 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
238 239 240 241 242 243 |
# File 'lib/better_auth/routes/session.rb', line 238 def self.parsed_session_response(ctx, session) { session: Schema.parse_output(ctx.context., "session", session[:session]), user: Schema.parse_output(ctx.context., "user", session[:user]) } end |
.persist_social_user(ctx, provider_id, user_info, account_info, callback_url: nil, disable_sign_up: false) ⇒ Object
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 |
# File 'lib/better_auth/routes/social.rb', line 318 def self.(ctx, provider_id, user_info, account_info, callback_url: nil, disable_sign_up: false) email = fetch_value(user_info, "email").to_s.downcase account_id = (account_info["accountId"] || account_info[:accountId] || account_info[:account_id] || fetch_value(user_info, "id")).to_s existing = ctx.context.internal_adapter.find_oauth_user(email, account_id, provider_id) if existing && existing[:linked_account] user = existing[:user] if ctx.context..account[:update_account_on_sign_in] != false update_data = account_storage_fields(account_info) ctx.context.internal_adapter.update_account(existing[:linked_account]["id"], update_data) unless update_data.empty? end verified_user = update_verified_email_on_link(ctx, user["id"], user["email"], user_info) user = verified_user if verified_user new_user = false elsif existing unless linkable_provider?(ctx, provider_id, user_info, implicit: true) return {error: "account not linked"} end user = existing[:user] ctx.context.internal_adapter.create_account(account_info.merge("providerId" => provider_id, "accountId" => account_id, "userId" => user["id"])) verified_user = update_verified_email_on_link(ctx, user["id"], user["email"], user_info) user = verified_user if verified_user new_user = false else return {error: "signup disabled"} if disable_sign_up created = ctx.context.internal_adapter.create_oauth_user( { email: email, name: fetch_value(user_info, "name").to_s, image: fetch_value(user_info, "image"), emailVerified: !!fetch_value(user_info, "emailVerified") }, account_info.merge("providerId" => provider_id, "accountId" => account_id), context: ctx ) user = created[:user] new_user = true end user = (ctx, user, user_info) if existing && provider_override_user_info_on_sign_in?(provider_id, ctx.context) 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
378 379 380 |
# File 'lib/better_auth/routes/account.rb', line 378 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
372 373 374 |
# File 'lib/better_auth/routes/social.rb', line 372 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
376 377 378 |
# File 'lib/better_auth/routes/social.rb', line 376 def self.provider_disable_sign_up?(provider) !!(fetch_value(provider, "disableSignUp") || fetch_value(fetch_value(provider, "options") || {}, "disableSignUp")) end |
.provider_override_user_info_on_sign_in?(provider_id, context) ⇒ Boolean
425 426 427 428 |
# File 'lib/better_auth/routes/social.rb', line 425 def self.provider_override_user_info_on_sign_in?(provider_id, context) provider = (context, provider_id) !!(fetch_value(provider, "overrideUserInfoOnSignIn") || fetch_value(fetch_value(provider, "options") || {}, "overrideUserInfoOnSignIn")) end |
.redirect_or_error(ctx, callback_url, error, code: nil) ⇒ Object
171 172 173 174 175 176 177 |
# File 'lib/better_auth/routes/email_verification.rb', line 171 def self.redirect_or_error(ctx, callback_url, error, code: nil) if callback_url separator = callback_url.include?("?") ? "&" : "?" raise ctx.redirect("#{callback_url}#{separator}error=#{code || error}") end raise APIError.new("UNAUTHORIZED", code: code, message: error) end |
.redirect_or_json(ctx, callback_url, data) ⇒ Object
179 180 181 182 183 |
# File 'lib/better_auth/routes/email_verification.rb', line 179 def self.redirect_or_json(ctx, callback_url, data) raise ctx.redirect(callback_url) if callback_url ctx.json(data) end |
.refresh_token ⇒ Object
132 133 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 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 |
# File 'lib/better_auth/routes/account.rb', line 132 def self.refresh_token Endpoint.new( path: "/refresh-token", method: "POST", body_schema: request_body_schema( required_strings: %w[providerId], optional_strings: %w[accountId userId] ), metadata: { openapi: { operationId: "refreshToken", description: "Refresh an OAuth provider access token", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { providerId: {type: "string"}, accountId: {type: ["string", "null"]}, userId: {type: ["string", "null"]} }, required: ["providerId"] ) ), responses: { "200" => OpenAPI.json_response( "Refreshed provider tokens", OpenAPI.object_schema( { accessToken: {type: ["string", "null"]}, refreshToken: {type: ["string", "null"]}, accessTokenExpiresAt: {type: ["string", "null"], format: "date-time"}, refreshTokenExpiresAt: {type: ["string", "null"], format: "date-time"}, scope: {type: ["string", "null"]}, idToken: {type: ["string", "null"]}, providerId: {type: "string"}, accountId: {type: "string"} }, required: ["providerId", "accountId"] ) ) } } } ) 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"] raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["VALIDATION_ERROR"]) if provider_id.to_s.empty? 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) account_id = body["accountId"] || body["account_id"] account = (ctx, provider_id, account_id, user_id) || find_provider_account(ctx, user_id, provider_id, account_id) raise APIError.new("BAD_REQUEST", message: "Account not found") unless account refresh_token = oauth_token_value(ctx, account["refreshToken"]) raise APIError.new("BAD_REQUEST", message: "Refresh token not found") if refresh_token.to_s.empty? begin tokens = call_provider(provider, :refresh_access_token, refresh_token) updated = update_account_tokens(ctx, account, tokens) values = token_hash(tokens) Cookies.(ctx, updated || account.merge(token_hash_for_storage(ctx, tokens))) rescue => error log(ctx.context, :error, "FAILED_TO_REFRESH_ACCESS_TOKEN #{error.}") raise APIError.new("BAD_REQUEST", code: "FAILED_TO_REFRESH_ACCESS_TOKEN", message: "Failed to refresh access token") end ctx.json({ accessToken: values["accessToken"], refreshToken: values["refreshToken"] || refresh_token, accessTokenExpiresAt: values["accessTokenExpiresAt"], refreshTokenExpiresAt: values["refreshTokenExpiresAt"] || account["refreshTokenExpiresAt"], scope: values["scope"] || account["scope"], idToken: values["idToken"] || account["idToken"], providerId: account["providerId"], accountId: account["accountId"] }) end end |
.request_body_schema(required_strings: [], required_nonempty_strings: [], email_strings: [], optional_strings: []) ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 |
# File 'lib/better_auth/routes/validation.rb', line 7 def self.request_body_schema(required_strings: [], required_nonempty_strings: [], email_strings: [], optional_strings: []) ->(body) { data = request_validation_hash(body) return false unless required_strings.all? { |key| request_string?(data, key) } return false unless required_nonempty_strings.all? { |key| request_string?(data, key) && !data[request_storage_key(key)].empty? } return false unless email_strings.all? { |key| request_string?(data, key) && REQUEST_EMAIL_PATTERN.match?(data[request_storage_key(key)]) } return false unless optional_strings.all? { |key| !data.key?(request_storage_key(key)) || request_string?(data, key) } data } end |
.request_only_session(ctx) ⇒ Object
200 201 202 203 204 205 206 207 |
# File 'lib/better_auth/routes/session.rb', line 200 def self.request_only_session(ctx) session = current_session(ctx, allow_nil: true) if !session && (ctx.request || !ctx.headers.empty?) raise APIError.new("UNAUTHORIZED", code: "UNAUTHORIZED", message: "Unauthorized") end session end |
.request_password_reset ⇒ Object
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 |
# File 'lib/better_auth/routes/password.rb', line 10 def self.request_password_reset Endpoint.new( path: "/request-password-reset", method: "POST", body_schema: request_body_schema(email_strings: %w[email]), metadata: { openapi: { operationId: "requestPasswordReset", description: "Request a password reset link", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { email: {type: "string", description: "The email address of the user"}, redirectTo: {type: ["string", "null"], description: "The URL to redirect to after reset"} }, required: ["email"] ) ), responses: { "200" => OpenAPI.json_response( "Password reset request processed", OpenAPI.status_response_schema( { message: {type: "string"} }, required: ["status", "message"] ) ) } } } ) do |ctx| sender = ctx.context..email_and_password[:send_reset_password] unless sender.respond_to?(:call) raise APIError.new("BAD_REQUEST", code: "RESET_PASSWORD_DISABLED", message: BASE_ERROR_CODES["RESET_PASSWORD_DISABLED"]) end body = normalize_hash(ctx.body) email = body["email"].to_s.downcase redirect_to = body["redirectTo"] || body["redirect_to"] validate_redirect_url!(ctx.context, redirect_to) 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..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 ) callback = redirect_to ? URI.encode_www_form_component(redirect_to) : "" url = "#{ctx.context.base_url}/reset-password/#{token}?callbackURL=#{callback}" begin sender.call({user: found[:user], url: url, token: token}, ctx.request) rescue => error log(ctx.context, :error, "RESET_PASSWORD_EMAIL_ERROR #{error.}") end ctx.json({status: true, message: PASSWORD_RESET_MESSAGE}) end end |
.request_password_reset_callback ⇒ Object
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 |
# File 'lib/better_auth/routes/password.rb', line 77 def self.request_password_reset_callback Endpoint.new( path: "/reset-password/:token", method: "GET", query_schema: request_query_schema(required_strings: %w[callbackURL]), metadata: { openapi: { operationId: "requestPasswordResetCallback", description: "Validate a password reset token and redirect to the callback URL", parameters: [ { name: "token", in: "path", required: true, schema: {type: "string"} }, { name: "callbackURL", in: "query", required: true, schema: {type: "string"} } ], responses: { "302" => {description: "Redirects to callback URL with token or error"} } } } ) do |ctx| token = ctx.params[:token].to_s callback_url = fetch_value(ctx.query, "callbackURL") 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 |
.request_query_schema(required_strings: [], optional_strings: []) ⇒ Object
19 20 21 22 23 24 25 26 27 |
# File 'lib/better_auth/routes/validation.rb', line 19 def self.request_query_schema(required_strings: [], optional_strings: []) ->(query) { data = request_validation_hash(query) return false unless required_strings.all? { |key| request_string?(data, key) } return false unless optional_strings.all? { |key| !data.key?(request_storage_key(key)) || request_string?(data, key) } data } end |
.request_storage_key(key) ⇒ Object
41 42 43 44 45 46 47 48 |
# File 'lib/better_auth/routes/validation.rb', line 41 def self.request_storage_key(key) key.to_s .gsub(/([a-z\d])([A-Z])/, "\\1_\\2") .tr("-", "_") .downcase .split("_") .then { |parts| ([parts.first] + parts.drop(1).map(&:capitalize)).join } end |
.request_string?(data, key) ⇒ Boolean
37 38 39 |
# File 'lib/better_auth/routes/validation.rb', line 37 def self.request_string?(data, key) data[request_storage_key(key)].is_a?(String) end |
.request_validation_hash(value) ⇒ Object
29 30 31 32 33 34 35 |
# File 'lib/better_auth/routes/validation.rb', line 29 def self.request_validation_hash(value) return {} unless value.is_a?(Hash) value.each_with_object({}) do |(key, object_value), result| result[request_storage_key(key)] = object_value end end |
.require_fresh_session!(ctx, session) ⇒ Object
363 364 365 366 367 368 369 370 371 |
# File 'lib/better_auth/routes/user.rb', line 363 def self.require_fresh_session!(ctx, session) fresh_age = ctx.context.session_config[:fresh_age].to_i return if fresh_age <= 0 created_at = Session.normalize_time(session[:session]["createdAt"] || session[:session]["created_at"]) return if created_at && created_at + fresh_age > Time.now raise APIError.new("BAD_REQUEST", code: "SESSION_EXPIRED", message: BASE_ERROR_CODES["SESSION_EXPIRED"]) end |
.reset_password ⇒ Object
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 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 |
# File 'lib/better_auth/routes/password.rb', line 119 def self.reset_password Endpoint.new( path: "/reset-password", method: "POST", body_schema: request_body_schema( required_strings: %w[newPassword], optional_strings: %w[token] ), query_schema: request_query_schema(optional_strings: %w[token]), metadata: { openapi: { operationId: "resetPassword", description: "Reset a password using a reset token", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { token: {type: "string", description: "The password reset token"}, newPassword: {type: "string", description: "The new password to set"} }, required: ["token", "newPassword"] ) ), responses: { "200" => OpenAPI.json_response("Password reset successfully", OpenAPI.status_response_schema) } } } ) 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..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) account = ctx.context.internal_adapter.find_accounts(user_id).find { |entry| entry["providerId"] == "credential" } if account ctx.context.internal_adapter.update_password(user_id, hashed) else ctx.context.internal_adapter.create_account(userId: user_id, providerId: "credential", accountId: user_id, password: hashed) end ctx.context.internal_adapter.delete_verification_value(verification["id"]) if (callback = ctx.context..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..email_and_password[:revoke_sessions_on_password_reset] ctx.json({status: true}) end end |
.resolve_default(value) ⇒ Object
220 221 222 |
# File 'lib/better_auth/routes/sign_up.rb', line 220 def self.resolve_default(value) value.respond_to?(:call) ? value.call : value end |
.revoke_other_sessions ⇒ Object
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 |
# File 'lib/better_auth/routes/session.rb', line 153 def self.revoke_other_sessions Endpoint.new( path: "/revoke-other-sessions", method: "POST", metadata: { openapi: { operationId: "revokeOtherSessions", description: "Revoke all sessions except the current one", responses: { "200" => OpenAPI.json_response("Other sessions revoked", OpenAPI.status_response_schema) } } } ) 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_session ⇒ Object
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 |
# File 'lib/better_auth/routes/session.rb', line 97 def self.revoke_session Endpoint.new( path: "/revoke-session", method: "POST", metadata: { openapi: { operationId: "revokeSession", description: "Revoke a session by token", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { token: {type: "string", description: "The session token to revoke"} }, required: ["token"] ) ), responses: { "200" => OpenAPI.json_response("Session revoked", OpenAPI.status_response_schema) } } } ) 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_sessions ⇒ Object
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/better_auth/routes/session.rb', line 132 def self.revoke_sessions Endpoint.new( path: "/revoke-sessions", method: "POST", metadata: { openapi: { operationId: "revokeSessions", description: "Revoke all sessions for the current user", responses: { "200" => OpenAPI.json_response("Sessions revoked", OpenAPI.status_response_schema) } } } ) do |ctx| session = current_session(ctx, sensitive: true) ctx.context.internal_adapter.delete_sessions(session[:user]["id"]) Cookies.(ctx) ctx.json({status: true}) end end |
.safe_additional_state(body) ⇒ Object
446 447 448 449 450 451 452 |
# File 'lib/better_auth/routes/social.rb', line 446 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("<", "<") .gsub(">", ">") .gsub('"', """) .gsub("'", "'") .gsub(/&(?!(?:amp|lt|gt|quot|#39|#x[0-9a-fA-F]+|#[0-9]+);)/, "&") end |
.send_change_email_verification(ctx, sender, user, current_email, new_email, callback_url) ⇒ Object
334 335 336 337 |
# File 'lib/better_auth/routes/user.rb', line 334 def self.send_change_email_verification(ctx, sender, user, current_email, new_email, callback_url) token = create_email_verification_token(ctx, current_email, update_to: new_email, extra: {"requestType" => "change-email-verification"}) sender.call({user: user.merge("email" => new_email), url: email_verification_url(ctx, token, callback_url), token: token}, ctx.request) end |
.send_change_email_verification_payload(ctx, user, update_to, callback_url) ⇒ Object
146 147 148 149 150 151 152 153 154 |
# File 'lib/better_auth/routes/email_verification.rb', line 146 def self.send_change_email_verification_payload(ctx, user, update_to, callback_url) sender = ctx.context..email_verification[:send_verification_email] return unless sender.respond_to?(:call) token = create_email_verification_token(ctx, user["email"], update_to: update_to, extra: {"requestType" => "change-email-verification"}) 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.merge("email" => update_to), url: url, token: token}, ctx.request) end |
.send_sign_in_verification_email(ctx, user, callback_url) ⇒ Object
99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/better_auth/routes/sign_in.rb', line 99 def self.send_sign_in_verification_email(ctx, user, callback_url) verification = ctx.context..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
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/better_auth/routes/sign_up.rb', line 230 def self.send_sign_up_verification_email(ctx, user, callback_url) verification = ctx.context..email_verification password_config = ctx.context..email_and_password send_on_sign_up = verification.key?(:send_on_sign_up) ? verification[:send_on_sign_up] : password_config[:require_email_verification] return unless send_on_sign_up 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_email ⇒ Object
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 |
# File 'lib/better_auth/routes/email_verification.rb', line 7 def self.send_verification_email Endpoint.new( path: "/send-verification-email", method: "POST", metadata: { openapi: { operationId: "sendVerificationEmail", description: "Send an email verification link", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { email: {type: "string", description: "The email address to verify"}, callbackURL: {type: ["string", "null"], description: "The URL to redirect to after verification"} }, required: ["email"] ) ), responses: { "200" => OpenAPI.json_response("Verification email sent", OpenAPI.status_response_schema) } } } ) do |ctx| sender = ctx.context..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
139 140 141 142 143 144 |
# File 'lib/better_auth/routes/email_verification.rb', line 139 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..email_verification[:send_verification_email].call({user: user, url: url, token: token}, ctx.request) end |
.session_needs_refresh?(ctx, session) ⇒ Boolean
219 220 221 222 223 224 225 226 227 |
# File 'lib/better_auth/routes/session.rb', line 219 def self.session_needs_refresh?(ctx, session) return false if truthy_query?(ctx.query, "disableRefresh") || ctx.context.session_config[:disable_session_refresh] update_age = ctx.context.session_config[:update_age].to_i return true if update_age.zero? updated_at = normalize_time(session["updatedAt"]) updated_at && updated_at + update_age <= Time.now end |
.session_overrides(ctx) ⇒ Object
249 250 251 252 253 254 |
# File 'lib/better_auth/routes/sign_up.rb', line 249 def self.session_overrides(ctx) { ipAddress: RequestIP.client_ip(ctx, ctx.context.).to_s, userAgent: ctx.headers["user-agent"].to_s } end |
.set_password ⇒ Object
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 133 134 135 136 |
# File 'lib/better_auth/routes/user.rb', line 99 def self.set_password Endpoint.new( path: "/set-password", method: "POST", metadata: { openapi: { operationId: "setPassword", description: "Set a password for the current user", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { newPassword: {type: "string", description: "The password to set"} }, required: ["newPassword"] ) ), responses: { "200" => OpenAPI.json_response("Password set", OpenAPI.status_response_schema) } } } ) 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..email_and_password) account = credential_account(ctx, session[:user]["id"]) raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["PASSWORD_ALREADY_SET"]) if account && account["password"] ctx.context.internal_adapter.link_account( userId: session[:user]["id"], providerId: "credential", accountId: session[:user]["id"], password: hash_password(ctx, new_password) ) ctx.json({status: true}) end end |
.set_verified_session_cookie(ctx, user) ⇒ Object
185 186 187 188 189 |
# File 'lib/better_auth/routes/email_verification.rb', line 185 def self.(ctx, user) session = current_session(ctx, allow_nil: true) session_data = session ? session[:session] : ctx.context.internal_adapter.create_session(user["id"]) Cookies.(ctx, {session: session_data, user: user}) end |
.sign_in_email ⇒ Object
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 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/better_auth/routes/sign_in.rb', line 7 def self.sign_in_email Endpoint.new( path: "/sign-in/email", method: "POST", body_schema: request_body_schema(required_strings: %w[email password]), metadata: { allowed_media_types: [ "application/x-www-form-urlencoded", "application/json" ], openapi: { operationId: "signInEmail", description: "Sign in with email and password", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { email: {type: "string", description: "Email of the user"}, password: {type: "string", description: "Password of the user"}, callbackURL: {type: ["string", "null"], description: "Callback URL to use as a redirect for email verification"}, rememberMe: {type: ["boolean", "null"], default: true, description: "If this is false, the session will not be remembered. Default is `true`."} }, required: ["email", "password"] ) ), responses: { "200" => OpenAPI.json_response( "Success - Returns either session details or redirect URL", OpenAPI.session_response_schema(description: "Session response when idToken is provided", nullable_url: true) ) } } } ) do |ctx| = ctx.context. email_config = .email_and_password if email_config[:enabled] != true raise APIError.new("BAD_REQUEST", code: "EMAIL_PASSWORD_DISABLED", message: BASE_ERROR_CODES["EMAIL_PASSWORD_DISABLED"]) 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"] validate_auth_callback_url!(ctx.context, callback_url, "callbackURL") 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"] || [] credential_account = accounts.find { |account| account["providerId"] == "credential" || account[:providerId] == "credential" } current_password = credential_account && (credential_account["password"] || credential_account[: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"] send_sign_in_verification_email(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.(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(, "user", user) }) end end |
.sign_in_social ⇒ Object
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 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 |
# File 'lib/better_auth/routes/social.rb', line 9 def self. Endpoint.new( path: "/sign-in/social", method: "POST", metadata: { openapi: { description: "Sign in with a social provider", operationId: "socialSignIn", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { provider: {type: "string"}, callbackURL: {type: ["string", "null"], description: "Callback URL to redirect to after the user has signed in"}, errorCallbackURL: {type: ["string", "null"], description: "Callback URL to redirect to if an error happens"}, newUserCallbackURL: {type: ["string", "null"]}, disableRedirect: {type: ["boolean", "null"], description: "Disable automatic redirection to the provider. Useful for handling the redirection yourself"}, requestSignUp: {type: ["boolean", "null"], description: "Explicitly request sign-up. Useful when disableImplicitSignUp is true for this provider"}, loginHint: {type: ["string", "null"], description: "The login hint to use for the authorization code request"}, additionalData: {type: ["string", "null"]}, scopes: {type: ["array", "null"], description: "Array of scopes to request from the provider. This will override the default scopes passed."}, idToken: { type: ["object", "null"], properties: { token: {type: "string", description: "ID token from the provider"}, accessToken: {type: ["string", "null"], description: "Access token from the provider"}, refreshToken: {type: ["string", "null"], description: "Refresh token from the provider"}, expiresAt: {type: ["number", "null"], description: "Expiry date of the token"}, nonce: {type: ["string", "null"], description: "Nonce used to generate the token"} }, required: ["token"] } }, required: ["provider"] ) ), responses: { "200" => OpenAPI.json_response( "Success - Returns either session details or redirect URL", OpenAPI.session_response_schema(description: "Session response when idToken is provided") ) } } } ) do |ctx| body = normalize_hash(ctx.body) provider_id = body["provider"].to_s provider = (ctx.context, provider_id) raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES["PROVIDER_NOT_FOUND"]) unless provider (ctx.context, body["callbackURL"] || body["callbackUrl"] || body["callback_url"], "INVALID_CALLBACK_URL") (ctx.context, body["errorCallbackURL"] || body["errorCallbackUrl"] || body["error_callback_url"], "INVALID_ERROR_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 = (ctx, provider, id_token) session_data = ( 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.(ctx, session_data) next ctx.json({ redirect: false, token: session_data[:session]["token"], url: nil, user: Schema.parse_output(ctx.context., "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_out ⇒ Object
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
# File 'lib/better_auth/routes/sign_out.rb', line 5 def self.sign_out Endpoint.new( path: "/sign-out", method: "POST", metadata: { openapi: { operationId: "signOut", description: "Sign out the current session", responses: { "200" => OpenAPI.json_response("Successfully signed out", OpenAPI.success_response_schema) } } } ) do |ctx| = ctx.context.[:session_token] token = ctx.(.name, ctx.context.secret) ctx.context.internal_adapter.delete_session(token) if token Cookies.(ctx) ctx.json({success: true}) end end |
.sign_up_email ⇒ Object
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 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 |
# File 'lib/better_auth/routes/sign_up.rb', line 10 def self.sign_up_email Endpoint.new( path: "/sign-up/email", method: "POST", body_schema: request_body_schema( required_strings: %w[name email], required_nonempty_strings: %w[password] ), metadata: { allowed_media_types: [ "application/x-www-form-urlencoded", "application/json" ], openapi: { operationId: "signUpWithEmailAndPassword", description: "Sign up a user using email and password", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { name: {type: "string", description: "The name of the user"}, email: {type: "string", description: "The email of the user"}, password: {type: "string", description: "The password of the user"}, image: {type: "string", description: "The profile image URL of the user"}, callbackURL: {type: "string", description: "The URL to use for email verification callback"}, rememberMe: {type: "boolean", description: "If this is false, the session will not be remembered. Default is `true`."} }, required: ["name", "email", "password"] ), required: false ), responses: { "200" => OpenAPI.json_response( "Successfully created user", OpenAPI.object_schema( { token: {type: "string", nullable: true, description: "Authentication token for the session"}, user: {type: "object", "$ref": "#/components/schemas/User"} }, required: ["user"] ) ), "422" => OpenAPI.error_response("Unprocessable Entity. User already exists or failed to create user.") } } } ) do |ctx| = ctx.context. email_config = .email_and_password if email_config[:enabled] != true || email_config[:disable_sign_up] raise APIError.new("BAD_REQUEST", code: "EMAIL_PASSWORD_SIGN_UP_DISABLED", message: BASE_ERROR_CODES["EMAIL_PASSWORD_SIGN_UP_DISABLED"]) 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"] validate_auth_callback_url!(ctx.context, callback_url, "callbackURL") validate_sign_up_input!(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) call_existing_sign_up_callback(ctx, email_config, existing) synthetic_user = synthetic_sign_up_user(ctx, body, email, name, image) next ctx.json({token: nil, user: Schema.parse_output(, "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 = create_sign_up_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.link_account( userId: created_user["id"], providerId: "credential", accountId: created_user["id"], password: hashed_password ) send_sign_up_verification_email(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(, "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.(ctx, {session: session, user: created_user}, dont_remember_me) ctx.json({token: session["token"], user: Schema.parse_output(, "user", created_user)}) end end end |
.social_provider(context, provider_id) ⇒ Object
292 293 294 295 296 297 298 299 |
# File 'lib/better_auth/routes/account.rb', line 292 def self.(context, provider_id) return nil if provider_id.to_s.empty? provider = context.[provider_id.to_sym] || context.[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
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 |
# File 'lib/better_auth/routes/social.rb', line 286 def self.(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")) user_info = 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_info[:user] || user_info["user"] if user_info 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
259 260 261 262 263 264 |
# File 'lib/better_auth/routes/session.rb', line 259 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
224 225 226 227 228 |
# File 'lib/better_auth/routes/sign_up.rb', line 224 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_additional_user_fields(ctx, data) ⇒ Object
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/better_auth/routes/sign_up.rb', line 204 def self.synthetic_additional_user_fields(ctx, data) additional = parse_declared_input(ctx, "user", data, allowed_base: []) configured = ctx.context..user[:additional_fields] || {} configured.each do |field, attributes| storage_field = Schema.storage_key(field) next if additional.key?(storage_field) field_attributes = normalize_hash(attributes || {}) next unless field_attributes.key?("defaultValue") || field_attributes.key?("default_value") default = field_attributes.key?("defaultValue") ? field_attributes["defaultValue"] : field_attributes["default_value"] additional[storage_field] = resolve_default(default) end additional end |
.synthetic_sign_up_user(ctx, body, email, name, image) ⇒ Object
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/better_auth/routes/sign_up.rb', line 178 def self.synthetic_sign_up_user(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 = synthetic_additional_user_fields(ctx, body.except(*reserved)) custom = ctx.context..email_and_password[:custom_synthetic_user] return core_fields.merge(additional) unless custom.respond_to?(:call) value = { core_fields: core_fields.except("id"), coreFields: core_fields.except("id"), additional_fields: additional, additionalFields: additional, id: core_fields["id"] } stringify_synthetic_user(custom.call(value)) end |
.token_hash(tokens) ⇒ Object
342 343 344 345 346 |
# File 'lib/better_auth/routes/account.rb', line 342 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
348 349 350 351 352 353 |
# File 'lib/better_auth/routes/account.rb', line 348 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
245 246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'lib/better_auth/routes/session.rb', line 245 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 |
.unlink_account ⇒ Object
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 |
# File 'lib/better_auth/routes/account.rb', line 32 def self.unlink_account Endpoint.new( path: "/unlink-account", method: "POST", body_schema: request_body_schema( required_strings: %w[providerId], optional_strings: %w[accountId] ), metadata: { openapi: { operationId: "unlinkAccount", description: "Unlink an account from the current user", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { providerId: {type: "string"}, accountId: {type: ["string", "null"]} }, required: ["providerId"] ) ), responses: { "200" => OpenAPI.json_response("Account unlinked", OpenAPI.status_response_schema) } } } ) do |ctx| session = current_session(ctx, sensitive: true, fresh: true) body = normalize_hash(ctx.body) accounts = ctx.context.internal_adapter.find_accounts(session[:user]["id"]) if accounts.length == 1 && !ctx.context..account.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"] raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["VALIDATION_ERROR"]) if provider_id.to_s.empty? account_id = body["accountId"] || body["account_id"] account = accounts.find do |entry| entry["providerId"] == provider_id && (account_id.to_s.empty? || entry["accountId"] == account_id) end raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["ACCOUNT_NOT_FOUND"]) unless account ctx.context.internal_adapter.delete_account(account["id"]) ctx.json({status: true}) end end |
.update_account_tokens(ctx, account, tokens) ⇒ Object
333 334 335 336 337 338 339 340 |
# File 'lib/better_auth/routes/account.rb', line 333 def self.update_account_tokens(ctx, account, tokens) return nil if account["id"].to_s.empty? data = account_token_update_hash(ctx, tokens) return nil if data.empty? ctx.context.internal_adapter.update_account(account["id"], data) end |
.update_session ⇒ Object
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 |
# File 'lib/better_auth/routes/session.rb', line 69 def self.update_session Endpoint.new( path: "/update-session", method: "POST", metadata: { openapi: { operationId: "updateSession", description: "Update the current session", responses: { "200" => OpenAPI.json_response("Updated session", OpenAPI.session_response_schema_pair) } } } ) do |ctx| session = current_session(ctx) raise APIError.new("BAD_REQUEST", code: "BODY_MUST_BE_AN_OBJECT", message: BASE_ERROR_CODES["BODY_MUST_BE_AN_OBJECT"]) unless ctx.body.is_a?(Hash) 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.(ctx, {session: merged, user: session[:user]}, Cookies.dont_remember?(ctx)) ctx.json({session: Schema.parse_output(ctx.context., "session", merged)}) end end |
.update_user ⇒ Object
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 |
# File 'lib/better_auth/routes/user.rb', line 8 def self.update_user Endpoint.new( path: "/update-user", method: "POST", metadata: { openapi: { operationId: "updateUser", description: "Update the current user's profile", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { name: {type: ["string", "null"], description: "The user's name"}, image: {type: ["string", "null"], description: "The user's profile image URL"} } ) ), responses: { "200" => OpenAPI.json_response("User updated", OpenAPI.status_response_schema) } } } ) do |ctx| session = current_session(ctx) raise APIError.new("BAD_REQUEST", code: "BODY_MUST_BE_AN_OBJECT", message: BASE_ERROR_CODES["BODY_MUST_BE_AN_OBJECT"]) unless ctx.body.is_a?(Hash) body = normalize_hash(ctx.body) 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.(ctx, {session: session[:session], user: updated}, Cookies.dont_remember?(ctx)) ctx.json({status: true}) end end |
.update_verified_email_on_link(ctx, user_id, current_email, social_user) ⇒ Object
475 476 477 478 479 480 |
# File 'lib/better_auth/routes/social.rb', line 475 def self.update_verified_email_on_link(ctx, user_id, current_email, ) return unless fetch_value(, "emailVerified") return unless fetch_value(, "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
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_auth_callback_url!(context, value, label) ⇒ Object
140 141 142 143 144 145 |
# File 'lib/better_auth/routes/sign_up.rb', line 140 def self.validate_auth_callback_url!(context, value, label) return if value.nil? || value.to_s.empty? return if context.trusted_origin?(value.to_s, allow_relative_paths: true) raise APIError.new("FORBIDDEN", message: "Invalid #{label}") end |
.validate_callback_url!(context, callback_url) ⇒ Object
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/better_auth/routes/password.rb', line 276 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
209 210 211 212 213 214 215 |
# File 'lib/better_auth/routes/password.rb', line 209 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_redirect_url!(context, redirect_url) ⇒ Object
293 294 295 296 297 298 299 |
# File 'lib/better_auth/routes/password.rb', line 293 def self.validate_redirect_url!(context, redirect_url) validate_callback_url!(context, redirect_url) rescue APIError => error raise error unless error. == BASE_ERROR_CODES["INVALID_CALLBACK_URL"] raise APIError.new("FORBIDDEN", message: BASE_ERROR_CODES["INVALID_REDIRECT_URL"]) end |
.validate_sign_up_input!(email, password, email_config) ⇒ Object
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/better_auth/routes/sign_up.rb', line 122 def self.validate_sign_up_input!(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
454 455 456 457 458 459 460 461 |
# File 'lib/better_auth/routes/social.rb', line 454 def self.(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. == BASE_ERROR_CODES["INVALID_CALLBACK_URL"] raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES[error_code]) end |
.verify_email ⇒ Object
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 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 133 134 135 136 137 |
# File 'lib/better_auth/routes/email_verification.rb', line 55 def self.verify_email Endpoint.new( path: "/verify-email", method: "GET", metadata: { openapi: { operationId: "verifyEmail", description: "Verify an email address by token", parameters: [ { name: "token", in: "query", required: true, schema: {type: "string"} }, { name: "callbackURL", in: "query", required: false, schema: {type: "string"} } ], responses: { "200" => OpenAPI.json_response( "Email verified", OpenAPI.object_schema( { status: {type: "boolean"}, user: {type: ["object", "null"], "$ref": "#/components/schemas/User"} }, required: ["status"] ) ) } } } ) 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 session = current_session(ctx, allow_nil: true) return redirect_or_error(ctx, callback_url, "invalid_user") if session && session[:user]["email"] != email request_type = payload["requestType"] || payload["request_type"] case request_type when "change-email-confirmation" send_change_email_verification_payload(ctx, user, update_to, callback_url) next redirect_or_json(ctx, callback_url, {status: true}) when "change-email-verification" updated = ctx.context.internal_adapter.update_user_by_email(email, email: update_to, emailVerified: true) updated_user = updated || user.merge("email" => update_to, "emailVerified" => true) call_option(ctx.context..email_verification[:after_email_verification], updated_user, ctx.request) (ctx, updated_user) next redirect_or_json(ctx, callback_url, {status: true, user: Schema.parse_output(ctx.context., "user", updated_user)}) else 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..email_verification[:send_verification_email].respond_to?(:call) (ctx, updated_user) next redirect_or_json(ctx, callback_url, {status: true, user: Schema.parse_output(ctx.context., "user", updated)}) end end if user["emailVerified"] next redirect_or_json(ctx, callback_url, {status: true, user: nil}) end call_option(ctx.context..email_verification[:before_email_verification], user, ctx.request) call_option(ctx.context..email_verification[:on_email_verification], user, ctx.request) updated = ctx.context.internal_adapter.update_user_by_email(email, emailVerified: true) call_option(ctx.context..email_verification[:after_email_verification], updated, ctx.request) (ctx, updated) if ctx.context..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
162 163 164 165 166 167 168 169 |
# File 'lib/better_auth/routes/email_verification.rb', line 162 def self.verify_email_token(ctx, token, callback_url) decoded, = JWT.decode(token.to_s, ctx.context.secret.to_s, true, algorithm: "HS256") decoded rescue JWT::ExpiredSignature redirect_or_error(ctx, callback_url, BASE_ERROR_CODES["TOKEN_EXPIRED"], code: "TOKEN_EXPIRED") rescue JWT::DecodeError redirect_or_error(ctx, callback_url, BASE_ERROR_CODES["INVALID_TOKEN"], code: "INVALID_TOKEN") end |
.verify_password ⇒ Object
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/better_auth/routes/password.rb', line 177 def self.verify_password Endpoint.new( path: "/verify-password", method: "POST", metadata: { openapi: { operationId: "verifyPassword", description: "Verify the current user's password", requestBody: OpenAPI.json_request_body( OpenAPI.object_schema( { password: {type: "string", description: "The password to verify"} }, required: ["password"] ) ), responses: { "200" => OpenAPI.json_response("Password verified", OpenAPI.status_response_schema) } } } ) do |ctx| session = current_session(ctx, sensitive: true) password = normalize_hash(ctx.body)["password"].to_s account = credential_account(ctx, session[:user]["id"]) valid = account && account["password"] && verify_password_value(ctx, password, account["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
234 235 236 237 238 239 240 241 |
# File 'lib/better_auth/routes/password.rb', line 234 def self.verify_password_value(ctx, password, digest) Password.verify( password: password, hash: digest, verifier: ctx.context..email_and_password.dig(:password, :verify), algorithm: ctx.context..password_hasher ) end |