Add Sign in with Apple (#11934)
* Add gem omniauth-apple * Integrate omniauth-apple * Integrate callback * Add fields * Add tests, fix bugs and make it all work * Show only enabled providers for the current user * Add default profile image for Apple * Remove localhost patch * Bring over the changed Apple username if the user changes it * More specs fixed * Incorporate feedback from PR * Fix specs * Simplify code and fix spec * Fix Broadcast generators to take into account the new provider * Fix spec * Generate a truly unique apple_username * Fix user specs * Add omniauth-apple-0.0.2 to vendor cache * Fix merge conflict and spec * Update VCR fastly sloan cassette * Revert "Generate a truly unique apple_username" This reverts commit 2462875575b0bbd6b3c1d56b25afcd3189671608. * Fix user specs * Fix specs * Fix specs * Hide Connect Apple button behind a feature flag * Revert "Hide Connect Apple button behind a feature flag" This reverts commit 105bde0373389a4eb9b6e948f60734c7e0e99cba. * Fix line lengths * Fix spec * ES tag * CSRF bypass for Apple callback * custom user_nickname in Apple provider with small tweaks + omniauth-apple bump * Fixes username specs * Makes Apple users default image Users::ProfileImageGenerator * Fallback to mascot_image_url in test environment to avoid breaking Travis * Fixes Apple CSRF error + makes default nickname more readable * Trigger Travis * Better devise config * Apple SiteConfig entires in /admin/config * Fixing specs * Adds beta_access? to Authentication::Providers::Provider * Fixes specs * Codeclimate double quote fix in Gemfile * Fixes /admin/config allowed params & adds feature flag for provider beta_access? * Remove Enfile & adds temporary docs * Adds custom apple auth provider settings * Fix authenticator spec * Fix configs spec (use last instead of first to avoid apple special case) * Remove dangling fields from /admin/config * updates feature flag * More test fixes * Hide config behind feature flag too * omniauth-apple bump * Takes care of edge case fallback * Reverse apple_username update * Adds auth_time to info hash in apple omniauth mock * Switch to next instead of nesting for feature flag * Fixes CVE-2015-9284 * Fixes specs after auth providers initiatior refactor from GET to POST * Spec fixes * More spec fixes * Fix Rails codebase reference link Co-authored-by: rhymes <rhymesete@gmail.com> Co-authored-by: rhymes <rhymes@hey.com>
This commit is contained in:
parent
f31470d8e5
commit
fcc5c0d2a1
42 changed files with 1064 additions and 134 deletions
1
Gemfile
1
Gemfile
|
|
@ -61,6 +61,7 @@ gem "nokogiri", "~> 1.10" # HTML, XML, SAX, and Reader parser
|
|||
gem "octokit", "~> 4.19" # Simple wrapper for the GitHub API
|
||||
gem "oj", "~> 3.10" # JSON parser and object serializer
|
||||
gem "omniauth", "~> 1.9" # A generalized Rack framework for multiple-provider authentication
|
||||
gem "omniauth-apple", "~> 1.0" # OmniAuth strategy for Sign In with Apple
|
||||
gem "omniauth-facebook", "~> 8.0" # OmniAuth strategy for Facebook
|
||||
gem "omniauth-github", "~> 1.3" # OmniAuth strategy for GitHub
|
||||
gem "omniauth-twitter", "~> 1.4" # OmniAuth strategy for Twitter
|
||||
|
|
|
|||
|
|
@ -475,6 +475,9 @@ GEM
|
|||
omniauth (1.9.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
omniauth-apple (1.0.1)
|
||||
jwt
|
||||
omniauth-oauth2
|
||||
omniauth-facebook (8.0.0)
|
||||
omniauth-oauth2 (~> 1.2)
|
||||
omniauth-github (1.4.0)
|
||||
|
|
@ -892,6 +895,7 @@ DEPENDENCIES
|
|||
octokit (~> 4.19)
|
||||
oj (~> 3.10)
|
||||
omniauth (~> 1.9)
|
||||
omniauth-apple (~> 1.0)
|
||||
omniauth-facebook (~> 8.0)
|
||||
omniauth-github (~> 1.3)
|
||||
omniauth-twitter (~> 1.4)
|
||||
|
|
|
|||
55
app/assets/images/apple-logo.svg
Normal file
55
app/assets/images/apple-logo.svg
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1000"
|
||||
viewBox="0 0 1000 1187.198"
|
||||
version="1.1"
|
||||
height="1187.198"
|
||||
id="svg2"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="Apple_1998.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.1767767"
|
||||
inkscape:cx="-1066.5045"
|
||||
inkscape:cy="964.94669"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="m 979.04184,925.18785 c -17.95397,41.47737 -39.20563,79.65705 -63.82824,114.75895 -33.56298,47.8528 -61.04356,80.9761 -82.22194,99.3698 -32.83013,30.192 -68.00529,45.6544 -105.67203,46.5338 -27.04089,0 -59.6512,-7.6946 -97.61105,-23.3035 -38.08442,-15.5358 -73.08371,-23.2303 -105.08578,-23.2303 -33.56296,0 -69.55888,7.6945 -108.06101,23.2303 -38.5608,15.6089 -69.62484,23.7432 -93.37541,24.5493 -36.12049,1.5389 -72.1237,-14.3632 -108.06101,-47.7796 -22.93711,-20.0059 -51.62684,-54.3017 -85.99592,-102.8874 C 92.254176,984.54592 61.937588,924.38175 38.187028,855.7902 12.750995,781.70252 0,709.95986 0,640.50361 0,560.94181 17.191859,492.32094 51.626869,434.81688 78.689754,388.62753 114.69299,352.19192 159.75381,325.44413 c 45.06086,-26.74775 93.74914,-40.37812 146.18212,-41.25019 28.68971,0 66.3125,8.8744 113.06613,26.31542 46.62174,17.49964 76.55727,26.37404 89.68198,26.37404 9.8124,0 43.06758,-10.37669 99.4431,-31.06405 53.31237,-19.18512 98.30724,-27.12887 135.16787,-23.99975 99.8828,8.06098 174.92313,47.43518 224.82789,118.37174 -89.33023,54.12578 -133.51903,129.93556 -132.63966,227.18753 0.8061,75.75115 28.28668,138.78795 82.2952,188.8393 24.47603,23.23022 51.81008,41.18421 82.22186,53.93522 -6.59525,19.12648 -13.557,37.44688 -20.95846,55.03446 z M 749.96366,23.751237 c 0,59.37343 -21.69138,114.810233 -64.92748,166.121963 -52.17652,60.99961 -115.28658,96.24803 -183.72426,90.68597 -0.87204,-7.12298 -1.37769,-14.61967 -1.37769,-22.49743 0,-56.99843 24.81315,-117.99801 68.87738,-167.873453 21.99909,-25.25281 49.978,-46.25018 83.90738,-63.00018 C 686.57507,10.688027 718.59913,1.5631274 748.71783,5.2734376e-4 749.59727,7.9378274 749.96366,15.875627 749.96366,23.750467 Z"
|
||||
id="path4"
|
||||
inkscape:connector-curvature="0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -113,6 +113,10 @@ module Admin
|
|||
github_secret
|
||||
facebook_key
|
||||
facebook_secret
|
||||
apple_client_id
|
||||
apple_key_id
|
||||
apple_pem
|
||||
apple_team_id
|
||||
auth_providers_to_enable
|
||||
invite_only_mode
|
||||
allow_email_password_registration
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
||||
include Devise::Controllers::Rememberable
|
||||
|
||||
# Rails actionpack only allows POST requests that come with an ORIGIN header
|
||||
# that matches `request.base_url`, it raises CSRF exception otherwise.
|
||||
# There is no way to allow specific ORIGIN values in order to securely bypass
|
||||
# trusted origins (i.e. Apple OAuth) so `protect_from_forgery` is skipped
|
||||
# ONLY when it's safe to do so (i.e. ORIGIN == 'https://appleid.apple.com').
|
||||
# The hardcoded CSRF check can be found in the method `valid_request_origin?`:
|
||||
# https://github.com/rails/rails/blob/901f12212c488f6edfcf6f8ad3230bce6b3d5792/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L449-L459
|
||||
protect_from_forgery unless: -> { safe_apple_callback_request? }
|
||||
|
||||
# Each available authentication method needs a related action that will be called
|
||||
# as a callback on successful redirect from the upstream OAuth provider
|
||||
Authentication::Providers.available.each do |provider_name|
|
||||
|
|
@ -31,6 +40,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
super
|
||||
end
|
||||
|
||||
def passthru
|
||||
redirect_to root_path(signin: "true")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def callback_for(provider)
|
||||
|
|
@ -96,4 +109,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
def user_persisted_but_username_taken?
|
||||
@user.persisted? && @user.errors_as_sentence.include?("username has already been taken")
|
||||
end
|
||||
|
||||
# We only bypass CSRF checks on Apple callback path & Apple trusted ORIGIN
|
||||
def safe_apple_callback_request?
|
||||
trusted_origin = Authentication::Providers::Apple::TRUSTED_CALLBACK_ORIGIN
|
||||
request.fullpath == Authentication::Providers::Apple::CALLBACK_PATH &&
|
||||
request.headers["ORIGIN"] == trusted_origin
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -120,6 +120,26 @@ module Constants
|
|||
"The \"App Secret\" portion of the Basic Settings section of the App page on the Facebook Developer Portal",
|
||||
placeholder: ""
|
||||
},
|
||||
apple_client_id: {
|
||||
description:
|
||||
"The \"App Bundle\" code for the Authentication Service configured in the Apple Developer Portal",
|
||||
placeholder: "com.example.app"
|
||||
},
|
||||
apple_team_id: {
|
||||
description:
|
||||
"The \"Team ID\" of your Apple Developer Account",
|
||||
placeholder: ""
|
||||
},
|
||||
apple_key_id: {
|
||||
description:
|
||||
"The \"Key ID\" from the Authentication Service configured in the Apple Developer Portal",
|
||||
placeholder: ""
|
||||
},
|
||||
apple_pem: {
|
||||
description:
|
||||
"The \"PEM\" key from the Authentication Service configured in the Apple Developer Portal",
|
||||
placeholder: "-----BEGIN PRIVATE KEY-----\nMIGTAQrux...QPe8Yb\n-----END PRIVATE KEY-----\\n"
|
||||
},
|
||||
favicon_url: {
|
||||
description: "Used as the site favicon",
|
||||
placeholder: IMAGE_PLACEHOLDER
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@ class SiteConfig < RailsSettings::Base
|
|||
field :github_secret, type: :string, default: ApplicationConfig["GITHUB_SECRET"]
|
||||
field :facebook_key, type: :string
|
||||
field :facebook_secret, type: :string
|
||||
field :apple_client_id, type: :string
|
||||
field :apple_key_id, type: :string
|
||||
field :apple_pem, type: :string
|
||||
field :apple_team_id, type: :string
|
||||
|
||||
# Campaign
|
||||
field :campaign_call_to_action, type: :string, default: "Share your project"
|
||||
|
|
@ -209,4 +213,15 @@ class SiteConfig < RailsSettings::Base
|
|||
def self.dev_to?
|
||||
app_domain == "dev.to"
|
||||
end
|
||||
|
||||
# Apple uses different keys than the usual `PROVIDER_NAME_key` or
|
||||
# `PROVIDER_NAME_secret` so these will help the generalized authentication
|
||||
# code to work, i.e. https://github.com/forem/forem/blob/master/app/helpers/authentication_helper.rb#L26-L29
|
||||
def self.apple_key
|
||||
return unless apple_client_id.present? && apple_key_id.present? &&
|
||||
apple_pem.present? && apple_team_id.present?
|
||||
|
||||
"present"
|
||||
end
|
||||
singleton_class.__send__(:alias_method, :apple_secret, :apple_key)
|
||||
end
|
||||
|
|
|
|||
88
app/services/authentication/providers/apple.rb
Normal file
88
app/services/authentication/providers/apple.rb
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
module Authentication
|
||||
module Providers
|
||||
# Apple authentication provider, uses omniauth-apple as backend
|
||||
class Apple < Provider
|
||||
OFFICIAL_NAME = "Apple".freeze
|
||||
SETTINGS_URL = "https://appleid.apple.com/account/manage".freeze
|
||||
TRUSTED_CALLBACK_ORIGIN = "https://appleid.apple.com".freeze
|
||||
CALLBACK_PATH = "/users/auth/apple/callback".freeze
|
||||
|
||||
def new_user_data
|
||||
# Apple sends `first_name` and `last_name` as separate fields
|
||||
name = "#{info.first_name} #{info.last_name}"
|
||||
timestamp = raw_info.id_info.auth_time
|
||||
|
||||
user_data = {
|
||||
email: info.email,
|
||||
apple_created_at: Time.zone.at(timestamp),
|
||||
apple_username: user_nickname,
|
||||
name: name
|
||||
}
|
||||
|
||||
if Rails.env.test?
|
||||
user_data[:profile_image] = SiteConfig.mascot_image_url
|
||||
else
|
||||
user_data[:remote_profile_image_url] = Users::ProfileImageGenerator.call
|
||||
end
|
||||
|
||||
user_data
|
||||
end
|
||||
|
||||
def existing_user_data
|
||||
# Apple by default will send nil `first_name` and `last_name` after
|
||||
# the first login. To cover the case where a user disconnects their
|
||||
# Apple authorization, signs in again and then changes their name,
|
||||
# we update the username only if the name is not nil
|
||||
apple_username = info.first_name.present? ? info.first_name.downcase : nil
|
||||
timestamp = raw_info.id_info.auth_time
|
||||
|
||||
data = { apple_created_at: Time.zone.at(timestamp) }
|
||||
data[:apple_username] = apple_username if apple_username
|
||||
data
|
||||
end
|
||||
|
||||
# For Apple we override this method because the `info` payload doesn't
|
||||
# include `nickname`. On top of not having a username, Apple allows users
|
||||
# to 'choose' the first_name & last_name sent our way so they are
|
||||
# definitely not assured to be unique. We still need `user_nickname` to
|
||||
# always be the same on each login so we use the email hash as suffix to
|
||||
# avoid collisions with other registrations with the same first_name
|
||||
def user_nickname
|
||||
if info.first_name.present? || info.last_name.present?
|
||||
# We sometimes get `info.first_name` and `info.last_name`
|
||||
[
|
||||
info.first_name&.downcase,
|
||||
info.last_name&.downcase,
|
||||
Digest::SHA512.hexdigest(info.email),
|
||||
].join("_")[0...25]
|
||||
else
|
||||
# This covers an edge case where the Apple Id has already given
|
||||
# permissions to the forem auth and we don't have anything else
|
||||
# to work with other than the email
|
||||
["user", Digest::SHA512.hexdigest(info.email)].join("_")[0...15]
|
||||
end
|
||||
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(
|
||||
provider_name,
|
||||
**kwargs,
|
||||
)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def cleanup_payload(auth_payload)
|
||||
auth_payload
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<!--
|
||||
Apple's config is different compared to the common OAuth provider implemented
|
||||
before, this makes the generalized `auth_provider_settings` not work well so it
|
||||
requires a custom partial
|
||||
-->
|
||||
<fieldset class="config-authentication-form">
|
||||
<div
|
||||
class="crayons-notice crayons-notice--warning auth-warning <%= provider_keys_configured?(provider.provider_name) ? "hidden" : "" %>">
|
||||
<strong>Note:</strong> This authentication provider will <strong>not</strong> be enabled if the key or secret are missing. Please make sure that you enter your key and secret correctly.
|
||||
</div>
|
||||
<div class="crayons-field">
|
||||
<%= admin_config_label :apple_client_id %>
|
||||
<p class="crayons-field__description">
|
||||
<%= Constants::SiteConfig::DETAILS[:apple_client_id][:description] %>
|
||||
</p>
|
||||
<%= f.text_field :apple_client_id,
|
||||
class: "crayons-textfield",
|
||||
value: SiteConfig.apple_client_id,
|
||||
placeholder: Constants::SiteConfig::DETAILS[:apple_client_id][:placeholder] %>
|
||||
</div>
|
||||
<div class="crayons-field">
|
||||
<%= admin_config_label :apple_key_id %>
|
||||
<p class="crayons-field__description">
|
||||
<%= Constants::SiteConfig::DETAILS[:apple_key_id][:description] %>
|
||||
<p>
|
||||
<%= f.text_field :apple_key_id,
|
||||
class: "crayons-textfield",
|
||||
value: SiteConfig.apple_key_id,
|
||||
placeholder: Constants::SiteConfig::DETAILS[:apple_key_id][:placeholder] %>
|
||||
</div>
|
||||
<div class="crayons-field">
|
||||
<%= admin_config_label :apple_pem %>
|
||||
<p class="crayons-field__description">
|
||||
<%= Constants::SiteConfig::DETAILS[:apple_pem][:description] %>
|
||||
<p>
|
||||
<%= f.text_field :apple_pem,
|
||||
class: "crayons-textfield",
|
||||
value: SiteConfig.apple_pem,
|
||||
placeholder: Constants::SiteConfig::DETAILS[:apple_pem][:placeholder] %>
|
||||
</div>
|
||||
<div class="crayons-field">
|
||||
<%= admin_config_label :apple_team_id %>
|
||||
<p class="crayons-field__description">
|
||||
<%= Constants::SiteConfig::DETAILS[:apple_team_id][:description] %>
|
||||
<p>
|
||||
<%= f.text_field :apple_team_id,
|
||||
class: "crayons-textfield",
|
||||
value: SiteConfig.apple_team_id,
|
||||
placeholder: Constants::SiteConfig::DETAILS[:apple_team_id][:placeholder] %>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<% if authentication_enabled_providers.include?(provider) %>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--danger"
|
||||
data-action="click->config#activateAuthProviderModal"
|
||||
data-tooltip="<%= tooltip_text_email_or_auth_provider_btns %>"
|
||||
data-auth-provider="<%= provider.provider_name %>"
|
||||
data-auth-provider-official="<%= provider.official_name %>"
|
||||
<%= disabled_attr_on_auth_provider_enablebtn %>>
|
||||
Disable
|
||||
</button>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--secondary"
|
||||
data-auth-provider="<%= provider.provider_name %>"
|
||||
data-action="click->config#hideAuthProviderSettings">
|
||||
Close
|
||||
</button>
|
||||
<% else %>
|
||||
<button
|
||||
class="crayons-btn crayons-btn--secondary"
|
||||
data-auth-provider="<%= provider.provider_name %>"
|
||||
data-action="click->config#disableAuthProvider">
|
||||
Undo
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -282,6 +282,7 @@
|
|||
readonly: true %>
|
||||
</div>
|
||||
<% authentication_available_providers.each do |provider| %>
|
||||
<% next if provider.provider_name == :apple && !Flipper.enabled?(:apple_auth) %>
|
||||
<section class="config-authentication__item mb-0">
|
||||
<figure class="config-authentication__item--icon">
|
||||
<%= inline_svg_tag("#{provider.provider_name}.svg", class: "crayons-icon", aria: true, title: "#{provider.official_name} logo") %>
|
||||
|
|
@ -313,7 +314,8 @@
|
|||
</button>
|
||||
</div>
|
||||
<div id="<%= provider.provider_name %>-auth-settings" class="config-authentication__item--body hidden">
|
||||
<%= render partial: "auth_provider_settings",
|
||||
<% auth_partial = provider.provider_name == :apple ? "apple_auth_provider_settings" : "auth_provider_settings" %>
|
||||
<%= render partial: auth_partial,
|
||||
locals: {
|
||||
f: f,
|
||||
provider: provider
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<% authentication_enabled_providers.each do |provider| %>
|
||||
<a
|
||||
href="<%= provider.sign_in_path(state: "navbar_basic") %>"
|
||||
class="crayons-btn crayons-btn--brand-<%= provider.provider_name %> crayons-btn--icon-left"
|
||||
data-no-instant>
|
||||
<%= inline_svg_tag("#{provider.provider_name}.svg", aria: true, class: "crayons-icon", title: provider.provider_name) %>
|
||||
Sign In with <%= provider.official_name %>
|
||||
</a>
|
||||
<% end %>
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
<div class="registration__actions-providers">
|
||||
<% authentication_enabled_providers.each do |provider| %>
|
||||
<a
|
||||
href="<%= provider.sign_in_path(state: "navbar_basic") %>"
|
||||
class="crayons-btn crayons-btn--l crayons-btn--brand-<%= provider.provider_name %> crayons-btn--icon-left whitespace-nowrap"
|
||||
data-no-instant>
|
||||
<%= inline_svg_tag("#{provider.provider_name}.svg", aria: true, class: "crayons-icon", title: provider.provider_name) %>
|
||||
<%= params[:state] == "new-user" ? "Sign up" : "Continue" %> with <%= provider.official_name %>
|
||||
</a>
|
||||
<% next if provider.provider_name == :apple && !Flipper.enabled?(:apple_auth) %>
|
||||
<%= form_with url: provider.sign_in_path(state: "navbar_basic"), class: "flex w-100", local: true do |f| %>
|
||||
<%= f.button type: :submit, class: "crayons-btn crayons-btn--l crayons-btn--brand-#{provider.provider_name} crayons-btn--icon-left grow-1 whitespace-nowrap" do %>
|
||||
<%= inline_svg_tag("#{provider.provider_name}.svg", aria: true, class: "crayons-icon", title: provider.provider_name) %>
|
||||
<%= params[:state] == "new-user" ? "Sign up" : "Continue" %> with <%= provider.official_name %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if params[:state] == "new-user" && SiteConfig.allow_email_password_registration && !SiteConfig.invite_only_mode %>
|
||||
<%= link_to "#{inline_svg_tag('email.svg', aria: true, class: 'crayons-icon', title: 'email')}Sign up with Email".html_safe,
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
<% authentication_enabled_providers.each do |provider| %>
|
||||
<a
|
||||
href="<%= provider.sign_in_path(state: "navbar_basic") %>"
|
||||
class="crayons-btn crayons-btn--l crayons-btn--brand-<%= provider.provider_name %> crayons-btn--icon-left"
|
||||
data-no-instant>
|
||||
<%= inline_svg_tag("#{provider.provider_name}.svg", aria: true, class: "crayons-icon", title: provider.provider_name) %>
|
||||
Sign In with <%= provider.official_name %>
|
||||
</a>
|
||||
<% end %>
|
||||
<a
|
||||
href="<%= sign_up_path %>"
|
||||
class="crayons-btn crayons-btn--secondary crayons-btn--l">
|
||||
More Sign In Options
|
||||
</a>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<% authentication_enabled_providers.each do |provider| %>
|
||||
<a
|
||||
href="<%= provider.sign_in_path(state: "signup-modal") %>"
|
||||
class="crayons-btn crayons-btn--xl crayons-btn--brand-<%= provider.provider_name %> crayons-btn--icon-left m-1"
|
||||
data-no-instant>
|
||||
<%= inline_svg_tag("#{provider.provider_name}.svg", aria: true, class: "crayons-icon", title: provider.provider_name) %>
|
||||
Sign In with <%= provider.official_name %>
|
||||
</a>
|
||||
<% end %>
|
||||
<a href="<%= sign_up_path %>"
|
||||
class="crayons-btn crayons-btn--secondary crayons-btn--xl m-1"
|
||||
data-no-instant>More Sign In Options</a>
|
||||
|
|
@ -1,19 +1,14 @@
|
|||
<% unless @user.authenticated_with_all_providers? %>
|
||||
<div class="crayons-card crayons-card--content-rows block">
|
||||
<% authentication_enabled_providers.each do |provider| %>
|
||||
<% next if provider.provider_name == :apple && !Flipper.enabled?(:apple_auth) %>
|
||||
<% unless @user.authenticated_through?(provider.provider_name) %>
|
||||
<a
|
||||
href="<%= provider.sign_in_path(state: "profile") %>"
|
||||
class="crayons-btn crayons-btn--icon-left crayons-btn--brand-<%= provider.provider_name %> m-1"
|
||||
data-no-instant>
|
||||
<%= inline_svg_tag(
|
||||
"#{provider.provider_name}.svg",
|
||||
aria: true,
|
||||
class: "crayons-icon",
|
||||
title: provider.official_name,
|
||||
) %>
|
||||
Connect <%= provider.official_name %> Account
|
||||
</a>
|
||||
<%= form_with url: provider.sign_in_path(state: "profile"), class: "flex w-100", local: true do |f| %>
|
||||
<%= f.button type: :submit, class: "crayons-btn crayons-btn--icon-left crayons-btn--brand-#{provider.provider_name} m-1" do %>
|
||||
<%= inline_svg_tag("#{provider.provider_name}.svg", aria: true, class: "crayons-icon", title: provider.provider_name) %>
|
||||
Connect <%= provider.official_name %> Account
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,15 @@ FACEBOOK_OMNIAUTH_SETUP = lambda do |env|
|
|||
env["omniauth.strategy"].options[:token_params][:parse] = :json
|
||||
end
|
||||
|
||||
APPLE_OMNIAUTH_SETUP = lambda do |env|
|
||||
env["omniauth.strategy"].options[:client_id] = SiteConfig.apple_client_id
|
||||
env["omniauth.strategy"].options[:scope] = "email name"
|
||||
env["omniauth.strategy"].options[:key_id] = SiteConfig.apple_key_id
|
||||
env["omniauth.strategy"].options[:pem] = SiteConfig.apple_pem
|
||||
env["omniauth.strategy"].options[:provider_ignores_state] = true
|
||||
env["omniauth.strategy"].options[:team_id] = SiteConfig.apple_team_id
|
||||
end
|
||||
|
||||
Devise.setup do |config|
|
||||
# The secret key used by Devise. Devise uses this key to generate
|
||||
# random tokens. Changing this key will render invalid all existing
|
||||
|
|
@ -304,6 +313,7 @@ Devise.setup do |config|
|
|||
config.omniauth :facebook, setup: FACEBOOK_OMNIAUTH_SETUP
|
||||
config.omniauth :github, setup: GITHUB_OMNIUATH_SETUP
|
||||
config.omniauth :twitter, setup: TWITTER_OMNIAUTH_SETUP
|
||||
config.omniauth :apple, setup: APPLE_OMNIAUTH_SETUP
|
||||
|
||||
# ==> Warden configuration
|
||||
# If you want to use other strategies, that are not supported by Devise, or
|
||||
|
|
|
|||
|
|
@ -1,2 +1,7 @@
|
|||
OmniAuth.config.logger = Rails.logger
|
||||
OmniAuth.config.full_host = proc { URL.url }
|
||||
# We can't use the 'omniauth-rails_csrf_protection' because of the custom way we
|
||||
# handle CSRF Tokens in `config/initializers/persistent_csrf_token_cookie.rb`.
|
||||
# Therefore we're manually taking the necessary steps to prevent CVE-2015-9284
|
||||
# https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284
|
||||
OmniAuth.config.allowed_request_methods = [:post]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
class AddAppleAuthenticationFields < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :users, :apple_created_at, :datetime
|
||||
add_column :users, :apple_username, :string
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
class AddIndexToUsersAppleUsername < ActiveRecord::Migration[5.2]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
add_index :users, :apple_username, algorithm: :concurrently
|
||||
end
|
||||
end
|
||||
|
|
@ -1204,6 +1204,8 @@ ActiveRecord::Schema.define(version: 2020_12_03_063435) do
|
|||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.datetime "apple_created_at"
|
||||
t.string "apple_username"
|
||||
t.integer "articles_count", default: 0, null: false
|
||||
t.string "available_for"
|
||||
t.integer "badge_achievements_count", default: 0, null: false
|
||||
|
|
@ -1344,6 +1346,7 @@ ActiveRecord::Schema.define(version: 2020_12_03_063435) do
|
|||
t.boolean "welcome_notifications", default: true, null: false
|
||||
t.datetime "workshop_expiration"
|
||||
t.string "youtube_url"
|
||||
t.index ["apple_username"], name: "index_users_on_apple_username"
|
||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||
t.index ["created_at"], name: "index_users_on_created_at"
|
||||
t.index ["email"], name: "index_users_on_email", unique: true
|
||||
|
|
|
|||
83
docs/backend/auth-apple.md
Normal file
83
docs/backend/auth-apple.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
title: Apple Authentication (beta)
|
||||
---
|
||||
|
||||
# Sign in with Apple Authentication
|
||||
|
||||
Forem allows you to authenticate using
|
||||
[Sign in with Apple](https://developer.apple.com/sign-in-with-apple/). In order
|
||||
to use this authentication method you'll need to be enrolled to the
|
||||
[Apple Developer Program](https://developer.apple.com/programs/) in order to
|
||||
retrieve the necessary credentials and an HTTPS supported URL for the callback
|
||||
configuration (HTTP won't work). Then you'll need to provide the keys to the
|
||||
Rails application.
|
||||
|
||||
#### Beta support
|
||||
|
||||
This authentication provider is currently marked as beta. This means it will be
|
||||
available but hidden from public access until more thoroughly tested.
|
||||
|
||||
If you want to make this feature publicly available (without the state
|
||||
parameter) you can enable the `apple_auth` feature flag from the Flipper
|
||||
dashboard or the Rails console with `Flipper.enable(:apple_auth)`.
|
||||
|
||||
# Apple Developer Portal Configuration
|
||||
|
||||
[Register/Sign in](https://developer.apple.com/account) to your Apple Developer
|
||||
Account.
|
||||
|
||||
## Service ID Configuration
|
||||
|
||||
1. [Create a Service ID](https://developer.apple.com/account/resources/identifiers/list/serviceId)
|
||||
|
||||

|
||||
|
||||
2. Name the Service and finalize the registration
|
||||
|
||||

|
||||
|
||||
3. Configure Domains and Subdomains & the callback URL. This example uses
|
||||
[ngrok](https://ngrok.io) for HTTPS support.
|
||||
|
||||

|
||||
|
||||
## Key Configuration
|
||||
|
||||
1. [Register a new Key](https://developer.apple.com/account/resources/authkeys/add).
|
||||
Enable the "Sign in with Apple" option and configure it so it's associated
|
||||
with the corresponding App ID
|
||||
|
||||

|
||||
|
||||
2. Download the Key
|
||||
|
||||

|
||||
|
||||
# Configuring the Rails Application
|
||||
|
||||
Now with both the Service ID and Key you'll need to enable Apple Authentication
|
||||
and pass in the credentials in the admin dashboard `/admin/config`.
|
||||
|
||||

|
||||
|
||||
Add the corresponding configuration data. Make sure the PEM key you downloaded
|
||||
has explicit linebreaks (`\n`), don't forget the one at the very end of it.
|
||||
|
||||

|
||||
|
||||
Save the changes and restart your server for these values to take effect.
|
||||
|
||||
## Email configuration
|
||||
|
||||
Apple uses what they call Private Email Relay Service to hide user's emails. For
|
||||
this to work first
|
||||
[create a new email source](https://developer.apple.com/account/resources/services/list).
|
||||
|
||||

|
||||
|
||||
Emails sent need to be authenticated and the configuration depends on the
|
||||
different providers available:
|
||||
|
||||
- [Mailchimp](https://mailchimp.com/help/set-up-custom-domain-authentication-dkim-and-spf/)
|
||||
- [SendGrid](https://sendgrid.com/docs/ui/account-and-settings/how-to-set-up-domain-authentication/)
|
||||
- [SES](https://docs.aws.amazon.com/es_es/ses/latest/DeveloperGuide/send-email-authentication-dkim.html)
|
||||
|
|
@ -10,6 +10,7 @@ and [OmniAuth](https://github.com/omniauth/omniauth).
|
|||
On Forem you can authenticate through Facebook, GitHub, or Twitter. Please check out
|
||||
the respective guides on how to authenticate:
|
||||
|
||||
- [Apple authentication](/backend/auth-apple)
|
||||
- [Facebook authentication](/backend/auth-facebook)
|
||||
- [Github authentication](/backend/auth-github)
|
||||
- [Twitter authentication](/backend/auth-twitter)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ title: Backend Guide
|
|||
items:
|
||||
- audit-log.md
|
||||
- authentication.md
|
||||
- auth-apple.md
|
||||
- auth-facebook.md
|
||||
- auth-twitter.md
|
||||
- auth-github.md
|
||||
|
|
|
|||
|
|
@ -22,13 +22,11 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
factory :twitter_connect_broadcast do
|
||||
title { "Welcome Notification: twitter_connect" }
|
||||
factory :apple_connect_broadcast do
|
||||
title { "Welcome Notification: apple_connect" }
|
||||
type_of { "Welcome" }
|
||||
processed_html do
|
||||
"You're on a roll! 🎉 Do you have a Twitter account? Consider " \
|
||||
"<a href='/settings'>connecting it</a> so we can @mention you if we share " \
|
||||
"your post via our Twitter account <a href='https://twitter.com/thePracticalDev'>@thePracticalDev</a>."
|
||||
"You're on a roll! 🎉 Do you have an Apple account? Consider <a href='/settings'>connecting it</a>."
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -50,6 +48,16 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
factory :twitter_connect_broadcast do
|
||||
title { "Welcome Notification: twitter_connect" }
|
||||
type_of { "Welcome" }
|
||||
processed_html do
|
||||
"You're on a roll! 🎉 Do you have a Twitter account? Consider " \
|
||||
"<a href='/settings'>connecting it</a> so we can @mention you if we share " \
|
||||
"your post via our Twitter account <a href='https://twitter.com/thePracticalDev'>@thePracticalDev</a>."
|
||||
end
|
||||
end
|
||||
|
||||
factory :customize_ux_broadcast do
|
||||
title { "Welcome Notification: customize_experience" }
|
||||
type_of { "Welcome" }
|
||||
|
|
|
|||
|
|
@ -28,6 +28,38 @@ RSpec.describe Identity, type: :model do
|
|||
omniauth_mock_providers_payload
|
||||
end
|
||||
|
||||
context "with Apple payload" do
|
||||
let(:auth_payload) { OmniAuth.config.mock_auth[:apple] }
|
||||
let(:provider) { Authentication::Providers::Apple.new(auth_payload) }
|
||||
|
||||
it "initializes a new identity from the auth payload" do
|
||||
identity = described_class.build_from_omniauth(provider)
|
||||
|
||||
expect(identity.new_record?).to be(true)
|
||||
expect(identity.provider).to eq("apple")
|
||||
expect(identity.uid).to eq(auth_payload.uid)
|
||||
expect(identity.token).to eq(auth_payload.credentials.token)
|
||||
expect(identity.secret).to be_nil
|
||||
expect(identity.auth_data_dump).to eq(provider.payload)
|
||||
end
|
||||
|
||||
it "finds an existing identity" do
|
||||
payload = provider.payload
|
||||
|
||||
existing_identity = described_class.create!(
|
||||
user: user,
|
||||
provider: payload.provider,
|
||||
uid: payload.uid,
|
||||
token: payload.credentials.token,
|
||||
secret: nil,
|
||||
auth_data_dump: payload,
|
||||
)
|
||||
|
||||
identity = described_class.build_from_omniauth(provider)
|
||||
expect(identity).to eq(existing_identity)
|
||||
end
|
||||
end
|
||||
|
||||
context "with Github payload" do
|
||||
let(:auth_payload) { OmniAuth.config.mock_auth[:github] }
|
||||
let(:provider) { Authentication::Providers::Github.new(auth_payload) }
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ RSpec.describe "/admin/config", type: :request do
|
|||
|
||||
describe "Authentication" do
|
||||
it "updates enabled authentication providers" do
|
||||
enabled = Authentication::Providers.available.first.to_s
|
||||
enabled = Authentication::Providers.available.last.to_s
|
||||
post admin_config_path, params: {
|
||||
site_config: {
|
||||
"#{enabled}_key": "someKey",
|
||||
|
|
@ -87,7 +87,7 @@ RSpec.describe "/admin/config", type: :request do
|
|||
end
|
||||
|
||||
it "strips empty elements" do
|
||||
provider = Authentication::Providers.available.first.to_s
|
||||
provider = Authentication::Providers.available.last.to_s
|
||||
enabled = "#{provider}, '', nil"
|
||||
post admin_config_path, params: {
|
||||
site_config: {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ RSpec.describe "Registrations", type: :request do
|
|||
|
||||
Authentication::Providers.enabled.each do |provider_name|
|
||||
provider = Authentication::Providers.get!(provider_name)
|
||||
next if provider.provider_name == :apple && !Flipper.enabled?(:apple_auth)
|
||||
|
||||
expect(response.body).to include("Continue with #{provider.official_name}")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,6 +15,204 @@ RSpec.describe Authentication::Authenticator, type: :service do
|
|||
end
|
||||
end
|
||||
|
||||
context "when authenticating through Apple", vcr: { cassette_name: "fastly_sloan" } do
|
||||
let!(:auth_payload) { OmniAuth.config.mock_auth[:apple] }
|
||||
let!(:service) { described_class.new(auth_payload) }
|
||||
|
||||
describe "new user" do
|
||||
it "creates a new user" do
|
||||
expect do
|
||||
service.call
|
||||
end.to change(User, :count).by(1)
|
||||
end
|
||||
|
||||
it "creates a new identity" do
|
||||
expect do
|
||||
service.call
|
||||
end.to change(Identity, :count).by(1)
|
||||
end
|
||||
|
||||
it "extracts the proper data from the auth payload" do
|
||||
user = service.call
|
||||
|
||||
info = auth_payload.info
|
||||
raw_info = auth_payload.extra.raw_info
|
||||
|
||||
expect(user.email).to eq(info.email)
|
||||
expect(user.name).to eq("#{info.first_name} #{info.last_name}")
|
||||
expect(user.profile_image).not_to be_nil
|
||||
expect(user.apple_created_at.to_i).to eq(raw_info.id_info.auth_time)
|
||||
expect(user.apple_username).to match(/#{info.first_name.downcase}_\w+/)
|
||||
end
|
||||
|
||||
it "sets default fields" do
|
||||
user = service.call
|
||||
|
||||
expect(user.password).to be_present
|
||||
expect(user.signup_cta_variant).to be_nil
|
||||
expect(user.saw_onboarding).to be(false)
|
||||
expect(user.editor_version).to eq("v2")
|
||||
end
|
||||
|
||||
it "sets the correct sign up cta variant" do
|
||||
user = described_class.call(auth_payload, cta_variant: "awesome")
|
||||
|
||||
expect(user.signup_cta_variant).to eq("awesome")
|
||||
end
|
||||
|
||||
it "sets remember_me for the new user" do
|
||||
user = service.call
|
||||
|
||||
expect(user.remember_me).to be(true)
|
||||
expect(user.remember_token).to be_present
|
||||
expect(user.remember_created_at).to be_present
|
||||
end
|
||||
|
||||
it "sets confirmed_at" do
|
||||
user = service.call
|
||||
|
||||
expect(user.confirmed_at).to be_present
|
||||
end
|
||||
|
||||
it "queues a slack message to be sent for a user whose identity is brand new" do
|
||||
auth_payload.extra.raw_info.id_info.auth_time = 1.minute.ago.to_i
|
||||
|
||||
sidekiq_assert_enqueued_with(job: Slack::Messengers::Worker) do
|
||||
described_class.call(auth_payload)
|
||||
end
|
||||
end
|
||||
|
||||
it "records successful identity creation metric" do
|
||||
allow(DatadogStatsClient).to receive(:increment)
|
||||
service.call
|
||||
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
"identity.created", tags: ["provider:apple"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "existing user" do
|
||||
let(:user) { create(:user, :with_identity, identities: [:apple]) }
|
||||
|
||||
before do
|
||||
auth_payload.info.email = user.email
|
||||
end
|
||||
|
||||
it "doesn't create a new user" do
|
||||
expect do
|
||||
service.call
|
||||
end.not_to change(User, :count)
|
||||
end
|
||||
|
||||
it "creates a new identity if the user doesn't have one" do
|
||||
user = create(:user)
|
||||
auth_payload.info.email = user.email
|
||||
auth_payload.uid = "#{user.email}-#{rand(10_000)}"
|
||||
|
||||
expect do
|
||||
described_class.call(auth_payload)
|
||||
end.to change(Identity, :count).by(1)
|
||||
end
|
||||
|
||||
it "does not create a new identity if the user has one" do
|
||||
expect do
|
||||
service.call
|
||||
end.not_to change(Identity, :count)
|
||||
end
|
||||
|
||||
it "does not record an identity creation metric" do
|
||||
allow(DatadogStatsClient).to receive(:increment)
|
||||
service.call
|
||||
|
||||
expect(DatadogStatsClient).not_to have_received(:increment)
|
||||
end
|
||||
|
||||
it "updates the proper data from the auth payload" do
|
||||
# simulate changing apple data
|
||||
auth_payload.extra.raw_info.id_info.auth_time = 10.days.ago.to_i
|
||||
|
||||
user = described_class.call(auth_payload)
|
||||
|
||||
raw_info = auth_payload.extra.raw_info
|
||||
|
||||
expect(user.apple_created_at.to_i).to eq(raw_info.id_info.auth_time)
|
||||
end
|
||||
|
||||
it "sets remember_me for the existing user" do
|
||||
user.update_columns(remember_token: nil, remember_created_at: nil)
|
||||
|
||||
service.call
|
||||
user.reload
|
||||
|
||||
expect(user.remember_me).to be(true)
|
||||
expect(user.remember_token).to be_present
|
||||
expect(user.remember_created_at).to be_present
|
||||
end
|
||||
|
||||
it "updates confirmed_at with the current UTC time" do
|
||||
original_confirmed_at = user.confirmed_at
|
||||
|
||||
Timecop.travel(1.minute.from_now) do
|
||||
service.call
|
||||
end
|
||||
|
||||
user.reload
|
||||
expect(
|
||||
user.confirmed_at.utc.to_i > original_confirmed_at.utc.to_i,
|
||||
).to be(true)
|
||||
end
|
||||
|
||||
it "updates the username when it is changed on the provider" do
|
||||
new_username = "new_username#{rand(1000)}"
|
||||
auth_payload.info.first_name = new_username
|
||||
|
||||
user = described_class.call(auth_payload)
|
||||
|
||||
expect(user.apple_username).to eq(new_username)
|
||||
end
|
||||
|
||||
it "does not update the username when the first_name is nil" do
|
||||
previos_username = user.apple_username
|
||||
auth_payload.info.first_name = nil
|
||||
|
||||
user = described_class.call(auth_payload)
|
||||
|
||||
expect(user.apple_username).to eq(previos_username)
|
||||
end
|
||||
|
||||
it "updates profile_updated_at when the username is changed" do
|
||||
original_profile_updated_at = user.profile_updated_at
|
||||
|
||||
new_username = "new_username#{rand(1000)}"
|
||||
auth_payload.info.first_name = new_username
|
||||
|
||||
Timecop.travel(1.minute.from_now) do
|
||||
described_class.call(auth_payload)
|
||||
end
|
||||
|
||||
user.reload
|
||||
expect(
|
||||
user.profile_updated_at.to_i > original_profile_updated_at.to_i,
|
||||
).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "user already logged in" do
|
||||
it "returns the current user if the identity exists" do
|
||||
user = create(:user, :with_identity, identities: [:apple])
|
||||
expect(described_class.call(auth_payload, current_user: user)).to eq(user)
|
||||
end
|
||||
|
||||
it "creates the identity if for any reason it does not exist" do
|
||||
user = create(:user)
|
||||
expect do
|
||||
described_class.call(auth_payload, current_user: user)
|
||||
end.to change(Identity, :count).by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when authenticating through Github" do
|
||||
let!(:auth_payload) { OmniAuth.config.mock_auth[:github] }
|
||||
let!(:service) { described_class.new(auth_payload) }
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ RSpec.describe Authentication::Providers, type: :service do
|
|||
|
||||
describe ".available" do
|
||||
it "lists the available providers" do
|
||||
available_providers = %i[facebook github twitter]
|
||||
available_providers = %i[apple facebook github twitter]
|
||||
expect(described_class.available).to eq(available_providers)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
|
|||
let!(:twitter_connect_broadcast) { create(:twitter_connect_broadcast) }
|
||||
let!(:github_connect_broadcast) { create(:github_connect_broadcast) }
|
||||
let!(:facebook_connect_broadcast) { create(:facebook_connect_broadcast) }
|
||||
let!(:apple_connect_broadcast) { create(:apple_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) }
|
||||
|
|
@ -23,10 +24,10 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
|
|||
end
|
||||
|
||||
it "requires a valid user id" do
|
||||
expect { described_class.call(User.last.id + 100) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { described_class.call(9999) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
describe "::call" do
|
||||
describe ".call" do
|
||||
let(:user) { create(:user, :with_identity, identities: ["github"], created_at: 1.week.ago) }
|
||||
|
||||
it "does not send a notification to an unsubscribed user" do
|
||||
|
|
@ -55,16 +56,20 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
|
|||
end.to change(user.notifications, :count).by(1)
|
||||
expect(user.notifications.last.notifiable).to eq(welcome_broadcast)
|
||||
|
||||
Timecop.travel(1.day.since)
|
||||
Timecop.travel(1.day.from_now)
|
||||
expect do
|
||||
sidekiq_perform_enqueued_jobs do
|
||||
described_class.call(user.id)
|
||||
end
|
||||
end.to change(user.notifications, :count).by(1)
|
||||
not_github = [facebook_connect_broadcast, twitter_connect_broadcast].include?(user.notifications.last.notifiable)
|
||||
not_github = [
|
||||
facebook_connect_broadcast,
|
||||
twitter_connect_broadcast,
|
||||
apple_connect_broadcast
|
||||
].include?(user.notifications.last.notifiable)
|
||||
expect(not_github).to be(true)
|
||||
|
||||
Timecop.travel(1.day.since)
|
||||
Timecop.travel(1.day.from_now)
|
||||
expect do
|
||||
sidekiq_perform_enqueued_jobs do
|
||||
described_class.call(user.id)
|
||||
|
|
@ -72,7 +77,7 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
|
|||
end.to change(user.notifications, :count).by(1)
|
||||
expect(user.notifications.last.notifiable).to eq(customize_feed_broadcast)
|
||||
|
||||
Timecop.travel(2.days.since)
|
||||
Timecop.travel(2.days.from_now)
|
||||
expect do
|
||||
sidekiq_perform_enqueued_jobs do
|
||||
described_class.call(user.id)
|
||||
|
|
@ -80,7 +85,7 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
|
|||
end.to change(user.notifications, :count).by(1)
|
||||
expect(user.notifications.last.notifiable).to eq(customize_ux_broadcast)
|
||||
|
||||
Timecop.travel(1.day.since)
|
||||
Timecop.travel(1.day.from_now)
|
||||
expect do
|
||||
sidekiq_perform_enqueued_jobs do
|
||||
described_class.call(user.id)
|
||||
|
|
@ -88,13 +93,14 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
|
|||
end.to change(user.notifications, :count).by(1)
|
||||
expect(user.notifications.last.notifiable).to eq(discuss_and_ask_broadcast)
|
||||
|
||||
Timecop.travel(1.day.since)
|
||||
Timecop.travel(1.day.from_now)
|
||||
expect do
|
||||
sidekiq_perform_enqueued_jobs do
|
||||
described_class.call(user.id)
|
||||
end
|
||||
end.to change(user.notifications, :count).by(1)
|
||||
expect(user.notifications.last.notifiable).to eq(download_app_broadcast)
|
||||
|
||||
Timecop.return
|
||||
end
|
||||
# rubocop:enable RSpec/MultipleExpectations
|
||||
|
|
@ -147,20 +153,25 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
|
|||
expect(Notification).not_to have_received(:send_welcome_notification).with(user.id, github_connect_broadcast.id)
|
||||
end
|
||||
|
||||
it "does not send duplicate notifications (twitter)" do
|
||||
user = create(:user, :with_identity, identities: ["github"], created_at: 1.day.ago)
|
||||
2.times do
|
||||
sidekiq_perform_enqueued_jobs { described_class.new(user.id).__send__(:send_authentication_notification) }
|
||||
Authentication::Providers.available.each do |provider_name|
|
||||
it "does not send duplicate notifications for #{provider_name}" do
|
||||
user = create(:user, :with_identity, identities: [provider_name], created_at: 1.day.ago)
|
||||
2.times do
|
||||
sidekiq_perform_enqueued_jobs { described_class.new(user.id).__send__(:send_authentication_notification) }
|
||||
end
|
||||
expect(user.notifications.count).to eq(1)
|
||||
end
|
||||
|
||||
it "generates and sends the appropriate broadcast for a #{provider_name} identity" do
|
||||
user = create(:user, :with_identity, identities: [provider_name], created_at: 1.day.ago)
|
||||
sidekiq_perform_enqueued_jobs { described_class.new(user.id).__send__(:send_authentication_notification) }
|
||||
expect(user.notifications.first.notifiable).not_to eq(public_send("#{provider_name}_connect_broadcast"))
|
||||
end
|
||||
expect(user.notifications.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#send_feed_customization_notification" do
|
||||
let!(:user) do
|
||||
omniauth_mock_providers_payload
|
||||
create(:user, :with_identity, identities: %w[twitter github], created_at: 3.days.ago)
|
||||
end
|
||||
let!(:user) { create(:user, :with_identity, created_at: 3.days.ago) }
|
||||
|
||||
it "does not send a notification to a newly-created user" do
|
||||
user.update!(created_at: Time.current)
|
||||
|
|
@ -189,7 +200,7 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
|
|||
end
|
||||
|
||||
describe "#send_ux_customization_notification" do
|
||||
let!(:user) { create(:user, :with_identity, identities: %w[twitter github], created_at: 5.days.ago) }
|
||||
let!(:user) { create(:user, :with_identity, created_at: 5.days.ago) }
|
||||
|
||||
it "does not send a notification to a newly-created user" do
|
||||
user.update!(created_at: Time.zone.now)
|
||||
|
|
@ -212,7 +223,7 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do
|
|||
end
|
||||
|
||||
describe "#send_discuss_and_ask_notification" do
|
||||
let!(:user) { create(:user, :with_identity, identities: %w[twitter github], created_at: 6.days.ago) }
|
||||
let!(:user) { create(:user, :with_identity, created_at: 6.days.ago) }
|
||||
|
||||
let!(:ask_question_broadcast) { create(:ask_question_broadcast) }
|
||||
let!(:start_discussion_broadcast) { create(:start_discussion_broadcast) }
|
||||
|
|
|
|||
44
spec/support/fixtures/vcr_cassettes/fastly_sloan.yml
Normal file
44
spec/support/fixtures/vcr_cassettes/fastly_sloan.yml
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -67,6 +67,46 @@ module OmniauthHelpers
|
|||
},
|
||||
).freeze
|
||||
|
||||
OMNIAUTH_PAYLOAD_APPLE = OmniAuth::AuthHash::InfoHash.new(
|
||||
{
|
||||
provider: "apple",
|
||||
uid: SecureRandom.hex,
|
||||
info: {
|
||||
sub: "001551.6fb185533f63469196d61e2ae126febf.1523",
|
||||
email: "hynnu385xiabcdef@privaterelay.appleid.com",
|
||||
first_name: "fname",
|
||||
last_name: "lname"
|
||||
},
|
||||
credentials: {
|
||||
token: SecureRandom.hex,
|
||||
refresh_token: SecureRandom.hex,
|
||||
expires_at: 1_589_475_606,
|
||||
expires: true
|
||||
},
|
||||
extra: {
|
||||
raw_info: {
|
||||
iss: "https://appleid.apple.com",
|
||||
aud: "test",
|
||||
exp: 1_589_472_606,
|
||||
iat: 1_589_472_606,
|
||||
sub: "001551.6fb185533f63469196d61e2ae126febf.1523",
|
||||
at_hash: SecureRandom.hex,
|
||||
email: "hynnu385xiabcdef@privaterelay.appleid.com",
|
||||
email_verified: true,
|
||||
is_private_email: true,
|
||||
nonce_supported: true,
|
||||
name: {
|
||||
first_name: "fname",
|
||||
last_name: "lname"
|
||||
},
|
||||
id_info: {
|
||||
auth_time: 1_589_472_002
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
).freeze
|
||||
|
||||
def omniauth_setup_invalid_credentials(provider)
|
||||
OmniAuth.config.mock_auth[provider] = :invalid_credentials
|
||||
end
|
||||
|
|
@ -122,6 +162,10 @@ module OmniauthHelpers
|
|||
OmniAuth.config.mock_auth[:facebook] = OMNIAUTH_PAYLOAD_FACEBOOK.dup
|
||||
end
|
||||
|
||||
def omniauth_mock_apple_payload
|
||||
OmniAuth.config.mock_auth[:apple] = OMNIAUTH_PAYLOAD_APPLE.dup
|
||||
end
|
||||
|
||||
def omniauth_mock_github_payload
|
||||
info = OMNIAUTH_BASIC_INFO[:info].merge(
|
||||
image: "https://dummyimage.com/400x400.jpg",
|
||||
|
|
|
|||
37
spec/system/authentication/beta_access_providers_spec.rb
Normal file
37
spec/system/authentication/beta_access_providers_spec.rb
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Beta provider access", type: :system do
|
||||
let(:providers) { %w[Facebook GitHub Twitter] }
|
||||
let(:beta_providers) { ["Apple"] }
|
||||
|
||||
before do
|
||||
allow(SiteConfig).to receive(:authentication_providers).and_return(Authentication::Providers.available)
|
||||
end
|
||||
|
||||
context "when a user tries to sign_up" do
|
||||
it "doesn't render beta providers by default" do
|
||||
visit sign_up_path
|
||||
|
||||
providers.each do |name|
|
||||
expect(page).to have_button("Continue with #{name}")
|
||||
end
|
||||
|
||||
beta_providers.each do |name|
|
||||
expect(page).not_to have_button("Continue with #{name}")
|
||||
end
|
||||
end
|
||||
|
||||
it "renders beta providers when passed in the correct state param" do
|
||||
Flipper.enable(:apple_auth)
|
||||
visit sign_up_path
|
||||
|
||||
providers.each do |name|
|
||||
expect(page).to have_button("Continue with #{name}")
|
||||
end
|
||||
|
||||
beta_providers.each do |name|
|
||||
expect(page).to have_button("Continue with #{name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,6 +6,9 @@ RSpec.describe "Omniauth redirect_uri", type: :system do
|
|||
# Avoid messing with other tests by resetting back SiteConfig.app_domain
|
||||
after { SiteConfig.app_domain = test_app_domain }
|
||||
|
||||
# Apple auth is in Beta so we need to enable the Feature Flag to test it
|
||||
before { Flipper.enable(:apple_auth) }
|
||||
|
||||
def provider_redirect_regex(provider_name)
|
||||
# URL encoding translates the query params (i.e. colons/slashes/etc)
|
||||
%r{
|
||||
|
|
@ -20,7 +23,8 @@ RSpec.describe "Omniauth redirect_uri", type: :system do
|
|||
allow(SiteConfig).to receive(:authentication_providers).and_return(Authentication::Providers.available)
|
||||
visit sign_up_path
|
||||
Authentication::Providers.available.each do |provider_name|
|
||||
provider_auth_url = find("a.crayons-btn--brand-#{provider_name}")["href"]
|
||||
provider_auth_button = find("button.crayons-btn--brand-#{provider_name}")
|
||||
provider_auth_url = provider_auth_button.find(:xpath, "..")["action"]
|
||||
expect(provider_auth_url).to match(provider_redirect_regex(provider_name))
|
||||
end
|
||||
|
||||
|
|
@ -29,7 +33,8 @@ RSpec.describe "Omniauth redirect_uri", type: :system do
|
|||
|
||||
# After an update the callback_url should now match SiteConfig.app_domain
|
||||
Authentication::Providers.available.each do |provider_name|
|
||||
provider_auth_url = find("a.crayons-btn--brand-#{provider_name}")["href"]
|
||||
provider_auth_button = find("button.crayons-btn--brand-#{provider_name}")
|
||||
provider_auth_url = provider_auth_button.find(:xpath, "..")["action"]
|
||||
expect(provider_auth_url).to match(provider_redirect_regex(provider_name))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ RSpec.describe "Redirects authentication using Referer", type: :system do
|
|||
it "redirects back to the main feed (root path)" do
|
||||
visit root_path
|
||||
click_link(login_link, match: :first)
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/", ignore_query: true)
|
||||
end
|
||||
|
|
@ -31,7 +31,7 @@ RSpec.describe "Redirects authentication using Referer", type: :system do
|
|||
it "redirects back to an article page" do
|
||||
visit "/#{article.slug}"
|
||||
click_link(login_link, match: :first)
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/#{article.slug}", ignore_query: true)
|
||||
end
|
||||
|
|
@ -41,7 +41,7 @@ RSpec.describe "Redirects authentication using Referer", type: :system do
|
|||
it "redirects back to the main feed as default" do
|
||||
Capybara.current_session.driver.header "Referer", ""
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/", ignore_query: true)
|
||||
end
|
||||
|
|
@ -51,7 +51,7 @@ RSpec.describe "Redirects authentication using Referer", type: :system do
|
|||
it "redirects back to the main feed as default" do
|
||||
Capybara.current_session.driver.header "Referer", "https://example.com"
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/", ignore_query: true)
|
||||
end
|
||||
|
|
|
|||
187
spec/system/authentication/user_logs_in_with_apple_spec.rb
Normal file
187
spec/system/authentication/user_logs_in_with_apple_spec.rb
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Authenticating with Apple", vcr: { cassette_name: "fastly_sloan" } do
|
||||
let(:sign_in_link) { "Continue with Apple" }
|
||||
|
||||
before do
|
||||
omniauth_mock_apple_payload
|
||||
Flipper.enable(:apple_auth)
|
||||
allow(SiteConfig).to receive(:authentication_providers).and_return(Authentication::Providers.available)
|
||||
end
|
||||
|
||||
context "when a user is new" do
|
||||
context "when using valid credentials" do
|
||||
it "creates a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.to change(User, :count).by(1)
|
||||
end
|
||||
|
||||
it "logs in and redirects to the onboarding" do
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/onboarding", ignore_query: true)
|
||||
expect(page.html).to include("onboarding-container")
|
||||
end
|
||||
|
||||
it "remembers the user" do
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
user = User.last
|
||||
|
||||
expect(user.remember_token).to be_present
|
||||
expect(user.remember_created_at).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context "when trying to register with an already existing username" do
|
||||
it "creates a new user with a temporary username" do
|
||||
# see Authentication::Providers::Apple#new_user_data
|
||||
username = OmniAuth.config.mock_auth[:apple].info.first_name.downcase
|
||||
user = create(:user, username: username)
|
||||
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.to change(User, :count).by(1)
|
||||
|
||||
expect(page).to have_current_path("/onboarding", ignore_query: true)
|
||||
expect(User.last.username).to include(user.username)
|
||||
end
|
||||
end
|
||||
|
||||
context "when using invalid credentials" do
|
||||
let(:params) do
|
||||
'{"callback_url"=>"http://localhost:3000/users/auth/apple/callback", "state"=>"navbar_basic"}'
|
||||
end
|
||||
|
||||
before do
|
||||
omniauth_setup_invalid_credentials(:apple)
|
||||
|
||||
allow(DatadogStatsClient).to receive(:increment)
|
||||
end
|
||||
|
||||
after do
|
||||
OmniAuth.config.on_failure = OmniauthHelpers.const_get("OMNIAUTH_DEFAULT_FAILURE_HANDLER")
|
||||
end
|
||||
|
||||
it "does not create a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.not_to change(User, :count)
|
||||
end
|
||||
|
||||
it "notifies Datadog about a callback error" do
|
||||
error = OmniAuth::Strategies::OAuth2::CallbackError.new(
|
||||
"Callback error", "Error reason", "https://example.com/error"
|
||||
)
|
||||
|
||||
omniauth_setup_authentication_error(error)
|
||||
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "apple", "{}")
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
"omniauth.failure", *args
|
||||
)
|
||||
end
|
||||
|
||||
it "notifies Datadog about an OAuth unauthorized error" do
|
||||
request = double
|
||||
allow(request).to receive(:code).and_return(401)
|
||||
allow(request).to receive(:message).and_return("unauthorized")
|
||||
error = OAuth::Unauthorized.new(request)
|
||||
omniauth_setup_authentication_error(error)
|
||||
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "apple", "{}")
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
"omniauth.failure", *args
|
||||
)
|
||||
end
|
||||
|
||||
it "notifies Datadog even with no OmniAuth error present" do
|
||||
error = nil
|
||||
omniauth_setup_authentication_error(error)
|
||||
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "apple", "{}")
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
"omniauth.failure", *args
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when a validation failure occurrs" do
|
||||
before do
|
||||
# A User is invalid if their name is more than 100 chars long
|
||||
OmniAuth.config.mock_auth[:apple].info.first_name = "X" * 101
|
||||
end
|
||||
|
||||
it "does not create a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.not_to change(User, :count)
|
||||
end
|
||||
|
||||
it "redirects to the registration page" do
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/users/sign_up")
|
||||
end
|
||||
|
||||
it "logs errors" do
|
||||
allow(Honeybadger).to receive(:notify)
|
||||
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(Honeybadger).to have_received(:notify).once
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when a user already exists" do
|
||||
let!(:auth_payload) { OmniAuth.config.mock_auth[:apple] }
|
||||
let(:user) { create(:user, :with_identity, identities: [:apple]) }
|
||||
|
||||
before do
|
||||
# Apple sends nil as `first_name` and `last_name` for an existing user
|
||||
auth_payload.info.first_name = auth_payload.info.last_name = nil
|
||||
auth_payload.info.email = user.email
|
||||
end
|
||||
|
||||
after do
|
||||
sign_out user
|
||||
end
|
||||
|
||||
context "when using valid credentials" do
|
||||
it "logs in" do
|
||||
visit sign_up_path
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/?signin=true")
|
||||
end
|
||||
end
|
||||
|
||||
context "when already signed in" do
|
||||
it "redirects to the feed" do
|
||||
sign_in user
|
||||
visit user_apple_omniauth_authorize_path
|
||||
|
||||
expect(page).to have_current_path("/?signin=true")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -13,13 +13,13 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
it "creates a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.to change(User, :count).by(1)
|
||||
end
|
||||
|
||||
it "logs in and redirects to the onboarding" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/onboarding", ignore_query: true)
|
||||
expect(page.html).to include("onboarding-container")
|
||||
|
|
@ -27,7 +27,7 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
|
||||
it "remembers the user" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
user = User.last
|
||||
|
||||
|
|
@ -45,13 +45,13 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
it "creates a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.to change(User, :count).by(1)
|
||||
end
|
||||
|
||||
it "logs in and redirects to the onboarding" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/onboarding", ignore_query: true)
|
||||
expect(page.html).to include("onboarding-container")
|
||||
|
|
@ -59,7 +59,7 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
|
||||
it "remembers the user" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
user = User.last
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.to change(User, :count).by(1)
|
||||
|
||||
expect(page).to have_current_path("/onboarding", ignore_query: true)
|
||||
|
|
@ -102,16 +102,16 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
it "does not create a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.not_to change(User, :count)
|
||||
end
|
||||
|
||||
it "does not log in" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/users/sign_in")
|
||||
expect(page).to have_link(sign_in_link)
|
||||
expect(page).to have_button(sign_in_link)
|
||||
end
|
||||
|
||||
it "notifies Datadog about a callback error" do
|
||||
|
|
@ -122,7 +122,7 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
omniauth_setup_authentication_error(error, params)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "facebook", params)
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
|
|
@ -138,7 +138,7 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
omniauth_setup_authentication_error(error, params)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "facebook", params)
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
|
|
@ -151,7 +151,7 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
omniauth_setup_authentication_error(error, params)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "facebook", params)
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
|
|
@ -169,13 +169,13 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
it "does not create a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.not_to change(User, :count)
|
||||
end
|
||||
|
||||
it "redirects to the registration page" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/users/sign_up")
|
||||
end
|
||||
|
|
@ -184,7 +184,7 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
allow(Honeybadger).to receive(:notify)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(Honeybadger).to have_received(:notify).once
|
||||
end
|
||||
|
|
@ -207,7 +207,7 @@ RSpec.describe "Authenticating with Facebook" do
|
|||
context "when using valid credentials" do
|
||||
it "logs in" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/?signin=true")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ RSpec.describe "Authenticating with GitHub" do
|
|||
it "creates a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.to change(User, :count).by(1)
|
||||
end
|
||||
|
||||
it "logs in and redirects to the onboarding" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/onboarding", ignore_query: true)
|
||||
expect(page.html).to include("onboarding-container")
|
||||
|
|
@ -27,7 +27,7 @@ RSpec.describe "Authenticating with GitHub" do
|
|||
|
||||
it "remembers the user" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
user = User.last
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ RSpec.describe "Authenticating with GitHub" do
|
|||
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.to change(User, :count).by(1)
|
||||
|
||||
expect(page).to have_current_path("/onboarding", ignore_query: true)
|
||||
|
|
@ -69,16 +69,16 @@ RSpec.describe "Authenticating with GitHub" do
|
|||
it "does not create a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.not_to change(User, :count)
|
||||
end
|
||||
|
||||
it "does not log in" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/users/sign_in")
|
||||
expect(page).to have_link(sign_in_link)
|
||||
expect(page).to have_button(sign_in_link)
|
||||
end
|
||||
|
||||
it "notifies Datadog about a callback error" do
|
||||
|
|
@ -89,7 +89,7 @@ RSpec.describe "Authenticating with GitHub" do
|
|||
omniauth_setup_authentication_error(error, params)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "github", params)
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
|
|
@ -105,7 +105,7 @@ RSpec.describe "Authenticating with GitHub" do
|
|||
omniauth_setup_authentication_error(error, params)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "github", params)
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
|
|
@ -118,7 +118,7 @@ RSpec.describe "Authenticating with GitHub" do
|
|||
omniauth_setup_authentication_error(error, params)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "github", params)
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
|
|
@ -136,13 +136,13 @@ RSpec.describe "Authenticating with GitHub" do
|
|||
it "does not create a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.not_to change(User, :count)
|
||||
end
|
||||
|
||||
it "redirects to the registration page" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/users/sign_up")
|
||||
end
|
||||
|
|
@ -151,7 +151,7 @@ RSpec.describe "Authenticating with GitHub" do
|
|||
allow(Honeybadger).to receive(:notify)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(Honeybadger).to have_received(:notify)
|
||||
end
|
||||
|
|
@ -173,7 +173,7 @@ RSpec.describe "Authenticating with GitHub" do
|
|||
context "when using valid credentials" do
|
||||
it "logs in" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/?signin=true")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ RSpec.describe "Authenticating with Twitter" do
|
|||
it "creates a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.to change(User, :count).by(1)
|
||||
end
|
||||
|
||||
it "logs in and redirects to the onboarding" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/onboarding?referrer=none")
|
||||
expect(page.html).to include("onboarding-container")
|
||||
|
|
@ -27,7 +27,7 @@ RSpec.describe "Authenticating with Twitter" do
|
|||
|
||||
it "remembers the user" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
user = User.last
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ RSpec.describe "Authenticating with Twitter" do
|
|||
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.to change(User, :count).by(1)
|
||||
|
||||
expect(page).to have_current_path("/onboarding?referrer=none")
|
||||
|
|
@ -65,16 +65,16 @@ RSpec.describe "Authenticating with Twitter" do
|
|||
it "does not create a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.not_to change(User, :count)
|
||||
end
|
||||
|
||||
it "does not log in" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/users/sign_in")
|
||||
expect(page).to have_link(sign_in_link)
|
||||
expect(page).to have_button(sign_in_link)
|
||||
end
|
||||
|
||||
it "notifies Datadog about a callback error" do
|
||||
|
|
@ -85,7 +85,7 @@ RSpec.describe "Authenticating with Twitter" do
|
|||
omniauth_setup_authentication_error(error)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "twitter", "{}")
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
|
|
@ -101,7 +101,7 @@ RSpec.describe "Authenticating with Twitter" do
|
|||
omniauth_setup_authentication_error(error)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "twitter", "{}")
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
|
|
@ -114,7 +114,7 @@ RSpec.describe "Authenticating with Twitter" do
|
|||
omniauth_setup_authentication_error(error)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
args = omniauth_failure_args(error, "twitter", "{}")
|
||||
expect(DatadogStatsClient).to have_received(:increment).with(
|
||||
|
|
@ -132,13 +132,13 @@ RSpec.describe "Authenticating with Twitter" do
|
|||
it "does not create a new user" do
|
||||
expect do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
end.not_to change(User, :count)
|
||||
end
|
||||
|
||||
it "redirects to the registration page" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/users/sign_up")
|
||||
end
|
||||
|
|
@ -147,7 +147,7 @@ RSpec.describe "Authenticating with Twitter" do
|
|||
allow(Honeybadger).to receive(:notify)
|
||||
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(Honeybadger).to have_received(:notify)
|
||||
end
|
||||
|
|
@ -165,7 +165,7 @@ RSpec.describe "Authenticating with Twitter" do
|
|||
context "when using valid credentials" do
|
||||
it "logs in" do
|
||||
visit sign_up_path
|
||||
click_link(sign_in_link, match: :first)
|
||||
click_on(sign_in_link, match: :first)
|
||||
|
||||
expect(page).to have_current_path("/?signin=true")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ RSpec.describe "Link on tags for post in notifications", type: :system do
|
|||
it "shows the sign-with page", js: true do
|
||||
Authentication::Providers.enabled.each do |provider_name|
|
||||
provider = Authentication::Providers.get!(provider_name)
|
||||
next if provider.provider_name == :apple && !Flipper.enabled?(:apple_auth)
|
||||
|
||||
expect(page).to have_content("Continue with #{provider.official_name}")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ RSpec.describe "Authenticating with a password" do
|
|||
user.lock_access!
|
||||
|
||||
visit sign_up_path
|
||||
click_link("Continue with GitHub", match: :first)
|
||||
click_on("Continue with GitHub", match: :first)
|
||||
|
||||
expect(page).to have_current_path("/?signin=true")
|
||||
expect(page).not_to have_text("Your account is locked.")
|
||||
|
|
|
|||
BIN
vendor/cache/omniauth-apple-1.0.1.gem
vendored
Normal file
BIN
vendor/cache/omniauth-apple-1.0.1.gem
vendored
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue