From 01950b48a18a9e26fcbf27aa8596dcc0724f89cc Mon Sep 17 00:00:00 2001 From: Patrick Gansterer Date: Sun, 24 Sep 2017 17:27:23 +0200 Subject: [PATCH] Add Discourse SSO to allow login into Discourse via Foodsoft --- app/controllers/application_controller.rb | 16 +++--- plugins/discourse/README.md | 16 +++--- .../app/controllers/discourse_controller.rb | 50 +++++-------------- .../controllers/discourse_login_controller.rb | 43 ++++++++++++++++ .../controllers/discourse_sso_controller.rb | 25 ++++++++++ .../sessions/new/insert_link.html.haml.deface | 2 +- plugins/discourse/config/routes.rb | 5 +- .../foodsoft_discourse/redirect_to_login.rb | 7 ++- 8 files changed, 110 insertions(+), 54 deletions(-) create mode 100644 plugins/discourse/app/controllers/discourse_login_controller.rb create mode 100644 plugins/discourse/app/controllers/discourse_sso_controller.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 38676b30..16bb2d2e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -142,15 +142,19 @@ class ApplicationController < ActionController::Base # end # def require_plugin_enabled(plugin) - unless plugin.enabled? - redirect_to root_path, alert: I18n.t('application.controller.error_feature_disabled') - end + redirect_to_root_with_feature_disabled_alert unless plugin.enabled? + end + + def require_config_enabled(config) + redirect_to_root_with_feature_disabled_alert unless FoodsoftConfig[config] end def require_config_disabled(config) - if FoodsoftConfig[config] - redirect_to root_path, alert: I18n.t('application.controller.error_feature_disabled') - end + redirect_to_root_with_feature_disabled_alert if FoodsoftConfig[config] + end + + def redirect_to_root_with_feature_disabled_alert + redirect_to root_path, alert: I18n.t('application.controller.error_feature_disabled') end # Redirect to the login page, used in authenticate, plugins can override this. diff --git a/plugins/discourse/README.md b/plugins/discourse/README.md index 4112bfc4..0009c491 100644 --- a/plugins/discourse/README.md +++ b/plugins/discourse/README.md @@ -1,8 +1,8 @@ FoodsoftDiscourse ================= -This plugin adds the possibility to log in via Discourse. A new button is added -to the login screen. +This plugin adds the possibility to log in via Discourse or act as and SSO +provider for Discourse. A new button is added to the login screen if enabled. This plugin is enabled by default in foodsoft, so you don't need to do anything to install it. If you still want to, for example when it has been disabled, @@ -13,10 +13,14 @@ gem 'foodsoft_discourse', path: 'plugins/foodsoft_discourse' ``` This plugin introduces the foodcoop config option `discourse_url`, which takes -the URL fo the Discourse installation (e.g. `https://forum.example.com`) and the -config option `discourse_sso_secret`, which must be set to the same values as -configured in the `sso secret` setting of the Discourse installation. The plugin -will be disabled if not both config options are set. +the URL for the Discourse installation (e.g. `https://forum.example.com`) and +the config option `discourse_sso_secret`, which must be set to the same values +as configured in the `sso secret` setting of the Discourse installation. The +plugin will be disabled if not both config options are set. + +If `discourse_sso` is set to `true` Foodsoft will act as an SSO provider for +Discourse. The `sso url` for Discourse is `/discourse/sso` relative to root url +of Foodsoft (e.g. `https://foodsoft.example.com/f/discourse/sso`). This plugin is part of the foodsoft package and uses the GPL-3 license (see foodsoft's LICENSE for the full license text). diff --git a/plugins/discourse/app/controllers/discourse_controller.rb b/plugins/discourse/app/controllers/discourse_controller.rb index 037fed4c..cf98613d 100644 --- a/plugins/discourse/app/controllers/discourse_controller.rb +++ b/plugins/discourse/app/controllers/discourse_controller.rb @@ -1,50 +1,26 @@ class DiscourseController < ApplicationController before_filter -> { require_plugin_enabled FoodsoftDiscourse } - skip_before_filter :authenticate - def initiate - discourse_url = FoodsoftConfig[:discourse_url] + protected - nonce = SecureRandom.hex() - return_sso_url = url_for(action: :callback, only_path: false) - payload = "nonce=#{nonce}&return_sso_url=#{return_sso_url}" - base64_payload = Base64.encode64 payload - sso = URI.escape base64_payload + def valid_signature? + return false if params[:sso].blank? || params[:sig].blank? + get_hmac_hex_string(params[:sso]) == params[:sig] + end + + def redirect_to_with_payload(url, payload) + base64_payload = Base64.encode64 payload.to_query + sso = CGI::escape base64_payload sig = get_hmac_hex_string base64_payload - - session[:discourse_sso_nonce] = nonce - redirect_to "#{discourse_url}/session/sso_provider?sso=#{sso}&sig=#{sig}" + redirect_to "#{url}#{url.include?('?') ? '&' : '?'}sso=#{sso}&sig=#{sig}" end - def callback - raise I18n.t('discourse.callback.invalid_signature') if get_hmac_hex_string(params[:sso]) != params[:sig] - - info = Rack::Utils.parse_query(Base64.decode64(params[:sso])) - info.symbolize_keys! - - raise I18n.t('discourse.callback.invalid_nonce') if info[:nonce] != session[:discourse_sso_nonce] - session[:discourse_sso_nonce] = nil - - id = info[:external_id].to_i - user = User.find_or_initialize_by(id: id) do |user| - user.id = id - user.password = SecureRandom.random_bytes(25) - end - user.nick = info[:username] - user.email = info[:email] - user.first_name = info[:name] - user.last_name = '' - user.last_login = Time.now - user.save! - - login_and_redirect_to_return_to user, :notice => I18n.t('discourse.callback.logged_in') - rescue => error - redirect_to login_url, :alert => error.to_s + def parse_payload + payload = Rack::Utils.parse_query Base64.decode64(params[:sso]) + payload.symbolize_keys! end - private - def get_hmac_hex_string payload discourse_sso_secret = FoodsoftConfig[:discourse_sso_secret] OpenSSL::HMAC.hexdigest 'sha256', discourse_sso_secret, payload diff --git a/plugins/discourse/app/controllers/discourse_login_controller.rb b/plugins/discourse/app/controllers/discourse_login_controller.rb new file mode 100644 index 00000000..e5717d2a --- /dev/null +++ b/plugins/discourse/app/controllers/discourse_login_controller.rb @@ -0,0 +1,43 @@ +class DiscourseLoginController < DiscourseController + + before_filter -> { require_config_disabled :discourse_sso } + skip_before_filter :authenticate + + def initiate + discourse_url = FoodsoftConfig[:discourse_url] + + nonce = SecureRandom.hex() + return_sso_url = url_for(action: :callback, only_path: false) + + session[:discourse_sso_nonce] = nonce + redirect_to_with_payload "#{discourse_url}/session/sso_provider", + nonce: nonce, + return_sso_url: return_sso_url + end + + def callback + raise I18n.t('discourse.callback.invalid_signature') unless valid_signature? + + payload = parse_payload + + raise I18n.t('discourse.callback.invalid_nonce') if payload[:nonce] != session[:discourse_sso_nonce] + session[:discourse_sso_nonce] = nil + + id = payload[:external_id].to_i + user = User.find_or_initialize_by(id: id) do |user| + user.id = id + user.password = SecureRandom.random_bytes(25) + end + user.nick = payload[:username] + user.email = payload[:email] + user.first_name = payload[:name] + user.last_name = '' + user.last_login = Time.now + user.save! + + login_and_redirect_to_return_to user, notice: I18n.t('discourse.callback.logged_in') + rescue => error + redirect_to login_url, alert: error.to_s + end + +end diff --git a/plugins/discourse/app/controllers/discourse_sso_controller.rb b/plugins/discourse/app/controllers/discourse_sso_controller.rb new file mode 100644 index 00000000..9bab89dc --- /dev/null +++ b/plugins/discourse/app/controllers/discourse_sso_controller.rb @@ -0,0 +1,25 @@ +class DiscourseSsoController < DiscourseController + + before_filter -> { require_config_enabled :discourse_sso } + + def sso + raise I18n.t('discourse.sso.invalid_signature') unless valid_signature? + + payload = parse_payload + nonce = payload[:nonce] + return_sso_url = payload[:return_sso_url] || "#{discourse_url}/session/sso_login" + + raise I18n.t('discourse.sso.nonce_missing') if nonce.blank? + + redirect_to_with_payload return_sso_url, + nonce: nonce, + email: current_user.email, + require_activation: true, + external_id: "#{FoodsoftConfig.scope}/#{current_user.id}", + username: current_user.nick, + name: current_user.name + rescue => error + redirect_to root_url, alert: error.to_s + end + +end diff --git a/plugins/discourse/app/overrides/sessions/new/insert_link.html.haml.deface b/plugins/discourse/app/overrides/sessions/new/insert_link.html.haml.deface index 385db468..970d49a7 100644 --- a/plugins/discourse/app/overrides/sessions/new/insert_link.html.haml.deface +++ b/plugins/discourse/app/overrides/sessions/new/insert_link.html.haml.deface @@ -1,5 +1,5 @@ / insert_after 'noscript' -- if FoodsoftDiscourse.enabled? +- if FoodsoftDiscourse.enabled? && !FoodsoftConfig[:discourse_sso] .center = link_to t('.discourse_login'), discourse_initiate_path, class: 'btn btn-primary' %hr diff --git a/plugins/discourse/config/routes.rb b/plugins/discourse/config/routes.rb index dde2ff15..da408523 100644 --- a/plugins/discourse/config/routes.rb +++ b/plugins/discourse/config/routes.rb @@ -2,8 +2,9 @@ Rails.application.routes.draw do scope '/:foodcoop' do - get '/discourse/callback' => 'discourse#callback' - get '/discourse/initiate' => 'discourse#initiate' + get '/discourse/callback' => 'discourse_login#callback' + get '/discourse/initiate' => 'discourse_login#initiate' + get '/discourse/sso' => 'discourse_sso#sso' end diff --git a/plugins/discourse/lib/foodsoft_discourse/redirect_to_login.rb b/plugins/discourse/lib/foodsoft_discourse/redirect_to_login.rb index e54db131..54326921 100644 --- a/plugins/discourse/lib/foodsoft_discourse/redirect_to_login.rb +++ b/plugins/discourse/lib/foodsoft_discourse/redirect_to_login.rb @@ -7,8 +7,11 @@ module FoodsoftDiscourse alias orig_redirect_to_login redirect_to_login def redirect_to_login(options={}) - return orig_redirect_to_login(options) unless FoodsoftDiscourse.enabled? - redirect_to discourse_initiate_path + if FoodsoftDiscourse.enabled? && !FoodsoftConfig[:discourse_sso] + redirect_to discourse_initiate_path + else + orig_redirect_to_login(options) + end end end