saml 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +6 -0
- data/README.md +71 -0
- data/lib/saml.rb +30 -0
- data/lib/saml/bindings.rb +17 -0
- data/lib/saml/bindings/http_post.rb +18 -0
- data/lib/saml/bindings/http_redirect.rb +72 -0
- data/lib/saml/core/assertion.rb +45 -0
- data/lib/saml/core/attribute.rb +25 -0
- data/lib/saml/core/attribute_statement.rb +20 -0
- data/lib/saml/core/authn_request.rb +15 -0
- data/lib/saml/core/authn_statement.rb +23 -0
- data/lib/saml/core/document.rb +25 -0
- data/lib/saml/core/logout_request.rb +24 -0
- data/lib/saml/core/request_abstract.rb +42 -0
- data/lib/saml/core/response.rb +28 -0
- data/lib/saml/core/status.rb +26 -0
- data/lib/saml/core/status_response.rb +24 -0
- data/lib/saml/core/subject.rb +13 -0
- data/lib/saml/core/xml_namespaces.rb +21 -0
- data/lib/saml/metadata/document.rb +14 -0
- data/lib/saml/metadata/endpoint.rb +22 -0
- data/lib/saml/metadata/entities_descriptor.rb +32 -0
- data/lib/saml/metadata/entity_descriptor.rb +40 -0
- data/lib/saml/metadata/idp_sso_descriptor.rb +25 -0
- data/lib/saml/metadata/indexed_endpoint.rb +19 -0
- data/lib/saml/metadata/key_descriptor.rb +21 -0
- data/lib/saml/metadata/role_descriptor.rb +21 -0
- data/lib/saml/metadata/sp_sso_descriptor.rb +23 -0
- data/lib/saml/metadata/sso_descriptor.rb +21 -0
- data/lib/saml/metadata/xml_namespaces.rb +19 -0
- data/lib/saml/session.rb +22 -0
- data/lib/saml/version.rb +3 -0
- data/saml.gemspec +24 -0
- data/spec/saml/bindings/http_redirect_spec.rb +49 -0
- data/spec/saml/core/assertion_spec.rb +50 -0
- data/spec/saml/core/attribute_spec.rb +36 -0
- data/spec/saml/core/attribute_statement_spec.rb +18 -0
- data/spec/saml/core/authn_request_spec.rb +39 -0
- data/spec/saml/core/authn_statement_spec.rb +17 -0
- data/spec/saml/core/logout_request_spec.rb +36 -0
- data/spec/saml/core/request_abstract_spec.rb +55 -0
- data/spec/saml/core/response_spec.rb +32 -0
- data/spec/saml/core/status_response_spec.rb +49 -0
- data/spec/saml/core/status_spec.rb +27 -0
- data/spec/saml/core/subject_spec.rb +17 -0
- data/spec/saml/metadata/endpoint_spec.rb +17 -0
- data/spec/saml/metadata/entities_descriptor_spec.rb +18 -0
- data/spec/saml/metadata/entity_descriptor_spec.rb +9 -0
- data/spec/saml/metadata/idp_sso_descriptor_spec.rb +10 -0
- data/spec/saml/metadata/indexed_endpoint_spec.rb +20 -0
- data/spec/saml/metadata/sp_sso_descriptor_spec.rb +8 -0
- data/spec/saml/metadata/sso_descriptor_spec.rb +9 -0
- data/spec/spec_helper.rb +20 -0
- metadata +131 -0
data/.travis.yml
ADDED
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
[![Build Status](https://secure.travis-ci.org/kjellm/saml.png)](http://travis-ci.org/kjellm/saml)
|
2
|
+
|
3
|
+
SAML implementation for Ruby
|
4
|
+
============================
|
5
|
+
|
6
|
+
SAML - Security Assertion Markup Language
|
7
|
+
|
8
|
+
|
9
|
+
Install
|
10
|
+
-------
|
11
|
+
|
12
|
+
gem install skeleton
|
13
|
+
|
14
|
+
|
15
|
+
Usage
|
16
|
+
-----
|
17
|
+
|
18
|
+
For an example of how it can be used, take a look at the feide gem
|
19
|
+
hosted at github https://github.com/kjellm/feide.
|
20
|
+
|
21
|
+
|
22
|
+
Documents describing SAML
|
23
|
+
-------------------------
|
24
|
+
|
25
|
+
# Technical overview
|
26
|
+
|
27
|
+
Read this first:
|
28
|
+
|
29
|
+
http://www.oasis-open.org/committees/download.php/27819/sstc-saml-tech-overview-2.0-cd-02.pdf
|
30
|
+
|
31
|
+
# The specification
|
32
|
+
|
33
|
+
Located at http://docs.oasis-open.org/security/saml/v2.0/
|
34
|
+
|
35
|
+
* http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
36
|
+
* http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf
|
37
|
+
* http://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf
|
38
|
+
* http://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
|
39
|
+
|
40
|
+
|
41
|
+
Author
|
42
|
+
------
|
43
|
+
|
44
|
+
Kjell-Magne Øierud <kjellm AT oierud DOT net>
|
45
|
+
|
46
|
+
Bugs
|
47
|
+
----
|
48
|
+
|
49
|
+
Report bugs to http://github.com/kjellm/saml/issues
|
50
|
+
|
51
|
+
License
|
52
|
+
-------
|
53
|
+
|
54
|
+
(The MIT License)
|
55
|
+
|
56
|
+
Copyright © 2012 Kjell-Magne Øierud
|
57
|
+
|
58
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
59
|
+
associated documentation files (the ‘Software’), to deal in the Software without restriction, including
|
60
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
61
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to
|
62
|
+
the following conditions:
|
63
|
+
|
64
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
65
|
+
portions of the Software.
|
66
|
+
|
67
|
+
THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
68
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
69
|
+
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
70
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
71
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/saml.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'saml/bindings'
|
2
|
+
require 'saml/bindings/http_post'
|
3
|
+
require 'saml/bindings/http_redirect'
|
4
|
+
|
5
|
+
require 'saml/core/assertion'
|
6
|
+
require 'saml/core/attribute'
|
7
|
+
require 'saml/core/attribute_statement'
|
8
|
+
require 'saml/core/authn_statement'
|
9
|
+
require 'saml/core/document'
|
10
|
+
require 'saml/core/request_abstract'
|
11
|
+
require 'saml/core/logout_request'
|
12
|
+
require 'saml/core/authn_request'
|
13
|
+
require 'saml/core/status_response'
|
14
|
+
require 'saml/core/status'
|
15
|
+
require 'saml/core/subject'
|
16
|
+
require 'saml/core/response'
|
17
|
+
require 'saml/core/xml_namespaces'
|
18
|
+
|
19
|
+
require 'saml/metadata/document'
|
20
|
+
require 'saml/metadata/entities_descriptor'
|
21
|
+
require 'saml/metadata/entity_descriptor'
|
22
|
+
require 'saml/metadata/endpoint'
|
23
|
+
require 'saml/metadata/indexed_endpoint'
|
24
|
+
require 'saml/metadata/key_descriptor'
|
25
|
+
require 'saml/metadata/sso_descriptor'
|
26
|
+
require 'saml/metadata/idp_sso_descriptor'
|
27
|
+
require 'saml/metadata/sp_sso_descriptor'
|
28
|
+
require 'saml/metadata/xml_namespaces'
|
29
|
+
|
30
|
+
require 'saml/version'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module SAML
|
2
|
+
module Bindings
|
3
|
+
|
4
|
+
def self.from_endpoint(endpoint)
|
5
|
+
klass = case endpoint.binding
|
6
|
+
when "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
7
|
+
Bindings::HTTPPost
|
8
|
+
when "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
9
|
+
Bindings::HTTPRedirect
|
10
|
+
else
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
klass.new
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module SAML
|
2
|
+
module Bindings
|
3
|
+
class HTTPPost
|
4
|
+
|
5
|
+
def build_response(rack_request)
|
6
|
+
xml = Core::Document.new(decode(rack_request.params["SAMLResponse"])).root
|
7
|
+
Core::Response.from_xml(xml)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def decode(str)
|
13
|
+
Base64.decode64(str)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "zlib"
|
3
|
+
require "cgi"
|
4
|
+
|
5
|
+
module SAML
|
6
|
+
module Bindings
|
7
|
+
class HTTPRedirect
|
8
|
+
|
9
|
+
def build_request(rack_response, endpoint, saml_request, relay_state=nil)
|
10
|
+
unless relay_state.nil?
|
11
|
+
raise ArgumentError.new("relay_state must not exceed 80 bytes") if relay_state.bytesize > 80
|
12
|
+
end
|
13
|
+
request = saml_request.to_xml.to_s
|
14
|
+
deflated_saml_request = deflate(request)
|
15
|
+
query = "SAMLRequest=#{deflated_saml_request}"
|
16
|
+
query += "&RelayState=#{url_enc(relay_state)}" unless relay_state.nil?
|
17
|
+
url = "#{endpoint.location}?#{query}"
|
18
|
+
rack_response.redirect url
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_response(rack_request)
|
22
|
+
xml_str = inflate(rack_request.params["SAMLResponse"])
|
23
|
+
xml = Core::Document.new(xml_str).root
|
24
|
+
Core::Response.from_xml(xml)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# Described in section 3.4.4.1
|
30
|
+
def deflate(str)
|
31
|
+
url_enc(base64_enc(compress(str)))
|
32
|
+
end
|
33
|
+
|
34
|
+
def inflate(str)
|
35
|
+
# FIXME do we never need to URL.decode?
|
36
|
+
decompress(base64_dec(str))
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def compress(str)
|
41
|
+
z = Zlib::Deflate.deflate(str, Zlib::BEST_COMPRESSION)
|
42
|
+
# The SAML standard requires RFC1951 compliance. Zlib::Deflate
|
43
|
+
# are RFC1950 compliant. By removing the 2 byte header and the
|
44
|
+
# 4 byte tail (checksum), what's left is a deflate stream as
|
45
|
+
# described in RFC1951.
|
46
|
+
z[2..-5]
|
47
|
+
end
|
48
|
+
|
49
|
+
def decompress(str)
|
50
|
+
z = Zlib::Inflate.new(-Zlib::MAX_WBITS) # Raw processing (no head or tail)
|
51
|
+
z.inflate(str)
|
52
|
+
end
|
53
|
+
|
54
|
+
def base64_enc(str)
|
55
|
+
Base64.encode64(str)
|
56
|
+
end
|
57
|
+
|
58
|
+
def base64_dec(str)
|
59
|
+
Base64.decode64(str)
|
60
|
+
end
|
61
|
+
|
62
|
+
def url_enc(str)
|
63
|
+
CGI.escape(str)
|
64
|
+
end
|
65
|
+
|
66
|
+
def url_dec(str)
|
67
|
+
CGI.unescape(str)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module SAML
|
2
|
+
module Core
|
3
|
+
class Assertion
|
4
|
+
|
5
|
+
attr_reader :id
|
6
|
+
attr_reader :version
|
7
|
+
attr_reader :issue_instant
|
8
|
+
attr_reader :issuer
|
9
|
+
|
10
|
+
attr_reader :subject
|
11
|
+
attr_reader :attribute_statement
|
12
|
+
attr_reader :authn_statements
|
13
|
+
attr_reader :conditions
|
14
|
+
|
15
|
+
def self.from_xml(xml); new.from_xml(xml); end
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@authn_statements = []
|
19
|
+
end
|
20
|
+
|
21
|
+
def from_xml(xml)
|
22
|
+
@id = xml.attributes['ID']
|
23
|
+
@version = xml.attributes['Version']
|
24
|
+
@issue_instant = xml.attributes['IssueInstant']
|
25
|
+
|
26
|
+
subject_element = xml.get_elements('saml:Subject')
|
27
|
+
unless subject_element.empty?
|
28
|
+
# @subject = Subject.from_xml(subject_element.first)
|
29
|
+
end
|
30
|
+
|
31
|
+
attribute_statements = xml.get_elements('saml:AttributeStatement')
|
32
|
+
unless attribute_statements.empty?
|
33
|
+
@attribute_statement = AttributeStatement.from_xml(attribute_statements.first)
|
34
|
+
end
|
35
|
+
|
36
|
+
xml.get_elements('saml:AuthnStatement').each do |as|
|
37
|
+
@authn_statements << AuthnStatement.from_xml(as)
|
38
|
+
end
|
39
|
+
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SAML
|
2
|
+
module Core
|
3
|
+
class Attribute
|
4
|
+
|
5
|
+
attr_accessor :name
|
6
|
+
attr_accessor :name_format
|
7
|
+
attr_accessor :attribute_values
|
8
|
+
|
9
|
+
def self.from_xml(xml)
|
10
|
+
attribute = new
|
11
|
+
attribute.name = xml.attributes['Name']
|
12
|
+
|
13
|
+
nf = xml.attributes['NameFormat']
|
14
|
+
attribute.name_format = nf.nil? ? 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified' : nf
|
15
|
+
|
16
|
+
values = []
|
17
|
+
xml.each_element() do |av|
|
18
|
+
values << av.to_s
|
19
|
+
end
|
20
|
+
attribute.attribute_values = values
|
21
|
+
attribute
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SAML
|
2
|
+
module Core
|
3
|
+
class AttributeStatement
|
4
|
+
|
5
|
+
attr_accessor :attributes
|
6
|
+
|
7
|
+
def self.from_xml(xml)
|
8
|
+
statement = new
|
9
|
+
attrs = []
|
10
|
+
xml.each_element('saml:Attribute') do |a|
|
11
|
+
attrs << Attribute.from_xml(a)
|
12
|
+
end
|
13
|
+
statement.attributes = attrs
|
14
|
+
|
15
|
+
statement
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SAML
|
2
|
+
module Core
|
3
|
+
class AuthnStatement
|
4
|
+
|
5
|
+
attr_reader :authn_instant
|
6
|
+
attr_reader :session_not_on_or_after
|
7
|
+
attr_reader :session_index
|
8
|
+
|
9
|
+
attr_reader :authn_context
|
10
|
+
|
11
|
+
def self.from_xml(xml); new.from_xml(xml); end
|
12
|
+
|
13
|
+
def from_xml(xml)
|
14
|
+
@authn_instant = xml.attributes['AuthnInstant']
|
15
|
+
@session_not_on_or_after = xml.attributes['SessionNotOnOrAfter']
|
16
|
+
@session_index = xml.attributes['SessionIndex']
|
17
|
+
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
|
3
|
+
module SAML
|
4
|
+
module Core
|
5
|
+
class Document < REXML::Document
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
super(*args)
|
10
|
+
XMLNamespaces.each {|k,v| add_namespace(k, v)}
|
11
|
+
end
|
12
|
+
|
13
|
+
# See REXML::Document#add_element
|
14
|
+
#
|
15
|
+
# Makes sure that all namespaces are added to the root element.
|
16
|
+
def add_element(name, attrs={})
|
17
|
+
ns = XMLNamespaces.map {|k, v| ["xmlns:#{k}", v]}
|
18
|
+
ns = Hash[*ns.flatten]
|
19
|
+
attrs.merge!(ns)
|
20
|
+
super(name, attrs)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SAML
|
2
|
+
module Core
|
3
|
+
class LogoutRequest < RequestAbstract
|
4
|
+
|
5
|
+
attr_accessor :name_id
|
6
|
+
|
7
|
+
def xml_document
|
8
|
+
xml = Document.new
|
9
|
+
root = xml.add_element("samlp:LogoutRequest")
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_xml
|
13
|
+
xml = super
|
14
|
+
|
15
|
+
unless @name_id.nil?
|
16
|
+
name_id_node = xml.root.add_element("saml:NameID")
|
17
|
+
name_id_node.text = @name_id
|
18
|
+
end
|
19
|
+
|
20
|
+
xml
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'uuid'
|
2
|
+
|
3
|
+
module SAML
|
4
|
+
module Core
|
5
|
+
class RequestAbstract
|
6
|
+
|
7
|
+
attr_reader :id
|
8
|
+
attr_reader :version
|
9
|
+
attr_reader :issue_instant
|
10
|
+
|
11
|
+
attr_accessor :issuer
|
12
|
+
|
13
|
+
def initialize(clock_class=Time)
|
14
|
+
@id = UUID.new.generate
|
15
|
+
@version = '2.0'
|
16
|
+
@issue_instant = clock_class.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
17
|
+
end
|
18
|
+
|
19
|
+
def xml_document
|
20
|
+
xml = Document.new
|
21
|
+
root = xml.add_element("samlp:RequestAbstract")
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def to_xml
|
26
|
+
xml = xml_document
|
27
|
+
root = xml.root
|
28
|
+
root.attributes['ID'] = @id
|
29
|
+
root.attributes['IssueInstant'] = @issue_instant
|
30
|
+
root.attributes['Version'] = @version
|
31
|
+
|
32
|
+
unless @issuer.nil?
|
33
|
+
issuer_node = root.add_element("saml:Issuer")
|
34
|
+
issuer_node.text = @issuer
|
35
|
+
end
|
36
|
+
|
37
|
+
xml
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|