discourse/app/controllers/session_controller.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

925 lines
30 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2013-02-06 03:16:51 +08:00
class SessionController < ApplicationController
DEV: Add routes and controller actions for passkeys (2/3) (#23587) This is part 2 (of 3) for passkeys support. This adds a hidden site setting plus routes and controller actions. 1. registering passkeys Passkeys are registered in a two-step process. First, `create_passkey` returns details for the browser to create a passkey. This includes - a challenge - the relying party ID and Origin - the user's secure identifier - the supported algorithms - the user's existing passkeys (if any) Then the browser creates a key with this information, and submits it to the server via `register_passkey`. 2. authenticating passkeys A similar process happens here as well. First, a challenge is created and sent to the browser. Then the browser makes a public key credential and submits it to the server via `passkey_auth_perform`. 3. renaming/deleting passkeys These routes allow changing the name of a key and deleting it. 4. checking if session is trusted for sensitive actions Since a passkey is a password replacement, we want to make sure to confirm the user's identity before allowing adding/deleting passkeys. The u/trusted-session GET route returns success if user has confirmed their session (and failed if user hasn't). In the frontend (in the next PR), we're using these routes to show the password confirmation screen. The `/u/confirm-session` route allows the user to confirm their session with a password. The latter route's functionality already existed in core, under the 2FA flow, but it has been abstracted into its own here so it can be used independently. Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
2023-10-12 02:36:54 +08:00
before_action :check_local_login_allowed,
only: %i[create forgot_password passkey_challenge passkey_login]
before_action :rate_limit_login, only: %i[create email_login]
skip_before_action :redirect_to_login_if_required
skip_before_action :redirect_to_profile_if_required
skip_before_action :preload_json,
:check_xhr,
only: %i[sso sso_login sso_provider destroy one_time_password]
2013-02-06 03:16:51 +08:00
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
skip_before_action :check_xhr, only: %i[second_factor_auth_show]
allow_in_staff_writes_only_mode :create, :email_login, :forgot_password
ACTIVATE_USER_KEY = "activate_user"
FORGOT_PASSWORD_EMAIL_LIMIT_PER_DAY = 6
def csrf
render json: { csrf: form_authenticity_token }
end
def sso
raise Discourse::NotFound unless SiteSetting.enable_discourse_connect?
destination_url = cookies[:destination_url] || session[:destination_url]
return_path = params[:return_path] || path("/")
if destination_url && return_path == path("/")
uri = URI.parse(destination_url)
return_path = "#{uri.path}#{uri.query ? "?#{uri.query}" : ""}"
end
session.delete(:destination_url)
cookies.delete(:destination_url)
sso = DiscourseConnect.generate_sso(return_path, secure_session: secure_session)
connect_verbose_warn { "Verbose SSO log: Started SSO process\n\n#{sso.diagnostics}" }
redirect_to sso_url(sso), allow_other_host: true
end
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
def sso_provider(payload = nil, confirmed_2fa_during_login = false)
raise Discourse::NotFound unless SiteSetting.enable_discourse_connect_provider
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
result =
run_second_factor!(
SecondFactor::Actions::DiscourseConnectProvider,
action_data: {
payload: payload,
confirmed_2fa_during_login: confirmed_2fa_during_login,
},
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
)
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
if result.second_factor_auth_skipped?
data = result.data
if data[:logout]
params[:return_url] = data[:return_sso_url]
destroy
return
end
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
if data[:no_current_user]
if data[:prompt] == "none"
redirect_to data[:sso_redirect_url], allow_other_host: true
return
else
cookies[:sso_payload] = payload || request.query_string
redirect_to path("/login")
return
end
end
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
if request.xhr?
# for the login modal
cookies[:sso_destination_url] = data[:sso_redirect_url]
render json: success_json.merge(redirect_url: data[:sso_redirect_url])
else
redirect_to data[:sso_redirect_url], allow_other_host: true
end
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
elsif result.no_second_factors_enabled?
if request.xhr?
# for the login modal
cookies[:sso_destination_url] = result.data[:sso_redirect_url]
else
redirect_to result.data[:sso_redirect_url], allow_other_host: true
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
end
elsif result.second_factor_auth_completed?
redirect_url = result.data[:sso_redirect_url]
render json: success_json.merge(redirect_url: redirect_url)
end
rescue DiscourseConnectProvider::BlankSecret
render plain: I18n.t("discourse_connect.missing_secret"), status: 400
rescue DiscourseConnectProvider::ParseError
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
# Do NOT pass the error text to the client, it would give them the correct signature
render plain: I18n.t("discourse_connect.login_error"), status: 422
rescue DiscourseConnectProvider::BlankReturnUrl
render plain: "return_sso_url is blank, it must be provided", status: 400
rescue DiscourseConnectProvider::InvalidParameterValueError => e
render plain: I18n.t("discourse_connect.invalid_parameter_value", param: e.param), status: 400
end
# For use in development mode only when login options could be limited or disabled.
# NEVER allow this to work in production.
2018-03-28 11:31:43 +08:00
if !Rails.env.production?
skip_before_action :check_xhr, only: [:become]
def become
raise Discourse::InvalidAccess if Rails.env.production?
raise Discourse::ReadOnly if @readonly_mode
if ENV["DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE"] != "1"
render(content_type: "text/plain", inline: <<~TEXT)
To enable impersonating any user without typing passwords set the following ENV var
export DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE=1
You can do that in your bashrc of bash profile file or the script you use to launch the web server
TEXT
return
end
user = User.find_by_username(params[:session_id])
raise "User #{params[:session_id]} not found" if user.blank?
log_on_user(user)
if params[:redirect] == "false"
render plain: "Signed in to #{params[:session_id]} successfully"
else
redirect_to path("/")
end
end
end
DEV: Set `config.eager_load = true` on CI (#25032) Why this change? When running system tests on our CI, we have been occasionally seeing server errors like: ``` Error encountered while proccessing /stylesheets/desktop_e58cf7f686aab173f9b778797f241913c2833c39.css NoMethodError: undefined method `+' for nil:NilClass /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/path/pattern.rb:139:in `[]' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:127:in `block (2 levels) in find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `each' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `each_with_index' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `block in find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:123:in `map!' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:123:in `find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:32:in `serve' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/routing/route_set.rb:852:in `call' ``` While looking through various Rails issues related to the error above, I came across https://github.com/rails/rails/pull/27647 which is a fix to fully initialize routes before the first request is handled. However, the routes are only fully initialize only if `config.eager_load` is set to `true`. There is no reason why `config.eager_load` shouldn't be `true` in the CI environment and this is what a new Rails 7.1 app is generated with. What does this change do? Enable `config.eager_load` when `env["CI"]` is present
2023-12-26 13:05:55 +08:00
if Rails.env.test?
skip_before_action :check_xhr, only: :test_second_factor_restricted_route
def test_second_factor_restricted_route
target_user = User.find_by_username(params[:username]) || current_user
raise "user required" if !target_user
DEV: Set `config.eager_load = true` on CI (#25032) Why this change? When running system tests on our CI, we have been occasionally seeing server errors like: ``` Error encountered while proccessing /stylesheets/desktop_e58cf7f686aab173f9b778797f241913c2833c39.css NoMethodError: undefined method `+' for nil:NilClass /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/path/pattern.rb:139:in `[]' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:127:in `block (2 levels) in find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `each' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `each_with_index' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `block in find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:123:in `map!' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:123:in `find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:32:in `serve' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/routing/route_set.rb:852:in `call' ``` While looking through various Rails issues related to the error above, I came across https://github.com/rails/rails/pull/27647 which is a fix to fully initialize routes before the first request is handled. However, the routes are only fully initialize only if `config.eager_load` is set to `true`. There is no reason why `config.eager_load` shouldn't be `true` in the CI environment and this is what a new Rails 7.1 app is generated with. What does this change do? Enable `config.eager_load` when `env["CI"]` is present
2023-12-26 13:05:55 +08:00
result =
run_second_factor!(TestSecondFactorAction, target_user: target_user) do |manager|
DEV: Set `config.eager_load = true` on CI (#25032) Why this change? When running system tests on our CI, we have been occasionally seeing server errors like: ``` Error encountered while proccessing /stylesheets/desktop_e58cf7f686aab173f9b778797f241913c2833c39.css NoMethodError: undefined method `+' for nil:NilClass /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/path/pattern.rb:139:in `[]' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:127:in `block (2 levels) in find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `each' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `each_with_index' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `block in find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:123:in `map!' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:123:in `find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:32:in `serve' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/routing/route_set.rb:852:in `call' ``` While looking through various Rails issues related to the error above, I came across https://github.com/rails/rails/pull/27647 which is a fix to fully initialize routes before the first request is handled. However, the routes are only fully initialize only if `config.eager_load` is set to `true`. There is no reason why `config.eager_load` shouldn't be `true` in the CI environment and this is what a new Rails 7.1 app is generated with. What does this change do? Enable `config.eager_load` when `env["CI"]` is present
2023-12-26 13:05:55 +08:00
manager.allow_backup_codes! if params[:allow_backup_codes]
end
if result.no_second_factors_enabled?
render json: { result: "no_second_factors_enabled" }
else
render json: { result: "second_factor_auth_completed" }
end
rescue StandardError => e
# Normally this would be checked by the consumer before calling `run_second_factor!`
# but since this is a test route, we allow passing a bad value into the API, catch the error
# and return a JSON response to assert against.
if e.message == "running 2fa against another user is not allowed"
render json: { result: "wrong user" }, status: 400
else
raise e
end
DEV: Set `config.eager_load = true` on CI (#25032) Why this change? When running system tests on our CI, we have been occasionally seeing server errors like: ``` Error encountered while proccessing /stylesheets/desktop_e58cf7f686aab173f9b778797f241913c2833c39.css NoMethodError: undefined method `+' for nil:NilClass /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/path/pattern.rb:139:in `[]' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:127:in `block (2 levels) in find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `each' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `each_with_index' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:126:in `block in find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:123:in `map!' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:123:in `find_routes' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/journey/router.rb:32:in `serve' /__w/discourse/discourse/vendor/bundle/ruby/3.2.0/gems/actionpack-7.0.7/lib/action_dispatch/routing/route_set.rb:852:in `call' ``` While looking through various Rails issues related to the error above, I came across https://github.com/rails/rails/pull/27647 which is a fix to fully initialize routes before the first request is handled. However, the routes are only fully initialize only if `config.eager_load` is set to `true`. There is no reason why `config.eager_load` shouldn't be `true` in the CI environment and this is what a new Rails 7.1 app is generated with. What does this change do? Enable `config.eager_load` when `env["CI"]` is present
2023-12-26 13:05:55 +08:00
end
end
def sso_login
raise Discourse::NotFound unless SiteSetting.enable_discourse_connect
raise Discourse::ReadOnly if @readonly_mode && !staff_writes_only_mode?
params.require(:sso)
params.require(:sig)
begin
sso = DiscourseConnect.parse(request.query_string, secure_session: secure_session)
rescue DiscourseConnect::PayloadParseError => e
connect_verbose_warn do
"Verbose SSO log: Payload is not base64\n\n#{e.message}\n\n#{sso&.diagnostics}"
end
return render_sso_error(text: I18n.t("discourse_connect.payload_parse_error"), status: 422)
rescue DiscourseConnect::SignatureError => e
connect_verbose_warn do
"Verbose SSO log: Signature verification failed\n\n#{e.message}\n\n#{sso&.diagnostics}"
end
# Do NOT pass the error text to the client, it would give them the correct signature
return render_sso_error(text: I18n.t("discourse_connect.signature_error"), status: 422)
end
if !sso.nonce_valid?
connect_verbose_warn { "Verbose SSO log: #{sso.nonce_error}\n\n#{sso.diagnostics}" }
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
return render_sso_error(text: I18n.t("discourse_connect.timeout_expired"), status: 419)
end
if ScreenedIpAddress.should_block?(request.remote_ip)
connect_verbose_warn do
"Verbose SSO log: IP address is blocked #{request.remote_ip}\n\n#{sso.diagnostics}"
end
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
return render_sso_error(text: I18n.t("discourse_connect.unknown_error"), status: 500)
end
return_path = sso.return_path
sso.expire_nonce!
begin
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
invite = validate_invitiation!(sso)
if user = sso.lookup_or_create_user(request.remote_ip)
raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
if user.suspended?
render_sso_error(text: failed_to_login(user)[:error], status: 403)
return
end
if SiteSetting.must_approve_users? && !user.approved?
redeem_invitation(invite, sso, user) if invite.present? && user.invited_user.blank?
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
if SiteSetting.discourse_connect_not_approved_url.present?
redirect_to SiteSetting.discourse_connect_not_approved_url, allow_other_host: true
else
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
render_sso_error(text: I18n.t("discourse_connect.account_not_approved"), status: 403)
end
return
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
# we only want to redeem the invite if
# the user has not already redeemed an invite
# (covers the same SSO user visiting an invite link)
elsif invite.present? && user.invited_user.blank?
redeem_invitation(invite, sso, user)
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
# we directly call user.activate here instead of going
# through the UserActivator path because we assume the account
# is valid from the SSO provider's POV and do not need to
# send an activation email to the user
user.activate
login_sso_user(sso, user)
topic = invite.topics.first
return_path = topic.present? ? path(topic.relative_url) : path("/")
elsif !user.active?
activation = UserActivator.new(user, request, session, cookies)
activation.finish
session["user_created_message"] = activation.message
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
return redirect_to(users_account_created_path)
else
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
login_sso_user(sso, user)
end
# If it's not a relative URL check the host
if return_path !~ %r{\A/[^/]}
begin
uri = URI(return_path)
if (uri.hostname == Discourse.current_hostname)
return_path = uri.to_s
elsif !domain_redirect_allowed?(uri.hostname)
return_path = path("/")
end
rescue StandardError
return_path = path("/")
end
end
# this can be done more surgically with a regex
# but it the edge case of never supporting redirects back to
# any url with `/session/sso` in it anywhere is reasonable
return_path = path("/") if return_path.include?(path("/session/sso"))
redirect_to return_path, allow_other_host: true
else
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
render_sso_error(text: I18n.t("discourse_connect.not_found"), status: 500)
end
rescue ActiveRecord::RecordInvalid => e
connect_verbose_warn { <<~TEXT }
Verbose SSO log: Record was invalid: #{e.record.class.name} #{e.record.id}
#{e.record.errors.to_h}
Attributes:
#{e.record.attributes.slice(*DiscourseConnectBase::ACCESSORS.map(&:to_s))}
SSO Diagnostics:
#{sso.diagnostics}
TEXT
text = nil
# If there's a problem with the email we can explain that
if (e.record.is_a?(User) && e.record.errors[:primary_email].present?)
if e.record.email.blank?
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
text = I18n.t("discourse_connect.no_email")
else
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
text =
I18n.t("discourse_connect.email_error", email: ERB::Util.html_escape(e.record.email))
end
end
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
render_sso_error(text: text || I18n.t("discourse_connect.unknown_error"), status: 500)
rescue DiscourseConnect::BlankExternalId
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
render_sso_error(text: I18n.t("discourse_connect.blank_id_error"), status: 500)
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
rescue Invite::ValidationFailed => e
render_sso_error(text: e.message, status: 400)
rescue Invite::RedemptionFailed => e
render_sso_error(text: I18n.t("discourse_connect.invite_redeem_failed"), status: 412)
rescue Invite::UserExists => e
render_sso_error(text: e.message, status: 412)
rescue => e
message = +"Failed to create or lookup user: #{e}."
message << " "
message << " #{sso.diagnostics}"
message << " "
message << " #{e.backtrace.join("\n")}"
Rails.logger.error(message)
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
render_sso_error(text: I18n.t("discourse_connect.unknown_error"), status: 500)
end
end
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
def login_sso_user(sso, user)
connect_verbose_warn do
"Verbose SSO log: User was logged on #{user.username}\n\n#{sso.diagnostics}"
end
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
log_on_user(user) if user.id != current_user&.id
end
2013-02-06 03:16:51 +08:00
def create
params.require(:login)
params.require(:password)
2013-02-06 03:16:51 +08:00
return invalid_credentials if params[:password].length > User.max_password_length
user = User.find_by_username_or_email(normalized_login_param)
raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
rate_limit_second_factor!(user)
if user.present?
password = params[:password]
# If their password is incorrect
if !user.confirm_password?(password)
invalid_credentials
return
end
2013-02-06 03:16:51 +08:00
# If the site requires user approval and the user is not approved yet
if login_not_approved_for?(user)
render json: login_not_approved
2013-02-06 03:16:51 +08:00
return
end
2013-11-28 09:39:59 +08:00
# User signed on with username and password, so let's prevent the invite link
# from being used to log in (if one exists).
Invite.invalidate_for_email(user.email)
# User's password has expired so they need to reset it
if user.password_expired?(password)
render json: { error: "expired", reason: "expired" }
return
end
2013-11-28 09:39:59 +08:00
else
invalid_credentials
return
end
2013-02-06 03:16:51 +08:00
if payload = login_error_check(user)
2020-01-15 18:27:12 +08:00
return render json: payload
end
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
second_factor_auth_result = authenticate_second_factor(user)
2020-01-15 18:27:12 +08:00
return render(json: @second_factor_failure_payload) if !second_factor_auth_result.ok
if user.active && user.email_confirmed?
login(user, second_factor_auth_result: second_factor_auth_result)
else
not_activated(user)
end
end
2013-02-06 03:16:51 +08:00
DEV: Add routes and controller actions for passkeys (2/3) (#23587) This is part 2 (of 3) for passkeys support. This adds a hidden site setting plus routes and controller actions. 1. registering passkeys Passkeys are registered in a two-step process. First, `create_passkey` returns details for the browser to create a passkey. This includes - a challenge - the relying party ID and Origin - the user's secure identifier - the supported algorithms - the user's existing passkeys (if any) Then the browser creates a key with this information, and submits it to the server via `register_passkey`. 2. authenticating passkeys A similar process happens here as well. First, a challenge is created and sent to the browser. Then the browser makes a public key credential and submits it to the server via `passkey_auth_perform`. 3. renaming/deleting passkeys These routes allow changing the name of a key and deleting it. 4. checking if session is trusted for sensitive actions Since a passkey is a password replacement, we want to make sure to confirm the user's identity before allowing adding/deleting passkeys. The u/trusted-session GET route returns success if user has confirmed their session (and failed if user hasn't). In the frontend (in the next PR), we're using these routes to show the password confirmation screen. The `/u/confirm-session` route allows the user to confirm their session with a password. The latter route's functionality already existed in core, under the 2FA flow, but it has been abstracted into its own here so it can be used independently. Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
2023-10-12 02:36:54 +08:00
def passkey_challenge
render json: DiscourseWebauthn.stage_challenge(current_user, secure_session)
end
def passkey_login
raise Discourse::NotFound unless SiteSetting.enable_passkeys
DEV: Add routes and controller actions for passkeys (2/3) (#23587) This is part 2 (of 3) for passkeys support. This adds a hidden site setting plus routes and controller actions. 1. registering passkeys Passkeys are registered in a two-step process. First, `create_passkey` returns details for the browser to create a passkey. This includes - a challenge - the relying party ID and Origin - the user's secure identifier - the supported algorithms - the user's existing passkeys (if any) Then the browser creates a key with this information, and submits it to the server via `register_passkey`. 2. authenticating passkeys A similar process happens here as well. First, a challenge is created and sent to the browser. Then the browser makes a public key credential and submits it to the server via `passkey_auth_perform`. 3. renaming/deleting passkeys These routes allow changing the name of a key and deleting it. 4. checking if session is trusted for sensitive actions Since a passkey is a password replacement, we want to make sure to confirm the user's identity before allowing adding/deleting passkeys. The u/trusted-session GET route returns success if user has confirmed their session (and failed if user hasn't). In the frontend (in the next PR), we're using these routes to show the password confirmation screen. The `/u/confirm-session` route allows the user to confirm their session with a password. The latter route's functionality already existed in core, under the 2FA flow, but it has been abstracted into its own here so it can be used independently. Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
2023-10-12 02:36:54 +08:00
params.require(:publicKeyCredential)
security_key =
::DiscourseWebauthn::AuthenticationService.new(
nil,
params[:publicKeyCredential],
session: secure_session,
factor_type: UserSecurityKey.factor_types[:first_factor],
).authenticate_security_key
user = User.where(id: security_key.user_id, active: true).first
if user.email_confirmed?
login(user, passkey_login: true)
DEV: Add routes and controller actions for passkeys (2/3) (#23587) This is part 2 (of 3) for passkeys support. This adds a hidden site setting plus routes and controller actions. 1. registering passkeys Passkeys are registered in a two-step process. First, `create_passkey` returns details for the browser to create a passkey. This includes - a challenge - the relying party ID and Origin - the user's secure identifier - the supported algorithms - the user's existing passkeys (if any) Then the browser creates a key with this information, and submits it to the server via `register_passkey`. 2. authenticating passkeys A similar process happens here as well. First, a challenge is created and sent to the browser. Then the browser makes a public key credential and submits it to the server via `passkey_auth_perform`. 3. renaming/deleting passkeys These routes allow changing the name of a key and deleting it. 4. checking if session is trusted for sensitive actions Since a passkey is a password replacement, we want to make sure to confirm the user's identity before allowing adding/deleting passkeys. The u/trusted-session GET route returns success if user has confirmed their session (and failed if user hasn't). In the frontend (in the next PR), we're using these routes to show the password confirmation screen. The `/u/confirm-session` route allows the user to confirm their session with a password. The latter route's functionality already existed in core, under the 2FA flow, but it has been abstracted into its own here so it can be used independently. Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
2023-10-12 02:36:54 +08:00
else
not_activated(user)
end
rescue ::DiscourseWebauthn::SecurityKeyError => err
render_json_error(err.message, status: 401)
end
def email_login_info
token = params[:token]
matched_token = EmailToken.confirmable(token, scope: EmailToken.scopes[:email_login])
user = matched_token&.user
check_local_login_allowed(user: user, check_login_via_email: true)
if matched_token
response = { can_login: true, token: token, token_email: matched_token.email }
matched_user = matched_token.user
if matched_user&.totp_enabled?
response.merge!(
second_factor_required: true,
backup_codes_enabled: matched_user&.backup_codes_enabled?,
totp_enabled: matched_user&.totp_enabled?,
)
end
if matched_user&.security_keys_enabled?
DiscourseWebauthn.stage_challenge(matched_user, secure_session)
response.merge!(
DiscourseWebauthn.allowed_credentials(matched_user, secure_session).merge(
security_key_required: true,
),
)
end
render json: response
else
render json: {
can_login: false,
error: I18n.t("email_login.invalid_token", base_url: Discourse.base_url),
}
end
end
def email_login
token = params[:token]
matched_token = EmailToken.confirmable(token, scope: EmailToken.scopes[:email_login])
user = matched_token&.user
check_local_login_allowed(user: user, check_login_via_email: true)
rate_limit_second_factor!(user)
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
if user.present? && !authenticate_second_factor(user).ok
2020-01-15 18:27:12 +08:00
return render(json: @second_factor_failure_payload)
end
if user = EmailToken.confirm(token, scope: EmailToken.scopes[:email_login])
if login_not_approved_for?(user)
return render json: login_not_approved
elsif payload = login_error_check(user)
return render json: payload
else
raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
user.update_timezone_if_missing(params[:timezone])
log_on_user(user)
return render json: success_json
end
end
render json: { error: I18n.t("email_login.invalid_token", base_url: Discourse.base_url) }
2013-02-06 03:16:51 +08:00
end
def one_time_password
@otp_username = otp_username = Discourse.redis.get "otp_#{params[:token]}"
if otp_username && user = User.find_by_username(otp_username)
if current_user&.username == otp_username
Discourse.redis.del "otp_#{params[:token]}"
return redirect_to path("/")
elsif request.post?
log_on_user(user)
Discourse.redis.del "otp_#{params[:token]}"
return redirect_to path("/")
else
# Display the form
end
else
@error = I18n.t("user_api_key.invalid_token")
end
render layout: "no_ember", locals: { hide_auth_buttons: true }
end
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
def second_factor_auth_show
nonce = params.require(:nonce)
challenge = nil
error_key = nil
user = nil
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
status_code = 200
begin
challenge =
SecondFactor::AuthManager.find_second_factor_challenge(
nonce: nonce,
secure_session: secure_session,
target_user: current_user,
)
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
rescue SecondFactor::BadChallenge => exception
error_key = exception.error_translation_key
status_code = exception.status_code
end
json = {}
if challenge
user = User.find(challenge[:target_user_id])
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
json.merge!(
totp_enabled: user.totp_enabled?,
backup_enabled: user.backup_codes_enabled?,
allowed_methods: challenge[:allowed_methods],
)
if user.security_keys_enabled?
DiscourseWebauthn.stage_challenge(user, secure_session)
json.merge!(DiscourseWebauthn.allowed_credentials(user, secure_session))
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
json[:security_keys_enabled] = true
else
json[:security_keys_enabled] = false
end
json[:description] = challenge[:description] if challenge[:description]
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
else
json[:error] = I18n.t(error_key)
end
respond_to do |format|
format.html do
store_preloaded("2fa_challenge_data", MultiJson.dump(json))
raise ApplicationController::RenderEmpty.new
end
format.json { render json: json, status: status_code }
end
end
def second_factor_auth_perform
nonce = params.require(:nonce)
challenge = nil
error_key = nil
user = nil
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
status_code = 200
begin
challenge =
SecondFactor::AuthManager.find_second_factor_challenge(
nonce: nonce,
secure_session: secure_session,
target_user: current_user,
)
user = User.find(challenge[:target_user_id])
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
rescue SecondFactor::BadChallenge => exception
error_key = exception.error_translation_key
status_code = exception.status_code
end
if error_key
json =
failed_json.merge(
ok: false,
error: I18n.t(error_key),
reason: "challenge_not_found_or_expired",
)
render json: failed_json.merge(json), status: status_code
return
end
# no proper error messages for these cases because the only way they can
# happen is if someone is messing with us.
# the first one can only happen if someone disables a 2FA method after
# they're redirected to the 2fa page and then uses the same method they've
# disabled.
second_factor_method = params[:second_factor_method].to_i
if !user.valid_second_factor_method_for_user?(second_factor_method)
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
raise Discourse::InvalidAccess.new
end
# and this happens if someone tries to use a 2FA method that's not accepted
# for the action they're trying to perform. e.g. using backup codes to
# grant someone admin status.
if !challenge[:allowed_methods].include?(second_factor_method)
raise Discourse::InvalidAccess.new
end
if !challenge[:successful]
rate_limit_second_factor!(user)
second_factor_auth_result = user.authenticate_second_factor(params, secure_session)
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
if second_factor_auth_result.ok
challenge[:successful] = true
challenge[:generated_at] += 1.minute.to_i
secure_session["current_second_factor_auth_challenge"] = challenge.to_json
else
error_json =
second_factor_auth_result
.to_h
.deep_symbolize_keys
.slice(:ok, :error, :reason)
.merge(failed_json)
render json: error_json, status: 400
return
end
end
render json: {
ok: true,
callback_method: challenge[:callback_method],
callback_path: challenge[:callback_path],
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
redirect_url: challenge[:redirect_url],
FEATURE: Centralized 2FA page (#15377) 2FA support in Discourse was added and grown gradually over the years: we first added support for TOTP for logins, then we implemented backup codes, and last but not least, security keys. 2FA usage was initially limited to logging in, but it has been expanded and we now require 2FA for risky actions such as adding a new admin to the site. As a result of this gradual growth of the 2FA system, technical debt has accumulated to the point where it has become difficult to require 2FA for more actions. We now have 5 different 2FA UI implementations and each one has to support all 3 2FA methods (TOTP, backup codes, and security keys) which makes it difficult to maintain a consistent UX for these different implementations. Moreover, there is a lot of repeated logic in the server-side code behind these 5 UI implementations which hinders maintainability even more. This commit is the first step towards repaying the technical debt: it builds a system that centralizes as much as possible of the 2FA server-side logic and UI. The 2 main components of this system are: 1. A dedicated page for 2FA with support for all 3 methods. 2. A reusable server-side class that centralizes the 2FA logic (the `SecondFactor::AuthManager` class). From a top-level view, the 2FA flow in this new system looks like this: 1. User initiates an action that requires 2FA; 2. Server is aware that 2FA is required for this action, so it redirects the user to the 2FA page if the user has a 2FA method, otherwise the action is performed. 3. User submits the 2FA form on the page; 4. Server validates the 2FA and if it's successful, the action is performed and the user is redirected to the previous page. A more technically-detailed explanation/documentation of the new system is available as a comment at the top of the `lib/second_factor/auth_manager.rb` file. Please note that the details are not set in stone and will likely change in the future, so please don't use the system in your plugins yet. Since this is a new system that needs to be tested, we've decided to migrate only the 2FA for adding a new admin to the new system at this time (in this commit). Our plan is to gradually migrate the remaining 2FA implementations to the new system. For screenshots of the 2FA page, see PR #15377 on GitHub.
2022-02-17 17:12:59 +08:00
},
status: 200
end
2013-02-06 03:16:51 +08:00
def forgot_password
params.require(:login)
2013-02-06 03:16:51 +08:00
if ScreenedIpAddress.should_block?(request.remote_ip)
return render_json_error(I18n.t("login.reset_not_allowed_from_ip_address"))
end
2014-08-18 08:55:30 +08:00
RateLimiter.new(nil, "forgot-password-hr-#{request.remote_ip}", 6, 1.hour).performed!
RateLimiter.new(nil, "forgot-password-min-#{request.remote_ip}", 3, 1.minute).performed!
user =
if SiteSetting.hide_email_address_taken && !current_user&.staff?
if !EmailAddressValidator.valid_value?(normalized_login_param)
raise Discourse::InvalidParameters.new(:login)
end
User.real.where(staged: false).find_by_email(Email.downcase(normalized_login_param))
else
User.real.where(staged: false).find_by_username_or_email(normalized_login_param)
end
if user
raise Discourse::ReadOnly if staff_writes_only_mode? && !user.staff?
enqueue_password_reset_for_user(user)
else
RateLimiter.new(
nil,
"forgot-password-login-hour-#{normalized_login_param}",
5,
1.hour,
).performed!
2013-02-06 03:16:51 +08:00
end
json = success_json
json[:user_found] = user.present? if !SiteSetting.hide_email_address_taken
render json: json
2014-08-18 08:55:30 +08:00
rescue RateLimiter::LimitExceeded
render_json_error(I18n.t("rate_limiter.slow_down"))
2013-02-06 03:16:51 +08:00
end
def current
if current_user.present?
render_serialized(current_user, CurrentUserSerializer, { login_method: login_method })
else
render body: nil, status: 404
end
end
2013-02-06 03:16:51 +08:00
def destroy
redirect_url = params[:return_url].presence || SiteSetting.logout_redirect.presence
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
sso = SiteSetting.enable_discourse_connect
only_one_authenticator =
!SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1
if SiteSetting.login_required && (sso || only_one_authenticator)
# In this situation visiting most URLs will start the auth process again
# Go to the `/login` page to avoid an immediate redirect
redirect_url ||= path("/login")
end
redirect_url ||= path("/")
event_data = {
redirect_url: redirect_url,
user: current_user,
client_ip: request&.ip,
user_agent: request&.user_agent,
}
DiscourseEvent.trigger(:before_session_destroy, event_data, **Discourse::Utils::EMPTY_KEYWORDS)
redirect_url = event_data[:redirect_url]
reset_session
log_off_user
if request.xhr?
render json: { redirect_url: redirect_url }
else
redirect_to redirect_url, allow_other_host: true
end
2013-02-06 03:16:51 +08:00
end
def get_honeypot_value
secure_session.set(HONEYPOT_KEY, honeypot_value, expires: 1.hour)
secure_session.set(CHALLENGE_KEY, challenge_value, expires: 1.hour)
render json: {
value: honeypot_value,
challenge: challenge_value,
expires_in: SecureSession.expiry,
}
end
def scopes
if is_api?
key = request.env[Auth::DefaultCurrentUserProvider::HEADER_API_KEY]
api_key = ApiKey.active.with_key(key).first
render_serialized(api_key.api_key_scopes, ApiKeyScopeSerializer, root: "scopes")
else
render body: nil, status: 404
end
end
2017-08-16 13:43:05 +08:00
protected
def normalized_login_param
login = params[:login].to_s
if login.present?
login = login[1..-1] if login[0] == "@"
User.normalize_username(login.strip)[0..100]
else
nil
end
end
def check_local_login_allowed(user: nil, check_login_via_email: false)
# admin-login can get around enabled SSO/disabled local logins
return if user&.admin?
if (check_login_via_email && !SiteSetting.enable_local_logins_via_email) ||
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 18:04:33 +08:00
SiteSetting.enable_discourse_connect || !SiteSetting.enable_local_logins
raise Discourse::InvalidAccess, "SSO takes over local login or the local login is disallowed."
2017-08-16 13:43:05 +08:00
end
end
2017-08-16 13:43:05 +08:00
private
def connect_verbose_warn(&blk)
Rails.logger.warn(blk.call) if SiteSetting.verbose_discourse_connect_logging
end
2020-01-15 18:27:12 +08:00
def authenticate_second_factor(user)
second_factor_authentication_result = user.authenticate_second_factor(params, secure_session)
if !second_factor_authentication_result.ok
failure_payload = second_factor_authentication_result.to_h
if user.security_keys_enabled?
DiscourseWebauthn.stage_challenge(user, secure_session)
failure_payload.merge!(DiscourseWebauthn.allowed_credentials(user, secure_session))
2020-01-15 18:27:12 +08:00
end
@second_factor_failure_payload = failed_json.merge(failure_payload)
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
return second_factor_authentication_result
2020-01-15 18:27:12 +08:00
end
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
second_factor_authentication_result
2020-01-15 18:27:12 +08:00
end
def login_error_check(user)
return failed_to_login(user) if user.suspended?
return not_allowed_from_ip_address(user) if ScreenedIpAddress.should_block?(request.remote_ip)
if ScreenedIpAddress.block_admin_login?(user, request.remote_ip)
admin_not_allowed_from_ip_address(user)
end
end
def login_not_approved_for?(user)
SiteSetting.must_approve_users? && !user.approved? && !user.admin?
end
def invalid_credentials
render json: { error: I18n.t("login.incorrect_username_email_or_password") }
end
def login_not_approved
{ error: I18n.t("login.not_approved") }
end
def not_activated(user)
session[ACTIVATE_USER_KEY] = user.id
render json: {
error: I18n.t("login.not_activated"),
reason: "not_activated",
sent_to_email: user.find_email || user.email,
current_email: user.email,
}
end
def not_allowed_from_ip_address(user)
{ error: I18n.t("login.not_allowed_from_ip_address", username: user.username) }
end
def admin_not_allowed_from_ip_address(user)
{ error: I18n.t("login.admin_not_allowed_from_ip_address", username: user.username) }
end
def failed_to_login(user)
{ error: user.suspended_message, reason: "suspended" }
end
def login(user, passkey_login: false, second_factor_auth_result: nil)
session.delete(ACTIVATE_USER_KEY)
user.update_timezone_if_missing(params[:timezone])
log_on_user(user)
if payload = cookies.delete(:sso_payload)
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
confirmed_2fa_during_login =
passkey_login ||
(
second_factor_auth_result&.ok && second_factor_auth_result.used_2fa_method.present? &&
second_factor_auth_result.used_2fa_method != UserSecondFactor.methods[:backup_codes]
)
FEATURE: Add 2FA support to the Discourse Connect Provider protocol (#16386) Discourse has the Discourse Connect Provider protocol that makes it possible to use a Discourse instance as an identity provider for external sites. As a natural extension to this protocol, this PR adds a new feature that makes it possible to use Discourse as a 2FA provider as well as an identity provider. The rationale for this change is that it's very difficult to implement 2FA support in a website and if you have multiple websites that need to have 2FA, it's unrealistic to build and maintain a separate 2FA implementation for each one. But with this change, you can piggyback on Discourse to take care of all the 2FA details for you for as many sites as you wish. To use Discourse as a 2FA provider, you'll need to follow this guide: https://meta.discourse.org/t/-/32974. It walks you through what you need to implement on your end/site and how to configure your Discourse instance. Once you're done, there is only one additional thing you need to do which is to include `require_2fa=true` in the payload that you send to Discourse. When Discourse sees `require_2fa=true`, it'll prompt the user to confirm their 2FA using whatever methods they've enabled (TOTP or security keys), and once they confirm they'll be redirected back to the return URL you've configured and the payload will contain `confirmed_2fa=true`. If the user has no 2FA methods enabled however, the payload will not contain `confirmed_2fa`, but it will contain `no_2fa_methods=true`. You'll need to be careful to re-run all the security checks and ensure the user can still access the resource on your site after they return from Discourse. This is very important because there's nothing that guarantees the user that will come back from Discourse after they confirm 2FA is the same user that you've redirected to Discourse. Internal ticket: t62183.
2022-04-13 20:04:09 +08:00
sso_provider(payload, confirmed_2fa_during_login)
else
render_serialized(user, UserSerializer)
end
end
def rate_limit_login
RateLimiter.new(
nil,
"login-hr-#{request.remote_ip}",
SiteSetting.max_logins_per_ip_per_hour,
1.hour,
).performed!
RateLimiter.new(
nil,
"login-min-#{request.remote_ip}",
SiteSetting.max_logins_per_ip_per_minute,
1.minute,
).performed!
end
def render_sso_error(status:, text:)
@sso_error = text
render status: status, layout: "no_ember"
end
# extension to allow plugins to customize the SSO URL
def sso_url(sso)
sso.to_url
end
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
# the invite_key will be present if set in InvitesController
# when the user visits an /invites/xxxx link; however we do
# not want to complete the SSO process of creating a user
# and redeeming the invite if the invite is not redeemable or
# for the wrong user
def validate_invitiation!(sso)
invite_key = secure_session["invite-key"]
return if invite_key.blank?
invite = Invite.find_by(invite_key: invite_key)
if invite.blank?
raise Invite::ValidationFailed.new(I18n.t("invite.not_found", base_url: Discourse.base_url))
end
if invite.redeemable?
if invite.is_email_invite? && sso.email != invite.email
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
raise Invite::ValidationFailed.new(I18n.t("invite.not_matching_email"))
end
elsif invite.expired?
raise Invite::ValidationFailed.new(I18n.t("invite.expired", base_url: Discourse.base_url))
elsif invite.redeemed?
raise Invite::ValidationFailed.new(
I18n.t(
"invite.not_found_template",
site_name: SiteSetting.title,
base_url: Discourse.base_url,
),
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
)
end
invite
end
def redeem_invitation(invite, sso, redeeming_user)
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
InviteRedeemer.new(
invite: invite,
username: sso.username,
name: sso.name,
ip_address: request.remote_ip,
session: session,
email: sso.email,
redeeming_user: redeeming_user,
FEATURE: Allow using invites when DiscourseConnect SSO is enabled (#12419) This PR allows invitations to be used when the DiscourseConnect SSO is enabled for a site (`enable_discourse_connect`) and local logins are disabled. Previously invites could not be accepted with SSO enabled simply because we did not have the code paths to handle that logic. The invitation methods that are supported include: * Inviting people to groups via email address * Inviting people to topics via email address * Using invitation links generated by the Invite Users UI in the /my/invited/pending route The flow works like this: 1. User visits an invite URL 2. The normal invitation validations (redemptions/expiry) happen at that point 3. We store the invite key in a secure session 4. The user clicks "Accept Invitation and Continue" (see below) 5. The user is redirected to /session/sso then to the SSO provider URL then back to /session/sso_login 6. We retrieve the invite based on the invite key in secure session. We revalidate the invitation. We show an error to the user if it is not valid. An additional check here for invites with an email specified is to check the SSO email matches the invite email 7. If the invite is OK we create the user via the normal SSO methods 8. We redeem the invite and activate the user. We clear the invite key in secure session. 9. If the invite had a topic we redirect the user there, otherwise we redirect to / Note that we decided for SSO-based invites the `must_approve_users` site setting is ignored, because the invite is a form of pre-approval, and because regular non-staff users cannot send out email invites or generally invite to the forum in this case. Also deletes some group invite checks as per https://github.com/discourse/discourse/pull/12353
2021-03-19 08:20:10 +08:00
).redeem
secure_session["invite-key"] = nil
# note - more specific errors are handled in the sso_login method
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
Rails.logger.warn("SSO invite redemption failed: #{e}")
raise Invite::RedemptionFailed
end
def domain_redirect_allowed?(hostname)
allowed_domains = SiteSetting.discourse_connect_allowed_redirect_domains
return false if allowed_domains.blank?
return true if allowed_domains.split("|").include?("*")
allowed_domains.split("|").include?(hostname)
end
def enqueue_password_reset_for_user(user)
RateLimiter.new(
nil,
"forgot-password-login-day-#{user.username}",
FORGOT_PASSWORD_EMAIL_LIMIT_PER_DAY,
1.day,
).performed!
email_token =
user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset])
Jobs.enqueue(
:critical_user_email,
type: "forgot_password",
user_id: user.id,
email_token: email_token.token,
)
end
2013-02-06 03:16:51 +08:00
end