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
- .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) ⇒ 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
- .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) ⇒ Object
- .redirect_or_json(ctx, callback_url, data) ⇒ Object
- .refresh_token ⇒ Object
- .request_password_reset ⇒ Object
- .request_password_reset_callback ⇒ Object
- .require_fresh_session!(ctx, session) ⇒ Object
- .reset_password ⇒ Object
- .revoke_other_sessions ⇒ Object
- .revoke_session ⇒ Object
- .revoke_sessions ⇒ Object
- .safe_additional_state(body) ⇒ 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
- .stringify_synthetic_user(value) ⇒ 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_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
156 157 158 159 160 161 162 163 164 165 |
# File 'lib/better_auth/routes/password.rb', line 156 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
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 |
.account_storage_fields(data) ⇒ Object
193 194 195 196 |
# File 'lib/better_auth/routes/account.rb', line 193 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
189 190 191 |
# File 'lib/better_auth/routes/account.rb', line 189 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
129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/better_auth/routes/sign_up.rb', line 129 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
107 108 109 |
# File 'lib/better_auth/routes/email_verification.rb', line 107 def self.call_option(callback, user, request) callback.call(user, request) if callback.respond_to?(:call) end |
.call_provider(provider, key, *arguments) ⇒ Object
216 217 218 219 220 221 |
# File 'lib/better_auth/routes/account.rb', line 216 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
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/better_auth/routes/social.rb', line 67 def self.callback_oauth Endpoint.new( path: "/callback/:providerId", method: ["GET", "POST"], metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]} ) do |ctx| if ctx.method == "POST" merged = normalize_hash(ctx.query).merge(normalize_hash(ctx.body)) query = URI.encode_www_form(merged.reject { |_key, value| value.nil? || value.to_s.empty? }) target = "#{ctx.context.base_url}/callback/#{fetch_value(ctx.params, "providerId")}" target = "#{target}?#{query}" unless query.empty? raise ctx.redirect(target) end source = ctx.query data = normalize_hash(source) provider_id = fetch_value(ctx.params, "providerId").to_s provider = (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
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 |
# File 'lib/better_auth/routes/user.rb', line 115 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"] existing_target = ctx.context.internal_adapter.find_user_by_email(new_email) if !session[:user]["emailVerified"] && ctx.context..user.dig(:change_email, :update_email_without_verification) next ctx.json({status: true}) if existing_target 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) next ctx.json({status: true}) if existing_target 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
193 194 195 196 197 198 |
# File 'lib/better_auth/routes/user.rb', line 193 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
200 201 202 203 204 205 206 207 208 209 |
# File 'lib/better_auth/routes/user.rb', line 200 def self.core_model_fields(model) case model.to_s when "user" %w[id name email emailVerified image createdAt updatedAt] when "session" %w[id expiresAt token ipAddress userAgent userId createdAt updatedAt] else %w[id createdAt updatedAt] end end |
.create_email_verification_token(ctx, email, update_to: nil, extra: {}) ⇒ Object
74 75 76 77 78 |
# File 'lib/better_auth/routes/email_verification.rb', line 74 def self.create_email_verification_token(ctx, email, update_to: nil, extra: {}) payload = {"email" => email.to_s.downcase}.merge(extra) payload["updateTo"] = update_to if update_to Crypto.sign_jwt(payload, ctx.context.secret, expires_in: ctx.context..email_verification[:expires_in] || 3600) end |
.create_sign_up_user(ctx, body, email, name, image) ⇒ Object
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/better_auth/routes/sign_up.rb', line 112 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
134 135 136 |
# File 'lib/better_auth/routes/password.rb', line 134 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
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/better_auth/routes/session.rb', line 84 def self.current_session(ctx, allow_nil: false, sensitive: false) data = Session.find_current( ctx, disable_cookie_cache: truthy_query?(ctx.query, "disableCookieCache"), disable_refresh: truthy_query?(ctx.query, "disableRefresh"), sensitive: sensitive ) return nil if allow_nil && data.nil? raise APIError.new("UNAUTHORIZED") unless data session = stringify_keys(data[:session] || data["session"]) ensure_fresh_session!(ctx, session) if sensitive { session: session, user: stringify_keys(data[:user] || data["user"]) } end |
.default_error_description(code) ⇒ Object
88 89 90 91 |
# File 'lib/better_auth/routes/error.rb', line 88 def self.default_error_description(code) "We encountered an unexpected error. You can find more information about this error at " \ "<a href=\"https://better-auth.com/docs/reference/errors/#{URI.encode_www_form_component(code)}\">Better Auth docs</a>." end |
.delete_current_user!(ctx, session) ⇒ Object
152 153 154 155 156 157 158 159 160 161 |
# File 'lib/better_auth/routes/user.rb', line 152 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
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 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) 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 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], 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
144 145 146 147 148 149 150 |
# File 'lib/better_auth/routes/user.rb', line 144 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
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/better_auth/routes/user.rb', line 99 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) callback_url = fetch_value(ctx.query, "callbackURL") validate_callback_url!(ctx.context, callback_url) delete_current_user!(ctx, session) raise ctx.redirect(callback_url) if callback_url ctx.json({success: true, message: "User deleted"}) end end |
.ensure_fresh_session!(ctx, session) ⇒ Object
104 105 106 107 108 109 110 111 112 |
# File 'lib/better_auth/routes/session.rb', line 104 def self.ensure_fresh_session!(ctx, session) fresh_age = ctx.context.session_config[:fresh_age].to_i return if fresh_age.zero? created_at = normalize_time(session["createdAt"]) return unless created_at && Time.now - created_at >= fresh_age raise APIError.new("FORBIDDEN", code: "SESSION_NOT_FRESH", message: BASE_ERROR_CODES.fetch("SESSION_NOT_FRESH")) end |
.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
138 139 140 |
# File 'lib/better_auth/routes/password.rb', line 138 def self.expired_time?(value) value && value < Time.now end |
.fetch_value(hash, key) ⇒ Object
142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/better_auth/routes/password.rb', line 142 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
108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/better_auth/routes/password.rb', line 108 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
120 121 122 123 |
# File 'lib/better_auth/routes/password.rb', line 120 def self.hasher_arity_accepts_context?(hasher) arity = hasher.arity arity != 1 && arity != -1 end |
.link_social ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/better_auth/routes/social.rb', line 134 def self. 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 (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
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 |
# File 'lib/better_auth/routes/social.rb', line 294 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
285 286 287 288 289 290 291 292 |
# File 'lib/better_auth/routes/social.rb', line 285 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 |
# 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
151 152 153 154 155 156 157 158 |
# File 'lib/better_auth/routes/session.rb', line 151 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
198 199 200 201 202 |
# File 'lib/better_auth/routes/sign_up.rb', line 198 def self.normalize_hash(value) value.each_with_object({}) do |(key, object_value), result| result[Schema.storage_key(key)] = object_value end end |
.normalize_time(value) ⇒ Object
114 115 116 117 118 119 120 121 |
# File 'lib/better_auth/routes/session.rb', line 114 def self.normalize_time(value) return value if value.is_a?(Time) return nil if value.nil? || value.to_s.empty? Time.parse(value.to_s) rescue ArgumentError nil end |
.oauth_error_url(base_url, error, description = nil) ⇒ Object
268 269 270 271 272 273 274 275 |
# File 'lib/better_auth/routes/social.rb', line 268 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
368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/better_auth/routes/social.rb', line 368 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
198 199 200 201 202 203 |
# File 'lib/better_auth/routes/account.rb', line 198 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
205 206 207 208 209 210 |
# File 'lib/better_auth/routes/account.rb', line 205 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 |
.override_social_user_info(ctx, user, user_info) ⇒ Object
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'lib/better_auth/routes/social.rb', line 335 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
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/user.rb', line 171 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
387 388 389 390 391 392 393 394 395 |
# File 'lib/better_auth/routes/social.rb', line 387 def self.parse_json_hash(value) return value if value.is_a?(Hash) return {} if value.nil? || value.to_s.empty? parsed = JSON.parse(value.to_s) parsed.is_a?(Hash) ? parsed : {} rescue JSON::ParserError {} end |
.parse_time(value) ⇒ Object
158 159 160 161 162 163 164 165 |
# File 'lib/better_auth/routes/account.rb', line 158 def self.parse_time(value) return value if value.is_a?(Time) return nil if value.nil? || value.to_s.empty? Time.parse(value.to_s) rescue ArgumentError nil end |
.parsed_session_response(ctx, session) ⇒ Object
123 124 125 126 127 128 |
# File 'lib/better_auth/routes/session.rb', line 123 def self.parsed_session_response(ctx, session) { session: Schema.parse_output(ctx.context., "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
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 |
# File 'lib/better_auth/routes/social.rb', line 224 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) ) 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
212 213 214 |
# File 'lib/better_auth/routes/account.rb', line 212 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
277 278 279 |
# File 'lib/better_auth/routes/social.rb', line 277 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
281 282 283 |
# File 'lib/better_auth/routes/social.rb', line 281 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
330 331 332 333 |
# File 'lib/better_auth/routes/social.rb', line 330 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) ⇒ Object
87 88 89 90 91 92 93 |
# File 'lib/better_auth/routes/email_verification.rb', line 87 def self.redirect_or_error(ctx, callback_url, error) if callback_url separator = callback_url.include?("?") ? "&" : "?" raise ctx.redirect("#{callback_url}#{separator}error=#{error}") end raise APIError.new("UNAUTHORIZED", message: error) end |
.redirect_or_json(ctx, callback_url, data) ⇒ Object
95 96 97 98 99 |
# File 'lib/better_auth/routes/email_verification.rb', line 95 def self.redirect_or_json(ctx, callback_url, data) raise ctx.redirect(callback_url) if callback_url ctx.json(data) end |
.refresh_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"] || 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_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 |
# 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 redirect_to = body["redirectTo"] || body["redirect_to"] validate_callback_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}" 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
41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/better_auth/routes/password.rb', line 41 def self.request_password_reset_callback Endpoint.new(path: "/reset-password/:token", method: "GET") do |ctx| token = ctx.params[:token].to_s callback_url = fetch_value(ctx.query, "callbackURL") || "/error" validate_callback_url!(ctx.context, callback_url) verification = ctx.context.internal_adapter.find_verification_value("reset-password:#{token}") unless verification && !expired_time?(verification["expiresAt"]) raise ctx.redirect(absolute_callback(ctx.context, callback_url, error: "INVALID_TOKEN")) end raise ctx.redirect(absolute_callback(ctx.context, callback_url, token: token)) end end |
.require_fresh_session!(ctx, session) ⇒ Object
163 164 165 166 167 168 169 |
# File 'lib/better_auth/routes/user.rb', line 163 def self.require_fresh_session!(ctx, session) fresh_age = ctx.context.session_config[:fresh_age].to_i return if fresh_age <= 0 updated_at = Session.normalize_time(session[:session]["updatedAt"] || session[:session]["updated_at"] || session[:session]["createdAt"] || session[:session]["created_at"]) raise APIError.new("UNAUTHORIZED") unless updated_at && updated_at + fresh_age > Time.now end |
.reset_password ⇒ Object
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/password.rb', line 56 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
69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/better_auth/routes/session.rb', line 69 def self.revoke_other_sessions Endpoint.new(path: "/revoke-other-sessions", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) current_token = session[:session]["token"] sessions = ctx.context.internal_adapter.list_sessions(session[:user]["id"]) sessions.each do |entry| data = stringify_keys(entry) next if Session.expired?(data) || data["token"] == current_token ctx.context.internal_adapter.delete_session(data["token"]) end ctx.json({status: true}) end end |
.revoke_session ⇒ Object
45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/better_auth/routes/session.rb', line 45 def self.revoke_session Endpoint.new(path: "/revoke-session", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) body = normalize_hash(ctx.body) token = body["token"].to_s found = ctx.context.internal_adapter.find_session(token) if found && stringify_keys(found[:session] || found["session"])["userId"] == session[:user]["id"] ctx.context.internal_adapter.delete_session(token) end ctx.json({status: true}) end end |
.revoke_sessions ⇒ Object
60 61 62 63 64 65 66 67 |
# File 'lib/better_auth/routes/session.rb', line 60 def self.revoke_sessions Endpoint.new(path: "/revoke-sessions", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) ctx.context.internal_adapter.delete_sessions(session[:user]["id"]) Cookies.(ctx) ctx.json({status: true}) end end |
.safe_additional_state(body) ⇒ Object
351 352 353 354 355 356 357 |
# File 'lib/better_auth/routes/social.rb', line 351 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_sign_in_verification_email(ctx, user, callback_url) ⇒ Object
77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/better_auth/routes/sign_in.rb', line 77 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
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/better_auth/routes/sign_up.rb', line 172 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
67 68 69 70 71 72 |
# File 'lib/better_auth/routes/email_verification.rb', line 67 def self.send_verification_email_payload(ctx, user, callback_url) token = create_email_verification_token(ctx, user["email"]) callback = URI.encode_www_form_component(callback_url || "/") url = "#{ctx.context.base_url}/verify-email?token=#{URI.encode_www_form_component(token)}&callbackURL=#{callback}" ctx.context..email_verification[:send_verification_email].call({user: user, url: url, token: token}, ctx.request) end |
.session_overrides(ctx) ⇒ Object
191 192 193 194 195 196 |
# File 'lib/better_auth/routes/sign_up.rb', line 191 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
101 102 103 104 105 |
# File 'lib/better_auth/routes/email_verification.rb', line 101 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 |
# 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] != true raise APIError.new("BAD_REQUEST", message: "Email and password is not enabled") end body = normalize_hash(ctx.body) email = body["email"].to_s password = body["password"].to_s callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"] remember_me = body.key?("rememberMe") ? body["rememberMe"] : body["remember_me"] 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 |
# File 'lib/better_auth/routes/social.rb', line 9 def self. Endpoint.new(path: "/sign-in/social", method: "POST") do |ctx| body = normalize_hash(ctx.body) provider_id = body["provider"].to_s provider = (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 |
# 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
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 |
# File 'lib/better_auth/routes/sign_up.rb', line 10 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] != true || email_config[:disable_sign_up] raise APIError.new("BAD_REQUEST", message: "Email and password sign up is not enabled") end body = normalize_hash(ctx.body) name = body["name"].to_s email = body["email"].to_s password = body["password"] image = body["image"] callback_url = body["callbackURL"] || body["callbackUrl"] || body["callback_url"] remember_me = body.key?("rememberMe") ? body["rememberMe"] : body["remember_me"] 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
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
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/better_auth/routes/social.rb', line 192 def self.(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
144 145 146 147 148 149 |
# File 'lib/better_auth/routes/session.rb', line 144 def self.stringify_keys(value) return value.each_with_object({}) { |(key, object_value), result| result[key.to_s] = stringify_keys(object_value) } if value.is_a?(Hash) return value.map { |entry| stringify_keys(entry) } if value.is_a?(Array) value end |
.stringify_synthetic_user(value) ⇒ Object
166 167 168 169 170 |
# File 'lib/better_auth/routes/sign_up.rb', line 166 def self.stringify_synthetic_user(value) return value.each_with_object({}) { |(key, object_value), result| result[Schema.storage_key(key)] = object_value } if value.is_a?(Hash) {} end |
.synthetic_sign_up_user(ctx, body, email, name, image) ⇒ Object
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/better_auth/routes/sign_up.rb', line 142 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 = parse_declared_input(ctx, "user", body.except(*reserved), allowed_base: []) 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"), additional_fields: additional, id: core_fields["id"] } stringify_synthetic_user(custom.call(value)) end |
.token_hash(tokens) ⇒ Object
176 177 178 179 180 |
# File 'lib/better_auth/routes/account.rb', line 176 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
182 183 184 185 186 187 |
# File 'lib/better_auth/routes/account.rb', line 182 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
130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/better_auth/routes/session.rb', line 130 def self.truthy_query?(query, key) snake_key = key.to_s .gsub(/([a-z\d])([A-Z])/, "\\1_\\2") .tr("-", "_") .downcase value = query[key] || query[key.to_sym] || query[Schema.storage_key(key)] || query[Schema.storage_key(key).to_sym] || query[snake_key] || query[snake_key.to_sym] value == true || value.to_s == "true" end |
.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 172 173 174 |
# 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? 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
31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/better_auth/routes/session.rb', line 31 def self.update_session Endpoint.new(path: "/update-session", method: "POST") do |ctx| session = current_session(ctx, sensitive: true) body = Routes.parse_declared_input(ctx, "session", ctx.body, allowed_base: []) raise APIError.new("BAD_REQUEST", message: "No fields to update") if body.empty? update = body.merge("updatedAt" => Time.now) updated = ctx.context.internal_adapter.update_session(session[:session]["token"], update) merged = session[:session].merge(updated || update) Cookies.(ctx, {session: merged, user: session[:user]}, Cookies.dont_remember?(ctx)) ctx.json(parsed_session_response(ctx, {session: merged, user: session[:user]})) 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 |
.update_verified_email_on_link(ctx, user_id, current_email, social_user) ⇒ Object
380 381 382 383 384 385 |
# File 'lib/better_auth/routes/social.rb', line 380 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
105 106 107 108 109 110 |
# File 'lib/better_auth/routes/sign_up.rb', line 105 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
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/better_auth/routes/password.rb', line 167 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
100 101 102 103 104 105 106 |
# File 'lib/better_auth/routes/password.rb', line 100 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
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/better_auth/routes/sign_up.rb', line 87 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
359 360 361 362 363 364 365 366 |
# File 'lib/better_auth/routes/social.rb', line 359 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
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/better_auth/routes/email_verification.rb', line 34 def self.verify_email Endpoint.new(path: "/verify-email", method: "GET") do |ctx| token = fetch_value(ctx.query, "token").to_s callback_url = fetch_value(ctx.query, "callbackURL") validate_callback_url!(ctx.context, callback_url) payload = verify_email_token(ctx, token, callback_url) email = payload["email"].to_s.downcase update_to = payload["updateTo"] || payload["update_to"] user_data = ctx.context.internal_adapter.find_user_by_email(email) return redirect_or_error(ctx, callback_url, "user_not_found") unless user_data user = user_data[:user] if update_to updated = ctx.context.internal_adapter.update_user_by_email(email, email: update_to, emailVerified: false) updated_user = updated || user.merge("email" => update_to, "emailVerified" => false) send_verification_email_payload(ctx, updated_user, callback_url) if ctx.context..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 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
80 81 82 83 84 85 |
# File 'lib/better_auth/routes/email_verification.rb', line 80 def self.verify_email_token(ctx, token, callback_url) payload = Crypto.verify_jwt(token, ctx.context.secret) return payload if payload redirect_or_error(ctx, callback_url, "invalid_token") end |
.verify_password ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/better_auth/routes/password.rb', line 88 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
125 126 127 128 129 130 131 132 |
# File 'lib/better_auth/routes/password.rb', line 125 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 |