openlogic-saml-sp 3.1.3

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,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
+