Skip to content

Commit 7369c5f

Browse files
author
Kirill Pimenov
committed
First public commit
0 parents  commit 7369c5f

11 files changed

+297
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
config/production.rb

.ruby-version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ruby-2.2.1

Gemfile

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
source 'https://rubygems.org'
2+
3+
gem 'grape'
4+
gem 'goliath'
5+
gem 'ruby-saml'

Gemfile.lock

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
GEM
2+
remote: https://rubygems.org/
3+
specs:
4+
activesupport (4.2.1)
5+
i18n (~> 0.7)
6+
json (~> 1.7, >= 1.7.7)
7+
minitest (~> 5.1)
8+
thread_safe (~> 0.3, >= 0.3.4)
9+
tzinfo (~> 1.1)
10+
addressable (2.3.8)
11+
async-rack (0.5.1)
12+
rack (~> 1.1)
13+
axiom-types (0.1.1)
14+
descendants_tracker (~> 0.0.4)
15+
ice_nine (~> 0.11.0)
16+
thread_safe (~> 0.3, >= 0.3.1)
17+
builder (3.2.2)
18+
coercible (1.0.0)
19+
descendants_tracker (~> 0.0.1)
20+
descendants_tracker (0.0.4)
21+
thread_safe (~> 0.3, >= 0.3.1)
22+
em-synchrony (1.0.4)
23+
eventmachine (>= 1.0.0.beta.1)
24+
em-websocket (0.3.8)
25+
addressable (>= 2.1.1)
26+
eventmachine (>= 0.12.9)
27+
equalizer (0.0.11)
28+
eventmachine (1.0.7)
29+
goliath (1.0.4)
30+
async-rack
31+
em-synchrony (>= 1.0.0)
32+
em-websocket (= 0.3.8)
33+
eventmachine (>= 1.0.0.beta.4)
34+
http_parser.rb (= 0.6.0)
35+
log4r
36+
multi_json
37+
rack (>= 1.2.2)
38+
rack-contrib
39+
rack-respond_to
40+
grape (0.11.0)
41+
activesupport
42+
builder
43+
hashie (>= 2.1.0)
44+
multi_json (>= 1.3.2)
45+
multi_xml (>= 0.5.2)
46+
rack (>= 1.3.0)
47+
rack-accept
48+
rack-mount
49+
virtus (>= 1.0.0)
50+
hashie (3.4.1)
51+
http_parser.rb (0.6.0)
52+
i18n (0.7.0)
53+
ice_nine (0.11.1)
54+
json (1.8.2)
55+
log4r (1.1.10)
56+
macaddr (1.7.1)
57+
systemu (~> 2.6.2)
58+
mini_portile (0.6.2)
59+
minitest (5.6.0)
60+
multi_json (1.11.0)
61+
multi_xml (0.5.5)
62+
nokogiri (1.6.6.2)
63+
mini_portile (~> 0.6.0)
64+
rack (1.6.0)
65+
rack-accept (0.4.5)
66+
rack (>= 0.4)
67+
rack-accept-media-types (0.9)
68+
rack-contrib (1.2.0)
69+
rack (>= 0.9.1)
70+
rack-mount (0.8.3)
71+
rack (>= 1.0.0)
72+
rack-respond_to (0.9.8)
73+
rack-accept-media-types (>= 0.6)
74+
ruby-saml (0.9.1)
75+
nokogiri (~> 1.6.0)
76+
uuid (~> 2.3)
77+
systemu (2.6.5)
78+
thread_safe (0.3.5)
79+
tzinfo (1.2.2)
80+
thread_safe (~> 0.1)
81+
uuid (2.3.7)
82+
macaddr (~> 1.0)
83+
virtus (1.0.5)
84+
axiom-types (~> 0.1)
85+
coercible (~> 1.0)
86+
descendants_tracker (~> 0.0, >= 0.0.3)
87+
equalizer (~> 0.0, >= 0.0.9)
88+
89+
PLATFORMS
90+
ruby
91+
92+
DEPENDENCIES
93+
goliath
94+
grape
95+
ruby-saml

Procfile

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
api: bundle exec ruby application.rb -s -e prod -p $PORT

app/apis/sso.rb

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
require 'grape'
2+
3+
require './lib/single_sign_on'
4+
require './app/helpers/saml_helpers'
5+
6+
module API
7+
class SSO < Grape::API
8+
format :txt
9+
10+
helpers ::SAMLHelpers
11+
12+
params do
13+
requires :sso, type: String
14+
requires :sig, type: String
15+
end
16+
get '/login' do
17+
cookies[:sso] = declared(params).sso
18+
cookies[:sig] = declared(params).sig
19+
20+
redirect saml_url(env)
21+
end
22+
23+
post '/saml_callback' do
24+
sso = cookies[:sso]
25+
sig = cookies[:sig]
26+
27+
user_data = parse_saml_payload(params[:SAMLResponse], env)
28+
29+
if user_data
30+
sign_on = SingleSignOn.parse sso, sig, env.encryption_key
31+
32+
sign_on.external_id = user_data[:external_id]
33+
sign_on.username = user_data[:username]
34+
sign_on.name = user_data[:name]
35+
sign_on.email = user_data[:email]
36+
37+
discourse_url = URI.parse env.discourse_url
38+
discourse_url.path = '/session/sso_login'
39+
redirect sign_on.to_url discourse_url.to_s
40+
else
41+
status 401
42+
end
43+
end
44+
end
45+
end

app/helpers/saml_helpers.rb

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require 'ruby-saml'
2+
3+
module SAMLHelpers
4+
5+
def saml_url(env)
6+
request = OneLogin::RubySaml::Authrequest.new
7+
request.create saml_settings(env)
8+
end
9+
10+
11+
def parse_saml_payload(saml_payload, env)
12+
response = OneLogin::RubySaml::Response.new(saml_payload, allowed_clock_drift: 1)
13+
response.settings = saml_settings(env)
14+
if response.is_valid?
15+
{
16+
external_id: saml_attribute(response, 'workforceID'),
17+
username: response.name_id.downcase,
18+
name: [saml_attribute(response, 'givenName'), saml_attribute(response, 'sn')].join(' '),
19+
email: saml_attribute(response, 'mail').downcase
20+
}
21+
else
22+
nil
23+
end
24+
end
25+
26+
private
27+
28+
def saml_settings(env)
29+
settings = OneLogin::RubySaml::Settings.new
30+
settings.assertion_consumer_service_url = "https://#{env.HTTP_HOST}/saml_callback"
31+
settings.issuer = env.saml[:issuer]
32+
settings.idp_sso_target_url = env.saml[:target_url]
33+
settings.idp_cert_fingerprint = env.saml[:cert_fingerprint]
34+
35+
settings
36+
end
37+
38+
def saml_attribute(response, att_name)
39+
response.attributes["/UserAttribute[@ldap:targetAttribute=\"#{att_name}\"]"]
40+
end
41+
42+
end

application.rb

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env ruby
2+
3+
require 'goliath'
4+
require_relative "app/apis/sso"
5+
6+
class Application < Goliath::API
7+
def response(env)
8+
API::SSO.call(env)
9+
end
10+
end

config/application.rb

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import(Goliath.env)

config/development.rb

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
config['encryption_key'] = 'ScarySecret'
2+
config['discourse_url'] = 'http://localhost:3000'
3+
4+
config['saml'] = {
5+
issuer: 'saml-auth-proxy.example.com',
6+
target_url: 'https://saml.example.com/saml2/sso',
7+
cert_fingerprint: '11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11'
8+
}

lib/single_sign_on.rb

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Based on https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb
2+
# All kudos and copyrights — to its original authors.
3+
class SingleSignOn
4+
ACCESSORS = [:nonce, :name, :username, :email, :avatar_url, :avatar_force_update,
5+
:about_me, :external_id, :return_sso_url, :admin, :moderator, :suppress_welcome_message]
6+
FIXNUMS = []
7+
BOOLS = [:avatar_force_update, :admin, :moderator, :suppress_welcome_message]
8+
NONCE_EXPIRY_TIME = 10.minutes
9+
10+
attr_accessor(*ACCESSORS)
11+
attr_accessor :sso_secret, :sso_url
12+
13+
def self.parse(payload, signature, sso_secret = nil)
14+
sso = new
15+
sso.sso_secret = sso_secret if sso_secret
16+
17+
if sso.sign(payload) != signature
18+
diags = "\n\nsso: #{payload}\n\nsig: #{signature}\n\nexpected sig: #{sso.sign(payload)}"
19+
if payload =~ /[^a-zA-Z0-9=\r\n\/+]/m
20+
raise RuntimeError, "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}"
21+
else
22+
raise RuntimeError, "Bad signature for payload #{diags}"
23+
end
24+
end
25+
26+
decoded = Base64.decode64(payload)
27+
decoded_hash = Rack::Utils.parse_query(decoded)
28+
29+
ACCESSORS.each do |k|
30+
val = decoded_hash[k.to_s]
31+
val = val.to_i if FIXNUMS.include? k
32+
if BOOLS.include? k
33+
val = ["true", "false"].include?(val) ? val == "true" : nil
34+
end
35+
sso.send("#{k}=", val)
36+
end
37+
38+
decoded_hash.each do |k,v|
39+
# 1234567
40+
# custom.
41+
#
42+
if k[0..6] == "custom."
43+
field = k[7..-1]
44+
sso.custom_fields[field] = v
45+
end
46+
end
47+
48+
sso
49+
end
50+
51+
def custom_fields
52+
@custom_fields ||= {}
53+
end
54+
55+
56+
def sign(payload)
57+
OpenSSL::HMAC.hexdigest("sha256", sso_secret, payload)
58+
end
59+
60+
61+
def to_url(base_url=nil)
62+
base = "#{base_url || sso_url}"
63+
"#{base}#{base.include?('?') ? '&' : '?'}#{payload}"
64+
end
65+
66+
def payload
67+
payload = Base64.encode64(unsigned_payload)
68+
"sso=#{CGI::escape(payload)}&sig=#{sign(payload)}"
69+
end
70+
71+
def unsigned_payload
72+
payload = {}
73+
ACCESSORS.each do |k|
74+
next if (val = send k) == nil
75+
76+
payload[k] = val
77+
end
78+
79+
if @custom_fields
80+
@custom_fields.each do |k,v|
81+
payload["custom.#{k}"] = v.to_s
82+
end
83+
end
84+
85+
Rack::Utils.build_query(payload)
86+
end
87+
88+
end

0 commit comments

Comments
 (0)