Module: BetterAuth::Routes
- Defined in:
- lib/better_auth/routes/ok.rb,
lib/better_auth/routes/user.rb,
lib/better_auth/routes/error.rb,
lib/better_auth/routes/social.rb,
lib/better_auth/routes/account.rb,
lib/better_auth/routes/session.rb,
lib/better_auth/routes/sign_in.rb,
lib/better_auth/routes/sign_up.rb,
lib/better_auth/routes/password.rb,
lib/better_auth/routes/sign_out.rb,
lib/better_auth/routes/email_verification.rb
Constant Summary collapse
- EMAIL_PATTERN =
/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/- PASSWORD_RESET_MESSAGE =
"If this email exists in our system, check your email for the reset link"
Class Method Summary collapse
- .absolute_callback(context, callback_url, params) ⇒ Object
- .access_token_expired?(account) ⇒ Boolean
- .account_cookie(ctx, provider_id, account_id = nil, user_id = nil) ⇒ Object
- .account_info ⇒ Object
- .append_query(url, query) ⇒ 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) ⇒ 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
- .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
- .list_accounts ⇒ Object
- .list_sessions ⇒ Object
- .log(context, level, message) ⇒ Object
- .normalize_hash(value) ⇒ Object
- .oauth_error_url(base_url, error, description = nil) ⇒ Object
- .oauth_token_for_storage(ctx, token) ⇒ Object
- .oauth_token_value(ctx, token) ⇒ Object
- .ok ⇒ Object
- .parse_declared_input(ctx, model, data, allowed_base: []) ⇒ Object
- .parse_time(value) ⇒ Object
- .parsed_session_response(ctx, session) ⇒ Object
- .persist_social_user(ctx, provider_id, user_info, account_info) ⇒ Object
- .provider_callable(provider, key) ⇒ Object
- .redirect_or_error(ctx, callback_url, error) ⇒ Object
- .redirect_or_json(ctx, callback_url, data) ⇒ Object
- .refresh_token ⇒ Object
- .request_password_reset ⇒ Object
- .request_password_reset_callback ⇒ Object
- .reset_password ⇒ Object
- .revoke_other_sessions ⇒ Object
- .revoke_session ⇒ Object
- .revoke_sessions ⇒ Object
- .sanitize_html(value) ⇒ 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_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
- .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
- .valid_error_code?(value) ⇒ Boolean
- .validate_password_length!(password, email_config) ⇒ Object
- .validate_sign_up_input!(email, password, email_config) ⇒ 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
154 155 156 157 158 159 160 161 162 |
# File 'lib/better_auth/routes/password.rb', line 154 def self.absolute_callback(context, callback_url, params) 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
153 154 155 156 |
# File 'lib/better_auth/routes/account.rb', line 153 def self.access_token_expired?(account) value = parse_time(account["accessTokenExpiresAt"]) value && value < Time.now + 5 end |
.account_cookie(ctx, provider_id, account_id = nil, user_id = nil) ⇒ Object
142 143 144 145 146 147 148 149 150 151 |
# File 'lib/better_auth/routes/account.rb', line 142 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 && 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
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/better_auth/routes/account.rb', line 104 def self.account_info Endpoint.new(path: "/account-info", method: "GET") 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 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 raise APIError.new("BAD_REQUEST", message: "Access token not found") if account["accessToken"].to_s.empty? info = call_provider(provider, :get_user_info, { accessToken: oauth_token_value(ctx, account["accessToken"]), access_token: oauth_token_value(ctx, account["accessToken"]), idToken: account["idToken"], scopes: account["scope"].to_s.split(",") }) ctx.json(info) end end |
.append_query(url, query) ⇒ Object
47 48 49 50 |
# File 'lib/better_auth/routes/error.rb', line 47 def self.append_query(url, query) separator = url.include?("?") ? "&" : "?" "#{url}#{separator}#{query}" end |
.call_option(callback, user, request) ⇒ Object
104 105 106 |
# File 'lib/better_auth/routes/email_verification.rb', line 104 def self.call_option(callback, user, request) callback.call(user, request) if callback.respond_to?(:call) end |
.call_provider(provider, key, *arguments) ⇒ Object
204 205 206 207 208 209 |
# File 'lib/better_auth/routes/account.rb', line 204 def self.call_provider(provider, key, *arguments) return provider.public_send(key, *arguments) if provider.respond_to?(key) callable = provider[key] || provider[key.to_s] callable.respond_to?(:call) ? callable.call(*arguments) : callable end |
.callback_oauth ⇒ Object
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 |
# File 'lib/better_auth/routes/social.rb', line 53 def self.callback_oauth Endpoint.new( path: "/callback/:providerId", method: ["GET", "POST"], metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]} ) do |ctx| source = (ctx.method == "POST") ? ctx.body.merge(ctx.query) : ctx.query data = normalize_hash(source) provider_id = fetch_value(ctx.params, "providerId").to_s provider = (ctx.context, provider_id) state = data["state"].to_s state_data = Crypto.verify_jwt(state, ctx.context.secret) || {} error_url = state_data["errorCallbackURL"] || "#{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")) if state.empty? 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 user_info = call_provider(provider, :get_user_info, token_hash(tokens)) 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? session_data = (ctx, provider_id, user, token_hash(tokens).merge("accountId" => fetch_value(user, "id").to_s)) Cookies.(ctx, session_data) callback_url = state_data["callbackURL"] || "/" raise ctx.redirect(callback_url) end end |
.change_email ⇒ Object
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 |
# File 'lib/better_auth/routes/user.rb', line 110 def self.change_email Endpoint.new(path: "/change-email", method: "POST") 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"] raise APIError.new("UNPROCESSABLE_ENTITY", message: BASE_ERROR_CODES["USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"]) if ctx.context.internal_adapter.find_user_by_email(new_email) if !session[:user]["emailVerified"] && ctx.context..user.dig(:change_email, :update_email_without_verification) updated = ctx.context.internal_adapter.update_user_by_email(session[:user]["email"], email: new_email) Cookies.(ctx, {session: session[:session], user: updated}) next ctx.json({status: true}) end 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) token = create_email_verification_token(ctx, session[:user]["email"], update_to: new_email, extra: {"requestType" => "change-email-verification"}) sender.call({user: session[:user].merge("email" => new_email), token: token}, ctx.request) ctx.json({status: true}) end end |
.change_password ⇒ Object
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/better_auth/routes/user.rb', line 20 def self.change_password Endpoint.new(path: "/change-password", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) body = normalize_hash(ctx.body) new_password = body["newPassword"] || body["new_password"] current_password = body["currentPassword"] || body["current_password"] validate_password_length!(new_password, ctx.context..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
175 176 177 178 179 180 |
# File 'lib/better_auth/routes/user.rb', line 175 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
182 183 184 185 186 187 188 189 190 191 |
# File 'lib/better_auth/routes/user.rb', line 182 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
71 72 73 74 75 |
# File 'lib/better_auth/routes/email_verification.rb', line 71 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
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/better_auth/routes/sign_up.rb', line 96 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 ) ) rescue APIError raise rescue raise APIError.new("UNPROCESSABLE_ENTITY", message: BASE_ERROR_CODES["FAILED_TO_CREATE_USER"]) end |
.credential_account(ctx, user_id) ⇒ Object
132 133 134 |
# File 'lib/better_auth/routes/password.rb', line 132 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) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/better_auth/routes/session.rb', line 83 def self.current_session(ctx, allow_nil: false, sensitive: false) data = Session.find_current( ctx, disable_cookie_cache: truthy_query?(ctx.query, "disableCookieCache"), disable_refresh: truthy_query?(ctx.query, "disableRefresh"), sensitive: sensitive ) return nil if allow_nil && data.nil? raise APIError.new("UNAUTHORIZED") unless data { session: stringify_keys(data[:session] || data["session"]), 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
144 145 146 147 148 149 150 151 |
# File 'lib/better_auth/routes/user.rb', line 144 def self.delete_current_user!(ctx, session) config = ctx.context..user[:delete_user] || {} call_option(config[:before_delete], session[:user], ctx.request) ctx.context.internal_adapter.delete_user(session[:user]["id"]) ctx.context.internal_adapter.delete_sessions(session[:user]["id"]) Cookies.(ctx) call_option(config[:after_delete], session[:user], ctx.request) end |
.delete_user ⇒ Object
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 |
# File 'lib/better_auth/routes/user.rb', line 63 def self.delete_user Endpoint.new(path: "/delete-user", method: "POST") do |ctx| enabled = ctx.context..user.dig(:delete_user, :enabled) raise APIError.new("NOT_FOUND") unless enabled session = current_session(ctx, sensitive: true) body = normalize_hash(ctx.body) 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 = ctx.context..user.dig(:delete_user, :send_delete_account_verification)) token = SecureRandom.hex(16) ctx.context.internal_adapter.create_verification_value( identifier: "delete-account-#{token}", value: session[:user]["id"], expiresAt: Time.now + ctx.context..user.dig(:delete_user, :delete_token_expires_in).to_i ) sender.call({user: session[:user], token: token}, ctx.request) next ctx.json({success: true, message: "Verification email sent"}) end delete_current_user!(ctx, session) ctx.json({success: true, message: "User deleted"}) end end |
.delete_user_by_token!(ctx, session, token) ⇒ Object
136 137 138 139 140 141 142 |
# File 'lib/better_auth/routes/user.rb', line 136 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
95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/better_auth/routes/user.rb', line 95 def self.delete_user_callback Endpoint.new(path: "/delete-user/callback", method: "GET") 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") delete_user_by_token!(ctx, session, token) delete_current_user!(ctx, session) callback_url = fetch_value(ctx.query, "callbackURL") raise ctx.redirect(callback_url) if callback_url ctx.json({success: true, message: "User deleted"}) end 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
136 137 138 |
# File 'lib/better_auth/routes/password.rb', line 136 def self.expired_time?(value) value && value < Time.now end |
.fetch_value(hash, key) ⇒ Object
140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/better_auth/routes/password.rb', line 140 def self.fetch_value(hash, key) snake_key = key.to_s .gsub(/([a-z\d])([A-Z])/, "\\1_\\2") .tr("-", "_") .downcase hash[key] || hash[key.to_s] || hash[key.to_sym] || hash[Schema.storage_key(key)] || hash[Schema.storage_key(key).to_sym] || hash[snake_key] || hash[snake_key.to_sym] end |
.find_provider_account(ctx, user_id, provider_id, account_id = nil) ⇒ Object
136 137 138 139 140 |
# File 'lib/better_auth/routes/account.rb', line 136 def self.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
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/better_auth/routes/account.rb', line 38 def self.get_access_token Endpoint.new(path: "/get-access-token", method: "POST") do |ctx| session = current_session(ctx, allow_nil: true) body = normalize_hash(ctx.body) user_id = session&.dig(:user, "id") || body["userId"] || body["user_id"] raise APIError.new("UNAUTHORIZED") if user_id.to_s.empty? provider_id = body["providerId"] || body["provider_id"] provider = (ctx.context, provider_id) raise APIError.new("BAD_REQUEST", message: "Provider #{provider_id} is not supported.") unless provider 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 if account["refreshToken"] && access_token_expired?(account) && provider_callable(provider, :refresh_access_token) 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))) end ctx.json({ 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 end |
.get_session ⇒ Object
5 6 7 8 9 10 11 12 13 14 15 16 17 |
# File 'lib/better_auth/routes/session.rb', line 5 def self.get_session Endpoint.new(path: "/get-session", method: "GET") do |ctx| session = current_session(ctx, allow_nil: true) next ctx.json(nil) unless session ctx.json(parsed_session_response(ctx, session)) rescue APIError raise rescue => error log(ctx.context, :error, "FAILED_TO_GET_SESSION #{error.}") raise APIError.new("INTERNAL_SERVER_ERROR", message: BASE_ERROR_CODES["FAILED_TO_GET_SESSION"]) end end |
.hash_password(ctx, password) ⇒ Object
106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/better_auth/routes/password.rb', line 106 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
118 119 120 121 |
# File 'lib/better_auth/routes/password.rb', line 118 def self.hasher_arity_accepts_context?(hasher) arity = hasher.arity arity != 1 && arity != -1 end |
.link_social ⇒ Object
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 |
# File 'lib/better_auth/routes/social.rb', line 93 def self. Endpoint.new(path: "/link-social", method: "POST") do |ctx| session = current_session(ctx) body = normalize_hash(ctx.body) provider_id = body["provider"].to_s provider = (ctx.context, provider_id) raise APIError.new("NOT_FOUND", message: BASE_ERROR_CODES["PROVIDER_NOT_FOUND"]) unless provider id_token = fetch_value(body, "idToken") if id_token data = (ctx, provider, id_token) email = fetch_value(data[:user], "email").to_s.downcase 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(data[:account].merge("userId" => session[:user]["id"])) end next ctx.json({url: "", status: true, redirect: false}) end raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_TOKEN"]) end end |
.list_accounts ⇒ Object
5 6 7 8 9 10 11 12 13 14 15 |
# File 'lib/better_auth/routes/account.rb', line 5 def self.list_accounts Endpoint.new(path: "/list-accounts", method: "GET") do |ctx| session = current_session(ctx) accounts = ctx.context.internal_adapter.find_accounts(session[:user]["id"]).map do |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
19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/better_auth/routes/session.rb', line 19 def self.list_sessions Endpoint.new(path: "/list-sessions", method: "GET") do |ctx| session = current_session(ctx) sessions = ctx.context.internal_adapter.list_sessions(session[:user]["id"]) active = sessions .map { |entry| stringify_keys(entry) } .select { |entry| !Session.expired?(entry) } .map { |entry| Schema.parse_output(ctx.context., "session", entry) } ctx.json(active) end end |
.log(context, level, message) ⇒ Object
128 129 130 131 132 133 134 135 |
# File 'lib/better_auth/routes/session.rb', line 128 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
139 140 141 142 143 |
# File 'lib/better_auth/routes/sign_up.rb', line 139 def self.normalize_hash(value) value.each_with_object({}) do |(key, object_value), result| result[Schema.storage_key(key)] = object_value end end |
.oauth_error_url(base_url, error, description = nil) ⇒ Object
179 180 181 182 183 184 185 186 |
# File 'lib/better_auth/routes/social.rb', line 179 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_token_for_storage(ctx, token) ⇒ Object
186 187 188 189 190 191 |
# File 'lib/better_auth/routes/account.rb', line 186 def self.oauth_token_for_storage(ctx, token) return token if token.to_s.empty? return token unless ctx.context..account[:encrypt_oauth_tokens] Crypto.symmetric_encrypt(key: ctx.context.secret, data: token) end |
.oauth_token_value(ctx, token) ⇒ Object
193 194 195 196 197 198 |
# File 'lib/better_auth/routes/account.rb', line 193 def self.oauth_token_value(ctx, token) return token if token.to_s.empty? return token unless ctx.context..account[:encrypt_oauth_tokens] Crypto.symmetric_decrypt(key: ctx.context.secret, 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 |
.parse_declared_input(ctx, model, data, allowed_base: []) ⇒ Object
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/better_auth/routes/user.rb', line 153 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_time(value) ⇒ Object
158 159 160 161 162 163 164 165 |
# File 'lib/better_auth/routes/account.rb', line 158 def self.parse_time(value) return value if value.is_a?(Time) return nil if value.nil? || value.to_s.empty? Time.parse(value.to_s) rescue ArgumentError nil end |
.parsed_session_response(ctx, session) ⇒ Object
100 101 102 103 104 105 |
# File 'lib/better_auth/routes/session.rb', line 100 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) ⇒ Object
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 |
# File 'lib/better_auth/routes/social.rb', line 152 def self.(ctx, provider_id, user_info, account_info) 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] elsif existing user = existing[:user] ctx.context.internal_adapter.create_account(account_info.merge("providerId" => provider_id, "accountId" => account_id, "userId" => user["id"])) else 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) ) user = created[:user] end session = ctx.context.internal_adapter.create_session(user["id"], false, session_overrides(ctx), true, ctx) {session: session, user: user} end |
.provider_callable(provider, key) ⇒ Object
200 201 202 |
# File 'lib/better_auth/routes/account.rb', line 200 def self.provider_callable(provider, key) provider.respond_to?(key) || (provider.is_a?(Hash) && (provider[key] || provider[key.to_s])) end |
.redirect_or_error(ctx, callback_url, error) ⇒ Object
84 85 86 87 88 89 90 |
# File 'lib/better_auth/routes/email_verification.rb', line 84 def self.redirect_or_error(ctx, callback_url, error) if callback_url separator = callback_url.include?("?") ? "&" : "?" raise ctx.redirect("#{callback_url}#{separator}error=#{error}") end raise APIError.new("UNAUTHORIZED", message: error) end |
.redirect_or_json(ctx, callback_url, data) ⇒ Object
92 93 94 95 96 |
# File 'lib/better_auth/routes/email_verification.rb', line 92 def self.redirect_or_json(ctx, callback_url, data) raise ctx.redirect(callback_url) if callback_url ctx.json(data) end |
.refresh_token ⇒ 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 96 97 98 99 100 101 102 |
# File 'lib/better_auth/routes/account.rb', line 69 def self.refresh_token Endpoint.new(path: "/refresh-token", method: "POST") do |ctx| session = current_session(ctx, allow_nil: true) body = normalize_hash(ctx.body) user_id = session&.dig(:user, "id") || body["userId"] || body["user_id"] raise APIError.new("BAD_REQUEST", message: "Either userId or session is required") if user_id.to_s.empty? provider_id = body["providerId"] || body["provider_id"] provider = (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? 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))) ctx.json({ accessToken: values["accessToken"], refreshToken: values["refreshToken"], accessTokenExpiresAt: values["accessTokenExpiresAt"], refreshTokenExpiresAt: values["refreshTokenExpiresAt"], scope: Array(values["scopes"]).join(","), idToken: values["idToken"] || account["idToken"], providerId: account["providerId"], accountId: account["accountId"] }) end 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 |
# File 'lib/better_auth/routes/password.rb', line 10 def self.request_password_reset Endpoint.new(path: "/request-password-reset", method: "POST") do |ctx| sender = ctx.context..email_and_password[:send_reset_password] raise APIError.new("BAD_REQUEST", message: "Reset password isn't enabled") unless sender.respond_to?(:call) body = normalize_hash(ctx.body) email = body["email"].to_s.downcase found = ctx.context.internal_adapter.find_user_by_email(email, include_accounts: true) unless found SecureRandom.hex(12) ctx.context.internal_adapter.find_verification_value("dummy-verification-token") next ctx.json({status: true, message: PASSWORD_RESET_MESSAGE}) end token = SecureRandom.hex(12) expires_in = ctx.context..email_and_password[:reset_password_token_expires_in] || 3600 ctx.context.internal_adapter.create_verification_value( identifier: "reset-password:#{token}", value: found[:user]["id"], expiresAt: Time.now + expires_in.to_i ) redirect_to = body["redirectTo"] || body["redirect_to"] callback = redirect_to ? URI.encode_www_form_component(redirect_to) : "" url = "#{ctx.context.base_url}/reset-password/#{token}?callbackURL=#{callback}" sender.call({user: found[:user], url: url, token: token}, ctx.request) ctx.json({status: true, message: PASSWORD_RESET_MESSAGE}) end end |
.request_password_reset_callback ⇒ Object
40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/better_auth/routes/password.rb', line 40 def self.request_password_reset_callback Endpoint.new(path: "/reset-password/:token", method: "GET") do |ctx| token = ctx.params[:token].to_s callback_url = fetch_value(ctx.query, "callbackURL") || "/error" 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 |
.reset_password ⇒ Object
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/better_auth/routes/password.rb', line 54 def self.reset_password Endpoint.new(path: "/reset-password", method: "POST") do |ctx| body = normalize_hash(ctx.body) token = body["token"] || fetch_value(ctx.query, "token") raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_TOKEN"]) if token.to_s.empty? password = body["newPassword"] || body["new_password"] validate_password_length!(password, ctx.context..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 |
.revoke_other_sessions ⇒ Object
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/better_auth/routes/session.rb', line 68 def self.revoke_other_sessions Endpoint.new(path: "/revoke-other-sessions", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) current_token = session[:session]["token"] sessions = ctx.context.internal_adapter.list_sessions(session[:user]["id"]) sessions.each do |entry| data = stringify_keys(entry) next if Session.expired?(data) || data["token"] == current_token ctx.context.internal_adapter.delete_session(data["token"]) end ctx.json({status: true}) end end |
.revoke_session ⇒ Object
44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/better_auth/routes/session.rb', line 44 def self.revoke_session Endpoint.new(path: "/revoke-session", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) body = normalize_hash(ctx.body) token = body["token"].to_s found = ctx.context.internal_adapter.find_session(token) if found && stringify_keys(found[:session] || found["session"])["userId"] == session[:user]["id"] ctx.context.internal_adapter.delete_session(token) end ctx.json({status: true}) end end |
.revoke_sessions ⇒ Object
59 60 61 62 63 64 65 66 |
# File 'lib/better_auth/routes/session.rb', line 59 def self.revoke_sessions Endpoint.new(path: "/revoke-sessions", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) ctx.context.internal_adapter.delete_sessions(session[:user]["id"]) Cookies.(ctx) ctx.json({status: true}) end 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_sign_in_verification_email(ctx, user, callback_url) ⇒ Object
75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/better_auth/routes/sign_in.rb', line 75 def self.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
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/better_auth/routes/sign_up.rb', line 113 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 |
# File 'lib/better_auth/routes/email_verification.rb', line 7 def self.send_verification_email Endpoint.new(path: "/send-verification-email", method: "POST") do |ctx| sender = ctx.context..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
64 65 66 67 68 69 |
# File 'lib/better_auth/routes/email_verification.rb', line 64 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_overrides(ctx) ⇒ Object
132 133 134 135 136 137 |
# File 'lib/better_auth/routes/sign_up.rb', line 132 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
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/better_auth/routes/user.rb', line 44 def self.set_password Endpoint.new(path: "/set-password", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) body = normalize_hash(ctx.body) new_password = body["newPassword"] || body["new_password"] validate_password_length!(new_password, ctx.context..email_and_password) account = credential_account(ctx, session[:user]["id"]) raise APIError.new("BAD_REQUEST", message: "user already has a password") 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
98 99 100 101 102 |
# File 'lib/better_auth/routes/email_verification.rb', line 98 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 |
# File 'lib/better_auth/routes/sign_in.rb', line 7 def self.sign_in_email Endpoint.new( path: "/sign-in/email", method: "POST", metadata: { allowed_media_types: [ "application/x-www-form-urlencoded", "application/json" ] } ) do |ctx| = ctx.context. email_config = .email_and_password if email_config[:enabled] == false raise APIError.new("BAD_REQUEST", message: "Email and password is not enabled") end body = normalize_hash(ctx.body) email = body["email"].to_s password = body["password"].to_s callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"] remember_me = body.key?("rememberMe") ? body["rememberMe"] : body["remember_me"] unless EMAIL_PATTERN.match?(email) raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_EMAIL"]) end found = ctx.context.internal_adapter.find_user_by_email(email, include_accounts: true) unless found hash_password(ctx, password) raise APIError.new("UNAUTHORIZED", message: BASE_ERROR_CODES["INVALID_EMAIL_OR_PASSWORD"]) end user = found[:user] || found["user"] accounts = found[:accounts] || found["accounts"] || [] 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
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 |
# File 'lib/better_auth/routes/social.rb', line 8 def self. Endpoint.new(path: "/sign-in/social", method: "POST") 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 id_token = fetch_value(body, "idToken") if id_token data = (ctx, provider, id_token) session_data = (ctx, provider_id, data[:user], data[:account]) 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 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"] }, ctx.context.secret, expires_in: 600 ) code_verifier = SecureRandom.hex(16) 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 |
# File 'lib/better_auth/routes/sign_out.rb', line 5 def self.sign_out Endpoint.new(path: "/sign-out", method: "POST") 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
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 |
# File 'lib/better_auth/routes/sign_up.rb', line 9 def self.sign_up_email Endpoint.new( path: "/sign-up/email", method: "POST", metadata: { allowed_media_types: [ "application/x-www-form-urlencoded", "application/json" ] } ) do |ctx| = ctx.context. email_config = .email_and_password if email_config[:enabled] == false || email_config[:disable_sign_up] raise APIError.new("BAD_REQUEST", message: "Email and password sign up is not enabled") end body = normalize_hash(ctx.body) name = body["name"].to_s email = body["email"].to_s password = body["password"] image = body["image"] callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"] remember_me = body.key?("rememberMe") ? body["rememberMe"] : body["remember_me"] 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 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
129 130 131 132 133 134 |
# File 'lib/better_auth/routes/account.rb', line 129 def self.(context, provider_id) 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
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 |
# File 'lib/better_auth/routes/social.rb', line 123 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 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 = 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 } } end |
.stringify_keys(value) ⇒ Object
121 122 123 124 125 126 |
# File 'lib/better_auth/routes/session.rb', line 121 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 |
.token_hash(tokens) ⇒ Object
173 174 175 176 177 |
# File 'lib/better_auth/routes/account.rb', line 173 def self.token_hash(tokens) data = normalize_hash(tokens || {}) data["scope"] = Array(data.delete("scopes")).join(",") if data.key?("scopes") data end |
.token_hash_for_storage(ctx, tokens) ⇒ Object
179 180 181 182 183 184 |
# File 'lib/better_auth/routes/account.rb', line 179 def self.token_hash_for_storage(ctx, tokens) data = token_hash(tokens) data["accessToken"] = oauth_token_for_storage(ctx, data["accessToken"]) if data.key?("accessToken") data["refreshToken"] = oauth_token_for_storage(ctx, data["refreshToken"]) if data.key?("refreshToken") data end |
.truthy_query?(query, key) ⇒ Boolean
107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/better_auth/routes/session.rb', line 107 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
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/better_auth/routes/account.rb', line 17 def self.unlink_account Endpoint.new(path: "/unlink-account", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) body = normalize_hash(ctx.body) accounts = ctx.context.internal_adapter.find_accounts(session[:user]["id"]) if accounts.length == 1 && !ctx.context..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"] 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
167 168 169 170 171 |
# File 'lib/better_auth/routes/account.rb', line 167 def self.update_account_tokens(ctx, account, tokens) return nil if account["id"].to_s.empty? ctx.context.internal_adapter.update_account(account["id"], token_hash_for_storage(ctx, tokens)) end |
.update_session ⇒ Object
31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/better_auth/routes/session.rb', line 31 def self.update_session Endpoint.new(path: "/update-session", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) body = Routes.parse_declared_input(ctx, "session", ctx.body, allowed_base: []) raise APIError.new("BAD_REQUEST", message: "No fields to update") if body.empty? updated = ctx.context.internal_adapter.update_session(session[:session]["token"], body) merged = session[:session].merge(updated || body) Cookies.(ctx, {session: merged, user: session[:user]}, Cookies.dont_remember?(ctx)) ctx.json({status: true}) end end |
.update_user ⇒ Object
5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# File 'lib/better_auth/routes/user.rb', line 5 def self.update_user Endpoint.new(path: "/update-user", method: "POST") do |ctx| session = current_session(ctx) body = normalize_hash(ctx.body) raise APIError.new("BAD_REQUEST", message: "Body must be an object") unless body.is_a?(Hash) raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["EMAIL_CAN_NOT_BE_UPDATED"]) if body.key?("email") update = parse_declared_input(ctx, "user", body, allowed_base: ["name", "image"]) raise APIError.new("BAD_REQUEST", message: "No fields to update") if update.empty? updated = ctx.context.internal_adapter.update_user(session[:user]["id"], update) Cookies.(ctx, {session: session[:session], user: updated}, Cookies.dont_remember?(ctx)) ctx.json({status: true}) end 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_password_length!(password, email_config) ⇒ Object
98 99 100 101 102 103 104 |
# File 'lib/better_auth/routes/password.rb', line 98 def self.validate_password_length!(password, email_config) unless password.is_a?(String) && !password.empty? raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["INVALID_PASSWORD"]) end raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["PASSWORD_TOO_SHORT"]) if password.length < email_config[:min_password_length].to_i raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES["PASSWORD_TOO_LONG"]) if password.length > email_config[:max_password_length].to_i end |
.validate_sign_up_input!(email, password, email_config) ⇒ Object
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/better_auth/routes/sign_up.rb', line 78 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 |
.verify_email ⇒ Object
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 |
# File 'lib/better_auth/routes/email_verification.rb', line 34 def self.verify_email Endpoint.new(path: "/verify-email", method: "GET") do |ctx| token = fetch_value(ctx.query, "token").to_s callback_url = fetch_value(ctx.query, "callbackURL") payload = verify_email_token(ctx, token, callback_url) email = payload["email"].to_s.downcase update_to = payload["updateTo"] || payload["update_to"] user_data = ctx.context.internal_adapter.find_user_by_email(email) return redirect_or_error(ctx, callback_url, "user_not_found") unless user_data user = user_data[:user] if update_to updated = ctx.context.internal_adapter.update_user_by_email(email, email: update_to, emailVerified: true) (ctx, updated || user.merge("email" => update_to, "emailVerified" => true)) next redirect_or_json(ctx, callback_url, {status: true, user: Schema.parse_output(ctx.context., "user", updated)}) 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
77 78 79 80 81 82 |
# File 'lib/better_auth/routes/email_verification.rb', line 77 def self.verify_email_token(ctx, token, callback_url) payload = Crypto.verify_jwt(token, ctx.context.secret) return payload if payload redirect_or_error(ctx, callback_url, "invalid_token") end |
.verify_password ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/better_auth/routes/password.rb', line 86 def self.verify_password Endpoint.new(path: "/verify-password", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) password = normalize_hash(ctx.body)["password"].to_s 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
123 124 125 126 127 128 129 130 |
# File 'lib/better_auth/routes/password.rb', line 123 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 |