openlogic-saml-sp 3.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|