2017-12-10 19:07:22 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-10-09 00:10:37 -04:00
|
|
|
class Auth::DefaultCurrentUserProvider
|
|
|
|
|
2018-02-18 18:12:51 -05:00
|
|
|
CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER"
|
|
|
|
API_KEY ||= "api_key"
|
2019-03-08 11:13:31 -05:00
|
|
|
API_USERNAME ||= "api_username"
|
|
|
|
HEADER_API_KEY ||= "HTTP_API_KEY"
|
|
|
|
HEADER_API_USERNAME ||= "HTTP_API_USERNAME"
|
|
|
|
HEADER_API_USER_EXTERNAL_ID ||= "HTTP_API_USER_EXTERNAL_ID"
|
|
|
|
HEADER_API_USER_ID ||= "HTTP_API_USER_ID"
|
2018-02-18 18:12:51 -05:00
|
|
|
USER_API_KEY ||= "HTTP_USER_API_KEY"
|
|
|
|
USER_API_CLIENT_ID ||= "HTTP_USER_API_CLIENT_ID"
|
|
|
|
API_KEY_ENV ||= "_DISCOURSE_API"
|
|
|
|
USER_API_KEY_ENV ||= "_DISCOURSE_USER_API"
|
2018-03-13 16:48:40 -04:00
|
|
|
TOKEN_COOKIE ||= ENV['DISCOURSE_TOKEN_COOKIE'] || "_t"
|
2018-02-18 18:12:51 -05:00
|
|
|
PATH_INFO ||= "PATH_INFO"
|
2016-07-27 22:58:49 -04:00
|
|
|
COOKIE_ATTEMPTS_PER_MIN ||= 10
|
2018-03-06 00:49:31 -05:00
|
|
|
BAD_TOKEN ||= "_DISCOURSE_BAD_TOKEN"
|
2013-10-09 00:10:37 -04:00
|
|
|
|
|
|
|
# do all current user initialization here
|
2018-09-04 02:17:05 -04:00
|
|
|
def initialize(env)
|
2013-10-09 00:10:37 -04:00
|
|
|
@env = env
|
|
|
|
@request = Rack::Request.new(env)
|
|
|
|
end
|
|
|
|
|
|
|
|
# our current user, return nil if none is found
|
|
|
|
def current_user
|
|
|
|
return @env[CURRENT_USER_KEY] if @env.key?(CURRENT_USER_KEY)
|
|
|
|
|
2014-10-23 22:38:00 -04:00
|
|
|
# bypass if we have the shared session header
|
|
|
|
if shared_key = @env['HTTP_X_SHARED_SESSION_KEY']
|
2019-12-03 04:05:53 -05:00
|
|
|
uid = Discourse.redis.get("shared_session_key_#{shared_key}")
|
2014-10-23 22:38:00 -04:00
|
|
|
user = nil
|
|
|
|
if uid
|
|
|
|
user = User.find_by(id: uid.to_i)
|
|
|
|
end
|
|
|
|
@env[CURRENT_USER_KEY] = user
|
|
|
|
return user
|
|
|
|
end
|
|
|
|
|
2014-05-22 18:13:25 -04:00
|
|
|
request = @request
|
2013-10-09 00:10:37 -04:00
|
|
|
|
2017-02-17 11:02:33 -05:00
|
|
|
user_api_key = @env[USER_API_KEY]
|
2019-03-12 19:16:42 -04:00
|
|
|
api_key = @env.blank? ? nil : @env[HEADER_API_KEY] || request[API_KEY]
|
2017-02-17 11:02:33 -05:00
|
|
|
|
|
|
|
auth_token = request.cookies[TOKEN_COOKIE] unless user_api_key || api_key
|
2013-10-09 00:10:37 -04:00
|
|
|
|
|
|
|
current_user = nil
|
|
|
|
|
|
|
|
if auth_token && auth_token.length == 32
|
2017-07-27 21:20:09 -04:00
|
|
|
limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN , 60)
|
2016-08-08 20:02:18 -04:00
|
|
|
|
2018-02-09 19:09:54 -05:00
|
|
|
if limiter.can_perform?
|
2017-02-13 14:01:01 -05:00
|
|
|
@user_token = UserAuthToken.lookup(auth_token,
|
|
|
|
seen: true,
|
|
|
|
user_agent: @env['HTTP_USER_AGENT'],
|
2017-03-07 13:27:34 -05:00
|
|
|
path: @env['REQUEST_PATH'],
|
2017-02-13 14:01:01 -05:00
|
|
|
client_ip: @request.ip)
|
|
|
|
|
2017-01-31 17:21:37 -05:00
|
|
|
current_user = @user_token.try(:user)
|
2016-08-08 20:02:18 -04:00
|
|
|
end
|
2016-07-27 22:58:49 -04:00
|
|
|
|
2018-03-06 00:49:31 -05:00
|
|
|
if !current_user
|
|
|
|
@env[BAD_TOKEN] = true
|
2016-07-27 22:58:49 -04:00
|
|
|
begin
|
2018-09-04 02:17:05 -04:00
|
|
|
limiter.performed!
|
2016-07-27 22:58:49 -04:00
|
|
|
rescue RateLimiter::LimitExceeded
|
2018-02-09 19:09:54 -05:00
|
|
|
raise Discourse::InvalidAccess.new(
|
|
|
|
'Invalid Access',
|
|
|
|
nil,
|
|
|
|
delete_cookie: TOKEN_COOKIE
|
|
|
|
)
|
2016-07-27 22:58:49 -04:00
|
|
|
end
|
|
|
|
end
|
2018-03-06 00:49:31 -05:00
|
|
|
elsif @env['HTTP_DISCOURSE_LOGGED_IN']
|
|
|
|
@env[BAD_TOKEN] = true
|
2013-10-09 00:10:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# possible we have an api call, impersonate
|
2017-02-17 11:02:33 -05:00
|
|
|
if api_key
|
2014-05-22 18:13:25 -04:00
|
|
|
current_user = lookup_api_user(api_key, request)
|
2017-10-20 10:30:13 -04:00
|
|
|
raise Discourse::InvalidAccess.new(I18n.t('invalid_api_credentials'), nil, custom_message: "invalid_api_credentials") unless current_user
|
2017-02-17 11:02:33 -05:00
|
|
|
raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active
|
2014-05-22 18:13:25 -04:00
|
|
|
@env[API_KEY_ENV] = true
|
2018-09-04 04:35:49 -04:00
|
|
|
rate_limit_admin_api_requests(api_key)
|
2013-10-09 00:10:37 -04:00
|
|
|
end
|
|
|
|
|
2016-08-15 03:58:33 -04:00
|
|
|
# user api key handling
|
2017-02-17 11:02:33 -05:00
|
|
|
if user_api_key
|
2016-08-15 03:58:33 -04:00
|
|
|
|
2017-12-10 19:07:22 -05:00
|
|
|
limiter_min = RateLimiter.new(nil, "user_api_min_#{user_api_key}", GlobalSetting.max_user_api_reqs_per_minute, 60)
|
|
|
|
limiter_day = RateLimiter.new(nil, "user_api_day_#{user_api_key}", GlobalSetting.max_user_api_reqs_per_day, 86400)
|
2016-08-15 03:58:33 -04:00
|
|
|
|
|
|
|
unless limiter_day.can_perform?
|
2018-09-04 02:17:05 -04:00
|
|
|
limiter_day.performed!
|
2016-08-15 03:58:33 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
unless limiter_min.can_perform?
|
2018-09-04 02:17:05 -04:00
|
|
|
limiter_min.performed!
|
2016-08-15 03:58:33 -04:00
|
|
|
end
|
|
|
|
|
2017-02-17 11:02:33 -05:00
|
|
|
current_user = lookup_user_api_user_and_update_key(user_api_key, @env[USER_API_CLIENT_ID])
|
2016-08-15 03:58:33 -04:00
|
|
|
raise Discourse::InvalidAccess unless current_user
|
2017-02-17 11:02:33 -05:00
|
|
|
raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active
|
2016-08-15 03:58:33 -04:00
|
|
|
|
2018-09-04 02:17:05 -04:00
|
|
|
limiter_min.performed!
|
|
|
|
limiter_day.performed!
|
2016-08-15 03:58:33 -04:00
|
|
|
|
2016-12-15 20:05:20 -05:00
|
|
|
@env[USER_API_KEY_ENV] = true
|
2016-08-15 03:58:33 -04:00
|
|
|
end
|
|
|
|
|
2017-02-17 11:02:33 -05:00
|
|
|
# keep this rule here as a safeguard
|
|
|
|
# under no conditions to suspended or inactive accounts get current_user
|
|
|
|
if current_user && (current_user.suspended? || !current_user.active)
|
|
|
|
current_user = nil
|
|
|
|
end
|
|
|
|
|
2018-07-18 11:04:57 -04:00
|
|
|
if current_user && should_update_last_seen?
|
|
|
|
u = current_user
|
2020-03-11 02:42:56 -04:00
|
|
|
ip = request.ip
|
|
|
|
|
2018-07-18 11:04:57 -04:00
|
|
|
Scheduler::Defer.later "Updating Last Seen" do
|
|
|
|
u.update_last_seen!
|
2020-03-11 02:42:56 -04:00
|
|
|
u.update_ip_address!(ip)
|
2018-07-18 11:04:57 -04:00
|
|
|
end
|
2020-03-11 20:16:00 -04:00
|
|
|
|
|
|
|
BookmarkReminderNotificationHandler.defer_at_desktop_reminder(
|
|
|
|
user: u, request_user_agent: @request.user_agent
|
|
|
|
)
|
2018-07-18 11:04:57 -04:00
|
|
|
end
|
|
|
|
|
2013-10-09 00:10:37 -04:00
|
|
|
@env[CURRENT_USER_KEY] = current_user
|
|
|
|
end
|
|
|
|
|
2016-07-24 22:07:31 -04:00
|
|
|
def refresh_session(user, session, cookies)
|
2017-01-31 17:21:37 -05:00
|
|
|
# if user was not loaded, no point refreshing session
|
|
|
|
# it could be an anonymous path, this would add cost
|
|
|
|
return if is_api? || !@env.key?(CURRENT_USER_KEY)
|
|
|
|
|
2017-02-17 11:02:33 -05:00
|
|
|
if !is_user_api? && @user_token && @user_token.user == user
|
2017-01-31 17:21:37 -05:00
|
|
|
rotated_at = @user_token.rotated_at
|
|
|
|
|
|
|
|
needs_rotation = @user_token.auth_token_seen ? rotated_at < UserAuthToken::ROTATE_TIME.ago : rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago
|
|
|
|
|
2018-05-03 21:11:44 -04:00
|
|
|
if needs_rotation
|
2017-01-31 17:21:37 -05:00
|
|
|
if @user_token.rotate!(user_agent: @env['HTTP_USER_AGENT'],
|
2017-07-27 21:20:09 -04:00
|
|
|
client_ip: @request.ip,
|
|
|
|
path: @env['REQUEST_PATH'])
|
2017-01-31 17:21:37 -05:00
|
|
|
cookies[TOKEN_COOKIE] = cookie_hash(@user_token.unhashed_auth_token)
|
2020-04-14 12:32:24 -04:00
|
|
|
DiscourseEvent.trigger(:user_session_refreshed, user)
|
2017-01-31 17:21:37 -05:00
|
|
|
end
|
|
|
|
end
|
2016-07-24 22:07:31 -04:00
|
|
|
end
|
2017-01-31 17:21:37 -05:00
|
|
|
|
2016-07-27 22:58:49 -04:00
|
|
|
if !user && cookies.key?(TOKEN_COOKIE)
|
2017-03-07 13:27:34 -05:00
|
|
|
cookies.delete(TOKEN_COOKIE)
|
2016-07-27 22:58:49 -04:00
|
|
|
end
|
2016-07-24 22:07:31 -04:00
|
|
|
end
|
|
|
|
|
2018-11-12 09:34:12 -05:00
|
|
|
def log_on_user(user, session, cookies, opts = {})
|
2018-10-25 18:29:28 -04:00
|
|
|
@user_token = UserAuthToken.generate!(
|
|
|
|
user_id: user.id,
|
|
|
|
user_agent: @env['HTTP_USER_AGENT'],
|
|
|
|
path: @env['REQUEST_PATH'],
|
|
|
|
client_ip: @request.ip,
|
2018-11-12 09:34:12 -05:00
|
|
|
staff: user.staff?,
|
2018-11-12 10:00:12 -05:00
|
|
|
impersonate: opts[:impersonate])
|
2016-07-25 21:37:41 -04:00
|
|
|
|
2017-01-31 17:21:37 -05:00
|
|
|
cookies[TOKEN_COOKIE] = cookie_hash(@user_token.unhashed_auth_token)
|
2020-03-17 11:48:24 -04:00
|
|
|
user.unstage!
|
2013-11-01 19:25:43 -04:00
|
|
|
make_developer_admin(user)
|
2016-04-26 13:08:19 -04:00
|
|
|
enable_bootstrap_mode(user)
|
2019-11-27 07:39:31 -05:00
|
|
|
|
|
|
|
UserAuthToken.enforce_session_count_limit!(user.id)
|
|
|
|
|
2013-10-09 00:10:37 -04:00
|
|
|
@env[CURRENT_USER_KEY] = user
|
|
|
|
end
|
|
|
|
|
2017-01-31 17:21:37 -05:00
|
|
|
def cookie_hash(unhashed_auth_token)
|
2017-02-23 12:01:28 -05:00
|
|
|
hash = {
|
2017-01-31 17:21:37 -05:00
|
|
|
value: unhashed_auth_token,
|
2016-10-16 21:11:15 -04:00
|
|
|
httponly: true,
|
|
|
|
expires: SiteSetting.maximum_session_age.hours.from_now,
|
2017-06-21 16:18:24 -04:00
|
|
|
secure: SiteSetting.force_https
|
2016-10-16 21:11:15 -04:00
|
|
|
}
|
2017-02-23 12:01:28 -05:00
|
|
|
|
|
|
|
if SiteSetting.same_site_cookies != "Disabled"
|
|
|
|
hash[:same_site] = SiteSetting.same_site_cookies
|
|
|
|
end
|
|
|
|
|
|
|
|
hash
|
2016-10-16 21:11:15 -04:00
|
|
|
end
|
|
|
|
|
2013-11-01 19:25:43 -04:00
|
|
|
def make_developer_admin(user)
|
|
|
|
if user.active? &&
|
|
|
|
!user.admin &&
|
|
|
|
Rails.configuration.respond_to?(:developer_emails) &&
|
|
|
|
Rails.configuration.developer_emails.include?(user.email)
|
2014-03-24 03:03:39 -04:00
|
|
|
user.admin = true
|
|
|
|
user.save
|
2013-11-01 19:25:43 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-04-26 13:08:19 -04:00
|
|
|
def enable_bootstrap_mode(user)
|
2018-05-13 11:00:02 -04:00
|
|
|
return if SiteSetting.bootstrap_mode_enabled
|
|
|
|
|
|
|
|
if user.admin && user.last_seen_at.nil? && user.is_singular_admin?
|
|
|
|
Jobs.enqueue(:enable_bootstrap_mode, user_id: user.id)
|
|
|
|
end
|
2016-04-26 13:08:19 -04:00
|
|
|
end
|
|
|
|
|
2013-10-09 00:10:37 -04:00
|
|
|
def log_off_user(session, cookies)
|
2017-01-31 17:21:37 -05:00
|
|
|
user = current_user
|
2018-05-13 11:00:02 -04:00
|
|
|
|
2017-01-31 17:21:37 -05:00
|
|
|
if SiteSetting.log_out_strict && user
|
|
|
|
user.user_auth_tokens.destroy_all
|
2016-05-18 03:27:54 -04:00
|
|
|
|
|
|
|
if user.admin && defined?(Rack::MiniProfiler)
|
|
|
|
# clear the profiling cookie to keep stuff tidy
|
2016-07-27 22:58:49 -04:00
|
|
|
cookies.delete("__profilin")
|
2016-05-18 03:27:54 -04:00
|
|
|
end
|
|
|
|
|
2016-07-04 05:20:30 -04:00
|
|
|
user.logged_out
|
2017-01-31 17:21:37 -05:00
|
|
|
elsif user && @user_token
|
|
|
|
@user_token.destroy
|
2015-01-27 20:56:25 -05:00
|
|
|
end
|
2017-08-31 00:06:56 -04:00
|
|
|
|
2019-03-19 08:39:13 -04:00
|
|
|
cookies.delete('authentication_data')
|
2016-07-27 22:58:49 -04:00
|
|
|
cookies.delete(TOKEN_COOKIE)
|
2013-10-09 00:10:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
# api has special rights return true if api was detected
|
|
|
|
def is_api?
|
|
|
|
current_user
|
2016-12-15 20:05:20 -05:00
|
|
|
!!(@env[API_KEY_ENV])
|
|
|
|
end
|
|
|
|
|
|
|
|
def is_user_api?
|
|
|
|
current_user
|
|
|
|
!!(@env[USER_API_KEY_ENV])
|
2013-10-09 00:10:37 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def has_auth_cookie?
|
2014-05-22 18:13:25 -04:00
|
|
|
cookie = @request.cookies[TOKEN_COOKIE]
|
2013-10-09 00:10:37 -04:00
|
|
|
!cookie.nil? && cookie.length == 32
|
|
|
|
end
|
2014-05-22 18:13:25 -04:00
|
|
|
|
|
|
|
def should_update_last_seen?
|
2019-01-22 05:07:48 -05:00
|
|
|
return false if Discourse.pg_readonly_mode?
|
|
|
|
|
2019-04-15 12:34:34 -04:00
|
|
|
api = !!(@env[API_KEY_ENV]) || !!(@env[USER_API_KEY_ENV])
|
|
|
|
|
|
|
|
if @request.xhr? || api
|
2020-03-26 02:35:32 -04:00
|
|
|
@env["HTTP_DISCOURSE_PRESENT"] == "true"
|
2017-02-28 12:34:57 -05:00
|
|
|
else
|
|
|
|
true
|
|
|
|
end
|
2014-05-22 18:13:25 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
protected
|
|
|
|
|
2016-10-14 01:05:27 -04:00
|
|
|
def lookup_user_api_user_and_update_key(user_api_key, client_id)
|
2020-04-07 09:42:52 -04:00
|
|
|
if api_key = UserApiKey.active.with_key(user_api_key).includes(:user).first
|
2016-10-14 01:05:27 -04:00
|
|
|
unless api_key.allow?(@env)
|
|
|
|
raise Discourse::InvalidAccess
|
|
|
|
end
|
|
|
|
|
2018-08-20 11:36:14 -04:00
|
|
|
api_key.update_columns(last_used_at: Time.zone.now)
|
2018-08-21 22:56:49 -04:00
|
|
|
|
2016-10-14 01:05:27 -04:00
|
|
|
if client_id.present? && client_id != api_key.client_id
|
2018-08-21 22:56:49 -04:00
|
|
|
|
|
|
|
# invalidate old dupe api key for client if needed
|
|
|
|
UserApiKey
|
|
|
|
.where(client_id: client_id, user_id: api_key.user_id)
|
|
|
|
.where('id <> ?', api_key.id)
|
|
|
|
.destroy_all
|
|
|
|
|
2016-10-14 01:05:27 -04:00
|
|
|
api_key.update_columns(client_id: client_id)
|
2016-08-15 03:58:33 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
api_key.user
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-05-22 18:13:25 -04:00
|
|
|
def lookup_api_user(api_key_value, request)
|
2019-12-12 06:45:00 -05:00
|
|
|
if api_key = ApiKey.active.with_key(api_key_value).includes(:user).first
|
2019-03-12 19:16:42 -04:00
|
|
|
api_username = header_api_key? ? @env[HEADER_API_USERNAME] : request[API_USERNAME]
|
2019-11-08 19:28:48 -05:00
|
|
|
|
2019-11-07 18:58:19 -05:00
|
|
|
if !header_api_key?
|
2020-04-06 18:55:44 -04:00
|
|
|
raise Discourse::InvalidAccess unless is_whitelisted_query_param_auth_route?(request)
|
2019-11-07 18:58:19 -05:00
|
|
|
end
|
2014-11-19 23:21:49 -05:00
|
|
|
|
2016-06-01 15:48:06 -04:00
|
|
|
if api_key.allowed_ips.present? && !api_key.allowed_ips.any? { |ip| ip.include?(request.ip) }
|
|
|
|
Rails.logger.warn("[Unauthorized API Access] username: #{api_username}, IP address: #{request.ip}")
|
2014-11-19 23:21:49 -05:00
|
|
|
return nil
|
|
|
|
end
|
|
|
|
|
2019-09-03 04:10:29 -04:00
|
|
|
user =
|
|
|
|
if api_key.user
|
|
|
|
api_key.user if !api_username || (api_key.user.username_lower == api_username.downcase)
|
|
|
|
elsif api_username
|
|
|
|
User.find_by(username_lower: api_username.downcase)
|
|
|
|
elsif user_id = header_api_key? ? @env[HEADER_API_USER_ID] : request["api_user_id"]
|
|
|
|
User.find_by(id: user_id.to_i)
|
|
|
|
elsif external_id = header_api_key? ? @env[HEADER_API_USER_EXTERNAL_ID] : request["api_user_external_id"]
|
|
|
|
SingleSignOnRecord.find_by(external_id: external_id.to_s).try(:user)
|
|
|
|
end
|
|
|
|
|
|
|
|
if user
|
|
|
|
api_key.update_columns(last_used_at: Time.zone.now)
|
2014-05-22 18:13:25 -04:00
|
|
|
end
|
2019-09-03 04:10:29 -04:00
|
|
|
|
|
|
|
user
|
2014-05-22 18:13:25 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-09-04 04:35:49 -04:00
|
|
|
private
|
|
|
|
|
2020-03-16 14:05:24 -04:00
|
|
|
def is_whitelisted_query_param_auth_route?(request)
|
2020-04-06 18:55:44 -04:00
|
|
|
(is_user_feed?(request) || is_handle_mail?(request))
|
2020-03-16 14:05:24 -04:00
|
|
|
end
|
|
|
|
|
2020-04-06 18:55:44 -04:00
|
|
|
def is_user_feed?(request)
|
2020-03-16 14:05:24 -04:00
|
|
|
return true if request.path.match?(/\/(c|t){1}\/\S*.(rss|json)/) && request.get? # topic or category route
|
|
|
|
return true if request.path.match?(/\/(latest|top|categories).(rss|json)/) && request.get? # specific routes with rss
|
2020-04-06 18:55:44 -04:00
|
|
|
return true if request.path.match?(/\/u\/\S*\/bookmarks.(ics|json)/) && request.get? # specific routes with ics
|
2020-03-16 14:05:24 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
def is_handle_mail?(request)
|
|
|
|
return true if request.path == "/admin/email/handle_mail" && request.post?
|
|
|
|
end
|
|
|
|
|
2019-03-12 19:16:42 -04:00
|
|
|
def header_api_key?
|
|
|
|
!!@env[HEADER_API_KEY]
|
|
|
|
end
|
|
|
|
|
2018-09-04 04:35:49 -04:00
|
|
|
def rate_limit_admin_api_requests(api_key)
|
|
|
|
return if Rails.env == "profile"
|
|
|
|
|
|
|
|
RateLimiter.new(
|
|
|
|
nil,
|
2019-12-12 06:45:00 -05:00
|
|
|
"admin_api_min_#{ApiKey.hash_key(api_key)}",
|
2018-09-04 04:35:49 -04:00
|
|
|
GlobalSetting.max_admin_api_reqs_per_key_per_minute,
|
|
|
|
60
|
|
|
|
).performed!
|
|
|
|
end
|
|
|
|
|
2013-10-09 00:10:37 -04:00
|
|
|
end
|