Login with Google (#15986)

This commit is contained in:
Josh Puetz 2022-01-13 10:25:52 -06:00 committed by GitHub
parent 681ab3aac2
commit 629c7114da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 250 additions and 13 deletions

View file

@ -63,6 +63,7 @@ gem "omniauth", "~> 2.0" # A generalized Rack framework for multiple-provider au
gem "omniauth-apple", "~> 1.0" # OmniAuth strategy for Sign In with Apple
gem "omniauth-facebook", "~> 9.0" # OmniAuth strategy for Facebook
gem "omniauth-github", "~> 2.0" # OmniAuth strategy for GitHub
gem "omniauth-google-oauth2", "~> 1.0"
gem "omniauth-rails_csrf_protection", "~> 1.0" # Provides CSRF protection on OmniAuth request endpoint on Rails application.
gem "omniauth-twitter", "~> 1.4" # OmniAuth strategy for Twitter
gem "parallel", "~> 1.21" # Run any kind of code in parallel processes

View file

@ -523,6 +523,11 @@ GEM
omniauth-github (2.0.0)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.7.1)
omniauth-google-oauth2 (1.0.0)
jwt (>= 2.0)
oauth2 (~> 1.1)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.7.1)
omniauth-oauth (1.2.0)
oauth
omniauth (>= 1.0, < 3)
@ -966,6 +971,7 @@ DEPENDENCIES
omniauth-apple (~> 1.0)
omniauth-facebook (~> 9.0)
omniauth-github (~> 2.0)
omniauth-google-oauth2 (~> 1.0)
omniauth-rails_csrf_protection (~> 1.0)
omniauth-twitter (~> 1.4)
parallel (~> 1.21)

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 640 640">
<path d="M326.331 274.255v109.761h181.49c-7.37 47.115-54.886 138.002-181.49 138.002-109.242 0-198.369-90.485-198.369-202.006 0-111.509 89.127-201.995 198.369-201.995 62.127 0 103.761 26.516 127.525 49.359l86.883-83.635C484.99 31.512 412.741-.012 326.378-.012 149.494-.012 6.366 143.116 6.366 320c0 176.884 143.128 320.012 320.012 320.012 184.644 0 307.256-129.876 307.256-312.653 0-21-2.244-36.993-5.008-52.997l-302.248-.13-.047.024z"/>
</svg>

After

Width:  |  Height:  |  Size: 681 B

View file

@ -24,6 +24,10 @@
--brand-email-bg: #24292e;
--brand-email-color: #fff;
--brand-email-bg-hover: #000;
--brand-google_oauth2-bg: #1da1f2;
--brand-google_oauth2-color: #fff;
--brand-google_oauth2-bg-hover: #0096f2;
}
// Basic styling
@ -325,6 +329,17 @@ label[class*='crayons-btn']:focus-within // label for input[type="file"] made to
--color-inverted-hover: var(--brand-email-color);
}
.crayons-btn--brand-google_oauth2 {
--bg: var(--brand-google_oauth2-bg);
--bg-hover: var(--brand-google_oauth2-bg-hover);
--color: var(--brand-google_oauth2-color);
--color-hover: var(--brand-google_oauth2-color);
--bg-inverted: var(--brand-google_oauth2-bg);
--bg-inverted-hover: var(--brand-google_oauth2-bg-hover);
--color-inverted: var(--brand-google_oauth2-color);
--color-inverted-hover: var(--brand-google_oauth2-color);
}
// Icon alone
.crayons-btn--icon,
.crayons-btn--icon-rounded {

View file

@ -4,9 +4,7 @@ module AuthenticationHelper
end
def authentication_available_providers
Authentication::Providers.available.map do |provider_name|
Authentication::Providers.const_get(provider_name.to_s.titleize)
end
Authentication::Providers.available_providers
end
def authentication_enabled_providers

View file

@ -59,6 +59,14 @@ module Constants
description: "The \"Client Secret\" portion of the GitHub Oauth Apps portal",
placeholder: ""
},
google_oauth2_key: {
description: "The \"Client ID\" portion of the OAuth 2.0 page on the Google Cloud Platform portal",
placeholder: ""
},
google_oauth2_secret: {
description: "The \"Client Secret\" portion of the OAuth 2.0 page on the Google Cloud Platform portal",
placeholder: ""
},
invite_only_mode: {
description: "Only users invited by email can join this community.",
placeholder: ""

View file

@ -21,6 +21,8 @@ module Settings
setting :forem_secret, type: :string
setting :github_key, type: :string, default: ApplicationConfig["GITHUB_KEY"]
setting :github_secret, type: :string, default: ApplicationConfig["GITHUB_SECRET"]
setting :google_oauth2_key, type: :string
setting :google_oauth2_secret, type: :string
setting :invite_only_mode, type: :boolean, default: false
setting :providers, type: :array, default: %w[]
setting :require_captcha_for_email_password_registration, type: :boolean, default: false

View file

@ -11,7 +11,7 @@ module Authentication
module Providers
# Retrieves a provider that is both available and enabled
def self.get!(provider_name)
name = provider_name.to_s.titleize
name = provider_name.to_s.camelize
unless available?(provider_name)
raise(
@ -32,12 +32,16 @@ module Authentication
def self.available
Authentication::Providers::Provider.subclasses.map do |subclass|
subclass.name.demodulize.downcase.to_sym
subclass.name.demodulize.underscore.downcase.to_sym
end.sort
end
def self.available_providers
Authentication::Providers::Provider.subclasses.sort_by(&:name)
end
def self.available?(provider_name)
Authentication::Providers.const_defined?(provider_name.to_s.titleize)
Authentication::Providers.const_defined?(provider_name.to_s.camelize)
end
# Returns enabled providers

View file

@ -0,0 +1,55 @@
module Authentication
module Providers
# Google OAuth 2.0 authentication provider, uses omniauth-google-oauth2 as backend
class GoogleOauth2 < Provider
OFFICIAL_NAME = "Google".freeze
SETTINGS_URL = "https://console.cloud.google.com/apis/credentials".freeze
def new_user_data
{
name: info.name,
email: info.email || "",
remote_profile_image_url: Users::SafeRemoteProfileImageUrl.call(@info.image),
google_oauth2_username: user_nickname
}
end
def existing_user_data
{
google_oauth2_username: info.name
}
end
# We're overriding this method because Google doesn't have a concept nickname or username.
# Instead: we'll construct one based on the user's name with some randomization thrown in based
# on uid, which is guaranteed to be present and unique from Google.
def user_nickname
[
info.name.sub(" ", "_"),
Digest::SHA512.hexdigest(auth_payload.uid),
].join("_")[0...25]
end
def self.official_name
OFFICIAL_NAME
end
def self.settings_url
SETTINGS_URL
end
def self.sign_in_path(**kwargs)
::Authentication::Paths.sign_in_path(
"google_oauth2",
**kwargs,
)
end
protected
def cleanup_payload(auth_payload)
auth_payload
end
end
end
end

View file

@ -34,7 +34,7 @@ module Authentication
end
def self.provider_name
name.demodulize.downcase.to_sym
name.demodulize.underscore.to_sym
end
def self.user_username_field

View file

@ -9,6 +9,7 @@ module Metrics
welcome_notification_customize_feed
welcome_notification_twitter_connect
welcome_notification_github_connect
welcome_notification_google_connect
welcome_notification_customize_experience
welcome_notification_discuss_and_ask
welcome_notification_download_app

View file

@ -18,6 +18,11 @@ GITHUB_OMNIAUTH_SETUP = lambda do |env|
env["omniauth.strategy"].options[:scope] = "user:email"
end
GOOGLE_OAUTH2_OMNIAUTH_SETUP = lambda do |env|
env["omniauth.strategy"].options[:client_id] = Settings::Authentication.google_oauth2_key
env["omniauth.strategy"].options[:client_secret] = Settings::Authentication.google_oauth2_secret
end
FACEBOOK_OMNIAUTH_SETUP = lambda do |env|
env["omniauth.strategy"].options[:client_id] = Settings::Authentication.facebook_key
env["omniauth.strategy"].options[:client_secret] = Settings::Authentication.facebook_secret
@ -321,6 +326,7 @@ Devise.setup do |config|
# Fun fact, unless Twitter is last, it doesn't work for some reason.
config.omniauth :facebook, setup: FACEBOOK_OMNIAUTH_SETUP
config.omniauth :github, setup: GITHUB_OMNIAUTH_SETUP
config.omniauth :google_oauth2, setup: GOOGLE_OAUTH2_OMNIAUTH_SETUP
config.omniauth :apple, setup: APPLE_OMNIAUTH_SETUP
config.omniauth :forem, setup: FOREM_OMNIAUTH_SETUP, strategy_class: OmniAuth::Strategies::Forem
config.omniauth :twitter, setup: TWITTER_OMNIAUTH_SETUP

View file

@ -0,0 +1,6 @@
class AddGoogleOAuthFields < ActiveRecord::Migration[6.1]
def change
add_column :users, :google_oauth2_username, :string
add_column :users, :google_oauth2_created_at, :datetime
end
end

View file

@ -0,0 +1,7 @@
class AddGoogleOAuthFieldsIndex < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
add_index :users, :google_oauth2_username, algorithm: :concurrently
end
end

View file

@ -1209,6 +1209,8 @@ ActiveRecord::Schema.define(version: 2021_12_22_040359) do
t.string "forem_username"
t.datetime "github_repos_updated_at", default: "2017-01-01 05:00:00"
t.string "github_username"
t.datetime "google_oauth2_created_at"
t.string "google_oauth2_username"
t.datetime "invitation_accepted_at"
t.datetime "invitation_created_at"
t.integer "invitation_limit"
@ -1270,6 +1272,7 @@ ActiveRecord::Schema.define(version: 2021_12_22_040359) do
t.index ["facebook_username"], name: "index_users_on_facebook_username"
t.index ["feed_fetched_at"], name: "index_users_on_feed_fetched_at"
t.index ["github_username"], name: "index_users_on_github_username", unique: true
t.index ["google_oauth2_username"], name: "index_users_on_google_oauth2_username"
t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true
t.index ["invitations_count"], name: "index_users_on_invitations_count"
t.index ["invited_by_id"], name: "index_users_on_invited_by_id"

View file

@ -369,6 +369,8 @@ seeder.create_if_none(Broadcast) do
"Consider <a href='/settings'>connecting it</a>.",
github_connect: "You're on a roll! 🎉 Do you have a GitHub account? " \
"Consider <a href='/settings'>connecting it</a> so you can pin any of your repos to your profile.",
google_oauth2_connect: "You're on a roll! 🎉 Do you have a Google account? " \
"Consider <a href='/settings'>connecting it</a>.",
apple_connect: "You're on a roll! 🎉 Do you have an Apple account? " \
"Consider <a href='/settings'>connecting it</a>.",
customize_feed:

View file

@ -0,0 +1,17 @@
module DataUpdateScripts
class InsertGoogleOuauth2ConnectBroadcastMessage
def run
return if Broadcast.find_by(title: "Welcome Notification: google_oauth2_connect")
message = "You're on a roll! 🎉 Do you have a Google account? " \
"Consider <a href='/settings'>connecting it</a>."
Broadcast.create!(
title: "Welcome Notification: google_oauth2_connect",
processed_html: message,
type_of: "Welcome",
active: true,
)
end
end
end

View file

@ -39,6 +39,15 @@ FactoryBot.define do
end
end
factory :google_oauth2_connect_broadcast do
title { "Welcome Notification: google_oauth2_connect" }
type_of { "Welcome" }
processed_html do
"You're on a roll! 🎉 Do you have a Google account? Consider " \
"<a href='/settings'>connecting it</a>."
end
end
factory :facebook_connect_broadcast do
title { "Welcome Notification: facebook_connect" }
type_of { "Welcome" }

View file

@ -34,7 +34,7 @@ RSpec.describe AuthenticationHelper, type: :helper do
allow(Authentication::Providers).to receive(:enabled).and_return(providers)
allow(user).to receive(:identities).and_return(user.identities.where(provider: providers))
expect(helper.signed_up_with(user)).to match(/GitHub and Twitter/)
expect(helper.signed_up_with(user)).to match(/Google and Twitter/)
expect(helper.signed_up_with(user)).to match(/use any of those/)
end

View file

@ -0,0 +1,19 @@
require "rails_helper"
require Rails.root.join(
"lib/data_update_scripts/20220107215659_insert_google_ouauth2_connect_broadcast_message.rb",
)
describe DataUpdateScripts::InsertGoogleOuauth2ConnectBroadcastMessage do
it "works without a Broadcast" do
expect do
described_class.new.run
end.to change(Broadcast, :count).by(1)
end
it "works when a Broadcast already exists" do
create(:google_oauth2_connect_broadcast)
expect do
described_class.new.run
end.not_to change(Broadcast, :count)
end
end

View file

@ -453,7 +453,7 @@ RSpec.describe User, type: :model do
case provider_name
when :apple
expect(new_user.username).to match(/valid_username_\w+/)
when :facebook
when :facebook, :google_oauth2
expect(new_user.username).to match(/fname_lname_\S*\z/)
else
expect(new_user.username).to eq("valid_username")
@ -472,7 +472,7 @@ RSpec.describe User, type: :model do
case provider_name
when :apple
expect(new_user.username).to match(/invalidusername_\w+/)
when :facebook
when :facebook, :google_oauth2
expect(new_user.username).to match(/fname_lname_\S*\z/)
else
expect(new_user.username).to eq("invalidusername")

View file

@ -24,15 +24,35 @@ RSpec.describe Authentication::Providers, type: :service do
)
expect(is_subclass_of).to be(true)
end
it "handles spaces in provider names" do
allow(Settings::Authentication).to receive(:providers).and_return(%w[google_oauth2])
expect do
described_class.get!(:google_oauth2)
end.not_to raise_error
end
end
describe ".available" do
it "lists the available providers" do
available_providers = %i[apple facebook forem github twitter]
available_providers = %i[apple facebook forem github google_oauth2 twitter]
expect(described_class.available).to eq(available_providers)
end
end
describe ".availble_providers" do
it "lists the available providers" do
available_providers = [Authentication::Providers::Forem,
Authentication::Providers::Facebook,
Authentication::Providers::Apple,
Authentication::Providers::Twitter,
Authentication::Providers::GoogleOauth2,
Authentication::Providers::Github].sort_by(&:name)
expect(described_class.available_providers).to eq(available_providers)
end
end
describe ".enabled" do
context "when one of the available providers is disabled" do
it "only lists those that remain enabled" do

View file

@ -11,6 +11,7 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
let!(:facebook_connect_broadcast) { create(:facebook_connect_broadcast) }
let!(:apple_connect_broadcast) { create(:apple_connect_broadcast) }
let!(:forem_connect_broadcast) { create(:forem_connect_broadcast) }
let!(:google_oauth2_connect_broadcast) { create(:google_oauth2_connect_broadcast) }
let!(:customize_feed_broadcast) { create(:customize_feed_broadcast) }
let!(:discuss_and_ask_broadcast) { create(:discuss_and_ask_broadcast) }
let!(:customize_ux_broadcast) { create(:customize_ux_broadcast) }
@ -70,6 +71,7 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
twitter_connect_broadcast,
apple_connect_broadcast,
forem_connect_broadcast,
google_oauth2_connect_broadcast,
].include?(user.notifications.last.notifiable)
expect(not_github).to be(true)

View file

@ -135,6 +135,55 @@ module OmniauthHelpers
},
).freeze
OMNIAUTH_PAYLOAD_GOOGLE_OAUTH2 = OmniAuth::AuthHash::InfoHash.new(
{
"provider" => "google_oauth2",
"uid" => "100000000000000000000",
"info" => {
"name" => "fname lname",
"email" => "john@example.com",
"first_name" => "fname",
"last_name" => "lname",
"image" => "https://dummyimage.com/400x400.jpg",
"urls" => {
"google" => "https://plus.google.com/+JohnSmith"
}
},
"credentials" => {
"token" => "TOKEN",
"refresh_token" => "REFRESH_TOKEN",
"expires_at" => 1_496_120_719,
"expires" => true
},
"extra" => {
"id_token" => "ID_TOKEN",
"id_info" => {
"azp" => "APP_ID",
"aud" => "APP_ID",
"sub" => "100000000000000000000",
"email" => "john@example.com",
"email_verified" => true,
"at_hash" => "HK6E_P6Dh8Y93mRNtsDB1Q",
"iss" => "accounts.google.com",
"iat" => 1_496_117_119,
"exp" => 1_496_120_719
},
"raw_info" => {
"sub" => "100000000000000000000",
"name" => "fname lname",
"given_name" => "fname",
"family_name" => "lname",
"profile" => "https://plus.google.com/+JohnSmith",
"picture" => "https://dummyimage.com/400x400.jpg",
"email" => "john@example.com",
"email_verified" => "true",
"locale" => "en",
"hd" => "company.com"
}
}
},
).freeze
def omniauth_setup_invalid_credentials(provider)
OmniAuth.config.mock_auth[provider] = :invalid_credentials
end
@ -198,6 +247,10 @@ module OmniauthHelpers
OmniAuth.config.mock_auth[:apple] = OMNIAUTH_PAYLOAD_APPLE.dup
end
def omniauth_mock_google_oauth2_payload
OmniAuth.config.mock_auth[:google_oauth2] = OMNIAUTH_PAYLOAD_GOOGLE_OAUTH2.dup
end
def omniauth_mock_github_payload
info = OMNIAUTH_BASIC_INFO[:info].merge(
image: "https://dummyimage.com/400x400.jpg",

View file

@ -16,8 +16,8 @@ RSpec.describe "Creator config edit", type: :system, js: true do
click_on("Show info", match: :first)
end
Authentication::Providers.available.each do |provider|
element = find(".config-authentication__item--label", text: /#{provider}/i)
Authentication::Providers.available_providers.each do |provider|
element = find(".config-authentication__item--label", text: /#{provider.official_name}/i)
expect(element).not_to be_nil
end
end

Binary file not shown.