saml 0.1

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