rack-oauth2-provider 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module Rack
2
+ module OAuth2
3
+ VERSION = "0.0.1"
4
+ end
5
+ 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
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'mocha'
4
+ require 'rack/test'
5
+ require 'rack/mock'
6
+
7
+ Spec::Runner.configure do |config|
8
+ config.mock_with :mocha
9
+ end
10
+
11
+ $LOAD_PATH.unshift(File.dirname __FILE__)
@@ -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