2017-12-11 11:07:22 +11:00
# frozen_string_literal: true
2020-10-06 17:20:15 +01:00
require_relative '../route_matcher'
2017-12-11 11:07:22 +11:00
2021-11-17 23:27:30 +03:00
# You may have seen references to v0 and v1 of our auth cookie in the codebase
# and you're not sure how they differ, so here is an explanation:
#
# From the very early days of Discourse, the auth cookie (_t) consisted only of
# a 32 characters random string that Discourse used to identify/lookup the
# current user. We didn't include any metadata with the cookie or encrypt/sign
# it.
#
# That was v0 of the auth cookie until Nov 2021 when we merged a change that
# required us to store additional metadata with the cookie so we could get more
# information about current user early in the request lifecycle before we
# performed database lookup. We also started encrypting and signing the cookie
# to prevent tampering and obfuscate user information that we include in the
# cookie. This is v1 of our auth cookie and we still use it to this date.
#
# We still accept v0 of the auth cookie to keep users logged in, but upon
# cookie rotation (which happen every 10 minutes) they'll be switched over to
# the v1 format.
#
# We'll drop support for v0 after Discourse 2.9 is released.
2013-10-09 15:10:37 +11:00
class Auth :: DefaultCurrentUserProvider
2018-02-19 10:12:51 +11:00
CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER"
API_KEY ||= "api_key"
2019-03-08 09:13:31 -07: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"
2020-05-12 13:35:36 +01:00
PARAMETER_USER_API_KEY ||= "user_api_key"
2018-02-19 10:12:51 +11: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-19 10:12:51 +11:00
PATH_INFO ||= "PATH_INFO"
2016-07-28 12:58:49 +10:00
COOKIE_ATTEMPTS_PER_MIN ||= 10
2018-03-06 16:49:31 +11:00
BAD_TOKEN ||= "_DISCOURSE_BAD_TOKEN"
2021-11-17 23:27:30 +03:00
DECRYPTED_AUTH_COOKIE = "_DISCOURSE_DECRYPTED_AUTH_COOKIE"
TOKEN_SIZE = 32
2013-10-09 15:10:37 +11:00
2020-05-13 12:54:28 +01:00
PARAMETER_API_PATTERNS ||= [
2020-10-06 17:20:15 +01:00
RouteMatcher . new (
methods : :get ,
actions : [
2020-05-12 13:35:36 +01:00
"posts#latest" ,
"posts#user_posts_feed" ,
"groups#posts_feed" ,
"groups#mentions_feed" ,
2020-05-12 16:08:35 +01:00
"list#user_topics_feed" ,
2020-05-12 13:35:36 +01:00
"list#category_feed" ,
2020-05-12 16:08:35 +01:00
"topics#feed" ,
"badges#show" ,
"tags#tag_feed" ,
"tags#show" ,
*[ :latest , :unread , :new , :read , :posted , :bookmarks ]. map { | f | "list# #{ f } _feed" },
*[ :all , :yearly , :quarterly , :monthly , :weekly , :daily ]. map { | p | "list#top_ #{ p } _feed" },
*[ :latest , :unread , :new , :read , :posted , :bookmarks ]. map { | f | "tags#show_ #{ f } " }
2020-05-12 13:35:36 +01:00
] ,
2020-10-06 17:20:15 +01:00
formats : :rss
),
RouteMatcher . new (
methods : :get ,
actions : "users#bookmarks" ,
formats : :ics
),
RouteMatcher . new (
methods : :post ,
actions : "admin/email#handle_mail" ,
formats : nil
2021-05-06 12:59:52 +10:00
),
2020-05-12 13:35:36 +01:00
]
2021-11-17 23:27:30 +03:00
def self . find_v0_auth_cookie ( request )
cookie = request . cookies [ TOKEN_COOKIE ]. presence
if cookie && cookie . size == TOKEN_SIZE
cookie
end
end
def self . find_v1_auth_cookie ( env )
return env [ DECRYPTED_AUTH_COOKIE ] if env . key? ( DECRYPTED_AUTH_COOKIE )
env [ DECRYPTED_AUTH_COOKIE ] = begin
request = ActionDispatch :: Request . new ( env )
# don't even initialize a cookie jar if we don't have a cookie at all
if request . cookies [ TOKEN_COOKIE ]. present?
request . cookie_jar . encrypted [ TOKEN_COOKIE ]
end
end
end
2013-10-09 15:10:37 +11:00
# do all current user initialization here
2018-09-04 14:17:05 +08:00
def initialize ( env )
2013-10-09 15:10:37 +11: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-24 13:38:00 +11:00
# bypass if we have the shared session header
if shared_key = @env [ 'HTTP_X_SHARED_SESSION_KEY' ]
2019-12-03 10:05:53 +01:00
uid = Discourse . redis . get ( "shared_session_key_ #{ shared_key } " )
2014-10-24 13:38:00 +11:00
user = nil
if uid
user = User . find_by ( id : uid . to_i )
end
@env [ CURRENT_USER_KEY ] = user
return user
end
2014-05-23 08:13:25 +10:00
request = @request
2013-10-09 15:10:37 +11:00
2017-02-17 11:02:33 -05:00
user_api_key = @env [ USER_API_KEY ]
2020-05-12 13:35:36 +01:00
api_key = @env [ HEADER_API_KEY ]
if ! @env . blank? && request [ PARAMETER_USER_API_KEY ] && api_parameter_allowed?
user_api_key ||= request [ PARAMETER_USER_API_KEY ]
end
if ! @env . blank? && request [ API_KEY ] && api_parameter_allowed?
api_key ||= request [ API_KEY ]
end
2017-02-17 11:02:33 -05:00
2021-11-17 23:27:30 +03:00
auth_token = find_auth_token
2013-10-09 15:10:37 +11:00
current_user = nil
2021-11-17 23:27:30 +03:00
if auth_token
2016-08-09 10:02:18 +10:00
limiter = RateLimiter . new ( nil , "cookie_auth_ #{ request . ip } " , COOKIE_ATTEMPTS_PER_MIN , 60 )
2018-02-09 19:09:54 -05:00
if limiter . can_perform?
2020-06-03 16:36:51 +08:00
@user_token = begin
UserAuthToken . lookup (
auth_token ,
seen : true ,
user_agent : @env [ 'HTTP_USER_AGENT' ] ,
path : @env [ 'REQUEST_PATH' ] ,
client_ip : @request . ip
)
rescue ActiveRecord :: ReadOnlyError
nil
end
2017-02-13 14:01:01 -05:00
2017-01-31 17:21:37 -05:00
current_user = @user_token . try ( :user )
2016-08-09 10:02:18 +10:00
end
2016-07-28 12:58:49 +10:00
2018-03-06 16:49:31 +11:00
if ! current_user
@env [ BAD_TOKEN ] = true
2016-07-28 12:58:49 +10:00
begin
2018-09-04 14:17:05 +08:00
limiter . performed!
2016-07-28 12:58:49 +10: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-28 12:58:49 +10:00
end
end
2018-03-06 16:49:31 +11:00
elsif @env [ 'HTTP_DISCOURSE_LOGGED_IN' ]
@env [ BAD_TOKEN ] = true
2013-10-09 15:10:37 +11:00
end
# possible we have an api call, impersonate
2017-02-17 11:02:33 -05:00
if api_key
2014-05-23 08:13:25 +10:00
current_user = lookup_api_user ( api_key , request )
2021-11-17 23:27:30 +03:00
if ! current_user
raise Discourse :: InvalidAccess . new (
I18n . t ( 'invalid_api_credentials' ),
nil ,
custom_message : "invalid_api_credentials"
)
end
2017-02-17 11:02:33 -05:00
raise Discourse :: InvalidAccess if current_user . suspended? || ! current_user . active
2021-11-17 23:27:30 +03:00
admin_api_key_limiter . performed! if ! Rails . env . profile?
2014-05-23 08:13:25 +10:00
@env [ API_KEY_ENV ] = true
2013-10-09 15:10:37 +11:00
end
2016-08-15 17:58:33 +10:00
# user api key handling
2017-02-17 11:02:33 -05:00
if user_api_key
2021-11-17 23:27:30 +03:00
@hashed_user_api_key = ApiKey . hash_key ( user_api_key )
2016-08-15 17:58:33 +10:00
2021-11-17 23:27:30 +03:00
user_api_key_obj = UserApiKey
. active
. joins ( :user )
. where ( key_hash : @hashed_user_api_key )
. includes ( :user , :scopes )
. first
2016-08-15 17:58:33 +10:00
2021-11-17 23:27:30 +03:00
raise Discourse :: InvalidAccess unless user_api_key_obj
2016-08-15 17:58:33 +10:00
2021-11-17 23:27:30 +03:00
user_api_key_limiter_60_secs . performed!
user_api_key_limiter_1_day . performed!
2016-08-15 17:58:33 +10:00
2021-11-17 23:27:30 +03:00
user_api_key_obj . ensure_allowed! ( @env )
current_user = user_api_key_obj . user
2017-02-17 11:02:33 -05:00
raise Discourse :: InvalidAccess if current_user . suspended? || ! current_user . active
2016-08-15 17:58:33 +10:00
2021-11-17 23:27:30 +03:00
if can_write?
user_api_key_obj . update_last_used ( @env [ USER_API_CLIENT_ID ] )
end
2016-08-15 17:58:33 +10:00
2016-12-16 12:05:20 +11:00
@env [ USER_API_KEY_ENV ] = true
2016-08-15 17:58:33 +10: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 16:04:57 +01:00
if current_user && should_update_last_seen?
u = current_user
2020-03-11 17:42:56 +11:00
ip = request . ip
2018-07-18 16:04:57 +01:00
Scheduler :: Defer . later "Updating Last Seen" do
u . update_last_seen!
2020-03-11 17:42:56 +11:00
u . update_ip_address! ( ip )
2018-07-18 16:04:57 +01:00
end
end
2013-10-09 15:10:37 +11:00
@env [ CURRENT_USER_KEY ] = current_user
end
2021-11-17 23:27:30 +03:00
def refresh_session ( user , session , cookie_jar )
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-04 11:11:44 +10:00
if needs_rotation
2017-01-31 17:21:37 -05:00
if @user_token . rotate! ( user_agent : @env [ 'HTTP_USER_AGENT' ] ,
2017-03-07 13:27:34 -05:00
client_ip : @request . ip ,
path : @env [ 'REQUEST_PATH' ] )
2021-11-17 23:27:30 +03:00
set_auth_cookie! ( @user_token . unhashed_auth_token , user , cookie_jar )
2020-04-14 06:32:24 -10:00
DiscourseEvent . trigger ( :user_session_refreshed , user )
2017-01-31 17:21:37 -05:00
end
end
2016-07-25 12:07:31 +10:00
end
2017-01-31 17:21:37 -05:00
2021-11-17 23:27:30 +03:00
if ! user && cookie_jar . key? ( TOKEN_COOKIE )
cookie_jar . delete ( TOKEN_COOKIE )
2016-07-28 12:58:49 +10:00
end
2016-07-25 12:07:31 +10:00
end
2021-11-17 23:27:30 +03:00
def log_on_user ( user , session , cookie_jar , opts = {})
2018-10-26 00:29:28 +02: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 16:34:12 +02:00
staff : user . staff? ,
2018-11-12 16:00:12 +01:00
impersonate : opts [ :impersonate ] )
2016-07-26 11:37:41 +10:00
2021-11-17 23:27:30 +03:00
set_auth_cookie! ( @user_token . unhashed_auth_token , user , cookie_jar )
2020-03-17 16:48:24 +01:00
user . unstage!
2013-11-02 10:25:43 +11:00
make_developer_admin ( user )
2016-04-26 22:38:19 +05:30
enable_bootstrap_mode ( user )
2019-11-27 12:39:31 +00:00
UserAuthToken . enforce_session_count_limit! ( user . id )
2013-10-09 15:10:37 +11:00
@env [ CURRENT_USER_KEY ] = user
end
2021-11-17 23:27:30 +03:00
def set_auth_cookie! ( unhashed_auth_token , user , cookie_jar )
data = {
token : unhashed_auth_token ,
user_id : user . id ,
trust_level : user . trust_level ,
issued_at : Time . zone . now . to_i
2016-10-17 12:11:15 +11:00
}
2017-02-23 12:01:28 -05:00
2020-09-11 15:11:13 +10:00
if SiteSetting . persistent_sessions
2021-11-17 23:27:30 +03:00
expires = SiteSetting . maximum_session_age . hours . from_now
2020-09-11 15:11:13 +10:00
end
2017-02-23 12:01:28 -05:00
if SiteSetting . same_site_cookies != "Disabled"
2021-11-17 23:27:30 +03:00
same_site = SiteSetting . same_site_cookies
2017-02-23 12:01:28 -05:00
end
2021-11-17 23:27:30 +03:00
cookie_jar . encrypted [ TOKEN_COOKIE ] = {
value : data ,
httponly : true ,
secure : SiteSetting . force_https ,
expires : expires ,
same_site : same_site
}
2016-10-17 12:11:15 +11:00
end
2013-11-02 10:25:43 +11: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 18:03:39 +11:00
user . admin = true
user . save
2013-11-02 10:25:43 +11:00
end
end
2016-04-26 22:38:19 +05:30
def enable_bootstrap_mode ( user )
2018-05-13 17:00:02 +02: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 22:38:19 +05:30
end
2021-11-17 23:27:30 +03:00
def log_off_user ( session , cookie_jar )
2017-01-31 17:21:37 -05:00
user = current_user
2018-05-13 17:00:02 +02:00
2017-01-31 17:21:37 -05:00
if SiteSetting . log_out_strict && user
user . user_auth_tokens . destroy_all
2016-05-18 17:27:54 +10:00
if user . admin && defined? ( Rack :: MiniProfiler )
# clear the profiling cookie to keep stuff tidy
2021-11-17 23:27:30 +03:00
cookie_jar . delete ( "__profilin" )
2016-05-18 17:27:54 +10:00
end
2016-07-04 17:20:30 +08:00
user . logged_out
2017-01-31 17:21:37 -05:00
elsif user && @user_token
@user_token . destroy
2015-01-28 12:56:25 +11:00
end
2017-08-31 12:06:56 +08:00
2021-11-17 23:27:30 +03:00
cookie_jar . delete ( 'authentication_data' )
cookie_jar . delete ( TOKEN_COOKIE )
2013-10-09 15:10:37 +11:00
end
# api has special rights return true if api was detected
def is_api?
current_user
2016-12-16 12:05:20 +11:00
!! ( @env [ API_KEY_ENV ] )
end
def is_user_api?
current_user
!! ( @env [ USER_API_KEY_ENV ] )
2013-10-09 15:10:37 +11:00
end
def has_auth_cookie?
2021-11-17 23:27:30 +03:00
find_auth_token . present?
2013-10-09 15:10:37 +11:00
end
2014-05-23 08:13:25 +10:00
def should_update_last_seen?
2020-07-21 13:43:28 +08:00
return false unless can_write?
2019-01-22 18:07:48 +08:00
2021-11-17 23:27:30 +03:00
api = !! @env [ API_KEY_ENV ] || !! @env [ USER_API_KEY_ENV ]
2019-04-15 17:34:34 +01:00
if @request . xhr? || api
2020-03-26 17:35:32 +11:00
@env [ "HTTP_DISCOURSE_PRESENT" ] == "true"
2017-02-28 12:34:57 -05:00
else
true
end
2014-05-23 08:13:25 +10:00
end
protected
def lookup_api_user ( api_key_value , request )
2019-12-12 11:45:00 +00:00
if api_key = ApiKey . active . with_key ( api_key_value ) . includes ( :user ) . first
2019-03-12 17:16:42 -06:00
api_username = header_api_key? ? @env [ HEADER_API_USERNAME ] : request [ API_USERNAME ]
2019-11-08 17:28:48 -07:00
2020-10-06 17:20:15 +01:00
unless api_key . request_allowed? ( @env )
2016-06-01 21:48:06 +02:00
Rails . logger . warn ( "[Unauthorized API Access] username: #{ api_username } , IP address: #{ request . ip } " )
2014-11-20 15:21:49 +11:00
return nil
end
2019-09-03 18:10:29 +10: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
2020-07-21 13:43:28 +08:00
if user && can_write?
2019-09-03 18:10:29 +10:00
api_key . update_columns ( last_used_at : Time . zone . now )
2014-05-23 08:13:25 +10:00
end
2019-09-03 18:10:29 +10:00
user
2014-05-23 08:13:25 +10:00
end
end
2018-09-04 16:35:49 +08:00
private
2020-08-24 10:24:52 +01:00
def parameter_api_patterns
PARAMETER_API_PATTERNS + DiscoursePluginRegistry . api_parameter_routes
end
2020-05-12 13:35:36 +01:00
# By default we only allow headers for sending API credentials
# However, in some scenarios it is essential to send them via url parameters
# so we need to add some exceptions
def api_parameter_allowed?
2020-10-06 17:20:15 +01:00
parameter_api_patterns . any? { | p | p . match? ( env : @env ) }
2020-03-16 12:05:24 -06:00
end
2019-03-12 17:16:42 -06:00
def header_api_key?
!! @env [ HEADER_API_KEY ]
end
2021-11-17 23:27:30 +03:00
def can_write?
@can_write ||= ! Discourse . pg_readonly_mode?
end
def admin_api_key_limiter
return @admin_api_key_limiter if @admin_api_key_limiter
2018-09-04 16:35:49 +08:00
2021-06-03 10:52:43 +01:00
limit = GlobalSetting . max_admin_api_reqs_per_minute . to_i
if GlobalSetting . respond_to? ( :max_admin_api_reqs_per_key_per_minute )
2021-11-12 08:52:59 -06:00
Discourse . deprecate ( "DISCOURSE_MAX_ADMIN_API_REQS_PER_KEY_PER_MINUTE is deprecated. Please use DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE" , drop_from : '2.9.0' )
2021-11-17 23:27:30 +03:00
limit = [
GlobalSetting . max_admin_api_reqs_per_key_per_minute . to_i ,
limit
]. max
2021-06-03 10:52:43 +01:00
end
2021-11-17 23:27:30 +03:00
@admin_api_key_limiter = RateLimiter . new (
2018-09-04 16:35:49 +08:00
nil ,
2021-06-03 10:52:43 +01:00
"admin_api_min" ,
limit ,
2021-11-17 23:27:30 +03:00
60 ,
error_code : "admin_api_key_rate_limit"
2021-06-03 10:52:43 +01:00
)
2021-11-17 23:27:30 +03:00
end
2021-06-03 10:52:43 +01:00
2021-11-17 23:27:30 +03:00
def user_api_key_limiter_60_secs
@user_api_key_limiter_60_secs ||= RateLimiter . new (
nil ,
"user_api_min_ #{ @hashed_user_api_key } " ,
GlobalSetting . max_user_api_reqs_per_minute ,
60 ,
error_code : "user_api_key_limiter_60_secs"
)
2018-09-04 16:35:49 +08:00
end
2021-11-17 23:27:30 +03:00
def user_api_key_limiter_1_day
@user_api_key_limiter_1_day ||= RateLimiter . new (
nil ,
"user_api_day_ #{ @hashed_user_api_key } " ,
GlobalSetting . max_user_api_reqs_per_day ,
86400 ,
error_code : "user_api_key_limiter_1_day"
)
2020-07-21 13:43:28 +08:00
end
2021-11-17 23:27:30 +03:00
def find_auth_token
return @auth_token if defined? ( @auth_token )
@auth_token = begin
if v0 = self . class . find_v0_auth_cookie ( @request )
v0
elsif v1 = self . class . find_v1_auth_cookie ( @env )
if v1 [ :issued_at ] >= SiteSetting . maximum_session_age . hours . ago . to_i
v1 [ :token ]
end
end
end
end
2013-10-09 15:10:37 +11:00
end