saml 0.1

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.
Files changed (54) hide show
  1. data/.travis.yml +6 -0
  2. data/README.md +71 -0
  3. data/lib/saml.rb +30 -0
  4. data/lib/saml/bindings.rb +17 -0
  5. data/lib/saml/bindings/http_post.rb +18 -0
  6. data/lib/saml/bindings/http_redirect.rb +72 -0
  7. data/lib/saml/core/assertion.rb +45 -0
  8. data/lib/saml/core/attribute.rb +25 -0
  9. data/lib/saml/core/attribute_statement.rb +20 -0
  10. data/lib/saml/core/authn_request.rb +15 -0
  11. data/lib/saml/core/authn_statement.rb +23 -0
  12. data/lib/saml/core/document.rb +25 -0
  13. data/lib/saml/core/logout_request.rb +24 -0
  14. data/lib/saml/core/request_abstract.rb +42 -0
  15. data/lib/saml/core/response.rb +28 -0
  16. data/lib/saml/core/status.rb +26 -0
  17. data/lib/saml/core/status_response.rb +24 -0
  18. data/lib/saml/core/subject.rb +13 -0
  19. data/lib/saml/core/xml_namespaces.rb +21 -0
  20. data/lib/saml/metadata/document.rb +14 -0
  21. data/lib/saml/metadata/endpoint.rb +22 -0
  22. data/lib/saml/metadata/entities_descriptor.rb +32 -0
  23. data/lib/saml/metadata/entity_descriptor.rb +40 -0
  24. data/lib/saml/metadata/idp_sso_descriptor.rb +25 -0
  25. data/lib/saml/metadata/indexed_endpoint.rb +19 -0
  26. data/lib/saml/metadata/key_descriptor.rb +21 -0
  27. data/lib/saml/metadata/role_descriptor.rb +21 -0
  28. data/lib/saml/metadata/sp_sso_descriptor.rb +23 -0
  29. data/lib/saml/metadata/sso_descriptor.rb +21 -0
  30. data/lib/saml/metadata/xml_namespaces.rb +19 -0
  31. data/lib/saml/session.rb +22 -0
  32. data/lib/saml/version.rb +3 -0
  33. data/saml.gemspec +24 -0
  34. data/spec/saml/bindings/http_redirect_spec.rb +49 -0
  35. data/spec/saml/core/assertion_spec.rb +50 -0
  36. data/spec/saml/core/attribute_spec.rb +36 -0
  37. data/spec/saml/core/attribute_statement_spec.rb +18 -0
  38. data/spec/saml/core/authn_request_spec.rb +39 -0
  39. data/spec/saml/core/authn_statement_spec.rb +17 -0
  40. data/spec/saml/core/logout_request_spec.rb +36 -0
  41. data/spec/saml/core/request_abstract_spec.rb +55 -0
  42. data/spec/saml/core/response_spec.rb +32 -0
  43. data/spec/saml/core/status_response_spec.rb +49 -0
  44. data/spec/saml/core/status_spec.rb +27 -0
  45. data/spec/saml/core/subject_spec.rb +17 -0
  46. data/spec/saml/metadata/endpoint_spec.rb +17 -0
  47. data/spec/saml/metadata/entities_descriptor_spec.rb +18 -0
  48. data/spec/saml/metadata/entity_descriptor_spec.rb +9 -0
  49. data/spec/saml/metadata/idp_sso_descriptor_spec.rb +10 -0
  50. data/spec/saml/metadata/indexed_endpoint_spec.rb +20 -0
  51. data/spec/saml/metadata/sp_sso_descriptor_spec.rb +8 -0
  52. data/spec/saml/metadata/sso_descriptor_spec.rb +9 -0
  53. data/spec/spec_helper.rb +20 -0
  54. metadata +131 -0
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.3
5
+ # uncomment this line if your project needs to run something other than `rake`:
6
+ script: bundle exec rspec spec
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,15 @@
1
+ require "rexml/document"
2
+ require "rexml/xpath"
3
+
4
+ module SAML
5
+ module Core
6
+ class AuthnRequest < RequestAbstract
7
+
8
+ def xml_document
9
+ xml = Document.new
10
+ root = xml.add_element("samlp:AuthnRequest")
11
+ end
12
+
13
+ end
14
+ end
15
+ 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