From fc5f0f2ec064710caba8aa2aa693e0a515f97690 Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Tue, 11 Oct 2022 22:15:30 +0100 Subject: [PATCH] Add support for Apple authentication Closes #2799 --- Gemfile | 1 + Gemfile.lock | 15 +++ app/assets/images/auth_providers/apple.svg | 3 + app/controllers/users_controller.rb | 2 +- config/initializers/omniauth.rb | 6 + config/locales/en.yml | 4 + config/settings.yml | 4 + config/settings/test.yml | 4 + lib/auth.rb | 1 + test/integration/login_test.rb | 150 +++++++++++++++++++++ test/integration/user_creation_test.rb | 144 ++++++++++++++++++++ 11 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 app/assets/images/auth_providers/apple.svg diff --git a/Gemfile b/Gemfile index 793ae931b..3a77fe087 100644 --- a/Gemfile +++ b/Gemfile @@ -79,6 +79,7 @@ gem "rack-uri_sanitizer" # Omniauth for authentication gem "multi_json" gem "omniauth", "~> 2.1.3" +gem "omniauth-apple" gem "omniauth-facebook" gem "omniauth-github" gem "omniauth-google-oauth2", ">= 0.6.0" diff --git a/Gemfile.lock b/Gemfile.lock index da3d4d7c9..a67374301 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,6 +88,7 @@ GEM uri (>= 0.13.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) + aes_key_wrap (1.1.0) annotaterb (4.18.0) activerecord (>= 6.0.0) activesupport (>= 6.0.0) @@ -130,6 +131,7 @@ GEM parser (>= 2.4) smart_properties bigdecimal (3.2.2) + bindata (2.5.1) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) bootsnap (1.18.6) @@ -279,6 +281,8 @@ GEM faraday-net_http (>= 2.0, < 3.5) json logger + faraday-follow_redirects (0.3.0) + faraday (>= 1, < 3) faraday-http-cache (2.5.1) faraday (>= 0.8) faraday-net_http (3.4.1) @@ -359,6 +363,13 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (2.13.2) + json-jwt (1.16.7) + activesupport (>= 4.2) + aes_key_wrap + base64 + bindata + faraday (~> 2.0) + faraday-follow_redirects jwt (2.10.2) base64 kgio (2.11.4) @@ -438,6 +449,9 @@ GEM hashie (>= 3.4.6) rack (>= 2.2.3) rack-protection + omniauth-apple (1.3.0) + json-jwt + omniauth-oauth2 omniauth-facebook (10.0.0) bigdecimal omniauth-oauth2 (>= 1.2, < 3) @@ -759,6 +773,7 @@ DEPENDENCIES minitest-focus multi_json omniauth (~> 2.1.3) + omniauth-apple omniauth-facebook omniauth-github omniauth-google-oauth2 (>= 0.6.0) diff --git a/app/assets/images/auth_providers/apple.svg b/app/assets/images/auth_providers/apple.svg new file mode 100644 index 000000000..82b0cddcd --- /dev/null +++ b/app/assets/images/auth_providers/apple.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a427d2bec..605f6dc03 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -118,7 +118,7 @@ class UsersController < ApplicationController email = auth_info[:info][:email] email_verified = case provider - when "google", "facebook", "microsoft", "github", "wikipedia" + when "google", "apple", "facebook", "microsoft", "github", "wikipedia" true else false diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index eb11d61aa..ce0c7773d 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -5,6 +5,7 @@ OmniAuth.config.failure_raise_out_environments = [] OmniAuth.config.allowed_request_methods = [:post, :patch] google_options = { :name => "google", :scope => "email", :access_type => "online" } +apple_options = { :name => "apple", :scope => "email name" } facebook_options = { :name => "facebook", :scope => "email", :client_options => { :site => "https://graph.facebook.com/v17.0", :authorize_url => "https://www.facebook.com/v17.0/dialog/oauth" } } microsoft_options = { :name => "microsoft", :scope => "openid User.Read" } github_options = { :name => "github", :scope => "user:email" } @@ -12,8 +13,13 @@ wikipedia_options = { :name => "wikipedia", :client_options => { :site => "https google_options[:openid_realm] = Settings.google_openid_realm if Settings.key?(:google_openid_realm) +apple_options[:team_id] = Settings.apple_team_id if Settings.key?(:apple_team_id) +apple_options[:key_id] = Settings.apple_key_id if Settings.key?(:apple_key_id) +apple_options[:pem] = Settings.apple_private_key if Settings.key?(:apple_private_key) + Rails.application.config.middleware.use OmniAuth::Builder do provider :google_oauth2, Settings.google_auth_id, Settings.google_auth_secret, google_options if Settings.key?(:google_auth_id) + provider :apple, Settings.apple_auth_id, "", apple_options if Settings.key?(:apple_auth_id) provider :facebook, Settings.facebook_auth_id, Settings.facebook_auth_secret, facebook_options if Settings.key?(:facebook_auth_id) provider :microsoft_graph, Settings.microsoft_auth_id, Settings.microsoft_auth_secret, microsoft_options if Settings.key?(:microsoft_auth_id) provider :github, Settings.github_auth_id, Settings.github_auth_secret, github_options if Settings.key?(:github_auth_id) diff --git a/config/locales/en.yml b/config/locales/en.yml index 93f13dd64..6fb41ff6f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -225,6 +225,7 @@ en: none: None openid: OpenID google: Google + apple: Apple facebook: Facebook microsoft: Microsoft github: GitHub @@ -2893,6 +2894,9 @@ en: google: title: Log in with Google alt: Google logo + apple: + title: Login with Apple + alt: Apple logo facebook: title: Log in with Facebook alt: Facebook logo diff --git a/config/settings.yml b/config/settings.yml index 8f96be94c..4368a5750 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -163,6 +163,10 @@ linkify_wiki_optional_path_prefix: "^/wiki(?=/[A-Z])" #microsoft_auth_secret: "" #wikipedia_auth_id: "" #wikipedia_auth_secret: "" +#apple_auth_id: "" +#apple_team_id: "" +#apple_key_id: "" +#apple_private_key: "" # Thunderforest authentication details #thunderforest_key: "" # Tracestrack authentication details diff --git a/config/settings/test.yml b/config/settings/test.yml index 3cf8c2836..e1ca1a982 100644 --- a/config/settings/test.yml +++ b/config/settings/test.yml @@ -12,6 +12,10 @@ github_auth_id: "dummy" github_auth_secret: "dummy" wikipedia_auth_id: "dummy" wikipedia_auth_secret: "dummy" +apple_auth_id: "dummy" +apple_team_id: "dummy" +apple_key_id: "dummy" +apple_private_key: "dummy" # Server URL for testing server_url: "test.host" # Storage services for testing diff --git a/lib/auth.rb b/lib/auth.rb index 63dc89f8f..16efe6d85 100644 --- a/lib/auth.rb +++ b/lib/auth.rb @@ -1,6 +1,7 @@ module Auth @providers = [] @providers << "google" if Settings.key?(:google_auth_id) + @providers << "apple" if Settings.key?(:apple_auth_id) @providers << "facebook" if Settings.key?(:facebook_auth_id) @providers << "microsoft" if Settings.key?(:microsoft_auth_id) @providers << "github" if Settings.key?(:github_auth_id) diff --git a/test/integration/login_test.rb b/test/integration/login_test.rb index 06b2d9e05..ab1b96f17 100644 --- a/test/integration/login_test.rb +++ b/test/integration/login_test.rb @@ -7,6 +7,7 @@ class LoginTest < ActionDispatch::IntegrationTest def teardown OmniAuth.config.mock_auth[:google] = nil + OmniAuth.config.mock_auth[:apple] = nil OmniAuth.config.mock_auth[:facebook] = nil OmniAuth.config.mock_auth[:microsoft] = nil OmniAuth.config.mock_auth[:github] = nil @@ -522,6 +523,155 @@ class LoginTest < ActionDispatch::IntegrationTest assert_equal "987654321", u.auth_uid end + def test_login_apple_success + user = create(:user, :auth_provider => "apple", :auth_uid => "1234567890") + OmniAuth.config.add_mock(:apple, :uid => user.auth_uid, :extra => { + :id_info => { "openid_id" => "http://localhost:1123/fred.bloggs" } + }) + + get "/login", :params => { :referer => "/history" } + assert_redirected_to login_path("cookie_test" => "true", "referer" => "/history") + follow_redirect! + assert_response :success + assert_template "sessions/new" + post auth_path(:provider => "apple", :origin => "/login?referer=%2Fhistory", :referer => "/history") + assert_redirected_to auth_success_path(:provider => "apple") + follow_redirect! + assert_response :redirect + follow_redirect! + assert_response :success + assert_template "changesets/history" + assert_select "span.username", user.display_name + end + + def test_login_apple_pending + user = create(:user, :pending, :auth_provider => "apple", :auth_uid => "1234567890") + OmniAuth.config.add_mock(:apple, :uid => user.auth_uid, :extra => { + :id_info => { "openid_id" => "http://localhost:1123/fred.bloggs" } + }) + + get "/login", :params => { :referer => "/history" } + assert_redirected_to login_path("cookie_test" => "true", "referer" => "/history") + follow_redirect! + assert_response :success + assert_template "sessions/new" + post auth_path(:provider => "apple", :origin => "/login?referer=%2Fhistory", :referer => "/history") + assert_redirected_to auth_success_path(:provider => "apple") + follow_redirect! + assert_response :redirect + follow_redirect! + assert_response :success + assert_template "confirm" + end + + def test_login_apple_suspended + user = create(:user, :suspended, :auth_provider => "apple", :auth_uid => "1234567890") + OmniAuth.config.add_mock(:apple, :uid => user.auth_uid, :extra => { + :id_info => { "openid_id" => "http://localhost:1123/fred.bloggs" } + }) + + get "/login", :params => { :referer => "/history" } + assert_redirected_to login_path("cookie_test" => "true", "referer" => "/history") + follow_redirect! + assert_response :success + assert_template "sessions/new" + post auth_path(:provider => "apple", :origin => "/login?referer=%2Fhistory", :referer => "/history") + assert_redirected_to auth_success_path(:provider => "apple") + follow_redirect! + assert_response :redirect + follow_redirect! + assert_response :success + assert_template "sessions/new" + assert_select "span.username", false + assert_select "div.alert.alert-danger", /your account has been suspended/ do + assert_select "a[href='mailto:openstreetmap@example.com']", "support" + end + end + + def test_login_apple_blocked + user = create(:user, :auth_provider => "apple", :auth_uid => "1234567890") + create(:user_block, :needs_view, :user => user) + OmniAuth.config.add_mock(:apple, :uid => user.auth_uid, :extra => { + :id_info => { "openid_id" => "http://localhost:1123/fred.bloggs" } + }) + + get "/login", :params => { :referer => "/history" } + assert_redirected_to login_path("cookie_test" => "true", "referer" => "/history") + follow_redirect! + assert_response :success + assert_template "sessions/new" + post auth_path(:provider => "apple", :origin => "/login?referer=%2Fhistory", :referer => "/history") + assert_redirected_to auth_success_path(:provider => "apple") + follow_redirect! + assert_response :redirect + follow_redirect! + assert_response :success + assert_template "user_blocks/show" + assert_select "span.username", user.display_name + end + + def test_login_apple_connection_failed + OmniAuth.config.mock_auth[:apple] = :connection_failed + + get "/login", :params => { :referer => "/history" } + assert_redirected_to login_path("cookie_test" => "true", "referer" => "/history") + follow_redirect! + assert_response :success + assert_template "sessions/new" + post auth_path(:provider => "apple", :origin => "/login?referer=%2Fhistory", :referer => "/history") + assert_redirected_to auth_success_path(:provider => "apple") + follow_redirect! + assert_redirected_to auth_failure_path(:strategy => "apple", :message => "connection_failed", :origin => "/login?referer=%2Fhistory") + follow_redirect! + assert_response :redirect + follow_redirect! + assert_response :success + assert_template "sessions/new" + assert_select "div.alert.alert-danger", "Connection to authentication provider failed" + assert_select "span.username", false + end + + def test_login_apple_invalid_credentials + OmniAuth.config.mock_auth[:apple] = :invalid_credentials + + get "/login", :params => { :referer => "/history" } + assert_redirected_to login_path("cookie_test" => "true", "referer" => "/history") + follow_redirect! + assert_response :success + assert_template "sessions/new" + post auth_path(:provider => "apple", :origin => "/login?referer=%2Fhistory", :referer => "/history") + assert_redirected_to auth_success_path(:provider => "apple") + follow_redirect! + assert_redirected_to auth_failure_path(:strategy => "apple", :message => "invalid_credentials", :origin => "/login?referer=%2Fhistory") + follow_redirect! + assert_response :redirect + follow_redirect! + assert_response :success + assert_template "sessions/new" + assert_select "div.alert.alert-danger", "Invalid authentication credentials" + assert_select "span.username", false + end + + def test_login_apple_unknown + OmniAuth.config.add_mock(:apple, :uid => "987654321", :extra => { + :id_info => { "openid_id" => "http://localhost:1123/fred.bloggs" } + }) + + get "/login", :params => { :referer => "/history" } + assert_redirected_to login_path("cookie_test" => "true", "referer" => "/history") + follow_redirect! + assert_response :success + assert_template "sessions/new" + post auth_path(:provider => "apple", :origin => "/login?referer=%2Fhistory", :referer => "/history") + assert_redirected_to auth_success_path(:provider => "apple") + follow_redirect! + assert_response :redirect + follow_redirect! + assert_response :success + assert_template "users/new" + assert_select "span.username", false + end + def test_login_facebook_success user = create(:user, :auth_provider => "facebook", :auth_uid => "1234567890") OmniAuth.config.add_mock(:facebook, :uid => user.auth_uid) diff --git a/test/integration/user_creation_test.rb b/test/integration/user_creation_test.rb index 1ed168953..fa1f78b76 100644 --- a/test/integration/user_creation_test.rb +++ b/test/integration/user_creation_test.rb @@ -9,6 +9,7 @@ class UserCreationTest < ActionDispatch::IntegrationTest def teardown OmniAuth.config.mock_auth[:google] = nil + OmniAuth.config.mock_auth[:apple] = nil OmniAuth.config.mock_auth[:facebook] = nil OmniAuth.config.mock_auth[:microsoft] = nil OmniAuth.config.mock_auth[:github] = nil @@ -361,6 +362,149 @@ class UserCreationTest < ActionDispatch::IntegrationTest assert_template "site/welcome" end + def test_user_create_apple_success + new_email = "newtester-apple@osm.org" + email_hmac = UsersController.message_hmac(new_email) + display_name = "new_tester-apple" + auth_uid = "123454321" + + OmniAuth.config.add_mock(:apple, + :uid => auth_uid, + :info => { :email => new_email, :name => display_name }) + + assert_difference("User.count") do + assert_no_difference("ActionMailer::Base.deliveries.size") do + perform_enqueued_jobs do + post auth_path(:provider => "apple", :origin => "/user/new") + assert_redirected_to auth_success_path(:provider => "apple") + follow_redirect! + assert_redirected_to :controller => :users, :action => "new", :nickname => display_name, + :email => new_email, :email_hmac => email_hmac, + :auth_provider => "apple", :auth_uid => auth_uid + post "/user", + :params => { :user => { :email => new_email, + :display_name => display_name, + :auth_provider => "apple", + :auth_uid => auth_uid }, + :email_hmac => email_hmac } + assert_redirected_to welcome_path + follow_redirect! + end + end + end + + # Check the page + assert_response :success + assert_template "site/welcome" + + ActionMailer::Base.deliveries.clear + end + + def test_user_create_apple_duplicate_email + dup_user = create(:user) + display_name = "new_tester-apple" + auth_uid = "123454321" + + OmniAuth.config.add_mock(:apple, + :uid => auth_uid, + :info => { :email => dup_user.email, :name => display_name }) + + post auth_path(:provider => "apple", :origin => "/user/new") + assert_redirected_to auth_success_path(:provider => "apple") + follow_redirect! + assert_redirected_to :controller => :users, :action => "new", :nickname => display_name, :email => dup_user.email, + :email_hmac => UsersController.message_hmac(dup_user.email), + :auth_provider => "apple", :auth_uid => auth_uid + follow_redirect! + + assert_response :success + assert_template "users/new" + assert_select "form > div > input.is-invalid#user_email" + + ActionMailer::Base.deliveries.clear + end + + def test_user_create_apple_failure + OmniAuth.config.mock_auth[:apple] = :connection_failed + + assert_difference("User.count", 0) do + assert_difference("ActionMailer::Base.deliveries.size", 0) do + perform_enqueued_jobs do + post auth_path(:provider => "apple", :origin => "/user/new") + assert_response :redirect + follow_redirect! + assert_redirected_to auth_failure_path(:strategy => "apple", :message => "connection_failed", :origin => "/user/new") + follow_redirect! + assert_redirected_to "/user/new" + end + end + end + + ActionMailer::Base.deliveries.clear + end + + def test_user_create_apple_redirect + orig_email = "redirect_tester_apple_orig@apple.com" + email_hmac = UsersController.message_hmac(orig_email) + new_email = "redirect_tester_apple@osm.org" + display_name = "redirect_tester_apple" + auth_uid = "123454321" + + OmniAuth.config.add_mock(:apple, + :uid => auth_uid, + :info => { :email => orig_email, :name => display_name }) + + assert_difference("User.count") do + assert_difference("ActionMailer::Base.deliveries.size", 1) do + perform_enqueued_jobs do + post auth_path(:provider => "apple", :origin => "/user/new") + assert_redirected_to auth_success_path(:provider => "apple") + follow_redirect! + assert_redirected_to :controller => :users, :action => "new", :nickname => display_name, + :email => orig_email, :email_hmac => email_hmac, + :auth_provider => "apple", :auth_uid => auth_uid + follow_redirect! + post "/user", + :params => { :user => { :email => new_email, + :email_hmac => email_hmac, + :display_name => display_name, + :auth_provider => "apple", + :auth_uid => auth_uid } } + assert_response :redirect + follow_redirect! + end + end + end + + # Check the e-mail + register_email = ActionMailer::Base.deliveries.first + + assert_equal register_email.to.first, new_email + # Check that the confirm account url is correct + confirm_regex = Regexp.new("confirm_string=([a-zA-Z0-9%_-]*)") + email_text_parts(register_email).each do |part| + assert_match confirm_regex, part.body.to_s + end + confirm_string = CGI.unescape(email_text_parts(register_email).first.body.match(confirm_regex)[1]) + + # Check the page + assert_response :success + assert_template "confirmations/confirm" + + ActionMailer::Base.deliveries.clear + + # Go to the confirmation page + get "/user/#{display_name}/confirm", :params => { :referer => "/welcome", :confirm_string => confirm_string } + assert_response :success + assert_template "confirmations/confirm" + + post "/user/#{display_name}/confirm", :params => { :referer => "/welcome", :confirm_string => confirm_string } + assert_response :redirect + follow_redirect! + assert_response :success + assert_template "site/welcome" + end + def test_user_create_facebook_success new_email = "newtester-facebook@osm.org" email_hmac = UsersController.message_hmac(new_email) -- 2.39.5