mirror of
https://github.com/discourse/discourse.git
synced 2024-11-30 09:03:45 +08:00
50bafd48cd
Why was the problem? ActiveRecord's query cache for the connection pool wasn't disabled after the `with a fake provider runs 'other_phase' for enabled auth methods` test in `omniauth_callbacks_controller_spec.rb` was run. This was because the Rack response body in `FakeAuthenticator::Strategy::other_phase` did not adhere to the expected Rack body format which is "typically an Array of String instances". Because this expectation was broken, it cascaded the problem down where it resulted in the ActiveRecord's query cache for the connection pool not being disabled as it normally should when the response body is closed. When the query cache is left enabled, common assertions pattern in RSpec like `expect { something }.to change { Group.count }` will fail since the query cache is enabled and the call first call to `Group.count` will cache the result to be reused later on. To see the bug in action, one can run the following command: `bundle exec rspec --seed 44747 spec/requests/omniauth_callbacks_controller_spec.rb:1150 spec/models/group_spec.rb:283`
1154 lines
39 KiB
Ruby
1154 lines
39 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "discourse_connect_base"
|
|
|
|
RSpec.describe Users::OmniauthCallbacksController do
|
|
fab!(:user)
|
|
|
|
before { OmniAuth.config.test_mode = true }
|
|
|
|
after do
|
|
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.enabled_setting = "ubuntu_login_enabled"
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
|
|
it "shows error when auth_overrides_email causes a validation error" do
|
|
SiteSetting.email_editable = false
|
|
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
|
|
|
|
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 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
|
|
SiteSetting.enable_discourse_connect_provider = true
|
|
SiteSetting.discourse_connect_secret = "topsecret"
|
|
|
|
@sso = DiscourseConnectBase.new
|
|
@sso.nonce = "mynonce"
|
|
@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)
|
|
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
|