Class: Auth::DefaultCurrentUserProvider
- Inherits:
-
Object
- Object
- Auth::DefaultCurrentUserProvider
- Defined in:
- lib/auth/default_current_user_provider.rb
Overview
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.
Constant Summary collapse
- CURRENT_USER_KEY =
"_DISCOURSE_CURRENT_USER"
- USER_TOKEN_KEY =
"_DISCOURSE_USER_TOKEN"
- API_KEY =
"api_key"
- 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"
- PARAMETER_USER_API_KEY =
"user_api_key"
- 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"
- TOKEN_COOKIE =
ENV["DISCOURSE_TOKEN_COOKIE"] || "_t"
- PATH_INFO =
"PATH_INFO"
- COOKIE_ATTEMPTS_PER_MIN =
10
- BAD_TOKEN =
"_DISCOURSE_BAD_TOKEN"
- DECRYPTED_AUTH_COOKIE =
"_DISCOURSE_DECRYPTED_AUTH_COOKIE"
- TOKEN_SIZE =
32
- PARAMETER_API_PATTERNS =
RouteMatcher.new( methods: :get, actions: [ "posts#latest", "posts#user_posts_feed", "groups#posts_feed", "groups#mentions_feed", "list#user_topics_feed", "list#category_feed", "topics#feed", "badges#show", "tags#tag_feed", "tags#show", *%i[latest unread new read posted bookmarks].map { |f| "list##{f}_feed" }, *%i[all yearly quarterly monthly weekly daily].map { |p| "list#top_#{p}_feed" }, *%i[latest unread new read posted bookmarks].map { |f| "tags#show_#{f}" }, ], formats: :rss, ), RouteMatcher.new(methods: :get, actions: "users#bookmarks", formats: :ics), RouteMatcher.new(methods: :post, actions: "admin/email#handle_mail", formats: nil)
Class Method Summary collapse
Instance Method Summary collapse
-
#current_user ⇒ Object
our current user, return nil if none is found.
- #enable_bootstrap_mode(user) ⇒ Object
- #has_auth_cookie? ⇒ Boolean
-
#initialize(env) ⇒ DefaultCurrentUserProvider
constructor
do all current user initialization here.
-
#is_api? ⇒ Boolean
api has special rights return true if api was detected.
- #is_user_api? ⇒ Boolean
- #log_off_user(session, cookie_jar) ⇒ Object
- #log_on_user(user, session, cookie_jar, opts = {}) ⇒ Object
-
#make_developer_admin(user) ⇒ Object
This is also used to set the first admin of the site via the finish installation & register -> user account activation for signup flow, since all admin emails are stored in DISCOURSE_DEVELOPER_EMAILS for self-hosters.
- #refresh_session(user, session, cookie_jar) ⇒ Object
- #set_auth_cookie!(unhashed_auth_token, user, cookie_jar) ⇒ Object
- #should_update_last_seen? ⇒ Boolean
Constructor Details
#initialize(env) ⇒ DefaultCurrentUserProvider
do all current user initialization here
92 93 94 95 96 |
# File 'lib/auth/default_current_user_provider.rb', line 92 def initialize(env) @env = env @request = Rack::Request.new(env) @user_token = env[USER_TOKEN_KEY] end |
Class Method Details
.find_v0_auth_cookie(request) ⇒ Object
71 72 73 74 75 |
# File 'lib/auth/default_current_user_provider.rb', line 71 def self.(request) = request.[TOKEN_COOKIE] if &.valid_encoding? && .present? && .size == TOKEN_SIZE end |
.find_v1_auth_cookie(env) ⇒ Object
77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/auth/default_current_user_provider.rb', line 77 def self.(env) return env[DECRYPTED_AUTH_COOKIE] if env.key?(DECRYPTED_AUTH_COOKIE) env[DECRYPTED_AUTH_COOKIE] = begin request = ActionDispatch::Request.new(env) = request.[TOKEN_COOKIE] # don't even initialize a cookie jar if we don't have a cookie at all if &.valid_encoding? && .present? request..encrypted[TOKEN_COOKIE]&.with_indifferent_access end end end |
Instance Method Details
#current_user ⇒ Object
our current user, return nil if none is found
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/auth/default_current_user_provider.rb', line 99 def current_user return @env[CURRENT_USER_KEY] if @env.key?(CURRENT_USER_KEY) # bypass if we have the shared session header if shared_key = @env["HTTP_X_SHARED_SESSION_KEY"] uid = Discourse.redis.get("shared_session_key_#{shared_key}") user = nil user = User.find_by(id: uid.to_i) if uid @env[CURRENT_USER_KEY] = user return user end request = @request user_api_key = @env[USER_API_KEY] 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 api_key ||= request[API_KEY] if !@env.blank? && request[API_KEY] && api_parameter_allowed? auth_token = find_auth_token current_user = nil if auth_token limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN, 60) if limiter.can_perform? @env[USER_TOKEN_KEY] = @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 current_user = @user_token.try(:user) current_user.authenticated_with_oauth = @user_token.authenticated_with_oauth if current_user end if !current_user @env[BAD_TOKEN] = true begin limiter.performed! rescue RateLimiter::LimitExceeded raise Discourse::InvalidAccess.new("Invalid Access", nil, delete_cookie: TOKEN_COOKIE) end end elsif @env["HTTP_DISCOURSE_LOGGED_IN"] @env[BAD_TOKEN] = true end # possible we have an api call, impersonate if api_key current_user = lookup_api_user(api_key, request) if !current_user raise Discourse::InvalidAccess.new( I18n.t("invalid_api_credentials"), nil, custom_message: "invalid_api_credentials", ) end raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active if !Rails.env.profile? admin_api_key_limiter.performed! # Don't enforce the default per ip limits for authenticated admin api # requests (@env["DISCOURSE_RATE_LIMITERS"] || []).each(&:rollback!) end @env[API_KEY_ENV] = true end # user api key handling if user_api_key @hashed_user_api_key = ApiKey.hash_key(user_api_key) user_api_key_obj = UserApiKey .active .joins(:user) .where(key_hash: @hashed_user_api_key) .includes(:user, :scopes, :client) .first raise Discourse::InvalidAccess unless user_api_key_obj user_api_key_limiter_60_secs.performed! user_api_key_limiter_1_day.performed! user_api_key_obj.ensure_allowed!(@env) current_user = user_api_key_obj.user raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active user_api_key_obj.update_last_used(@env[USER_API_CLIENT_ID]) if can_write? @env[USER_API_KEY_ENV] = true end # keep this rule here as a safeguard # under no conditions to suspended or inactive accounts get current_user current_user = nil if current_user && (current_user.suspended? || !current_user.active) if current_user && should_update_last_seen? ip = request.ip user_id = current_user.id old_ip = current_user.ip_address Scheduler::Defer.later "Updating Last Seen" do if User.should_update_last_seen?(user_id) if u = User.find_by(id: user_id) u.update_last_seen!(Time.zone.now, force: true) end end User.update_ip_address!(user_id, new_ip: ip, old_ip: old_ip) end end @env[CURRENT_USER_KEY] = current_user end |
#enable_bootstrap_mode(user) ⇒ Object
319 320 321 322 323 324 325 |
# File 'lib/auth/default_current_user_provider.rb', line 319 def enable_bootstrap_mode(user) 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 end |
#has_auth_cookie? ⇒ Boolean
359 360 361 |
# File 'lib/auth/default_current_user_provider.rb', line 359 def find_auth_token.present? end |
#is_api? ⇒ Boolean
api has special rights return true if api was detected
349 350 351 352 |
# File 'lib/auth/default_current_user_provider.rb', line 349 def is_api? current_user !!(@env[API_KEY_ENV]) end |
#is_user_api? ⇒ Boolean
354 355 356 357 |
# File 'lib/auth/default_current_user_provider.rb', line 354 def is_user_api? current_user !!(@env[USER_API_KEY_ENV]) end |
#log_off_user(session, cookie_jar) ⇒ Object
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 |
# File 'lib/auth/default_current_user_provider.rb', line 327 def log_off_user(session, ) user = current_user if SiteSetting.log_out_strict && user user.user_auth_tokens.destroy_all if user.admin && defined?(Rack::MiniProfiler) # clear the profiling cookie to keep stuff tidy .delete("__profilin") end user.logged_out elsif user && @user_token @user_token.destroy DiscourseEvent.trigger(:user_logged_out, user) end .delete("authentication_data") .delete(TOKEN_COOKIE) end |
#log_on_user(user, session, cookie_jar, opts = {}) ⇒ Object
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
# File 'lib/auth/default_current_user_provider.rb', line 262 def log_on_user(user, session, , opts = {}) @env[USER_TOKEN_KEY] = @user_token = UserAuthToken.generate!( user_id: user.id, user_agent: @env["HTTP_USER_AGENT"], path: @env["REQUEST_PATH"], client_ip: @request.ip, staff: user.staff?, impersonate: opts[:impersonate], authenticated_with_oauth: opts[:authenticated_with_oauth], ) (@user_token.unhashed_auth_token, user, ) user.unstage! make_developer_admin(user) enable_bootstrap_mode(user) UserAuthToken.enforce_session_count_limit!(user.id) @env[CURRENT_USER_KEY] = user end |
#make_developer_admin(user) ⇒ Object
This is also used to set the first admin of the site via the finish installation & register -> user account activation for signup flow, since all admin emails are stored in DISCOURSE_DEVELOPER_EMAILS for self-hosters.
310 311 312 313 314 315 316 317 |
# File 'lib/auth/default_current_user_provider.rb', line 310 def make_developer_admin(user) if user.active? && !user.admin && Rails.configuration.respond_to?(:developer_emails) && Rails.configuration.developer_emails.include?(user.email) user.admin = true user.save Group.refresh_automatic_groups!(:staff, :admins) end end |
#refresh_session(user, session, cookie_jar) ⇒ Object
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 |
# File 'lib/auth/default_current_user_provider.rb', line 230 def refresh_session(user, session, ) # 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) if !is_user_api? && @user_token && @user_token.user == user rotated_at = @user_token.rotated_at needs_rotation = ( if @user_token.auth_token_seen rotated_at < UserAuthToken::ROTATE_TIME.ago else rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago end ) if needs_rotation if @user_token.rotate!( user_agent: @env["HTTP_USER_AGENT"], client_ip: @request.ip, path: @env["REQUEST_PATH"], ) (@user_token.unhashed_auth_token, user, ) DiscourseEvent.trigger(:user_session_refreshed, user) end end end .delete(TOKEN_COOKIE) if !user && .key?(TOKEN_COOKIE) end |
#set_auth_cookie!(unhashed_auth_token, user, cookie_jar) ⇒ Object
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 |
# File 'lib/auth/default_current_user_provider.rb', line 284 def (unhashed_auth_token, user, ) data = { token: unhashed_auth_token, user_id: user.id, username: user.username, trust_level: user.trust_level, issued_at: Time.zone.now.to_i, } expires = SiteSetting.maximum_session_age.hours.from_now if SiteSetting.persistent_sessions same_site = SiteSetting. if SiteSetting. != "Disabled" .encrypted[TOKEN_COOKIE] = { value: data, httponly: true, secure: SiteSetting.force_https, expires: expires, same_site: same_site, } end |
#should_update_last_seen? ⇒ Boolean
363 364 365 366 367 368 369 370 371 372 373 |
# File 'lib/auth/default_current_user_provider.rb', line 363 def should_update_last_seen? return false unless can_write? api = !!@env[API_KEY_ENV] || !!@env[USER_API_KEY_ENV] if @request.xhr? || api @env["HTTP_DISCOURSE_PRESENT"] == "true" else true end end |