Add Discourse SSO to allow login into Discourse via Foodsoft
This commit is contained in:
parent
b5e5d7d246
commit
01950b48a1
8 changed files with 110 additions and 54 deletions
|
@ -142,15 +142,19 @@ class ApplicationController < ActionController::Base
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
def require_plugin_enabled(plugin)
|
def require_plugin_enabled(plugin)
|
||||||
unless plugin.enabled?
|
redirect_to_root_with_feature_disabled_alert unless plugin.enabled?
|
||||||
redirect_to root_path, alert: I18n.t('application.controller.error_feature_disabled')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_config_enabled(config)
|
||||||
|
redirect_to_root_with_feature_disabled_alert unless FoodsoftConfig[config]
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_config_disabled(config)
|
def require_config_disabled(config)
|
||||||
if FoodsoftConfig[config]
|
redirect_to_root_with_feature_disabled_alert if FoodsoftConfig[config]
|
||||||
redirect_to root_path, alert: I18n.t('application.controller.error_feature_disabled')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def redirect_to_root_with_feature_disabled_alert
|
||||||
|
redirect_to root_path, alert: I18n.t('application.controller.error_feature_disabled')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Redirect to the login page, used in authenticate, plugins can override this.
|
# Redirect to the login page, used in authenticate, plugins can override this.
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
FoodsoftDiscourse
|
FoodsoftDiscourse
|
||||||
=================
|
=================
|
||||||
|
|
||||||
This plugin adds the possibility to log in via Discourse. A new button is added
|
This plugin adds the possibility to log in via Discourse or act as and SSO
|
||||||
to the login screen.
|
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
|
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,
|
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
|
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
|
the URL for the Discourse installation (e.g. `https://forum.example.com`) and
|
||||||
config option `discourse_sso_secret`, which must be set to the same values as
|
the config option `discourse_sso_secret`, which must be set to the same values
|
||||||
configured in the `sso secret` setting of the Discourse installation. The plugin
|
as configured in the `sso secret` setting of the Discourse installation. The
|
||||||
will be disabled if not both config options are set.
|
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
|
This plugin is part of the foodsoft package and uses the GPL-3 license (see
|
||||||
foodsoft's LICENSE for the full license text).
|
foodsoft's LICENSE for the full license text).
|
||||||
|
|
|
@ -1,49 +1,25 @@
|
||||||
class DiscourseController < ApplicationController
|
class DiscourseController < ApplicationController
|
||||||
|
|
||||||
before_filter -> { require_plugin_enabled FoodsoftDiscourse }
|
before_filter -> { require_plugin_enabled FoodsoftDiscourse }
|
||||||
skip_before_filter :authenticate
|
|
||||||
|
|
||||||
def initiate
|
protected
|
||||||
discourse_url = FoodsoftConfig[:discourse_url]
|
|
||||||
|
|
||||||
nonce = SecureRandom.hex()
|
def valid_signature?
|
||||||
return_sso_url = url_for(action: :callback, only_path: false)
|
return false if params[:sso].blank? || params[:sig].blank?
|
||||||
payload = "nonce=#{nonce}&return_sso_url=#{return_sso_url}"
|
get_hmac_hex_string(params[:sso]) == params[:sig]
|
||||||
base64_payload = Base64.encode64 payload
|
end
|
||||||
sso = URI.escape base64_payload
|
|
||||||
|
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
|
sig = get_hmac_hex_string base64_payload
|
||||||
|
redirect_to "#{url}#{url.include?('?') ? '&' : '?'}sso=#{sso}&sig=#{sig}"
|
||||||
session[:discourse_sso_nonce] = nonce
|
|
||||||
redirect_to "#{discourse_url}/session/sso_provider?sso=#{sso}&sig=#{sig}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def callback
|
def parse_payload
|
||||||
raise I18n.t('discourse.callback.invalid_signature') if get_hmac_hex_string(params[:sso]) != params[:sig]
|
payload = Rack::Utils.parse_query Base64.decode64(params[:sso])
|
||||||
|
payload.symbolize_keys!
|
||||||
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
|
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
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def get_hmac_hex_string payload
|
def get_hmac_hex_string payload
|
||||||
discourse_sso_secret = FoodsoftConfig[:discourse_sso_secret]
|
discourse_sso_secret = FoodsoftConfig[:discourse_sso_secret]
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1,5 +1,5 @@
|
||||||
/ insert_after 'noscript'
|
/ insert_after 'noscript'
|
||||||
- if FoodsoftDiscourse.enabled?
|
- if FoodsoftDiscourse.enabled? && !FoodsoftConfig[:discourse_sso]
|
||||||
.center
|
.center
|
||||||
= link_to t('.discourse_login'), discourse_initiate_path, class: 'btn btn-primary'
|
= link_to t('.discourse_login'), discourse_initiate_path, class: 'btn btn-primary'
|
||||||
%hr
|
%hr
|
||||||
|
|
|
@ -2,8 +2,9 @@ Rails.application.routes.draw do
|
||||||
|
|
||||||
scope '/:foodcoop' do
|
scope '/:foodcoop' do
|
||||||
|
|
||||||
get '/discourse/callback' => 'discourse#callback'
|
get '/discourse/callback' => 'discourse_login#callback'
|
||||||
get '/discourse/initiate' => 'discourse#initiate'
|
get '/discourse/initiate' => 'discourse_login#initiate'
|
||||||
|
get '/discourse/sso' => 'discourse_sso#sso'
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,11 @@ module FoodsoftDiscourse
|
||||||
alias orig_redirect_to_login redirect_to_login
|
alias orig_redirect_to_login redirect_to_login
|
||||||
|
|
||||||
def redirect_to_login(options={})
|
def redirect_to_login(options={})
|
||||||
return orig_redirect_to_login(options) unless FoodsoftDiscourse.enabled?
|
if FoodsoftDiscourse.enabled? && !FoodsoftConfig[:discourse_sso]
|
||||||
redirect_to discourse_initiate_path
|
redirect_to discourse_initiate_path
|
||||||
|
else
|
||||||
|
orig_redirect_to_login(options)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue