rack-oauth2-provider 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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