saml-sp 3.1.2
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 +191 -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 +132 -0
data/History.txt
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
3.1.2 / 2010-06-39
|
2
|
+
---
|
3
|
+
|
4
|
+
* OSS release (Yay!)
|
5
|
+
|
6
|
+
3.0.4 / 2010-04-26
|
7
|
+
---
|
8
|
+
|
9
|
+
* Improved error messages when unable to interpret assertions documents properly.
|
10
|
+
* Improved debug logging around parsing assertion documents.
|
11
|
+
|
12
|
+
3.0.2 / 2010-04-26
|
13
|
+
---
|
14
|
+
|
15
|
+
* Fixed ArtifaceResolve request document such that it is valid against
|
16
|
+
the SAML schema.
|
17
|
+
|
18
|
+
3.0.0 / 2010-04-23
|
19
|
+
---
|
20
|
+
|
21
|
+
* Added way to specify both the IdP and SP identifiers in the config file.
|
22
|
+
|
23
|
+
2.1.0 / 2010-03-26
|
24
|
+
----
|
25
|
+
|
26
|
+
* Improved logging of artifact resolver registration.
|
27
|
+
* Promiscuous basic auth support.
|
28
|
+
|
29
|
+
2.0.0 / 2010-01-29
|
30
|
+
----
|
31
|
+
|
32
|
+
* Added issuer verification to artifact resolution to protect against
|
33
|
+
spoofing.
|
34
|
+
* Removed attempt to load config file from non-existent directory for
|
35
|
+
Rails apps.
|
36
|
+
|
37
|
+
1.0.0 / 2010-01-22
|
38
|
+
------
|
39
|
+
|
40
|
+
* 1 major enhancement
|
41
|
+
* Birthday!
|
42
|
+
|
43
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
|
2
|
+
(The MIT License)
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
a copy of this software and associated documentation files (the
|
6
|
+
'Software'), to deal in the Software without restriction, including
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
18
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
19
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
20
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
21
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
saml-sp
|
2
|
+
====
|
3
|
+
|
4
|
+
by OpenLogic
|
5
|
+
http://openlogic.com
|
6
|
+
peter.williams@openlogic.com
|
7
|
+
|
8
|
+
## STATUS:
|
9
|
+
|
10
|
+
This library is stable and under active development.
|
11
|
+
|
12
|
+
Version identifiers follow
|
13
|
+
[rational version ](http://docs.rubygems.org/read/chapter/7#page26).
|
14
|
+
The major version number does not indicate this library is complete.
|
15
|
+
|
16
|
+
## DESCRIPTION:
|
17
|
+
|
18
|
+
Support for being a SAML 2.0 service provider in an HTTP artifact
|
19
|
+
binding SSO conversation.
|
20
|
+
|
21
|
+
## SYNOPSIS:
|
22
|
+
|
23
|
+
This library provides parsing of SAML 2.0 artifacts. For example.
|
24
|
+
|
25
|
+
artifact = Saml2::Type4Artifact.new_from_string(params['SAMLart']) # => #<Saml2::Type4Artifact ...>
|
26
|
+
artifact.source_id # => 'a314Xc8KaSd4fEJAd8R'
|
27
|
+
artifact.type_code # => 4
|
28
|
+
|
29
|
+
Once you have an artifact you can resolve it into it's associated assertion:
|
30
|
+
|
31
|
+
assertion = artifact.resolve # => #<Saml2::Assertion>
|
32
|
+
|
33
|
+
With the assertion you can identify the user and retrieve attributes:
|
34
|
+
|
35
|
+
assertion.subject_name_id # => '1234'
|
36
|
+
assertion['mail'] # => 'john.doe@idp.example'
|
37
|
+
|
38
|
+
### Configuration
|
39
|
+
|
40
|
+
If you are using Rails the SamlSp will automatically load
|
41
|
+
configuration info from `config/saml_sp.conf`.
|
42
|
+
|
43
|
+
For non-Rails apps the saml-sp configuration file can be place in the
|
44
|
+
application configuration directory and loaded using the following
|
45
|
+
code during application startup.
|
46
|
+
|
47
|
+
SamlSp::Config.load_file(APP_ROOT + "/config/saml_sp.conf")
|
48
|
+
|
49
|
+
#### Logging
|
50
|
+
|
51
|
+
If you are using saml-sp in a rails app it will automatically log to
|
52
|
+
the Rails default logger. For non-Rails apps you can specify a Logger
|
53
|
+
object to be used in the config file.
|
54
|
+
|
55
|
+
logger MY_APP_LOGGER
|
56
|
+
|
57
|
+
|
58
|
+
#### Artifact Resolution Service
|
59
|
+
|
60
|
+
For artifact resolution to take place you need to configure an
|
61
|
+
artifact resolution service for the artifacts source. This is done by
|
62
|
+
adding block similar to the following to your saml-sp config file.
|
63
|
+
|
64
|
+
artifact_resolution_service {
|
65
|
+
source_id 'opaque-id-of-the-idp'
|
66
|
+
uri 'https://samlar.idp.example/resolve-artifact'
|
67
|
+
identity_provider 'http://idp.example/'
|
68
|
+
service_provider 'http://your-domain.example/'
|
69
|
+
http_basic_auth {
|
70
|
+
realm 'the-idp-realm'
|
71
|
+
user_id 'my-user-id'
|
72
|
+
password 'my-password'
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
The configuration details are:
|
77
|
+
|
78
|
+
* source_id:
|
79
|
+
The id of the source that this resolution service can
|
80
|
+
resolve. This is a 20 octet binary string.
|
81
|
+
|
82
|
+
* uri:
|
83
|
+
The endpoint to which artifact resolve requests should be sent.
|
84
|
+
|
85
|
+
* identity_provider:
|
86
|
+
The URI identifying the identity provider that issues assertions
|
87
|
+
using the source id specified.
|
88
|
+
|
89
|
+
* service_provider:
|
90
|
+
The URI identifying the your software (the service provider) to
|
91
|
+
the identity provider.
|
92
|
+
|
93
|
+
* http_basic_auth:
|
94
|
+
(Optional) The credentials needed to authenticate with the IdP
|
95
|
+
using HTTP basic authentication.
|
96
|
+
|
97
|
+
#### Promiscuous Auth
|
98
|
+
|
99
|
+
If the IdP does not provide proper HTTP challenge responses you can
|
100
|
+
specify the HTTP auth in promiscuous mode. For example,
|
101
|
+
|
102
|
+
http_basic_auth {
|
103
|
+
promiscuous
|
104
|
+
user_id 'my-user-id'
|
105
|
+
password 'my-password'
|
106
|
+
}
|
107
|
+
|
108
|
+
In promiscuous mode the credentials are sent with every request to
|
109
|
+
this resolutions service regardless of it's realm.
|
110
|
+
|
111
|
+
|
112
|
+
## REQUIREMENTS:
|
113
|
+
|
114
|
+
* Nokogiri
|
115
|
+
* Resourcful
|
116
|
+
* uuidtools
|
117
|
+
|
118
|
+
## INSTALL:
|
119
|
+
|
120
|
+
* sudo gem install saml-sp
|
121
|
+
|
122
|
+
## LICENSE:
|
123
|
+
|
124
|
+
Copyright (c) 2010 OpenLogic
|
125
|
+
|
126
|
+
Licensed under the MIT License. See LICENSE.txt
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
begin
|
2
|
+
require 'jeweler'
|
3
|
+
Jeweler::Tasks.new do |gemspec|
|
4
|
+
gemspec.name = 'saml-sp'
|
5
|
+
gemspec.summary = 'SAML 2.0 SSO Sevice Provider Library'
|
6
|
+
gemspec.email = 'pezra@barelyenough.org'
|
7
|
+
gemspec.authors = ["OpenLogic", "Peter Williams"]
|
8
|
+
gemspec.add_dependency 'nokogiri'
|
9
|
+
gemspec.add_dependency 'openlogic-resourceful'
|
10
|
+
gemspec.add_dependency 'uuidtools'
|
11
|
+
gemspec.add_development_dependency 'rspec'
|
12
|
+
gemspec.files = FileList["[A-Z]*", "{bin,generators,lib,test,spec,rails}/**/*"]
|
13
|
+
end
|
14
|
+
Jeweler::GemcutterTasks.new
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'spec/rake/spectask'
|
20
|
+
Spec::Rake::SpecTask.new
|
21
|
+
|
22
|
+
task 'test:run' => :spec
|
23
|
+
# EOF
|
24
|
+
|
25
|
+
|
26
|
+
# Copyright (c) 2010 OpenLogic
|
27
|
+
#
|
28
|
+
# Licensed under MIT license. See LICENSE.txt
|
29
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.1.2
|
data/lib/saml-sp.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module SamlSp
|
4
|
+
|
5
|
+
# :stopdoc:
|
6
|
+
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
7
|
+
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
8
|
+
VERSION = ::File.read(PATH + 'VERSION').strip
|
9
|
+
# :startdoc:
|
10
|
+
|
11
|
+
# Returns the version string for the library.
|
12
|
+
#
|
13
|
+
def self.version
|
14
|
+
VERSION
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the library path for the module. If any arguments are given,
|
18
|
+
# they will be joined to the end of the libray path using
|
19
|
+
# <tt>File.join</tt>.
|
20
|
+
#
|
21
|
+
def self.libpath( *args )
|
22
|
+
args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns the lpath for the module. If any arguments are given,
|
26
|
+
# they will be joined to the end of the path using
|
27
|
+
# <tt>File.join</tt>.
|
28
|
+
#
|
29
|
+
def self.path( *args )
|
30
|
+
args.empty? ? PATH : ::File.join(PATH, args.flatten)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Logger that does nothing
|
34
|
+
BITBUCKET_LOGGER = Logger.new(nil)
|
35
|
+
class << BITBUCKET_LOGGER
|
36
|
+
def add(*args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# The logger saml-sp should use
|
41
|
+
def self.logger
|
42
|
+
@@logger ||= BITBUCKET_LOGGER
|
43
|
+
end
|
44
|
+
|
45
|
+
# Set the logger for saml-sp
|
46
|
+
def self.logger=(a_logger)
|
47
|
+
@@logger = a_logger
|
48
|
+
end
|
49
|
+
|
50
|
+
module Logging
|
51
|
+
def logger
|
52
|
+
SamlSp.logger
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.included(base)
|
56
|
+
base.extend(self)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
autoload :Config, 'saml_sp/config'
|
61
|
+
end # module SamlSp
|
62
|
+
|
63
|
+
require 'rubygems'
|
64
|
+
|
65
|
+
autoload :Saml2, 'saml2'
|
66
|
+
|
67
|
+
|
68
|
+
# Copyright (c) 2010 OpenLogic
|
69
|
+
#
|
70
|
+
# Licensed under MIT license. See LICENSE.txt
|
data/lib/saml2.rb
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
gem 'openlogic-resourceful'
|
3
|
+
require 'resourceful'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'uuidtools'
|
6
|
+
|
7
|
+
module Saml2
|
8
|
+
class NoSuchResolverError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
class RequestDeniedError < StandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
class AnomalousResponseIssuerError < StandardError
|
15
|
+
def self.new_from_issuers(expected, actual)
|
16
|
+
new "Issuer should have been <#{expected}> but was <#{actual}>"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ArtifactResolver
|
21
|
+
attr_reader :source_id, :resolution_service_uri, :idp_id, :sp_id
|
22
|
+
attr_reader :basic_auth_realm, :basic_auth_user_id, :basic_auth_password
|
23
|
+
|
24
|
+
# Initialize and register a new artifact resolver.
|
25
|
+
#
|
26
|
+
# @param [string] source_id An opacque identifier used by the IDP
|
27
|
+
# to identify artifact that can be resolved by this service.
|
28
|
+
#
|
29
|
+
# @param [string] resolution_service_uri The URI that will resolve
|
30
|
+
# artifacts into assertions.
|
31
|
+
#
|
32
|
+
# @param [String] idp_id The URI identifying the assertion issuer at this
|
33
|
+
# source.
|
34
|
+
#
|
35
|
+
# @param [String] sp_id The URI identifying (for this source) the service
|
36
|
+
# provider. IOW, the id of your application.
|
37
|
+
def initialize(source_id, resolution_service_uri, idp_id, sp_id)
|
38
|
+
@source_id = source_id
|
39
|
+
@resolution_service_uri = Addressable::URI.parse(resolution_service_uri)
|
40
|
+
@idp_id = idp_id
|
41
|
+
@sp_id = sp_id
|
42
|
+
ArtifactResolverRegistry.register self
|
43
|
+
end
|
44
|
+
|
45
|
+
# Set HTTP basic authentication credentials
|
46
|
+
def basic_auth_credentials(user_id, password, realm = nil)
|
47
|
+
@basic_auth_realm = realm
|
48
|
+
@basic_auth_user_id = user_id
|
49
|
+
@basic_auth_password = password
|
50
|
+
end
|
51
|
+
|
52
|
+
def logger
|
53
|
+
SamlSp.logger
|
54
|
+
end
|
55
|
+
|
56
|
+
def http
|
57
|
+
@http ||= Resourceful::HttpAccessor.new(:authenticators => authenticator, :logger => logger)
|
58
|
+
end
|
59
|
+
|
60
|
+
def authenticator
|
61
|
+
return nil unless basic_auth_user_id
|
62
|
+
|
63
|
+
if basic_auth_realm
|
64
|
+
Resourceful::BasicAuthenticator.new(basic_auth_realm, basic_auth_user_id, basic_auth_password)
|
65
|
+
else
|
66
|
+
Resourceful::PromiscuousBasicAuthenticator.new(basic_auth_user_id, basic_auth_password)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Resolve `artifact` into an Assertion.
|
71
|
+
#
|
72
|
+
# @param [Saml2::Type4Artifact] The artifact to resolve.
|
73
|
+
#
|
74
|
+
# @return [Saml2::Assertion]
|
75
|
+
#
|
76
|
+
# @raise [RequestDeniedError] When the resolution service refuses
|
77
|
+
# to resolve the artifact.
|
78
|
+
#
|
79
|
+
# @raise [AnomalousResponseIssuerError] When the issuer of the
|
80
|
+
# response do not match the idp_id for this source.
|
81
|
+
def resolve(artifact)
|
82
|
+
soap_body = request_document_for(artifact)
|
83
|
+
logger.debug{"ArtifactResolve request body:\n#{soap_body.gsub(/^/, "\t")}"}
|
84
|
+
resp = http.resource(resolution_service_uri).post(soap_body,
|
85
|
+
'Accept' => 'application/soap+xml',
|
86
|
+
'Content-Type' => 'application/soap+xml')
|
87
|
+
|
88
|
+
doc = Nokogiri::XML.parse(resp.body)
|
89
|
+
assert_successful_response(doc)
|
90
|
+
|
91
|
+
assertion = Assertion.new_from_xml(doc)
|
92
|
+
|
93
|
+
raise AnomalousResponseIssuerError.new_from_issuers(idp_id, assertion.issuer) unless
|
94
|
+
assertion.issuer == idp_id
|
95
|
+
|
96
|
+
assertion
|
97
|
+
|
98
|
+
rescue Resourceful::UnsuccessfulHttpRequestError => e
|
99
|
+
|
100
|
+
logger.debug {
|
101
|
+
body = e.http_request.body
|
102
|
+
body.rewind
|
103
|
+
"Artifact resolution request:\n" + body.read.gsub(/^/, ' ')}
|
104
|
+
logger.debug {"Artifact resolution response:\n" + e.http_response.body.gsub(/^/, ' ')}
|
105
|
+
raise
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_s
|
109
|
+
"Resolver for <#{idp_id}> (#{Base64.encode64(source_id).strip})"
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def assert_successful_response(resp_doc)
|
115
|
+
response_code = resp_doc.at("//sp:StatusCode/@Value", namespaces).content.strip
|
116
|
+
return if response_code == 'urn:oasis:names:tc:SAML:2.0:status:Success'
|
117
|
+
|
118
|
+
# Request was not handled successfully
|
119
|
+
err_message = "Request failed"
|
120
|
+
|
121
|
+
status_message_elem = resp_doc.at("//sp:StatusMessage", namespaces)
|
122
|
+
if status_message_elem
|
123
|
+
err_message << " because \"#{status_message_elem.content.strip}\""
|
124
|
+
end
|
125
|
+
|
126
|
+
err_message << ". (status code: #{response_code})"
|
127
|
+
|
128
|
+
if status_details_elem = resp_doc.at("//sp:StatusDetail", namespaces)
|
129
|
+
logger.debug "Details for resolve artifact failure (status code: #{response_code}):\n" + status_details_elem.content
|
130
|
+
end
|
131
|
+
|
132
|
+
raise RequestDeniedError, err_message
|
133
|
+
end
|
134
|
+
|
135
|
+
def namespaces
|
136
|
+
{'sp' => 'urn:oasis:names:tc:SAML:2.0:protocol',
|
137
|
+
'sa' => 'urn:oasis:names:tc:SAML:2.0:assertion'}
|
138
|
+
end
|
139
|
+
def request_document_for(artifact)
|
140
|
+
<<XML
|
141
|
+
<?xml version="1.0"?>
|
142
|
+
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
|
143
|
+
<SOAP-ENV:Body>
|
144
|
+
<ArtifactResolve IssueInstant="2006-12-15T15:35:12.068Z"
|
145
|
+
Version="2.0"
|
146
|
+
ID="_#{UUIDTools::UUID.random_create}"
|
147
|
+
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
148
|
+
xmlns="urn:oasis:names:tc:SAML:2.0:protocol">
|
149
|
+
<saml:Issuer>#{sp_id}</saml:Issuer>
|
150
|
+
<Artifact>#{artifact.to_s}</Artifact>
|
151
|
+
</ArtifactResolve>
|
152
|
+
</SOAP-ENV:Body>
|
153
|
+
</SOAP-ENV:Envelope>
|
154
|
+
XML
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Returns an artifact resolver that can be used to resolve artifacts
|
159
|
+
# from the specified source.
|
160
|
+
#
|
161
|
+
# @param [String] source_id The id of the source of interest.
|
162
|
+
def self.ArtifactResolver(source_id)
|
163
|
+
ArtifactResolverRegistry.lookup_by_source_id(source_id)
|
164
|
+
end
|
165
|
+
|
166
|
+
ArtifactResolverRegistry = Class.new do
|
167
|
+
include SamlSp::Logging
|
168
|
+
|
169
|
+
def register(resolver)
|
170
|
+
resolvers_table[resolver.source_id] = resolver
|
171
|
+
|
172
|
+
logger.info "saml-sp: #{resolver}' registered"
|
173
|
+
end
|
174
|
+
|
175
|
+
def lookup_by_source_id(source_id)
|
176
|
+
resolvers_table[source_id] || raise(NoSuchResolverError, "No resolver registered for source `#{Base64.encode64(source_id).strip}`")
|
177
|
+
end
|
178
|
+
|
179
|
+
protected
|
180
|
+
|
181
|
+
def resolvers_table
|
182
|
+
@resolvers_table ||= {}
|
183
|
+
end
|
184
|
+
end.new
|
185
|
+
|
186
|
+
end
|
187
|
+
|
188
|
+
# Copyright (c) 2010 OpenLogic
|
189
|
+
#
|
190
|
+
# Licensed under MIT license. See LICENSE.txt
|
191
|
+
|