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
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 openlogic-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 = 'openlogic-saml-sp'
|
5
|
+
gemspec.summary = 'SAML 2.0 SSO Sevice Provider Library'
|
6
|
+
gemspec.email = 'gbettridge@openlogic.com'
|
7
|
+
gemspec.authors = ["OpenLogic", "Peter Williams","Glen Aultman-Bettridge"]
|
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.3
|
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,192 @@
|
|
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 opaque 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.info{"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
|
+
logger.info{"ArtifactResolve response body:\n#{resp.gsub(/^/, "\t")}"}
|
89
|
+
doc = Nokogiri::XML.parse(resp.body)
|
90
|
+
assert_successful_response(doc)
|
91
|
+
|
92
|
+
assertion = Assertion.new_from_xml(doc)
|
93
|
+
|
94
|
+
raise AnomalousResponseIssuerError.new_from_issuers(idp_id, assertion.issuer) unless
|
95
|
+
assertion.issuer == idp_id
|
96
|
+
|
97
|
+
assertion
|
98
|
+
|
99
|
+
rescue Resourceful::UnsuccessfulHttpRequestError => e
|
100
|
+
|
101
|
+
logger.debug {
|
102
|
+
body = e.http_request.body
|
103
|
+
body.rewind
|
104
|
+
"Artifact resolution request:\n" + body.read.gsub(/^/, ' ')}
|
105
|
+
logger.debug {"Artifact resolution response:\n" + e.http_response.body.gsub(/^/, ' ')}
|
106
|
+
raise
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_s
|
110
|
+
"Resolver for <#{idp_id}> (#{Base64.encode64(source_id).strip})"
|
111
|
+
end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
def assert_successful_response(resp_doc)
|
116
|
+
response_code = resp_doc.at("//sp:StatusCode/@Value", namespaces).content.strip
|
117
|
+
return if response_code == 'urn:oasis:names:tc:SAML:2.0:status:Success'
|
118
|
+
|
119
|
+
# Request was not handled successfully
|
120
|
+
err_message = "Request failed"
|
121
|
+
|
122
|
+
status_message_elem = resp_doc.at("//sp:StatusMessage", namespaces)
|
123
|
+
if status_message_elem
|
124
|
+
err_message << " because \"#{status_message_elem.content.strip}\""
|
125
|
+
end
|
126
|
+
|
127
|
+
err_message << ". (status code: #{response_code})"
|
128
|
+
|
129
|
+
if status_details_elem = resp_doc.at("//sp:StatusDetail", namespaces)
|
130
|
+
logger.debug "Details for resolve artifact failure (status code: #{response_code}):\n" + status_details_elem.content
|
131
|
+
end
|
132
|
+
|
133
|
+
raise RequestDeniedError, err_message
|
134
|
+
end
|
135
|
+
|
136
|
+
def namespaces
|
137
|
+
{'sp' => 'urn:oasis:names:tc:SAML:2.0:protocol',
|
138
|
+
'sa' => 'urn:oasis:names:tc:SAML:2.0:assertion'}
|
139
|
+
end
|
140
|
+
def request_document_for(artifact)
|
141
|
+
<<XML
|
142
|
+
<?xml version="1.0"?>
|
143
|
+
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
|
144
|
+
<SOAP-ENV:Body>
|
145
|
+
<ArtifactResolve IssueInstant="2006-12-15T15:35:12.068Z"
|
146
|
+
Version="2.0"
|
147
|
+
ID="_#{UUIDTools::UUID.random_create}"
|
148
|
+
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
|
149
|
+
xmlns="urn:oasis:names:tc:SAML:2.0:protocol">
|
150
|
+
<saml:Issuer>#{sp_id}</saml:Issuer>
|
151
|
+
<Artifact>#{artifact.to_s}</Artifact>
|
152
|
+
</ArtifactResolve>
|
153
|
+
</SOAP-ENV:Body>
|
154
|
+
</SOAP-ENV:Envelope>
|
155
|
+
XML
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns an artifact resolver that can be used to resolve artifacts
|
160
|
+
# from the specified source.
|
161
|
+
#
|
162
|
+
# @param [String] source_id The id of the source of interest.
|
163
|
+
def self.ArtifactResolver(source_id)
|
164
|
+
ArtifactResolverRegistry.lookup_by_source_id(source_id)
|
165
|
+
end
|
166
|
+
|
167
|
+
ArtifactResolverRegistry = Class.new do
|
168
|
+
include SamlSp::Logging
|
169
|
+
|
170
|
+
def register(resolver)
|
171
|
+
resolvers_table[resolver.source_id] = resolver
|
172
|
+
|
173
|
+
logger.info "saml-sp: #{resolver}' registered"
|
174
|
+
end
|
175
|
+
|
176
|
+
def lookup_by_source_id(source_id)
|
177
|
+
resolvers_table[source_id] || raise(NoSuchResolverError, "No resolver registered for source `#{Base64.encode64(source_id).strip}`")
|
178
|
+
end
|
179
|
+
|
180
|
+
protected
|
181
|
+
|
182
|
+
def resolvers_table
|
183
|
+
@resolvers_table ||= {}
|
184
|
+
end
|
185
|
+
end.new
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
# Copyright (c) 2010 OpenLogic
|
190
|
+
#
|
191
|
+
# Licensed under MIT license. See LICENSE.txt
|
192
|
+
|