mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-23 15:48:43 -05:00
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:
parent
12e587c9b3
commit
c10e3df012
8 changed files with 74 additions and 9 deletions
|
@ -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)
|
||||||
render_serialized(user, UserSerializer)
|
|
||||||
|
if payload = session.delete(:sso_payload)
|
||||||
|
sso_provider(payload)
|
||||||
|
else
|
||||||
|
render_serialized(user, UserSerializer)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue