discourse/spec/requests/omniauth_callbacks_controller_spec.rb

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

1169 lines
39 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
require "discourse_connect_base"
RSpec.describe Users::OmniauthCallbacksController do
fab!(:user)
before { OmniAuth.config.test_mode = true }
after do
2017-05-09 11:26:24 +08:00
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] = nil
Rails.application.env_config["omniauth.origin"] = nil
OmniAuth.config.test_mode = false
end
describe ".find_authenticator" do
it "fails if a provider is disabled" do
SiteSetting.enable_twitter_logins = false
expect do Users::OmniauthCallbacksController.find_authenticator("twitter") end.to raise_error(
Discourse::InvalidAccess,
)
end
it "fails for unknown" do
expect do
Users::OmniauthCallbacksController.find_authenticator("twitter1")
end.to raise_error(Discourse::InvalidAccess)
end
it "finds an authenticator when enabled" do
SiteSetting.enable_twitter_logins = true
expect(Users::OmniauthCallbacksController.find_authenticator("twitter")).not_to eq(nil)
end
context "with a plugin-contributed auth provider" do
let :provider do
provider = Auth::AuthProvider.new
provider.authenticator =
Class
.new(Auth::Authenticator) do
def name
"ubuntu"
end
def enabled?
SiteSetting.ubuntu_login_enabled
end
end
.new
provider
end
before { DiscoursePluginRegistry.register_auth_provider(provider) }
after { DiscoursePluginRegistry.reset! }
it "finds an authenticator when enabled" do
SiteSetting.stubs(:ubuntu_login_enabled).returns(true)
expect(Users::OmniauthCallbacksController.find_authenticator("ubuntu")).to be(
provider.authenticator,
)
end
it "fails if an authenticator is disabled" do
SiteSetting.stubs(:ubuntu_login_enabled).returns(false)
expect { Users::OmniauthCallbacksController.find_authenticator("ubuntu") }.to raise_error(
Discourse::InvalidAccess,
)
end
end
end
describe "Google Oauth2" do
before { SiteSetting.enable_google_oauth2_logins = true }
it "should display the failure message if needed" do
get "/auth/failure"
expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("login.omniauth_error.generic"))
end
describe "request" do
it "should error for non existant authenticators" do
post "/auth/fake_auth"
expect(response.status).to eq(404)
get "/auth/fake_auth"
expect(response.status).to eq(403)
end
it "should error for disabled authenticators" do
SiteSetting.enable_google_oauth2_logins = false
post "/auth/google_oauth2"
expect(response.status).to eq(404)
get "/auth/google_oauth2"
expect(response.status).to eq(403)
end
it "should handle common errors" do
OmniAuth::Strategies::GoogleOauth2
.any_instance
.stubs(:mock_request_call)
.raises(
OAuth::Unauthorized.new(
mock().tap do |m|
m.stubs(:code).returns(403)
m.stubs(:message).returns("Message")
end,
),
)
post "/auth/google_oauth2"
expect(response.status).to eq(302)
expect(response.location).to include("/auth/failure?message=request_error")
OmniAuth::Strategies::GoogleOauth2
.any_instance
.stubs(:mock_request_call)
.raises(JWT::InvalidIatError.new)
post "/auth/google_oauth2"
expect(response.status).to eq(302)
expect(response.location).to include("/auth/failure?message=invalid_iat")
end
it "should only start auth with a POST request" do
post "/auth/google_oauth2"
expect(response.status).to eq(302)
get "/auth/google_oauth2"
expect(response.status).to eq(200)
end
context "with CSRF protection enabled" do
before { ActionController::Base.allow_forgery_protection = true }
after { ActionController::Base.allow_forgery_protection = false }
it "should be CSRF protected" do
post "/auth/google_oauth2"
expect(response.status).to eq(302)
expect(response.location).to include("/auth/failure?message=csrf_detected")
post "/auth/google_oauth2", params: { authenticity_token: "faketoken" }
expect(response.status).to eq(302)
expect(response.location).to include("/auth/failure?message=csrf_detected")
get "/session/csrf.json"
token = response.parsed_body["csrf"]
post "/auth/google_oauth2", params: { authenticity_token: token }
expect(response.status).to eq(302)
end
it "should not be CSRF protected if it is the only auth method" do
get "/auth/google_oauth2"
expect(response.status).to eq(200)
SiteSetting.enable_local_logins = false
get "/auth/google_oauth2"
expect(response.status).to eq(302)
end
end
end
context "when in readonly mode" do
use_redis_snapshotting
it "should return a 503" do
Discourse.enable_readonly_mode
get "/auth/google_oauth2/callback"
expect(response.code).to eq("503")
end
end
context "when in staff writes only mode" do
use_redis_snapshotting
before { Discourse.enable_readonly_mode(Discourse::STAFF_WRITES_ONLY_MODE_KEY) }
it "returns a 503 for non-staff" do
mock_auth(user.email, user.username, user.name)
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(503)
logged_on_user = Discourse.current_user_provider.new(request.env).current_user
expect(logged_on_user).to eq(nil)
end
it "completes for staff" do
user.update!(admin: true)
mock_auth(user.email, user.username, user.name)
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
logged_on_user = Discourse.current_user_provider.new(request.env).current_user
expect(logged_on_user).not_to eq(nil)
end
end
context "without an `omniauth.auth` env" do
it "should return a 404" do
get "/auth/eviltrout/callback"
expect(response.code).to eq("404")
end
end
describe "when user not found" do
let(:email) { "somename@gmail.com" }
let(:username) { "somename" }
let(:name) { "Some Name" }
before { mock_auth(email, username, name) }
it "should return the right response" do
destination_url = "/somepath"
Rails.application.env_config["omniauth.origin"] = destination_url
events = DiscourseEvent.track_events { get "/auth/google_oauth2/callback.json" }
expect(events.any? { |e| e[:event_name] == :before_auth }).to eq(true)
expect(
events.any? do |e|
e[:event_name] === :after_auth && Auth::GoogleOAuth2Authenticator === e[:params][0] &&
!e[:params][1].failed?
end,
).to eq(true)
expect(response.status).to eq(302)
data = JSON.parse(cookies[:authentication_data])
expect(data["email"]).to eq(email)
expect(data["username"]).to eq(username)
expect(data["name"]).to eq(name)
expect(data["auth_provider"]).to eq("google_oauth2")
expect(data["email_valid"]).to eq(true)
expect(data["can_edit_username"]).to eq(true)
expect(data["destination_url"]).to eq(destination_url)
expect(read_secure_session["oauth"]).to eq("true")
end
it "should return the right response for staged users" do
Fabricate(:user, username: username, email: email, staged: true)
destination_url = "/somepath"
Rails.application.env_config["omniauth.origin"] = destination_url
events = DiscourseEvent.track_events { get "/auth/google_oauth2/callback.json" }
expect(events.any? { |e| e[:event_name] == :before_auth }).to eq(true)
expect(
events.any? do |e|
e[:event_name] === :after_auth && Auth::GoogleOAuth2Authenticator === e[:params][0] &&
!e[:params][1].failed?
end,
).to eq(true)
expect(response.status).to eq(302)
data = JSON.parse(cookies[:authentication_data])
expect(data["email"]).to eq(email)
expect(data["username"]).to eq(username)
expect(data["auth_provider"]).to eq("google_oauth2")
expect(data["email_valid"]).to eq(true)
expect(data["can_edit_username"]).to eq(true)
expect(data["name"]).to eq("Some Name")
expect(data["destination_url"]).to eq(destination_url)
end
it "should include destination url in response" do
destination_url = "/cookiepath"
cookies[:destination_url] = destination_url
get "/auth/google_oauth2/callback.json"
data = JSON.parse(cookies[:authentication_data])
expect(data["destination_url"]).to eq(destination_url)
end
it "should return an associate url when multiple login methods are enabled" do
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
data = JSON.parse(cookies[:authentication_data])
expect(data["associate_url"]).to start_with("/associate/")
SiteSetting.enable_local_logins = false
get "/auth/google_oauth2/callback.json"
data = JSON.parse(cookies[:authentication_data])
expect(data["associate_url"]).to eq(nil)
end
it "does not use email for username suggestions if disabled in settings" do
SiteSetting.use_email_for_username_and_name_suggestions = false
username = ""
name = ""
email = "billmailbox@test.com"
mock_auth(email, username, name)
get "/auth/google_oauth2/callback.json"
data = JSON.parse(cookies[:authentication_data])
expect(data["username"]).to eq("user1") # not "billmailbox" that can be extracted from email
end
it "uses email for username suggestions if enabled in settings" do
SiteSetting.use_email_for_username_and_name_suggestions = true
username = ""
name = ""
email = "billmailbox@test.com"
mock_auth(email, username, name)
get "/auth/google_oauth2/callback.json"
data = JSON.parse(cookies[:authentication_data])
expect(data["username"]).to eq("billmailbox")
end
it "stops using name for username suggestions if disabled in settings" do
SiteSetting.use_name_for_username_suggestions = false
username = ""
name = "John Smith"
email = "billmailbox@test.com"
mock_auth(email, username, name)
get "/auth/google_oauth2/callback.json"
data = JSON.parse(cookies[:authentication_data])
expect(data["username"]).to eq("user1")
end
describe "when site is invite_only" do
before { SiteSetting.invite_only = true }
it "should return the right response without any origin" do
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
data = JSON.parse(response.cookies["authentication_data"])
expect(data["requires_invite"]).to eq(true)
end
it "returns the right response for an invalid origin" do
Rails.application.env_config["omniauth.origin"] = "/invitesinvites"
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
end
it "should return the right response when origin is invites page" do
origin =
Rails.application.routes.url_helpers.invite_url(
Fabricate(:invite).invite_key,
host: Discourse.base_url,
)
Rails.application.env_config["omniauth.origin"] = origin
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
expect(response).to redirect_to(origin)
data = JSON.parse(response.cookies["authentication_data"])
expect(data["requires_invite"]).to eq(nil)
end
end
end
describe "when user has been verified" do
let(:uid) { 12_345 }
before { mock_auth(user.email, "Somenickname", "Some name", uid) }
it "should return the right response" do
expect(user.email_confirmed?).to eq(false)
events = DiscourseEvent.track_events { get "/auth/google_oauth2/callback.json" }
expect(events.map { |event| event[:event_name] }).to include(
:user_logged_in,
:user_first_logged_in,
)
expect(response.status).to eq(302)
data = JSON.parse(cookies[:authentication_data])
expect(data["authenticated"]).to eq(true)
expect(data["awaiting_activation"]).to eq(false)
expect(data["awaiting_approval"]).to eq(false)
expect(data["not_allowed_from_ip_address"]).to eq(false)
expect(data["admin_not_allowed_from_ip_address"]).to eq(false)
user.reload
expect(user.email_confirmed?).to eq(true)
end
it "should return the authenticated response with the correct path for subfolders" do
set_subfolder "/forum"
events = DiscourseEvent.track_events { get "/auth/google_oauth2/callback.json" }
expect(
response.headers["Set-Cookie"].match(%r{^authentication_data=.*; path=/forum}),
).not_to eq(nil)
expect(events.map { |event| event[:event_name] }).to include(
:user_logged_in,
:user_first_logged_in,
)
expect(response.status).to eq(302)
data = JSON.parse(response.cookies["authentication_data"])
expect(data["authenticated"]).to eq(true)
expect(data["awaiting_activation"]).to eq(false)
expect(data["awaiting_approval"]).to eq(false)
expect(data["not_allowed_from_ip_address"]).to eq(false)
expect(data["admin_not_allowed_from_ip_address"]).to eq(false)
user.reload
expect(user.email_confirmed?).to eq(true)
end
it "should confirm email even when the tokens are expired" do
user.email_tokens.update_all(confirmed: false, expired: true)
user.reload
expect(user.email_confirmed?).to eq(false)
events = DiscourseEvent.track_events { get "/auth/google_oauth2/callback.json" }
expect(events.map { |event| event[:event_name] }).to include(
:user_logged_in,
:user_first_logged_in,
)
expect(response.status).to eq(302)
user.reload
expect(user.email_confirmed?).to eq(true)
end
it "should treat a staged user the same as an unregistered user" do
user.update!(staged: true, registration_ip_address: nil)
user.reload
expect(user.staged).to eq(true)
expect(user.registration_ip_address).to eq(nil)
events = DiscourseEvent.track_events { get "/auth/google_oauth2/callback.json" }
expect(events.map { |event| event[:event_name] }).to include(:before_auth, :after_auth)
expect(response.status).to eq(302)
data = JSON.parse(cookies[:authentication_data])
expect(data["username"]).to eq("Somenickname")
user.reload
expect(user.staged).to eq(true)
expect(user.registration_ip_address).to eq(nil)
# Now register
UsersController.any_instance.stubs(:honeypot_value).returns(nil)
UsersController.any_instance.stubs(:challenge_value).returns(nil)
post "/u.json",
params: {
name: "My new name",
username: "mynewusername",
email: user.email,
}
expect(response.status).to eq(200)
user.reload
expect(user.staged).to eq(false)
expect(user.registration_ip_address).to be_present
expect(user.name).to eq("My new name")
end
it "should activate user with matching email" do
user.update!(password: "securepassword", active: false, registration_ip_address: "1.1.1.1")
user.reload
expect(user.active).to eq(false)
expect(user.confirm_password?("securepassword")).to eq(true)
get "/auth/google_oauth2/callback.json"
user.reload
expect(user.active).to eq(true)
# Delete the password, it may have been set by someone else
expect(user.confirm_password?("securepassword")).to eq(false)
end
it "should work if the user has no email_tokens, and an invite" do
# Confirming existing email_tokens has a side effect of redeeming invites.
# Pretend we don't have any email_tokens
user.email_tokens.destroy_all
invite = Fabricate(:invite, invited_by: Fabricate(:admin))
invite.update_column(:email, user.email) # (avoid validation)
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
expect(invite.reload.invalidated_at).not_to eq(nil)
end
it "should update name/username/email when SiteSetting.auth_overrides_* are enabled" do
SiteSetting.email_editable = false
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.auth_overrides_email = true
SiteSetting.auth_overrides_name = true
SiteSetting.auth_overrides_username = true
UserAssociatedAccount.create!(
provider_name: "google_oauth2",
user_id: user.id,
provider_uid: uid,
)
old_email = user.email
user.update!(name: "somename", username: "somusername", email: "email@example.com")
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
user.reload
expect(user.email).to eq(old_email)
expect(user.username).to eq("Somenickname")
expect(user.name).to eq("Some name")
end
it "should preserve username when several users login with the same username" do
SiteSetting.auth_overrides_username = true
# if several users have username "bill" on the external site,
# they will have usernames bill, bill1, bill2 etc in Discourse:
Fabricate(:user, username: "bill")
Fabricate(:user, username: "bill1")
Fabricate(:user, username: "bill2")
Fabricate(:user, username: "bill4")
# the number should be preserved during subsequent logins
# bill3 should remain bill3
user.update!(username: "bill3")
uid = "12345"
UserAssociatedAccount.create!(
provider_name: "google_oauth2",
user_id: user.id,
provider_uid: uid,
)
mock_auth(user.email, "bill", uid)
get "/auth/google_oauth2/callback.json"
user.reload
expect(user.username).to eq("bill3")
end
it "will not update email if not verified" do
SiteSetting.email_editable = false
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.auth_overrides_email = true
OmniAuth.config.mock_auth[:google_oauth2][:extra][:raw_info][:email_verified] = false
UserAssociatedAccount.create!(
provider_name: "google_oauth2",
user_id: user.id,
provider_uid: "123545",
)
old_email = user.email
user.update!(email: "email@example.com")
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
user.reload
expect(user.email).to eq("email@example.com")
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
it "shows error when auth_overrides_email causes a validation error" do
SiteSetting.email_editable = false
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.auth_overrides_email = true
UserAssociatedAccount.create!(
provider_name: "google_oauth2",
user_id: user.id,
provider_uid: uid,
)
google_email = user.email
user.update!(email: "anotheremail@example.com")
Fabricate(:user, email: google_email) # Another user has the google account email
get "/auth/google_oauth2/callback"
expect(response.status).to eq(200)
expect(response.body).to include(I18n.t("errors.messages.taken"))
expect(session[:current_user_id]).to eq(nil)
user.reload
expect(user.email).to eq("anotheremail@example.com")
end
2020-01-15 18:27:12 +08:00
context "when user has TOTP enabled" do
before { user.create_totp(enabled: true) }
it "should return the right response" do
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
data = JSON.parse(cookies[:authentication_data])
expect(data["email"]).to eq(user.email)
expect(data["omniauth_disallow_totp"]).to eq(true)
user.update!(email: "different@user.email")
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
expect(JSON.parse(cookies[:authentication_data])["email"]).to eq(user.email)
end
end
context "when user has TOTP enabled but enforce_second_factor_on_external_auth is false" do
before { user.create_totp(enabled: true) }
it "should return the right response" do
SiteSetting.enforce_second_factor_on_external_auth = false
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
data = JSON.parse(cookies[:authentication_data])
expect(data["authenticated"]).to eq(true)
end
end
2020-01-15 18:27:12 +08:00
context "when user has security key enabled" do
before { Fabricate(:user_security_key_with_random_credential, user: user) }
it "should return the right response" do
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
data = JSON.parse(cookies[:authentication_data])
expect(data["email"]).to eq(user.email)
expect(data["omniauth_disallow_totp"]).to eq(true)
user.update!(email: "different@user.email")
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
expect(JSON.parse(cookies[:authentication_data])["email"]).to eq(user.email)
end
end
context "when sso_payload cookie exist" do
before do
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_provider = true
SiteSetting.discourse_connect_secret = "topsecret"
@sso = DiscourseConnectBase.new
@sso.nonce = "mynonce"
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.sso_secret = SiteSetting.discourse_connect_secret
@sso.return_sso_url = "http://somewhere.over.rainbow/sso"
cookies[:sso_payload] = @sso.payload
provider_uid = 12_345
UserAssociatedAccount.create!(
provider_name: "google_oauth2",
provider_uid: provider_uid,
user: user,
)
mock_auth(user.email, nil, nil, provider_uid)
end
it "should return the right response" do
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
data = JSON.parse(cookies[:authentication_data])
expect(data["destination_url"]).to match(%r{/session/sso_provider\?sso\=.*\&sig\=.*})
end
end
context "when user has not verified his email" do
before do
provider_uid = "12345"
UserAssociatedAccount.create!(
provider_name: "google_oauth2",
provider_uid: provider_uid,
user: user,
)
user.update!(active: false)
another_email = "another_email@test.com"
mock_auth(another_email, nil, nil, provider_uid)
end
it "should return the right response" do
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
data = JSON.parse(cookies[:authentication_data])
expect(user.reload.active).to eq(false)
expect(data["authenticated"]).to eq(false)
expect(data["awaiting_activation"]).to eq(true)
end
end
it "doesn't attempt redirect to external origin" do
post "/auth/google_oauth2?origin=https://example.com/external"
get "/auth/google_oauth2/callback"
expect(response.status).to eq 302
expect(response.location).to eq "http://test.localhost/"
cookie_data = JSON.parse(response.cookies["authentication_data"])
expect(cookie_data["destination_url"]).to eq("/")
end
it "redirects to internal origin" do
post "/auth/google_oauth2?origin=http://test.localhost/t/123"
get "/auth/google_oauth2/callback"
expect(response.status).to eq 302
expect(response.location).to eq "http://test.localhost/t/123"
cookie_data = JSON.parse(response.cookies["authentication_data"])
expect(cookie_data["destination_url"]).to eq("/t/123")
end
it "redirects to internal origin on subfolder" do
set_subfolder "/subpath"
post "/auth/google_oauth2?origin=http://test.localhost/subpath/t/123"
get "/auth/google_oauth2/callback"
expect(response.status).to eq 302
expect(response.location).to eq "http://test.localhost/subpath/t/123"
cookie_data = JSON.parse(response.cookies["authentication_data"])
expect(cookie_data["destination_url"]).to eq("/subpath/t/123")
end
it "never redirects to /auth/ origin" do
post "/auth/google_oauth2?origin=http://test.localhost/auth/google_oauth2"
get "/auth/google_oauth2/callback"
expect(response.status).to eq 302
expect(response.location).to eq "http://test.localhost/"
cookie_data = JSON.parse(response.cookies["authentication_data"])
expect(cookie_data["destination_url"]).to eq("/")
end
it "never redirects to /auth/ origin on subfolder" do
set_subfolder "/subpath"
post "/auth/google_oauth2?origin=http://test.localhost/subpath/auth/google_oauth2"
get "/auth/google_oauth2/callback"
expect(response.status).to eq 302
expect(response.location).to eq "http://test.localhost/subpath"
cookie_data = JSON.parse(response.cookies["authentication_data"])
expect(cookie_data["destination_url"]).to eq("/subpath")
end
it "redirects to relative origin" do
post "/auth/google_oauth2?origin=/t/123"
get "/auth/google_oauth2/callback"
expect(response.status).to eq 302
expect(response.location).to eq "http://test.localhost/t/123"
cookie_data = JSON.parse(response.cookies["authentication_data"])
expect(cookie_data["destination_url"]).to eq("/t/123")
end
it "redirects with query" do
post "/auth/google_oauth2?origin=/t/123?foo=bar"
get "/auth/google_oauth2/callback"
expect(response.status).to eq 302
expect(response.location).to eq "http://test.localhost/t/123?foo=bar"
cookie_data = JSON.parse(response.cookies["authentication_data"])
expect(cookie_data["destination_url"]).to eq("/t/123?foo=bar")
end
it "removes authentication_data cookie on logout" do
post "/auth/google_oauth2?origin=https://example.com/external"
get "/auth/google_oauth2/callback"
provider = log_in_user(Fabricate(:user))
expect(cookies["authentication_data"]).to be
log_out_user(provider)
expect(cookies["authentication_data"]).to be_nil
end
it "removes disallowed characters from username" do
username = "strange_name*&^"
fixed_username = "strange_name"
mock_auth("user.with.strange.username@gmail.com", username)
get "/auth/google_oauth2/callback.json"
data = JSON.parse(cookies[:authentication_data])
expect(data["username"]).to eq(fixed_username)
end
context "when groups are enabled" do
let(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
let(:group1) { { id: "12345", name: "group1" } }
let(:group2) { { id: "67890", name: "group2" } }
let(:uid) { "12345" }
let(:token) { "1245678" }
let(:domain) { "mydomain.com" }
def mock_omniauth_for_groups(groups)
mock_auth = OmniAuth.config.mock_auth[:google_oauth2]
OmniAuth.config.mock_auth[:google_oauth2] = mock_auth
Rails.application.env_config["omniauth.auth"] = mock_auth
SiteSetting.google_oauth2_hd_groups_service_account_admin_email = "admin@example.com"
SiteSetting.google_oauth2_hd_groups_service_account_json = {
"private_key" => private_key.to_s,
:"client_email" => "discourse-group-sync@example.iam.gserviceaccount.com",
}.to_json
SiteSetting.google_oauth2_hd_groups = true
stub_request(:post, "https://oauth2.googleapis.com/token").to_return do |request|
jwt = Rack::Utils.parse_query(request.body)["assertion"]
decoded_token = JWT.decode(jwt, private_key.public_key, true, { algorithm: "RS256" })
{
status: 200,
body: { "access_token" => token, "type" => "bearer" }.to_json,
headers: {
"Content-Type" => "application/json",
},
}
end
stub_request(
:get,
"https://admin.googleapis.com/admin/directory/v1/groups?userKey=#{mock_auth.uid}",
)
.with(headers: { "Authorization" => "Bearer #{token}" })
.to_return do
{
status: 200,
body: { groups: groups }.to_json,
headers: {
"Content-Type" => "application/json",
},
}
end
end
before do
SiteSetting.google_oauth2_hd = domain
SiteSetting.google_oauth2_hd_groups_service_account_admin_email = "test@example.com"
SiteSetting.google_oauth2_hd_groups_service_account_json = "{}"
SiteSetting.google_oauth2_hd_groups = true
end
it "updates associated groups" do
mock_omniauth_for_groups([group1, group2])
get "/auth/google_oauth2/callback.json", params: { code: "abcde", hd: domain }
expect(response.status).to eq(302)
associated_groups = AssociatedGroup.where(provider_name: "google_oauth2")
expect(associated_groups.length).to eq(2)
expect(associated_groups.exists?(name: group1[:name])).to eq(true)
expect(associated_groups.exists?(name: group2[:name])).to eq(true)
user_associated_groups = UserAssociatedGroup.where(user_id: user.id)
expect(user_associated_groups.length).to eq(2)
expect(
user_associated_groups.exists?(associated_group_id: associated_groups.first.id),
).to eq(true)
expect(
user_associated_groups.exists?(associated_group_id: associated_groups.second.id),
).to eq(true)
mock_omniauth_for_groups([group1])
get "/auth/google_oauth2/callback.json", params: { code: "abcde", hd: domain }
expect(response.status).to eq(302)
user_associated_groups = UserAssociatedGroup.where(user_id: user.id)
expect(user_associated_groups.length).to eq(1)
expect(
user_associated_groups.exists?(associated_group_id: associated_groups.first.id),
).to eq(true)
expect(
user_associated_groups.exists?(associated_group_id: associated_groups.second.id),
).to eq(false)
mock_omniauth_for_groups([])
get "/auth/google_oauth2/callback.json", params: { code: "abcde", hd: domain }
expect(response.status).to eq(302)
user_associated_groups = UserAssociatedGroup.where(user_id: user.id)
expect(user_associated_groups.length).to eq(0)
expect(
user_associated_groups.exists?(associated_group_id: associated_groups.first.id),
).to eq(false)
expect(
user_associated_groups.exists?(associated_group_id: associated_groups.second.id),
).to eq(false)
end
it "handles failure to retrieve groups" do
mock_omniauth_for_groups([])
get "/auth/google_oauth2/callback.json", params: { code: "abcde", hd: domain }
expect(response.status).to eq(302)
associated_groups = AssociatedGroup.where(provider_name: "google_oauth2")
expect(associated_groups.exists?).to eq(false)
end
end
end
context "when attempting reconnect" do
fab!(:user2) { Fabricate(:user) }
let(:user1_provider_id) { "12345" }
let(:user2_provider_id) { "123456" }
before do
UserAssociatedAccount.create!(
provider_name: "google_oauth2",
provider_uid: user1_provider_id,
user: user,
)
UserAssociatedAccount.create!(
provider_name: "google_oauth2",
provider_uid: user2_provider_id,
user: user2,
)
mock_auth("someother_email@test.com", nil, nil, user1_provider_id)
end
it "should not reconnect normally" do
# Log in normally
post "/auth/google_oauth2"
expect(response.status).to eq(302)
expect(session[:auth_reconnect]).to eq(false)
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
expect(session[:current_user_id]).to eq(user.id)
# Log into another user
OmniAuth.config.mock_auth[:google_oauth2].uid = user2_provider_id
post "/auth/google_oauth2"
expect(response.status).to eq(302)
expect(session[:auth_reconnect]).to eq(false)
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
expect(session[:current_user_id]).to eq(user2.id)
expect(UserAssociatedAccount.count).to eq(2)
end
it "should redirect to associate URL if parameter supplied" do
# Log in normally
post "/auth/google_oauth2?reconnect=true"
expect(response.status).to eq(302)
expect(session[:auth_reconnect]).to eq(true)
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
expect(session[:current_user_id]).to eq(user.id)
# Clear cookie after login
expect(session[:auth_reconnect]).to eq(nil)
# Disconnect
UserAssociatedAccount.find_by(user_id: user.id).destroy
# Reconnect flow:
post "/auth/google_oauth2?reconnect=true"
expect(response.status).to eq(302)
expect(session[:auth_reconnect]).to eq(true)
OmniAuth.config.mock_auth[:google_oauth2].uid = user2_provider_id
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
expect(response.redirect_url).to start_with("http://test.localhost/associate/")
expect(session[:current_user_id]).to eq(user.id)
2019-07-24 18:45:36 +08:00
expect(UserAssociatedAccount.count).to eq(1) # Reconnect has not yet happened
end
it 'stores and redirects to \'origin\' parameter' do
# Log in normally
post "/auth/google_oauth2?origin=http://test.localhost/atesturl"
expect(response.status).to eq(302)
expect(session[:destination_url]).to eq("http://test.localhost/atesturl")
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
expect(response.redirect_url).to eq("http://test.localhost/atesturl")
end
end
context "after changing email" do
def login(identity)
mock_auth(identity[:email], nil, nil, "123545#{identity[:username]}")
get "/auth/google_oauth2/callback.json"
expect(response.status).to eq(302)
JSON.parse(cookies[:authentication_data])
end
it "activates the correct email" do
old_email = "old@email.com"
old_identity = { name: "Bob", username: "bob", email: old_email }
user = Fabricate(:user, email: old_email)
new_email = "new@email.com"
new_identity = { name: "Bob", username: "boguslaw", email: new_email }
updater = EmailUpdater.new(guardian: user.guardian, user: user)
updater.change_to(new_email)
user.reload
expect(user.email).to eq(old_email)
response = login(old_identity)
expect(response["authenticated"]).to eq(true)
user.reload
expect(user.email).to eq(old_email)
delete "/session/#{user.username}" # log out
response = login(new_identity)
expect(response["authenticated"]).to eq(nil)
expect(response["email"]).to eq(new_email)
end
end
context "when user is staged" do
fab!(:staged_user) do
Fabricate(:user, username: "staged_user", email: "staged.user@gmail.com", staged: true)
end
it "should use username of the staged user if username is not present in payload" do
mock_auth(staged_user.email, nil)
get "/auth/google_oauth2/callback.json"
data = JSON.parse(cookies[:authentication_data])
expect(data["username"]).to eq(staged_user.username)
end
it "should use username of the staged user if username in payload is the same" do
# it's important to check this, because we had regressions
# when usernames were changed to the same username with "1" added at the end
mock_auth(staged_user.email, staged_user.username)
get "/auth/google_oauth2/callback.json"
data = JSON.parse(cookies[:authentication_data])
expect(data["username"]).to eq(staged_user.username)
end
it "should override username of the staged user if payload contains a new username" do
new_username = "new_username"
mock_auth(staged_user.email, new_username)
get "/auth/google_oauth2/callback.json"
data = JSON.parse(cookies[:authentication_data])
expect(data["username"]).to eq(new_username)
end
end
def mock_auth(email, nickname = nil, name = nil, uid = "12345")
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
provider: "google_oauth2",
uid: uid,
info: OmniAuth::AuthHash::InfoHash.new(email: email, nickname: nickname, name: name),
extra: {
raw_info: OmniAuth::AuthHash.new(email_verified: true),
},
)
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2]
end
end
describe "with a fake provider" do
class FakeAuthenticator < Auth::ManagedAuthenticator
class Strategy
include OmniAuth::Strategy
def other_phase
[418, {}, ["I am a teapot"]]
end
end
def name
"fake"
end
def enabled?
false
end
def register_middleware(omniauth)
omniauth.provider Strategy, name: :fake
end
end
let(:fake_auth_provider) { Auth::AuthProvider.new(authenticator: FakeAuthenticator.new) }
before do
DiscoursePluginRegistry.register_auth_provider(fake_auth_provider)
OmniAuth.config.test_mode = false
end
after { DiscoursePluginRegistry.auth_providers.delete(fake_auth_provider) }
it "does not run 'other_phase' for disabled auth methods" do
get "/auth/fake/blah"
expect(response.status).to eq(404)
end
it "does not leak 'other_phase' for disabled auth methods onto other methods" do
get "/auth/twitter/blah"
expect(response.status).to eq(404)
end
it "runs 'other_phase' for enabled auth methods" do
FakeAuthenticator.any_instance.stubs(:enabled?).returns(true)
get "/auth/fake/blah"
expect(response.status).to eq(418)
end
end
end