FEATURE: implement SSO provider on Discourse so Auth can be farmed to it

FEATURE: pass return_sso_url to SSO endpoints, for easier return
This commit is contained in:
Sam 2014-11-26 17:25:54 +11:00
parent 12e587c9b3
commit c10e3df012
8 changed files with 74 additions and 9 deletions

View file

@ -1,9 +1,10 @@
require_dependency 'rate_limiter' require_dependency 'rate_limiter'
require_dependency 'single_sign_on'
class SessionController < ApplicationController class SessionController < ApplicationController
skip_before_filter :redirect_to_login_if_required skip_before_filter :redirect_to_login_if_required
skip_before_filter :check_xhr, only: ['sso', 'sso_login', 'become'] skip_before_filter :check_xhr, only: ['sso', 'sso_login', 'become', 'sso_provider']
def csrf def csrf
render json: {csrf: form_authenticity_token } render json: {csrf: form_authenticity_token }
@ -17,6 +18,25 @@ class SessionController < ApplicationController
end end
end end
def sso_provider(payload=nil)
payload ||= request.query_string
if SiteSetting.enable_sso_provider
sso = SingleSignOn.parse(payload, SiteSetting.sso_secret)
if current_user
sso.name = current_user.name
sso.username = current_user.username
sso.email = current_user.email
sso.external_id = current_user.id.to_s
redirect_to sso.to_url(sso.return_sso_url)
else
session[:sso_payload] = request.query_string
redirect_to '/login'
end
else
render nothing: true, status: 404
end
end
# For use in development mode only when login options could be limited or disabled. # For use in development mode only when login options could be limited or disabled.
# NEVER allow this to work in production. # NEVER allow this to work in production.
def become def become
@ -83,6 +103,7 @@ class SessionController < ApplicationController
login = params[:login].strip login = params[:login].strip
login = login[1..-1] if login[0] == "@" login = login[1..-1] if login[0] == "@"
if user = User.find_by_username_or_email(login) if user = User.find_by_username_or_email(login)
# If their password is correct # If their password is correct
@ -200,7 +221,12 @@ class SessionController < ApplicationController
def login(user) def login(user)
log_on_user(user) log_on_user(user)
if payload = session.delete(:sso_payload)
sso_provider(payload)
else
render_serialized(user, UserSerializer) render_serialized(user, UserSerializer)
end end
end
end end

View file

@ -14,6 +14,7 @@ class DiscourseSingleSignOn < SingleSignOn
sso = new sso = new
sso.nonce = SecureRandom.hex sso.nonce = SecureRandom.hex
sso.register_nonce(return_path) sso.register_nonce(return_path)
sso.return_sso_url = Discourse.base_url + "/session/sso_login"
sso.to_url sso.to_url
end end

View file

@ -787,6 +787,7 @@ en:
block_common_passwords: "Don't allow passwords that are in the 10,000 most common passwords." block_common_passwords: "Don't allow passwords that are in the 10,000 most common passwords."
enable_sso: "Enable single sign on via an external site (Note: disables invites)" enable_sso: "Enable single sign on via an external site (Note: disables invites)"
enable_sso_provider: "Implement Discourse SSO protocol at the /session/sso_provider endpoint, requires sso_secret to be set"
sso_url: "URL of single sign on endpoint" sso_url: "URL of single sign on endpoint"
sso_secret: "Secret string used to encrypt/decrypt SSO information, be sure it is 10 chars or longer" sso_secret: "Secret string used to encrypt/decrypt SSO information, be sure it is 10 chars or longer"
sso_overrides_email: "Overrides local email with external site email from SSO payload (WARNING: discrepancies can occur due to normalization of local emails)" sso_overrides_email: "Overrides local email with external site email from SSO payload (WARNING: discrepancies can occur due to normalization of local emails)"

View file

@ -197,6 +197,7 @@ Discourse::Application.routes.draw do
get "session/sso" => "session#sso" get "session/sso" => "session#sso"
get "session/sso_login" => "session#sso_login" get "session/sso_login" => "session#sso_login"
get "session/sso_provider" => "session#sso_provider"
get "session/current" => "session#current" get "session/current" => "session#current"
get "session/csrf" => "session#csrf" get "session/csrf" => "session#csrf"
get "composer-messages" => "composer_messages#index" get "composer-messages" => "composer_messages#index"

View file

@ -221,6 +221,7 @@ login:
enable_sso: enable_sso:
client: true client: true
default: false default: false
enable_sso_provider: false
sso_url: '' sso_url: ''
sso_secret: '' sso_secret: ''
sso_overrides_email: false sso_overrides_email: false

View file

@ -1,6 +1,6 @@
class SingleSignOn class SingleSignOn
ACCESSORS = [:nonce, :name, :username, :email, :avatar_url, :avatar_force_update, ACCESSORS = [:nonce, :name, :username, :email, :avatar_url, :avatar_force_update,
:about_me, :external_id] :about_me, :external_id, :return_sso_url]
FIXNUMS = [] FIXNUMS = []
NONCE_EXPIRY_TIME = 10.minutes NONCE_EXPIRY_TIME = 10.minutes

View file

@ -26,9 +26,9 @@ describe SessionController do
@sso_url = "http://somesite.com/discourse_sso" @sso_url = "http://somesite.com/discourse_sso"
@sso_secret = "shjkfdhsfkjh" @sso_secret = "shjkfdhsfkjh"
SiteSetting.stubs("enable_sso").returns(true) SiteSetting.enable_sso = true
SiteSetting.stubs("sso_url").returns(@sso_url) SiteSetting.sso_url = @sso_url
SiteSetting.stubs("sso_secret").returns(@sso_secret) SiteSetting.sso_secret = @sso_secret
# We have 2 options, either fabricate an admin or don't # We have 2 options, either fabricate an admin or don't
# send welcome messages # send welcome messages
@ -97,6 +97,7 @@ describe SessionController do
it 'allows login to existing account with valid nonce' do it 'allows login to existing account with valid nonce' do
sso = get_sso('/hello/world') sso = get_sso('/hello/world')
sso.external_id = '997' sso.external_id = '997'
sso.sso_url = "http://somewhere.over.com/sso_login"
user = Fabricate(:user) user = Fabricate(:user)
user.create_single_sign_on_record(external_id: '997', last_payload: '') user.create_single_sign_on_record(external_id: '997', last_payload: '')
@ -116,6 +117,40 @@ describe SessionController do
response.code.should == '500' response.code.should == '500'
end end
it 'can act as an SSO provider' do
SiteSetting.enable_sso_provider = true
SiteSetting.enable_sso = false
SiteSetting.enable_local_logins = true
SiteSetting.sso_secret = "topsecret"
sso = SingleSignOn.new
sso.nonce = "mynonce"
sso.sso_secret = SiteSetting.sso_secret
sso.return_sso_url = "http://somewhere.over.rainbow/sso"
get :sso_provider, Rack::Utils.parse_query(sso.payload)
response.should redirect_to("/login")
user = Fabricate(:user, password: "frogs", active: true)
EmailToken.update_all(confirmed: true)
xhr :post, :create, login: user.username, password: "frogs", format: :json
location = response.header["Location"]
location.should =~ /^http:\/\/somewhere.over.rainbow\/sso/
payload = location.split("?")[1]
sso2 = SingleSignOn.parse(payload, "topsecret")
sso2.email.should == user.email
sso2.name.should == user.name
sso2.username.should == user.username
sso2.external_id == user.id.to_s
end
describe 'local attribute override from SSO payload' do describe 'local attribute override from SSO payload' do
before do before do
SiteSetting.stubs("sso_overrides_email").returns(true) SiteSetting.stubs("sso_overrides_email").returns(true)

View file

@ -5,9 +5,9 @@ describe DiscourseSingleSignOn do
@sso_url = "http://somesite.com/discourse_sso" @sso_url = "http://somesite.com/discourse_sso"
@sso_secret = "shjkfdhsfkjh" @sso_secret = "shjkfdhsfkjh"
SiteSetting.stubs("enable_sso").returns(true) SiteSetting.enable_sso = true
SiteSetting.stubs("sso_url").returns(@sso_url) SiteSetting.sso_url = @sso_url
SiteSetting.stubs("sso_secret").returns(@sso_secret) SiteSetting.sso_secret = @sso_secret
end end
def make_sso def make_sso