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.
- data/History.txt +43 -0
- data/LICENSE.txt +21 -0
- data/README.md +126 -0
- data/Rakefile +29 -0
- data/VERSION +1 -0
- data/lib/saml-sp.rb +70 -0
- data/lib/saml2.rb +11 -0
- data/lib/saml2/artifact_resolver.rb +192 -0
- data/lib/saml2/assertion.rb +100 -0
- data/lib/saml2/type4_artifact.rb +55 -0
- data/lib/saml2/unexpected_type_code_error.rb +8 -0
- data/lib/saml_sp/config.rb +119 -0
- data/rails/init.rb +11 -0
- data/spec/saml2/artifact_resolver_spec.rb +169 -0
- data/spec/saml2/assertion_spec.rb +177 -0
- data/spec/saml2/type4_artifact_spec.rb +66 -0
- data/spec/saml_sp/config_spec.rb +299 -0
- data/spec/spec_helper.rb +25 -0
- metadata +130 -0
@@ -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,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
|
+
|
data/rails/init.rb
ADDED
@@ -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
|
+
|