rack-oauth2-provider 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/rack/oauth2/assertion_profile.rb +58 -0
- data/lib/rack/oauth2/provider.rb +5 -0
- data/lib/simple_web_token_builder.rb +44 -0
- data/rakefile +51 -0
- data/spec/rack/oauth2/assertion_profile.test.rb +72 -0
- data/spec/simple_web_token_builder_test.rb +54 -0
- data/spec/specs_config.rb +11 -0
- data/vendor/information_card.rb +11 -0
- data/vendor/information_card/saml_token.rb +142 -0
- data/vendor/information_card/xml_canonicalizer.rb +99 -0
- data/vendor/rexml/parsers.rb +244 -0
- metadata +96 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rack/auth/abstract/handler'
|
2
|
+
require 'rack/auth/abstract/request'
|
3
|
+
require 'rack/auth/wrap'
|
4
|
+
$LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), "../../")))
|
5
|
+
require 'simple_web_token_builder'
|
6
|
+
require File.expand_path(File.join(File.dirname(__FILE__), "../../../vendor/information_card"))
|
7
|
+
|
8
|
+
module Rack
|
9
|
+
module OAuth2
|
10
|
+
class AssertionProfile < Rack::Auth::AbstractHandler
|
11
|
+
def initialize(app, opts = {})
|
12
|
+
@app = app
|
13
|
+
@opts = opts
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
request = Request.new(env)
|
18
|
+
|
19
|
+
if (request.assertion_profile? && request.format == :saml)
|
20
|
+
InformationCard::Config.audience_scope, InformationCard::Config.audiences = :site, [@opts[:scope]]
|
21
|
+
token = InformationCard::SamlToken.create(request.token)
|
22
|
+
|
23
|
+
unless token.valid?
|
24
|
+
return [400, {'Content-Type' => "application/x-www-form-urlencoded"}, "error=unauthorized_client"]
|
25
|
+
end
|
26
|
+
|
27
|
+
# conver the received claims into SWT
|
28
|
+
swt = token_builder.build(token.claims)
|
29
|
+
return [200, {'Content-Type' => "application/x-www-form-urlencoded"}, "access_token=#{CGI.escape(swt)}"]
|
30
|
+
end
|
31
|
+
|
32
|
+
return @app.call(env)
|
33
|
+
end
|
34
|
+
|
35
|
+
def token_builder
|
36
|
+
@token_builder ||= SimpleWebToken::SimpleWebTokenBuilder.new(@opts)
|
37
|
+
end
|
38
|
+
|
39
|
+
class Request < Rack::Request
|
40
|
+
def initialize(env)
|
41
|
+
super(env)
|
42
|
+
end
|
43
|
+
|
44
|
+
def assertion_profile?
|
45
|
+
self.params["type"] =~ /assertion/i
|
46
|
+
end
|
47
|
+
|
48
|
+
def format
|
49
|
+
(self.params["format"] or "saml").downcase.to_sym
|
50
|
+
end
|
51
|
+
|
52
|
+
def token
|
53
|
+
self.params["assertion"]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'base64'
|
3
|
+
require 'hmac-sha2'
|
4
|
+
|
5
|
+
module SimpleWebToken
|
6
|
+
class SimpleWebTokenBuilder
|
7
|
+
attr_accessor :shared_secret, :issuer, :audience, :expiration
|
8
|
+
|
9
|
+
def initialize(opts = {})
|
10
|
+
raise InvalidOption, :shared_secret unless opts[:shared_secret]
|
11
|
+
self.shared_secret = opts[:shared_secret]
|
12
|
+
self.issuer = opts[:issuer]
|
13
|
+
self.audience = opts[:audience]
|
14
|
+
self.expiration = (opts[:expiration] or 3600)
|
15
|
+
end
|
16
|
+
|
17
|
+
def build(claims)
|
18
|
+
token = (convert(claims) + default_claim_set).join("&")
|
19
|
+
return token += "&HMACSHA256=#{CGI.escape(sign(token))}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def sign(bare_token)
|
23
|
+
signature = Base64.encode64(HMAC::SHA256.new(Base64.decode64(self.shared_secret)).update(bare_token.toutf8).digest).strip
|
24
|
+
end
|
25
|
+
|
26
|
+
def convert(claims)
|
27
|
+
claims.map{|k, v| claim_pair(k, v)}
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_claim_set
|
31
|
+
default_claims = []
|
32
|
+
default_claims << claim_pair(:issuer, self.issuer) if(self.issuer)
|
33
|
+
default_claims << claim_pair(:audience, self.audience) if(self.audience)
|
34
|
+
default_claims << claim_pair(:expires_on, Time.now.to_i + self.expiration)
|
35
|
+
return default_claims
|
36
|
+
end
|
37
|
+
|
38
|
+
def claim_pair(key, value)
|
39
|
+
new_key = key.to_s.downcase.split("_").map{|l| l.capitalize.strip}.join("")
|
40
|
+
value = [value].flatten.uniq.join(",")
|
41
|
+
[new_key, value.to_s].map{|s| CGI.escape(s)}.join("=")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require 'rake/rdoctask'
|
6
|
+
|
7
|
+
require 'lib/rack/oauth2/provider'
|
8
|
+
|
9
|
+
task :default => ["run_with_rcov"]
|
10
|
+
|
11
|
+
Spec::Rake::SpecTask.new('run_with_rcov') do |t|
|
12
|
+
t.spec_files = FileList['spec/**/*.rb'].reject{|f| f.include?('functional')}
|
13
|
+
t.rcov = true
|
14
|
+
t.rcov_opts = ['--text-report', '--exclude', "exclude.*/.gem,spec,Library,#{ENV['GEM_HOME']}", '--sort', 'coverage' ]
|
15
|
+
t.spec_opts = ["--colour", "--loadby random", "--format progress", "--backtrace"]
|
16
|
+
end
|
17
|
+
|
18
|
+
namespace :docs do
|
19
|
+
Rake::RDocTask.new do |t|
|
20
|
+
t.rdoc_dir = 'sdk/public/'
|
21
|
+
t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
|
22
|
+
t.options << '--charset' << 'utf-8'
|
23
|
+
t.rdoc_files.include('README.rdoc')
|
24
|
+
t.rdoc_files.include('lib/**/*.rb')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
namespace :dist do
|
30
|
+
spec = Gem::Specification.new do |s|
|
31
|
+
s.name = 'rack-oauth2-provider'
|
32
|
+
s.version = Gem::Version.new(Rack::OAuth2::VERSION)
|
33
|
+
s.summary = "Rack Middleware that authenticates users based on the different profiles of oAuth 2.0 standard"
|
34
|
+
s.description = "A simple implementation of provider protocols of oAuth 2.0 (right now just AssertionProfile)."
|
35
|
+
s.email = 'johnny.halife@me.com'
|
36
|
+
s.author = 'Johnny G. Halife & Ezequiel Morito'
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.files = FileList['rakefile', 'lib/**/*.rb', 'vendor/**/*.rb']
|
39
|
+
s.test_files = Dir['spec/**/*']
|
40
|
+
s.has_rdoc = true
|
41
|
+
s.rdoc_options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
|
42
|
+
|
43
|
+
# Dependencies
|
44
|
+
s.add_dependency 'ruby-hmac'
|
45
|
+
s.add_dependency 'information_card'
|
46
|
+
end
|
47
|
+
|
48
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
49
|
+
pkg.need_tar = true
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), "../../")))
|
2
|
+
require 'specs_config.rb'
|
3
|
+
require 'lib/rack/oauth2/assertion_profile'
|
4
|
+
|
5
|
+
describe "Rack middleware behavior for oAuth 2.0 Assertion Profile" do
|
6
|
+
before do
|
7
|
+
@opts = {:audience => "http://localhost",
|
8
|
+
:issuer => "http://localhost/issue",
|
9
|
+
:shared_secret => "N4QeKa3c062VBjnVK6fb+rnwURkcwGXh7EoNK34n0uM="}
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should do anything if it's not an authentication request for AssertionProfile'" do
|
13
|
+
env = Rack::MockRequest.env_for("/", {})
|
14
|
+
|
15
|
+
(mock_app = mock).expects(:call).with(env).once
|
16
|
+
SimpleWebToken::SimpleWebTokenBuilder.any_instance.expects(:build).never
|
17
|
+
InformationCard::SamlToken.expects(:create).never
|
18
|
+
|
19
|
+
response_code, headers, body = Rack::OAuth2::AssertionProfile.new(mock_app, @opts).call(env)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should return 400 when the given assertion isn't well-formed nor valid" do
|
23
|
+
env = Rack::MockRequest.env_for("/", {'rack.input' => "type=assertion&format=saml&assertion=very_invalid_assertion"})
|
24
|
+
|
25
|
+
(mock_app = mock).expects(:call).with(env).never
|
26
|
+
|
27
|
+
mock_request = mock do
|
28
|
+
expects(:assertion_profile?).returns(true)
|
29
|
+
expects(:format).returns(:saml)
|
30
|
+
expects(:token).returns("invalid_token")
|
31
|
+
end
|
32
|
+
|
33
|
+
(invalid_token = mock).expects(:valid?).returns(false)
|
34
|
+
InformationCard::SamlToken.expects(:create).returns(invalid_token)
|
35
|
+
Rack::OAuth2::AssertionProfile::Request.expects(:new).with(env).returns(mock_request)
|
36
|
+
|
37
|
+
response_code, headers, body = Rack::OAuth2::AssertionProfile.new(mock_app, @opts).call(env)
|
38
|
+
|
39
|
+
response_code.should == 400
|
40
|
+
headers['Content-Type'].should == "application/x-www-form-urlencoded"
|
41
|
+
body.should == "error=unauthorized_client"
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should return 200 when the given assertion is valid and include the access_token on the body" do
|
45
|
+
env = Rack::MockRequest.env_for("/", {'rack.input' => "type=assertion&format=saml&assertion=very_valid_assertion"})
|
46
|
+
|
47
|
+
(mock_app = mock).expects(:call).with(env).never
|
48
|
+
|
49
|
+
mock_request = mock do
|
50
|
+
expects(:assertion_profile?).returns(true)
|
51
|
+
expects(:format).returns(:saml)
|
52
|
+
expects(:token).returns("very_valid_assertion")
|
53
|
+
end
|
54
|
+
|
55
|
+
valid_token = mock do
|
56
|
+
expects(:valid?).returns(true)
|
57
|
+
expects(:claims).returns({})
|
58
|
+
end
|
59
|
+
|
60
|
+
InformationCard::SamlToken.expects(:create).with("very_valid_assertion").returns(valid_token)
|
61
|
+
Rack::OAuth2::AssertionProfile::Request.expects(:new).with(env).returns(mock_request)
|
62
|
+
|
63
|
+
(mock_builder = mock).expects(:build).with({}).returns("token")
|
64
|
+
SimpleWebToken::SimpleWebTokenBuilder.expects(:new).with(@opts).returns(mock_builder)
|
65
|
+
|
66
|
+
response_code, headers, body = Rack::OAuth2::AssertionProfile.new(mock_app, @opts).call(env)
|
67
|
+
|
68
|
+
response_code.should == 200
|
69
|
+
headers['Content-Type'].should == "application/x-www-form-urlencoded"
|
70
|
+
body.should == "access_token=token"
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec/specs_config'
|
2
|
+
require 'lib/simple_web_token_builder'
|
3
|
+
|
4
|
+
describe "The Simple Web Token Builder behavior" do
|
5
|
+
before do
|
6
|
+
@opts = {:audience => "http://localhost",
|
7
|
+
:issuer => "http://localhost/issue",
|
8
|
+
:shared_secret => "N4QeKa3c062VBjnVK6fb+rnwURkcwGXh7EoNK34n0uM="}
|
9
|
+
end
|
10
|
+
|
11
|
+
it "turn the given key into pascal case and encode both key and value" do
|
12
|
+
builder = SimpleWebToken::SimpleWebTokenBuilder.new(@opts)
|
13
|
+
builder.claim_pair(:given_name, "johnny halife").should == "GivenName=johnny+halife"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should turn values that are arrays into csv" do
|
17
|
+
builder = SimpleWebToken::SimpleWebTokenBuilder.new(@opts)
|
18
|
+
builder.claim_pair(:projects, ["*", "foo"]).should == "Projects=#{CGI.escape("*,foo")}"
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
it "should generate each value pair for the given dictionary" do
|
23
|
+
builder = SimpleWebToken::SimpleWebTokenBuilder.new(@opts)
|
24
|
+
result = builder.convert({:age => 24})
|
25
|
+
result[0].should == "Age=24"
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should return default claims" do
|
29
|
+
(mock_time ||= mock).expects(:to_i).returns(1)
|
30
|
+
Time.expects(:now).returns(mock_time)
|
31
|
+
|
32
|
+
builder = SimpleWebToken::SimpleWebTokenBuilder.new(@opts)
|
33
|
+
default_claims_set = builder.default_claim_set
|
34
|
+
default_claims_set[0].should == "Issuer=#{CGI.escape("http://localhost/issue")}"
|
35
|
+
default_claims_set[1].should == "Audience=#{CGI.escape("http://localhost")}"
|
36
|
+
default_claims_set[2].should == "ExpiresOn=#{CGI.escape("3601")}"
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should sign the token" do
|
40
|
+
builder = SimpleWebToken::SimpleWebTokenBuilder.new(@opts)
|
41
|
+
expected_signature = Base64.encode64(HMAC::SHA256.new(Base64.decode64(@opts[:shared_secret])).update("Foo=Bar".toutf8).digest).strip
|
42
|
+
builder.sign("Foo=Bar").should == expected_signature
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should build the token" do
|
46
|
+
builder = SimpleWebToken::SimpleWebTokenBuilder.new(@opts)
|
47
|
+
|
48
|
+
builder.expects(:convert).with({:name => "johnny"}).returns(["Name=johnny"])
|
49
|
+
builder.expects(:default_claim_set).returns([])
|
50
|
+
builder.expects(:sign).with("Name=johnny").returns("foo")
|
51
|
+
|
52
|
+
builder.build({:name => "johnny"}).should == "Name=johnny&HMACSHA256=foo"
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# dependency
|
2
|
+
require 'information_card'
|
3
|
+
|
4
|
+
# standard items
|
5
|
+
require 'rexml/document'
|
6
|
+
require 'rexml/xpath'
|
7
|
+
|
8
|
+
# vendored items
|
9
|
+
require 'vendor/information_card/saml_token'
|
10
|
+
require 'vendor/information_card/xml_canonicalizer'
|
11
|
+
require 'vendor/rexml/parsers'
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# monkeypatching the SamlToken class to verify token signatures with X509 Certificates
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module InformationCard
|
5
|
+
include REXML
|
6
|
+
|
7
|
+
# added first the tree claim types
|
8
|
+
class ClaimTypes
|
9
|
+
@@claims = {
|
10
|
+
:name => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
|
11
|
+
:email => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
12
|
+
:upn => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
|
13
|
+
:group => "http://schemas.xmlsoap.org/claims/Group",
|
14
|
+
:given_name => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
|
15
|
+
:surname => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
|
16
|
+
:street_address => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress",
|
17
|
+
:locality => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/locality",
|
18
|
+
:state_province => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/stateorprovince",
|
19
|
+
:postal_code => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode",
|
20
|
+
:country => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country",
|
21
|
+
:home_phone => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone",
|
22
|
+
:other_phone => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone",
|
23
|
+
:mobile_phone => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone",
|
24
|
+
:date_of_birth => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth",
|
25
|
+
:gender => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/gender",
|
26
|
+
:ppid => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier",
|
27
|
+
:webpage => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/webpage",
|
28
|
+
:project => "http://schemas.southworksinc.com/project",
|
29
|
+
:organization => "http://schemas.microsoft.com/ws/2008/06/identity/claims/organization"
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
class SamlToken < IdentityToken
|
34
|
+
def errors
|
35
|
+
return @errors
|
36
|
+
end
|
37
|
+
|
38
|
+
def verify_digest
|
39
|
+
working_doc = REXML::Document.new(@doc.to_s)
|
40
|
+
|
41
|
+
assertion_node = XPath.first(working_doc, "saml:Assertion", {"saml" => Namespaces::SAML_ASSERTION})
|
42
|
+
signature_node = XPath.first(assertion_node, "ds:Signature", {"ds" => Namespaces::DS})
|
43
|
+
signed_info_node = XPath.first(signature_node, "ds:SignedInfo", {"ds" => Namespaces::DS})
|
44
|
+
digest_value_node = XPath.first(signed_info_node, "ds:Reference/ds:DigestValue", {"ds" => Namespaces::DS})
|
45
|
+
|
46
|
+
digest_value = digest_value_node.text
|
47
|
+
|
48
|
+
signature_node.remove
|
49
|
+
digest_errors = []
|
50
|
+
canonicalizer = InformationCard::XmlCanonicalizer.new
|
51
|
+
|
52
|
+
reference_nodes = XPath.match(signed_info_node, "ds:Reference", {"ds" => Namespaces::DS})
|
53
|
+
# TODO: Check specification to see if digest is required.
|
54
|
+
@errors[:digest] = "No reference nodes to check digest" and return if reference_nodes.nil? or reference_nodes.empty?
|
55
|
+
|
56
|
+
reference_nodes.each do |node|
|
57
|
+
uri = node.attributes['URI']
|
58
|
+
nodes_to_verify = XPath.match(working_doc, "saml:Assertion[@AssertionID='#{uri[1..uri.size]}']", {"saml" => Namespaces::SAML_ASSERTION})
|
59
|
+
|
60
|
+
nodes_to_verify.each do |node|
|
61
|
+
canonicalized_signed_info = canonicalizer.canonicalize(node)
|
62
|
+
signed_node_hash_sha1 = Base64.encode64(Digest::SHA1.digest(canonicalized_signed_info)).chomp
|
63
|
+
signed_node_hash_sha256 = Base64.encode64(Digest::SHA256.digest(canonicalized_signed_info)).chomp
|
64
|
+
unless signed_node_hash_sha1 == digest_value
|
65
|
+
unless signed_node_hash_sha256 == digest_value
|
66
|
+
digest_errors << "Invalid Digest for #{uri}. Expected #{signed_node_hash} but was #{digest_value}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
@errors[:digest] = digest_errors unless digest_errors.empty?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def verify_signature
|
76
|
+
working_doc = REXML::Document.new(@doc.to_s)
|
77
|
+
|
78
|
+
assertion_node = XPath.first(working_doc, "saml:Assertion", {"saml" => Namespaces::SAML_ASSERTION})
|
79
|
+
signature_node = XPath.first(assertion_node, "ds:Signature", {"ds" => Namespaces::DS})
|
80
|
+
certificate_value_node = XPath.first(signature_node, "KeyInfo/X509Data/X509Certificate")
|
81
|
+
certificate = get_X509Certificate(certificate_value_node.text)
|
82
|
+
|
83
|
+
# TODO: here you should validate that the presented certificate is valid
|
84
|
+
|
85
|
+
public_key_string = certificate.public_key
|
86
|
+
signed_info_node = XPath.first(signature_node, "ds:SignedInfo", {"ds" => Namespaces::DS})
|
87
|
+
signature_value_node = XPath.first(signature_node, "ds:SignatureValue", {"ds" => Namespaces::DS})
|
88
|
+
canonicalized_signed_info = InformationCard::XmlCanonicalizer.new.canonicalize(signed_info_node)
|
89
|
+
signature = Base64.decode64(signature_value_node.text)
|
90
|
+
|
91
|
+
unless public_key_string.verify(OpenSSL::Digest::SHA1.new, signature, canonicalized_signed_info)
|
92
|
+
unless public_key_string.verify(OpenSSL::Digest::SHA256.new, signature, canonicalized_signed_info)
|
93
|
+
@errors[:signature] = "Invalid Signature"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def validate_conditions
|
99
|
+
conditions = XPath.first(@doc, "//saml:Conditions", "saml" => Namespaces::SAML_ASSERTION)
|
100
|
+
|
101
|
+
condition_errors = {}
|
102
|
+
not_before_time = Time.parse(conditions.attributes['NotBefore'])
|
103
|
+
condition_errors[:not_before] = "Time is before #{not_before_time}" if Time.now.utc < not_before_time
|
104
|
+
|
105
|
+
not_on_or_after_time = Time.parse(conditions.attributes['NotOnOrAfter'])
|
106
|
+
condition_errors[:not_on_or_after] = "Time is on or after #{not_on_or_after_time}" if Time.now.utc >= not_on_or_after_time
|
107
|
+
|
108
|
+
@errors[:conditions] = condition_errors unless condition_errors.empty?
|
109
|
+
end
|
110
|
+
|
111
|
+
def process_claims
|
112
|
+
attribute_nodes = XPath.match(@doc, "//saml:AttributeStatement/saml:Attribute", {"saml" => Namespaces::SAML_ASSERTION})
|
113
|
+
attribute_nodes.each do |node|
|
114
|
+
key = ClaimTypes.lookup(node.attributes['AttributeNamespace'], node.attributes['AttributeName'])
|
115
|
+
value_nodes = XPath.match(node, "saml:AttributeValue", "saml" => Namespaces::SAML_ASSERTION)
|
116
|
+
|
117
|
+
if (value_nodes.length < 2 or value_nodes.empty?)
|
118
|
+
@claims[key] = value_nodes[0].text
|
119
|
+
else
|
120
|
+
claim_values = []
|
121
|
+
value_nodes.each{ |value_node|
|
122
|
+
claim_values << value_node.text
|
123
|
+
}
|
124
|
+
@claims[key] = claim_values
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def get_X509Certificate(certificate)
|
130
|
+
encoding = "-----BEGIN CERTIFICATE-----\n"
|
131
|
+
offset = 0;
|
132
|
+
# strip out the newlines
|
133
|
+
certificate.delete!("=\n")
|
134
|
+
while (segment = certificate[offset, 64])
|
135
|
+
encoding = encoding + segment + "\n"
|
136
|
+
offset += 64
|
137
|
+
end
|
138
|
+
encoding = encoding + "-----END CERTIFICATE-----\n"
|
139
|
+
OpenSSL::X509::Certificate.new(encoding)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# Portions of this class were inspired by the XML::Util::XmlCanonicalizer class written by Roland Schmitt
|
2
|
+
|
3
|
+
module InformationCard
|
4
|
+
include REXML
|
5
|
+
|
6
|
+
class XmlCanonicalizer
|
7
|
+
def initialize
|
8
|
+
@canonicalized_xml = ''
|
9
|
+
end
|
10
|
+
|
11
|
+
def canonicalize(element)
|
12
|
+
document = REXML::Document.new(element.to_s)
|
13
|
+
|
14
|
+
#TODO: Do we need this check?
|
15
|
+
if element.instance_of?(REXML::Element)
|
16
|
+
namespace = element.namespace(element.prefix)
|
17
|
+
if not namespace.empty?
|
18
|
+
if not element.prefix.empty?
|
19
|
+
document.root.add_namespace(element.prefix, namespace)
|
20
|
+
else
|
21
|
+
document.root.add_namespace(namespace)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
document.each_child{ |node| write_node(node, nil) }
|
27
|
+
|
28
|
+
@canonicalized_xml.strip
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def write_node(node, scoped_prefixes)
|
34
|
+
case node.node_type
|
35
|
+
when :text
|
36
|
+
write_text(node)
|
37
|
+
when :element
|
38
|
+
write_element(node, scoped_prefixes)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def write_text(node)
|
43
|
+
if node.value.strip.empty?
|
44
|
+
@canonicalized_xml << node.value
|
45
|
+
else
|
46
|
+
@canonicalized_xml << normalize_whitespace(node.value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def write_element(node, scoped_prefixes)
|
51
|
+
scoped_prefixes ||= []
|
52
|
+
@canonicalized_xml << "<#{node.expanded_name}"
|
53
|
+
write_namespaces(node, scoped_prefixes)
|
54
|
+
write_attributes(node)
|
55
|
+
@canonicalized_xml << ">"
|
56
|
+
node.each_child{ |child|
|
57
|
+
prefixes = Array.new(scoped_prefixes)
|
58
|
+
write_node(child, prefixes) }
|
59
|
+
@canonicalized_xml << "</#{node.expanded_name}>"
|
60
|
+
end
|
61
|
+
|
62
|
+
def write_namespaces(node, scoped_prefixes)
|
63
|
+
scoped_prefixes ||= []
|
64
|
+
|
65
|
+
prefixes = ["xmlns"] + node.prefixes.uniq
|
66
|
+
|
67
|
+
prefixes.sort!.each do |prefix|
|
68
|
+
namespace = node.namespace(prefix)
|
69
|
+
|
70
|
+
unless prefix.empty? or (prefix == 'xmlns' and namespace.empty?) or scoped_prefixes.include?(prefix)
|
71
|
+
scoped_prefixes << prefix
|
72
|
+
|
73
|
+
@canonicalized_xml << " "
|
74
|
+
@canonicalized_xml << "xmlns:" if not prefix == 'xmlns'
|
75
|
+
@canonicalized_xml << normalize_whitespace("#{prefix}=\"#{namespace}\"")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def write_attributes(node)
|
81
|
+
attributes = []
|
82
|
+
|
83
|
+
node.attributes.sort.each do |key, attribute|
|
84
|
+
attributes << attribute if not attribute.prefix =~ /^xmlns/
|
85
|
+
end
|
86
|
+
|
87
|
+
attributes.each do |attribute|
|
88
|
+
unless attribute.nil? or attribute.name == "xmlns"
|
89
|
+
prefix = (attribute.prefix == "saml" || attribute.prefix == "ds") ? nil : attribute.prefix + ":"
|
90
|
+
@canonicalized_xml << " #{prefix}#{attribute.name}=\"#{normalize_whitespace(attribute.to_s)}\""
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def normalize_whitespace(input)
|
96
|
+
input.gsub(/\s+/, ' ').strip
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
# monkeypatching the whole pull method, only to comment out the lines 205 to 210
|
2
|
+
# because an error when trying to load a XML by calling REXML::Document.new(element)
|
3
|
+
# and this element has in the root node a namespace
|
4
|
+
|
5
|
+
module REXML
|
6
|
+
module Parsers
|
7
|
+
class BaseParser
|
8
|
+
def pull
|
9
|
+
if @closed
|
10
|
+
x, @closed = @closed, nil
|
11
|
+
return [ :end_element, x ]
|
12
|
+
end
|
13
|
+
return [ :end_document ] if empty?
|
14
|
+
return @stack.shift if @stack.size > 0
|
15
|
+
#STDERR.puts @source.encoding
|
16
|
+
@source.read if @source.buffer.size<2
|
17
|
+
#STDERR.puts "BUFFER = #{@source.buffer.inspect}"
|
18
|
+
if @document_status == nil
|
19
|
+
#@source.consume( /^\s*/um )
|
20
|
+
word = @source.match( /^((?:\s+)|(?:<[^>]*>))/um )
|
21
|
+
word = word[1] unless word.nil?
|
22
|
+
#STDERR.puts "WORD = #{word.inspect}"
|
23
|
+
case word
|
24
|
+
when COMMENT_START
|
25
|
+
return [ :comment, @source.match( COMMENT_PATTERN, true )[1] ]
|
26
|
+
when XMLDECL_START
|
27
|
+
#STDERR.puts "XMLDECL"
|
28
|
+
results = @source.match( XMLDECL_PATTERN, true )[1]
|
29
|
+
version = VERSION.match( results )
|
30
|
+
version = version[1] unless version.nil?
|
31
|
+
encoding = ENCODING.match(results)
|
32
|
+
encoding = encoding[1] unless encoding.nil?
|
33
|
+
@source.encoding = encoding
|
34
|
+
standalone = STANDALONE.match(results)
|
35
|
+
standalone = standalone[1] unless standalone.nil?
|
36
|
+
return [ :xmldecl, version, encoding, standalone ]
|
37
|
+
when INSTRUCTION_START
|
38
|
+
return [ :processing_instruction, *@source.match(INSTRUCTION_PATTERN, true)[1,2] ]
|
39
|
+
when DOCTYPE_START
|
40
|
+
md = @source.match( DOCTYPE_PATTERN, true )
|
41
|
+
@nsstack.unshift(curr_ns=Set.new)
|
42
|
+
identity = md[1]
|
43
|
+
close = md[2]
|
44
|
+
identity =~ IDENTITY
|
45
|
+
name = $1
|
46
|
+
raise REXML::ParseException.new("DOCTYPE is missing a name") if name.nil?
|
47
|
+
pub_sys = $2.nil? ? nil : $2.strip
|
48
|
+
long_name = $4.nil? ? nil : $4.strip
|
49
|
+
uri = $6.nil? ? nil : $6.strip
|
50
|
+
args = [ :start_doctype, name, pub_sys, long_name, uri ]
|
51
|
+
if close == ">"
|
52
|
+
@document_status = :after_doctype
|
53
|
+
@source.read if @source.buffer.size<2
|
54
|
+
md = @source.match(/^\s*/um, true)
|
55
|
+
@stack << [ :end_doctype ]
|
56
|
+
else
|
57
|
+
@document_status = :in_doctype
|
58
|
+
end
|
59
|
+
return args
|
60
|
+
when /^\s+/
|
61
|
+
else
|
62
|
+
@document_status = :after_doctype
|
63
|
+
@source.read if @source.buffer.size<2
|
64
|
+
md = @source.match(/\s*/um, true)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
if @document_status == :in_doctype
|
68
|
+
md = @source.match(/\s*(.*?>)/um)
|
69
|
+
case md[1]
|
70
|
+
when SYSTEMENTITY
|
71
|
+
match = @source.match( SYSTEMENTITY, true )[1]
|
72
|
+
return [ :externalentity, match ]
|
73
|
+
|
74
|
+
when ELEMENTDECL_START
|
75
|
+
return [ :elementdecl, @source.match( ELEMENTDECL_PATTERN, true )[1] ]
|
76
|
+
|
77
|
+
when ENTITY_START
|
78
|
+
match = @source.match( ENTITYDECL, true ).to_a.compact
|
79
|
+
match[0] = :entitydecl
|
80
|
+
ref = false
|
81
|
+
if match[1] == '%'
|
82
|
+
ref = true
|
83
|
+
match.delete_at 1
|
84
|
+
end
|
85
|
+
# Now we have to sort out what kind of entity reference this is
|
86
|
+
if match[2] == 'SYSTEM'
|
87
|
+
# External reference
|
88
|
+
match[3] = match[3][1..-2] # PUBID
|
89
|
+
match.delete_at(4) if match.size > 4 # Chop out NDATA decl
|
90
|
+
# match is [ :entity, name, SYSTEM, pubid(, ndata)? ]
|
91
|
+
elsif match[2] == 'PUBLIC'
|
92
|
+
# External reference
|
93
|
+
match[3] = match[3][1..-2] # PUBID
|
94
|
+
match[4] = match[4][1..-2] # HREF
|
95
|
+
# match is [ :entity, name, PUBLIC, pubid, href ]
|
96
|
+
else
|
97
|
+
match[2] = match[2][1..-2]
|
98
|
+
match.pop if match.size == 4
|
99
|
+
# match is [ :entity, name, value ]
|
100
|
+
end
|
101
|
+
match << '%' if ref
|
102
|
+
return match
|
103
|
+
when ATTLISTDECL_START
|
104
|
+
md = @source.match( ATTLISTDECL_PATTERN, true )
|
105
|
+
raise REXML::ParseException.new( "Bad ATTLIST declaration!", @source ) if md.nil?
|
106
|
+
element = md[1]
|
107
|
+
contents = md[0]
|
108
|
+
|
109
|
+
pairs = {}
|
110
|
+
values = md[0].scan( ATTDEF_RE )
|
111
|
+
values.each do |attdef|
|
112
|
+
unless attdef[3] == "#IMPLIED"
|
113
|
+
attdef.compact!
|
114
|
+
val = attdef[3]
|
115
|
+
val = attdef[4] if val == "#FIXED "
|
116
|
+
pairs[attdef[0]] = val
|
117
|
+
if attdef[0] =~ /^xmlns:(.*)/
|
118
|
+
@nsstack[0] << $1
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
return [ :attlistdecl, element, pairs, contents ]
|
123
|
+
when NOTATIONDECL_START
|
124
|
+
md = nil
|
125
|
+
if @source.match( PUBLIC )
|
126
|
+
md = @source.match( PUBLIC, true )
|
127
|
+
vals = [md[1],md[2],md[4],md[6]]
|
128
|
+
elsif @source.match( SYSTEM )
|
129
|
+
md = @source.match( SYSTEM, true )
|
130
|
+
vals = [md[1],md[2],nil,md[4]]
|
131
|
+
else
|
132
|
+
raise REXML::ParseException.new( "error parsing notation: no matching pattern", @source )
|
133
|
+
end
|
134
|
+
return [ :notationdecl, *vals ]
|
135
|
+
when CDATA_END
|
136
|
+
@document_status = :after_doctype
|
137
|
+
@source.match( CDATA_END, true )
|
138
|
+
return [ :end_doctype ]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
begin
|
142
|
+
if @source.buffer[0] == ?<
|
143
|
+
if @source.buffer[1] == ?/
|
144
|
+
@nsstack.shift
|
145
|
+
last_tag = @tags.pop
|
146
|
+
#md = @source.match_to_consume( '>', CLOSE_MATCH)
|
147
|
+
md = @source.match( CLOSE_MATCH, true )
|
148
|
+
raise REXML::ParseException.new( "Missing end tag for "+
|
149
|
+
"'#{last_tag}' (got \"#{md[1]}\")",
|
150
|
+
@source) unless last_tag == md[1]
|
151
|
+
return [ :end_element, last_tag ]
|
152
|
+
elsif @source.buffer[1] == ?!
|
153
|
+
md = @source.match(/\A(\s*[^>]*>)/um)
|
154
|
+
#STDERR.puts "SOURCE BUFFER = #{source.buffer}, #{source.buffer.size}"
|
155
|
+
raise REXML::ParseException.new("Malformed node", @source) unless md
|
156
|
+
if md[0][2] == ?-
|
157
|
+
md = @source.match( COMMENT_PATTERN, true )
|
158
|
+
return [ :comment, md[1] ] if md
|
159
|
+
else
|
160
|
+
md = @source.match( CDATA_PATTERN, true )
|
161
|
+
return [ :cdata, md[1] ] if md
|
162
|
+
end
|
163
|
+
raise REXML::ParseException.new( "Declarations can only occur "+
|
164
|
+
"in the doctype declaration.", @source)
|
165
|
+
elsif @source.buffer[1] == ??
|
166
|
+
md = @source.match( INSTRUCTION_PATTERN, true )
|
167
|
+
return [ :processing_instruction, md[1], md[2] ] if md
|
168
|
+
raise REXML::ParseException.new( "Bad instruction declaration",
|
169
|
+
@source)
|
170
|
+
else
|
171
|
+
# Get the next tag
|
172
|
+
md = @source.match(TAG_MATCH, true)
|
173
|
+
unless md
|
174
|
+
# Check for missing attribute quotes
|
175
|
+
raise REXML::ParseException.new("missing attribute quote", @source) if @source.match(MISSING_ATTRIBUTE_QUOTES )
|
176
|
+
raise REXML::ParseException.new("malformed XML: missing tag start", @source)
|
177
|
+
end
|
178
|
+
attributes = {}
|
179
|
+
prefixes = Set.new
|
180
|
+
prefixes << md[2] if md[2]
|
181
|
+
@nsstack.unshift(curr_ns=Set.new)
|
182
|
+
if md[4].size > 0
|
183
|
+
attrs = md[4].scan( ATTRIBUTE_PATTERN )
|
184
|
+
raise REXML::ParseException.new( "error parsing attributes: [#{attrs.join ', '}], excess = \"#$'\"", @source) if $' and $'.strip.size > 0
|
185
|
+
attrs.each { |a,b,c,d,e|
|
186
|
+
if b == "xmlns"
|
187
|
+
if c == "xml"
|
188
|
+
if d != "http://www.w3.org/XML/1998/namespace"
|
189
|
+
msg = "The 'xml' prefix must not be bound to any other namespace "+
|
190
|
+
"(http://www.w3.org/TR/REC-xml-names/#ns-decl)"
|
191
|
+
raise REXML::ParseException.new( msg, @source, self )
|
192
|
+
end
|
193
|
+
elsif c == "xmlns"
|
194
|
+
msg = "The 'xmlns' prefix must not be declared "+
|
195
|
+
"(http://www.w3.org/TR/REC-xml-names/#ns-decl)"
|
196
|
+
raise REXML::ParseException.new( msg, @source, self)
|
197
|
+
end
|
198
|
+
curr_ns << c
|
199
|
+
elsif b
|
200
|
+
prefixes << b unless b == "xml"
|
201
|
+
end
|
202
|
+
attributes[a] = e
|
203
|
+
}
|
204
|
+
end
|
205
|
+
|
206
|
+
# Verify that all of the prefixes have been defined
|
207
|
+
#for prefix in prefixes
|
208
|
+
# unless @nsstack.find{|k| k.member?(prefix)}
|
209
|
+
# raise UndefinedNamespaceException.new(prefix,@source,self)
|
210
|
+
# end
|
211
|
+
#end
|
212
|
+
|
213
|
+
if md[6]
|
214
|
+
@closed = md[1]
|
215
|
+
@nsstack.shift
|
216
|
+
else
|
217
|
+
@tags.push( md[1] )
|
218
|
+
end
|
219
|
+
return [ :start_element, md[1], attributes ]
|
220
|
+
end
|
221
|
+
else
|
222
|
+
md = @source.match( TEXT_PATTERN, true )
|
223
|
+
if md[0].length == 0
|
224
|
+
@source.match( /(\s+)/, true )
|
225
|
+
end
|
226
|
+
#STDERR.puts "GOT #{md[1].inspect}" unless md[0].length == 0
|
227
|
+
#return [ :text, "" ] if md[0].length == 0
|
228
|
+
# unnormalized = Text::unnormalize( md[1], self )
|
229
|
+
# return PullEvent.new( :text, md[1], unnormalized )
|
230
|
+
return [ :text, md[1] ]
|
231
|
+
end
|
232
|
+
rescue REXML::UndefinedNamespaceException
|
233
|
+
raise
|
234
|
+
rescue REXML::ParseException
|
235
|
+
raise
|
236
|
+
rescue Exception, NameError => error
|
237
|
+
raise REXML::ParseException.new( "Exception parsing",
|
238
|
+
@source, self, (error ? error : $!) )
|
239
|
+
end
|
240
|
+
return [ :dummy ]
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-oauth2-provider
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Johnny G. Halife & Ezequiel Morito
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-20 00:00:00 -03:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: ruby-hmac
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: information_card
|
34
|
+
prerelease: false
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
segments:
|
40
|
+
- 0
|
41
|
+
version: "0"
|
42
|
+
type: :runtime
|
43
|
+
version_requirements: *id002
|
44
|
+
description: A simple implementation of provider protocols of oAuth 2.0 (right now just AssertionProfile).
|
45
|
+
email: johnny.halife@me.com
|
46
|
+
executables: []
|
47
|
+
|
48
|
+
extensions: []
|
49
|
+
|
50
|
+
extra_rdoc_files: []
|
51
|
+
|
52
|
+
files:
|
53
|
+
- rakefile
|
54
|
+
- lib/rack/oauth2/assertion_profile.rb
|
55
|
+
- lib/rack/oauth2/provider.rb
|
56
|
+
- lib/simple_web_token_builder.rb
|
57
|
+
- vendor/information_card/saml_token.rb
|
58
|
+
- vendor/information_card/xml_canonicalizer.rb
|
59
|
+
- vendor/information_card.rb
|
60
|
+
- vendor/rexml/parsers.rb
|
61
|
+
has_rdoc: true
|
62
|
+
homepage:
|
63
|
+
licenses: []
|
64
|
+
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options:
|
67
|
+
- --line-numbers
|
68
|
+
- --inline-source
|
69
|
+
- -A cattr_accessor=object
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.3.6
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Rack Middleware that authenticates users based on the different profiles of oAuth 2.0 standard
|
93
|
+
test_files:
|
94
|
+
- spec/rack/oauth2/assertion_profile.test.rb
|
95
|
+
- spec/simple_web_token_builder_test.rb
|
96
|
+
- spec/specs_config.rb
|