diff --git a/.env.test b/.env.test index 9a20a69526..e1d7d47cc2 100644 --- a/.env.test +++ b/.env.test @@ -6,3 +6,6 @@ STRIPE_SECRET_TEST_API_KEY="bogus_key" STRIPE_CUSTOMER="bogus_customer" SITE_URL="test.host" + +OPENID_APP_ID="test-provider" +OPENID_APP_SECRET="12345" diff --git a/app/controllers/admin/oidc_settings_controller.rb b/app/controllers/admin/oidc_settings_controller.rb new file mode 100644 index 0000000000..f668442bc0 --- /dev/null +++ b/app/controllers/admin/oidc_settings_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Admin + class OidcSettingsController < Spree::Admin::BaseController + def index; end + end +end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb new file mode 100644 index 0000000000..b3db1607ef --- /dev/null +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class OmniauthCallbacksController < Devise::OmniauthCallbacksController + def openid_connect + spree_current_user.link_from_omniauth(request.env["omniauth.auth"]) + + redirect_to admin_oidc_settings_path + end + + def failure + error_message = request.env["omniauth.error"].to_s + flash[:error] = t("devise.oidc.failure", error: error_message) + + super + end +end diff --git a/app/models/spree/user.rb b/app/models/spree/user.rb index b032836217..7ec00897e9 100644 --- a/app/models/spree/user.rb +++ b/app/models/spree/user.rb @@ -7,8 +7,10 @@ module Spree searchable_attributes :email devise :database_authenticatable, :token_authenticatable, :registerable, :recoverable, - :rememberable, :trackable, :validatable, - :encryptable, :confirmable, encryptor: 'authlogic_sha512', reconfirmable: true + :rememberable, :trackable, :validatable, :omniauthable, + :encryptable, :confirmable, + encryptor: 'authlogic_sha512', reconfirmable: true, + omniauth_providers: [:openid_connect] has_many :orders belongs_to :ship_address, class_name: 'Spree::Address' @@ -44,6 +46,8 @@ module Spree after_create :associate_customers, :associate_orders validate :limit_owned_enterprises + validates :uid, uniqueness: true, if: lambda { uid.present? } + validates_email :uid, if: lambda { uid.present? } class DestroyWithOrdersError < StandardError; end @@ -51,6 +55,10 @@ module Spree User.admin.count > 0 end + def link_from_omniauth(auth) + update!(provider: auth.provider, uid: auth.uid) + end + # Whether a user has a role or not. def has_spree_role?(role_in_question) spree_roles.where(name: role_in_question.to_s).any? diff --git a/app/views/admin/oidc_settings/index.html.haml b/app/views/admin/oidc_settings/index.html.haml new file mode 100644 index 0000000000..4a6878eed6 --- /dev/null +++ b/app/views/admin/oidc_settings/index.html.haml @@ -0,0 +1,25 @@ +- content_for :page_title do + = t(".title") + += render 'admin/shared/enterprises_sub_menu' + +%div + %h2= t(".connect") + %br + + - if spree_current_user.provider == 'openid_connect' && spree_current_user.uid.present? + = t(".already_connected") + = spree_current_user.uid + %br + %br + + = t(".view_account") + = link_to t(".les_communs_link"), "#{ Devise.omniauth_configs[:openid_connect].options[:issuer] }/account" + + - else + = t(".link_your_account") + %br + %br + = button_to t(".link_account_button"), + Spree::Core::Engine.routes.url_helpers.spree_user_openid_connect_omniauth_authorize_path(auth_type: "login"), + method: :post diff --git a/app/views/admin/shared/_enterprises_sub_menu.html.haml b/app/views/admin/shared/_enterprises_sub_menu.html.haml index 33db103b3c..3de9f007d4 100644 --- a/app/views/admin/shared/_enterprises_sub_menu.html.haml +++ b/app/views/admin/shared/_enterprises_sub_menu.html.haml @@ -2,3 +2,5 @@ %ul#sub_nav.inline-menu{"data-hook" => "admin_enterprise_sub_tabs"} = tab :enterprises, url: main_app.admin_enterprises_path = tab :enterprise_relationships, url: main_app.admin_enterprise_relationships_path + - if ENV["OPENID_APP_ID"].present? && ENV["OPENID_APP_SECRET"].present? + = tab :oidc_settings, url: main_app.admin_oidc_settings_path diff --git a/app/views/spree/admin/shared/_tabs.html.haml b/app/views/spree/admin/shared/_tabs.html.haml index 696a3d29a6..6613a24643 100644 --- a/app/views/spree/admin/shared/_tabs.html.haml +++ b/app/views/spree/admin/shared/_tabs.html.haml @@ -4,7 +4,7 @@ = tab :orders, :subscriptions, :customer_details, :adjustments, :payments, :return_authorizations, url: admin_orders_path('q[s]' => 'completed_at desc'), icon: 'icon-shopping-cart' = tab :reports, url: main_app.admin_reports_path, icon: 'icon-file' = tab :general_settings, :mail_methods, :tax_categories, :tax_rates, :tax_settings, :zones, :countries, :states, :payment_methods, :taxonomies, :shipping_methods, :shipping_categories, :enterprise_fees, :contents, :invoice_settings, :matomo_settings, :stripe_connect_settings, label: 'configuration', icon: 'icon-wrench', url: edit_admin_general_settings_path -= tab :enterprises, :enterprise_relationships, url: main_app.admin_enterprises_path += tab :enterprises, :enterprise_relationships, :oidc_settings, url: main_app.admin_enterprises_path = tab :customers, url: main_app.admin_customers_path = tab :enterprise_groups, url: main_app.admin_enterprise_groups_path, label: 'groups' - if can? :admin, Spree::User diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index dac38f44c7..ab762473db 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -142,3 +142,25 @@ Devise::TokenAuthenticatable.setup do |config| # Defines name of the authentication token params key config.token_authentication_key = :auth_token end + +if ENV["OPENID_APP_ID"].present? && ENV["OPENID_APP_SECRET"].present? + Devise.setup do |config| + protocol = Rails.env.development? ? "http://" : "https://" + config.omniauth :openid_connect, { + name: :openid_connect, + issuer: "https://login.lescommuns.org/auth/realms/data-food-consortium", + scope: [:openid, :profile, :email], + response_type: :code, + uid_field: "email", + discovery: true, + client_auth_method: :jwks, + + client_options: { + identifier: ENV["OPENID_APP_ID"], + secret: ENV["OPENID_APP_SECRET"], + redirect_uri: "#{protocol}#{ENV["SITE_URL"]}/user/spree_user/auth/openid_connect/callback", + jwks_uri: 'https://login.lescommuns.org/auth/realms/data-food-consortium/protocol/openid-connect/certs' + } + } + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index b8fc654b0b..033f8ee6ea 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -224,7 +224,8 @@ en: updated_not_active: "Your password has been reset, but your email has not been confirmed yet." updated: "Your password was changed successfully. You are now signed in." send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." - + oidc: + failure: "Could not sign in: %{error}" home_page_alert_html: "Home page alert HTML" hub_signup_case_studies_html: "Hub signup case studies HTML" hub_signup_detail_html: "Hub signup detail HTML" @@ -1423,6 +1424,15 @@ en: formatted_data: Formatted Data packing: name: "Packing Reports" + oidc_settings: + index: + title: "OIDC Settings" + connect: "Connect Your Account" + already_connected: "Your account is already linked to this DFC authorization account:" + les_communs_link: "Les Communs Open ID server" + link_your_account: "You need first to link your account with the authorization provider used by DFC (Les Communs Open ID Connect)." + link_account_button: "Link your Les Communs OIDC Account" + view_account: "To view your account, see:" subscriptions: index: title: "Subscriptions" diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 99beb534d0..a7e900dce7 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -107,6 +107,8 @@ Openfoodnetwork::Application.routes.draw do put :unpause, on: :member end + resources :oidc_settings, only: :index + resources :subscription_line_items, only: [], format: :json do post :build, on: :collection end diff --git a/config/routes/spree.rb b/config/routes/spree.rb index df57d57bfa..48e8f99c43 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -9,12 +9,14 @@ end # Overriding Devise routes to use our own controller Spree::Core::Engine.routes.draw do devise_for :spree_user, + :router_name => "spree", :class_name => 'Spree::User', :controllers => { :sessions => 'spree/user_sessions', :registrations => 'user_registrations', :passwords => 'user_passwords', - :confirmations => 'user_confirmations'}, - :skip => [:unlocks, :omniauth_callbacks], + :confirmations => 'user_confirmations', + :omniauth_callbacks => "omniauth_callbacks" }, + :skip => [:unlocks], :path_names => { :sign_out => 'logout' }, :path_prefix => :user diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index 881d07b94b..a99a18941d 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -240,4 +240,18 @@ describe Spree::User do expect(user.disabled).to be_falsey end end + + describe "#link_from_omniauth" do + let!(:user) { create(:user, email: "user@email.com") } + let(:auth) { double(:auth, provider: "openid_connect", uid: "user@email.com") } + + it "creates a user without errors" do + user.link_from_omniauth(auth) + + expect(user.errors.present?).to be false + expect(user.confirmed?).to be true + expect(user.provider).to eq "openid_connect" + expect(user.uid).to eq "user@email.com" + end + end end diff --git a/spec/requests/omniauth_callbacks_controller_spec.rb b/spec/requests/omniauth_callbacks_controller_spec.rb new file mode 100644 index 0000000000..b96e79e7b0 --- /dev/null +++ b/spec/requests/omniauth_callbacks_controller_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe OmniauthCallbacksController, type: :request do + include AuthenticationHelper + + OmniAuth.config.test_mode = true + + subject do + login_as user + post '/user/spree_user/auth/openid_connect/callback', params: { code: 'code123' } + + request.env['devise.mapping'] = Devise.mappings[:spree_user] + request.env['omniauth.auth'] = omniauth_response + end + + let(:user) { create(:user) } + + context 'when the omniauth setup is returning with an authorization' do + let!(:omniauth_response) do + OmniAuth.config.mock_auth[:openid_connect] = OmniAuth::AuthHash.new( + 'provider' => 'openid_connect', + 'uid' => 'john@doe.com', + 'info' => { + 'email' => 'john@doe.com', + 'first_name' => 'John', + 'last_name' => 'Doe' + } + ) + end + + it 'is successful' do + subject + + expect(user.provider).to eq "openid_connect" + expect(user.uid).to eq "john@doe.com" + expect(request.cookies[:omniauth_connect]).to be_nil + expect(response.status).to eq(302) + end + end + + context 'when the omniauth openid_connect is mocked with an error' do + let!(:omniauth_response) do + OmniAuth.config.mock_auth[:openid_connect] = :invalid_credentials + end + + it 'fails with bad auth data' do + subject + + expect(user.provider).to be_nil + expect(user.uid).to be_nil + expect(request.cookies[:omniauth_connect]).to be_nil + expect(response.status).to eq(302) + end + end +end