openlogic-saml-sp 3.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,100 @@
1
+ require 'nokogiri'
2
+ require 'saml2/artifact_resolver'
3
+
4
+ module Saml2
5
+ class InvalidAssertionError < ArgumentError
6
+ end
7
+
8
+ class Assertion
9
+ module Parsing
10
+ def self.item(name, xpath)
11
+ module_eval(<<-ITEM_METH)
12
+ def #{name}_from(node)
13
+ target_node = node.at('#{xpath}')
14
+ raise InvalidAssertionError, "#{name} missing (xpath: `#{xpath}`)" unless target_node
15
+
16
+ target_node.content.strip
17
+ end
18
+ ITEM_METH
19
+ end
20
+
21
+ item :issuer, '//asrt:Assertion/asrt:Issuer'
22
+ item :subject_name_id, '//asrt:Assertion/asrt:Subject/asrt:NameID'
23
+ item :attribute_name, '@Name'
24
+ item :attribute_value, './asrt:AttributeValue'
25
+
26
+ def each_attribute_node_from(doc, &blk)
27
+ attribute_nodes = doc.search('//asrt:Assertion/asrt:AttributeStatement/asrt:Attribute')
28
+
29
+ attribute_nodes.each &blk
30
+ end
31
+ end
32
+ extend Parsing
33
+
34
+ # Resolves an artifact into the Assertion it represents
35
+ #
36
+ # @param [Saml2::Type4Artifact, String] artifact The artifact to
37
+ # resolve
38
+ #
39
+ # @return [Saml2::Assertion] The assertion represented by
40
+ # specified artifact
41
+ def self.new_from_artifact(artifact)
42
+ artifact = if artifact.respond_to? :resolve
43
+ artifact
44
+ else
45
+ Type4Artifact.new_from_string(artifact)
46
+ end
47
+
48
+
49
+ artifact.resolve
50
+ end
51
+
52
+ def self.logger
53
+ SamlSp.logger
54
+ end
55
+
56
+ def self.new_from_xml(xml_assertion)
57
+ doc = case xml_assertion
58
+ when Nokogiri::XML::Node
59
+ xml_assertion
60
+ else
61
+ Nokogiri::XML.parse(xml_assertion)
62
+ end
63
+ logger.debug {"Parsing assertion: \n" + doc.to_xml(:indent => 2).gsub(/^/, "\t")}
64
+
65
+
66
+ doc.root.add_namespace_definition('asrt', 'urn:oasis:names:tc:SAML:2.0:assertion')
67
+
68
+ attrs = Hash.new
69
+ each_attribute_node_from(doc) do |node|
70
+ attrs[attribute_name_from(node)] = attribute_value_from(node)
71
+ end
72
+
73
+ new(issuer_from(doc), subject_name_id_from(doc), attrs)
74
+
75
+ end
76
+
77
+ attr_reader :issuer, :subject_name_id
78
+
79
+ def initialize(issuer, subject_name_id, attributes)
80
+ @issuer = issuer
81
+ @subject_name_id = subject_name_id
82
+ @attributes = attributes
83
+ end
84
+
85
+ def [](attr_name)
86
+ attributes[attr_name.to_s]
87
+ end
88
+
89
+ protected
90
+
91
+ attr_reader :attributes
92
+
93
+ end
94
+ end
95
+
96
+
97
+ # Copyright (c) 2010 OpenLogic
98
+ #
99
+ # Licensed under MIT license. See LICENSE.txt
100
+
@@ -0,0 +1,55 @@
1
+ require 'base64'
2
+ require 'saml2/unexpected_type_code_error'
3
+
4
+ module Saml2
5
+ class Type4Artifact
6
+ attr_reader :endpoint_index, :source_id, :message_handle
7
+
8
+ # Parse an type 4 SAML 2.0 artifact such as one received in a
9
+ # `SAMLart` HTTP request parameter as part of a HTTP artifact
10
+ # binding SSO handshake.
11
+ #
12
+ # @param [String] artifact_string a base64 encoded SAML 2.0
13
+ # artifact
14
+ # @return [Saml2::Type4Artifact] the parsed artifact
15
+ def self.new_from_string(artifact_string)
16
+ unencoded_artifact = Base64.decode64 artifact_string
17
+
18
+ type_code, *rest = unencoded_artifact.unpack('nna20a20')
19
+
20
+ raise UnexpectedTypeCodeError.new("Incorrect artifact type (expected type code 4 but found #{type_code}") unless type_code == 4
21
+
22
+ new *rest
23
+ end
24
+
25
+ def initialize(endpoint_index, source_id, message_handle)
26
+ @endpoint_index = endpoint_index
27
+ @source_id = source_id
28
+ @message_handle = message_handle
29
+ end
30
+
31
+ # The type code of this artifact
32
+ def type_code
33
+ 4
34
+ end
35
+
36
+ # @return [String] base64 encoded version of self
37
+ def to_s
38
+ Base64.encode64([4, endpoint_index, source_id, message_handle].pack('nna20a20')).strip
39
+ end
40
+
41
+ # Resolve the artifact into an Assertion
42
+ #
43
+ # @return [Saml2::Assertion] the assertion to which the artifact
44
+ # is a reference
45
+ def resolve
46
+ Saml2::ArtifactResolver(source_id).resolve(self)
47
+ end
48
+ end
49
+ end
50
+
51
+
52
+ # Copyright (c) 2010 OpenLogic
53
+ #
54
+ # Licensed under MIT license. See LICENSE.txt
55
+
@@ -0,0 +1,8 @@
1
+ class UnexpectedTypeCodeError < StandardError
2
+ end
3
+
4
+
5
+ # Copyright (c) 2010 OpenLogic
6
+ #
7
+ # Licensed under MIT license. See LICENSE.txt
8
+
@@ -0,0 +1,119 @@
1
+ module SamlSp
2
+ class ConfigurationError < StandardError
3
+ end
4
+
5
+ class ConfigBlock
6
+ # Interpret a config block.
7
+ def interpret(config_block, filename = nil)
8
+ if filename
9
+ instance_eval config_block, filename
10
+ elsif config_block.respond_to? :call
11
+ instance_eval &config_block
12
+ else
13
+ instance_eval config_block
14
+ end
15
+
16
+ self
17
+ end
18
+
19
+ def self.inherited(subclass)
20
+ subclass.extend ClassMethods
21
+ end
22
+
23
+ NOVAL_MARKER = Object.new
24
+
25
+ module ClassMethods
26
+ def config_item(name)
27
+ class_eval(<<METHOD)
28
+ def #{name}(val=NOVAL_MARKER)
29
+ if NOVAL_MARKER.equal? val
30
+ @#{name}
31
+ else
32
+ @#{name} = val
33
+ end
34
+ end
35
+ METHOD
36
+ end
37
+ end
38
+ end
39
+
40
+ class Config < ConfigBlock
41
+ def self.load_file(filename)
42
+ SamlSp.logger.info "saml-sp: Loading config file '#{filename}'"
43
+
44
+ new.interpret File.read(filename), filename
45
+ end
46
+
47
+ def interpret(config, filename = nil)
48
+ if filename
49
+ instance_eval config, filename
50
+ else
51
+ instance_eval config
52
+ end
53
+ end
54
+
55
+ def logger(logger)
56
+ SamlSp.logger = logger
57
+ end
58
+
59
+ def artifact_resolution_service(&blk)
60
+ dsl = ResolutionSerivceConfig.new
61
+ dsl.interpret(blk)
62
+ end
63
+ end
64
+
65
+
66
+ class ResolutionSerivceConfig < ConfigBlock
67
+ config_item :source_id
68
+ config_item :uri
69
+ config_item :identity_provider
70
+ config_item :service_provider
71
+ config_item :logger
72
+
73
+ def interpret(config_block, filename = nil)
74
+ super
75
+
76
+ raise ConfigurationError, "Incomplete artifact resolution service information" unless @source_id && @uri && @identity_provider && @service_provider
77
+
78
+ resolver = Saml2::ArtifactResolver.new(@source_id, @uri, @identity_provider, @service_provider)
79
+
80
+ if @auth_info
81
+ resolver.basic_auth_credentials(@auth_info.user_id, @auth_info.password, @auth_info.realm)
82
+ end
83
+
84
+ resolver
85
+ end
86
+
87
+ def http_basic_auth(&blk)
88
+ @auth_info = HttpBasicAuthConfig.new
89
+ @auth_info.interpret(blk)
90
+ end
91
+ end
92
+
93
+ class HttpBasicAuthConfig < ConfigBlock
94
+ config_item :realm
95
+ config_item :user_id
96
+ config_item :password
97
+
98
+ def promiscuous
99
+ @promiscuous = true
100
+ end
101
+
102
+ def interpret(blk, filename = nil)
103
+ super
104
+
105
+ raise ConfigurationError, "Incomplete HTTP basic auth credentials" unless valid?
106
+
107
+ self
108
+ end
109
+
110
+ def valid?
111
+ (@realm || @promiscuous) && @user_id && @password
112
+ end
113
+ end
114
+ end
115
+
116
+ # Copyright (c) 2010 OpenLogic
117
+ #
118
+ # Licensed under MIT license. See LICENSE.txt
119
+
@@ -0,0 +1,11 @@
1
+ SamlSp.logger = Rails.logger
2
+
3
+ config_file = File.join(Rails.root, 'config/saml_sp.conf')
4
+ SamlSp::Config.load_file config_file if File.exists? config_file
5
+
6
+
7
+
8
+ # Copyright (c) 2010 OpenLogic
9
+ #
10
+ # Licensed under MIT license. See LICENSE.txt
11
+
@@ -0,0 +1,169 @@
1
+ require File.join(File.dirname(__FILE__), '../spec_helper')
2
+
3
+ describe Saml2::ArtifactResolver do
4
+ describe "lookups" do
5
+ before do
6
+ @resolver = Saml2::ArtifactResolver.new('a-source-id', 'https://idp.invalid/resolution-service', 'http://idp.invalid', 'http://sp.invalid')
7
+ end
8
+
9
+ it "should have pseudo-class lookup method" do
10
+ Saml2::ArtifactResolver('a-source-id').should == @resolver
11
+ end
12
+
13
+ it "should raise error when resolver is not found" do
14
+ lambda {
15
+ Saml2::ArtifactResolver('not-a-known-source-id')
16
+ }.should raise_error Saml2::NoSuchResolverError
17
+ end
18
+ end
19
+
20
+
21
+ describe "successfully resolving artifact" do
22
+ before do
23
+ @resolver = Saml2::ArtifactResolver.new('a-source-id', 'https://idp.invalid/resolution-service', 'http://idp.invalid', 'http://sp.invalid')
24
+ @resolver.basic_auth_credentials('myuserid', 'mypasswd', 'myrealm')
25
+
26
+ @artifact = Saml2::Type4Artifact.new(0, '01234567890123456789', 'abcdefghijklmnopqrst')
27
+ FakeWeb.register_uri(:post, 'https://idp.invalid/resolution-service', :body => SUCCESSFUL_SAML_RESP)
28
+ end
29
+
30
+ it "should parse reponse into an assertion" do
31
+ @resolver.resolve(@artifact).should be_kind_of(Saml2::Assertion)
32
+ end
33
+
34
+ it "should extract issuer from response" do
35
+ @resolver.resolve(@artifact).issuer.should == 'http://idp.invalid'
36
+ end
37
+
38
+ end
39
+
40
+ describe "denied artifact resolution request" do
41
+ before do
42
+ @resolver = Saml2::ArtifactResolver.new('a-source-id', 'https://idp.invalid/resolution-service', 'http://idp.invalid', 'http://sp.invalid')
43
+ @resolver.basic_auth_credentials('myuserid', 'mypasswd', 'myrealm')
44
+
45
+ @artifact = Saml2::Type4Artifact.new(0, '01234567890123456789', 'abcdefghijklmnopqrst')
46
+ FakeWeb.register_uri(:post, 'https://idp.invalid/resolution-service', :body => DENIED_SAML_RESP)
47
+ end
48
+
49
+ it "should raise exception" do
50
+ lambda {
51
+ @resolver.resolve(@artifact)
52
+ }.should raise_error(Saml2::RequestDeniedError)
53
+ end
54
+ end
55
+
56
+
57
+ SUCCESSFUL_SAML_RESP = <<-SAML_RESP
58
+ <SOAP-ENV:Envelope
59
+ xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
60
+ <SOAP-ENV:Body>
61
+ <ArtifactResponse
62
+ ID="_423adb988f2673de74553f9f26ff27eda8af"
63
+ InResponseTo="_gIPoW.YXQpZj17m.EpboPCp9cT"
64
+ IssueInstant="2006-11-28T23:07:43.738+00:00"
65
+ Version="2.0"
66
+ xmlns="urn:oasis:names:tc:SAML:2.0:protocol">
67
+ <ns1:Issuer xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion">
68
+ http://idp.invalid
69
+ </ns1:Issuer>
70
+
71
+ <Status>
72
+ <StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
73
+ </Status>
74
+
75
+ <Response
76
+ Destination="https://service_provider/SAMLConsumer"
77
+ ID="_dcfacebe4f2fca1cbdae749c5f5738995e0"
78
+ IssueInstant="2006-11-28T23:04:32Z"
79
+ Version="2.0">
80
+ <ns2:Issuer
81
+ Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
82
+ xmlns:ns2="urn:oasis:names:tc:SAML:2.0:assertion">
83
+ http://idp.invalid
84
+ </ns2:Issuer>
85
+
86
+ <Status>
87
+ <StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
88
+ </Status>
89
+
90
+ <ns3:Assertion
91
+ ID="_1ebc0cd2f88ade6396bccb22fc20a42792c4"
92
+ IssueInstant="2006-11-28T23:04:32Z"
93
+ Version="2.0"
94
+ xmlns:ns3="urn:oasis:names:tc:SAML:2.0:assertion">
95
+ <ns3:Issuer
96
+ Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
97
+ http://idp.invalid
98
+ </ns3:Issuer>
99
+
100
+ <ns3:Subject>
101
+ <ns3:NameID
102
+ Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">
103
+ 12345678
104
+ </ns3:NameID>
105
+ </ns3:Subject>
106
+
107
+ <ns3:Conditions
108
+ NotBefore="2006-11-28T22:54:32Z"
109
+ NotOnOrAfter="2006-11-28T23:24:32Z">
110
+ <ns3:AudienceRestriction>
111
+ <ns3:Audience>https://sp.invalid</ns3:Audience>
112
+ </ns3:AudienceRestriction>
113
+ </ns3:Conditions>
114
+
115
+ <ns3:AuthnStatement
116
+ AuthnInstant="2006-11-28T23:03:14Z"
117
+ SessionIndex="MQSnyIps57sm2wRDKP+f9PsY+2A=nFfVrw=="
118
+ SessionNotOnOrAfter="2006-11-28T23:24:32Z">
119
+ <ns3:AuthnContext>
120
+ <ns3:AuthnContextClassRef>
121
+ urn:oasis:names:tc:SAML:2.0:ac:classes:Password
122
+ </ns3:AuthnContextClassRef>
123
+ </ns3:AuthnContext>
124
+ </ns3:AuthnStatement>
125
+
126
+ <ns3:AttributeStatement>
127
+ <ns3:Attribute
128
+ Name="cn"
129
+ NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
130
+ <ns3:AttributeValue>Smith, James</ns3:AttributeValue>
131
+ </ns3:Attribute>
132
+ </ns3:AttributeStatement>
133
+
134
+ </ns3:Assertion>
135
+ </Response>
136
+ </ArtifactResponse>
137
+ </SOAP-ENV:Body>
138
+ </SOAP-ENV:Envelope>
139
+ SAML_RESP
140
+
141
+ DENIED_SAML_RESP = <<-SAML_RESP
142
+ <SOAP-ENV:Envelope
143
+ xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
144
+ <SOAP-ENV:Body>
145
+ <ArtifactResponse
146
+ ID="_423adb988f2673de74553f9f26ff27eda8af"
147
+ InResponseTo="_gIPoW.YXQpZj17m.EpboPCp9cT"
148
+ IssueInstant="2006-11-28T23:07:43.738+00:00"
149
+ Version="2.0"
150
+ xmlns="urn:oasis:names:tc:SAML:2.0:protocol">
151
+ <ns1:Issuer xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion">
152
+ https://idp.invalid
153
+ </ns1:Issuer>
154
+
155
+ <Status>
156
+ <StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:RequestDenied" />
157
+ </Status>
158
+ </ArtifactResponse>
159
+ </SOAP-ENV:Body>
160
+ </SOAP-ENV:Envelope>
161
+ SAML_RESP
162
+
163
+ end
164
+
165
+
166
+ # Copyright (c) 2010 OpenLogic
167
+ #
168
+ # Licensed under MIT license. See LICENSE.txt
169
+