ruby-saml-mod 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 OneLogin, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,7 @@
1
+ == SAML toolkit for Ruby on Rails
2
+
3
+ Documentation from onelogin: http://support.onelogin.com/entries/165434-saml-toolkit-for-ruby-on-rails
4
+
5
+ The example folder has a rails 2.3.5 example application.
6
+
7
+ A Gem will eventually be created.
@@ -0,0 +1,15 @@
1
+ require 'zlib'
2
+ require "base64"
3
+ require "rexml/document"
4
+ require "xml_sec"
5
+
6
+ module Onelogin
7
+ end
8
+
9
+ require 'onelogin/saml/auth_request'
10
+ require 'onelogin/saml/response'
11
+ require 'onelogin/saml/settings'
12
+ require 'onelogin/saml/name_identifiers'
13
+ require 'onelogin/saml/status_codes'
14
+ require 'onelogin/saml/meta_data'
15
+ require 'onelogin/saml/log_out_request'
@@ -0,0 +1,36 @@
1
+ module Onelogin::Saml
2
+ class AuthRequest
3
+ def self.create(settings)
4
+ id = Onelogin::Saml::AuthRequest.generate_unique_id(42)
5
+ issue_instant = Onelogin::Saml::AuthRequest.get_timestamp
6
+
7
+ request =
8
+ "<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"#{id}\" Version=\"2.0\" IssueInstant=\"#{issue_instant}\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" AssertionConsumerServiceURL=\"#{settings.assertion_consumer_service_url}\">" +
9
+ "<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{settings.issuer}</saml:Issuer>\n" +
10
+ "<samlp:NameIDPolicy xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Format=\"#{settings.name_identifier_format}\" AllowCreate=\"true\"></samlp:NameIDPolicy>\n" +
11
+ "<samlp:RequestedAuthnContext xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Comparison=\"exact\">" +
12
+ "<saml:AuthnContextClassRef xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext>\n" +
13
+ "</samlp:AuthnRequest>"
14
+
15
+ deflated_request = Zlib::Deflate.deflate(request, 9)[2..-5]
16
+ base64_request = Base64.encode64(deflated_request)
17
+ encoded_request = CGI.escape(base64_request)
18
+
19
+ settings.idp_sso_target_url + "?SAMLRequest=" + encoded_request
20
+ end
21
+
22
+ private
23
+
24
+ def self.generate_unique_id(length)
25
+ chars = ("a".."f").to_a + ("0".."9").to_a
26
+ chars_len = chars.size
27
+ unique_id = ""
28
+ 1.upto(length) { |i| unique_id << chars[rand(chars_len-1)] }
29
+ unique_id
30
+ end
31
+
32
+ def self.get_timestamp
33
+ Time.new().strftime("%Y-%m-%dT%H:%M:%SZ")
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,22 @@
1
+ module Onelogin::Saml
2
+ class LogOutRequest
3
+ def self.create(settings, session)
4
+ id = Onelogin::Saml::AuthRequest.generate_unique_id(42)
5
+ issue_instant = Onelogin::Saml::AuthRequest.get_timestamp
6
+
7
+ logout_request = "<samlp:LogoutRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"#{id}\" Version=\"2.0\" IssueInstant=\"#{issue_instant}\"> " +
8
+ "<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{settings.issuer}</saml:Issuer>" +
9
+ "<saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" NameQualifier=\"#{session[:name_qualifier]}\" SPNameQualifier=\"#{settings.issuer}\" Format=\"urn:oasis:names:tc:SAML:2.0:nameid-format:transient\">#{session[:name_id]}</saml:NameID>" +
10
+ "<samlp:SessionIndex xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">#{session[:session_index]}</samlp:SessionIndex>" +
11
+ "</samlp:LogoutRequest>";
12
+
13
+ deflated_logout_request = Zlib::Deflate.deflate(logout_request, 9)[2..-5]
14
+ base64_logout_request = Base64.encode64(deflated_logout_request)
15
+ encoded_logout_request = CGI.escape(base64_logout_request)
16
+
17
+ redirect_url = settings.idp_slo_target_url + "?SAMLRequest=" + encoded_logout_request
18
+
19
+ return redirect_url
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module Onelogin::Saml
2
+ class MetaData
3
+ def self.create(settings)
4
+ %{<?xml version="1.0"?>
5
+ <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="#{settings.issuer}">
6
+ <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
7
+ <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="#{settings.sp_slo_url}"/>
8
+ <AssertionConsumerService index="0" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="#{settings.assertion_consumer_service_url}"/>
9
+ </SPSSODescriptor>
10
+ <ContactPerson contactType="technical">
11
+ <SurName>#{settings.tech_contact_name}</SurName>
12
+ <EmailAddress>mailto:#{settings.tech_contact_email}</EmailAddress>
13
+ </ContactPerson>
14
+ </EntityDescriptor>}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module Onelogin::Saml
2
+ module NameIdentifiers
3
+ # See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf section 8.3 for further documentation
4
+ EMAIL = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
5
+ ENTITY = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
6
+ KERBEROS = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos"
7
+ PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
8
+ TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
9
+ UNSPECIFIED = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
10
+ WINDOWS_DOMAIN = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName"
11
+ X509 = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"
12
+
13
+ ALL_IDENTIFIERS = [EMAIL, ENTITY, KERBEROS, PERSISTENT, TRANSIENT, UNSPECIFIED, WINDOWS_DOMAIN, X509]
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ module Onelogin::Saml
2
+ class Response
3
+
4
+ attr_accessor :settings, :document, :response
5
+ attr_accessor :name_id, :name_qualifier, :session_index
6
+ attr_accessor :status_code, :status_message
7
+ def initialize(response)
8
+ @response = response
9
+ @document = XMLSecurity::SignedDocument.new(Base64.decode64(@response))
10
+ @name_id = @document.elements["/samlp:Response/saml:Assertion/saml:Subject/saml:NameID"].text rescue nil
11
+ @name_qualifier = @document.elements["/samlp:Response/saml:Assertion/saml:Subject/saml:NameID"].attributes["NameQualifier"] rescue nil
12
+ @session_index = @document.elements["/samlp:Response/saml:Assertion/saml:AuthnStatement"].attributes["SessionIndex"] rescue nil
13
+ @status_code = @document.elements["/samlp:Response/samlp:Status/samlp:StatusCode"].attributes["Value"] rescue nil
14
+ @status_message = @document.elements["/samlp:Response/samlp:Status/samlp:StatusCode"].text rescue nil
15
+ end
16
+
17
+ def logger=(val)
18
+ @logger = val
19
+ end
20
+
21
+ def is_valid?
22
+ unless @response.blank?
23
+ @document.validate(@settings.idp_cert_fingerprint, @logger) unless !@settings.idp_cert_fingerprint
24
+ end
25
+ end
26
+
27
+ def success_status?
28
+ @status_code == Onelogin::Saml::StatusCodes::SUCCESS_URI
29
+ end
30
+
31
+ def auth_failure?
32
+ @status_code == Onelogin::Saml::StatusCodes::AUTHN_FAILED_URI
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,45 @@
1
+ module Onelogin::Saml
2
+ class Settings
3
+
4
+ def initialize(atts={})
5
+ atts.each do |key, val|
6
+ if self.respond_to? "#{key}="
7
+ self.send "#{key}=", val
8
+ end
9
+ end
10
+ end
11
+
12
+ # The URL at which the SAML assertion should be received.
13
+ attr_accessor :assertion_consumer_service_url
14
+
15
+ # The name of your application.
16
+ attr_accessor :issuer
17
+
18
+ #
19
+ attr_accessor :sp_name_qualifier
20
+
21
+ # The IdP URL to which the authentication request should be sent.
22
+ attr_accessor :idp_sso_target_url
23
+
24
+ # The IdP URL to which the logout request should be sent.
25
+ attr_accessor :idp_slo_target_url
26
+
27
+ # The certificate fingerprint. This is provided from the identity provider when setting up the relationship.
28
+ attr_accessor :idp_cert_fingerprint
29
+
30
+ # Describes the format of the username required by this application.
31
+ # For email: Onelogin::Saml::NameIdentifiers::EMAIL
32
+ attr_accessor :name_identifier_format
33
+
34
+ ## Attributes for the metadata
35
+
36
+ # The logout url of your application
37
+ attr_accessor :sp_slo_url
38
+
39
+ # The name of the technical contact for your application
40
+ attr_accessor :tech_contact_name
41
+
42
+ # The email of the technical contact for your application
43
+ attr_accessor :tech_contact_email
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ module Onelogin::Saml
2
+ module StatusCodes
3
+ # See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf section 3.2.2 for further documentation
4
+ SUCCESS_URI = "urn:oasis:names:tc:SAML:2.0:status:Success"
5
+ REQUESTER_URI = "urn:oasis:names:tc:SAML:2.0:status:Requester"
6
+ RESPONDER_URI = "urn:oasis:names:tc:SAML:2.0:status:Responder"
7
+ VERSION_MISMATCH_URI = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"
8
+ AUTHN_FAILED_URI = "urn:oasis:names:tc:SAML:2.0:status:AuthnFailed"
9
+ INVALID_ATTR_NAME_VALUE_URI = "urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue"
10
+ INVALID_NAMEID_POLICY_URI = "urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy"
11
+ NO_AUTHN_CONTEXT_URI = "urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext"
12
+ NO_AVAILABLE_IDP_URI = "urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP"
13
+ NO_PASSIVE_URI = "urn:oasis:names:tc:SAML:2.0:status:NoPassive"
14
+ NO_SUPPORTED_IDP_URI = "urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP"
15
+ PARTIAL_LOGOUT_URI = "urn:oasis:names:tc:SAML:2.0:status:PartialLogout"
16
+ PROXY_COUNT_EXCEEDED_URI = "urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded"
17
+ REQUEST_DENIED_URI = "urn:oasis:names:tc:SAML:2.0:status:RequestDenied"
18
+ REQUEST_UNSUPPORTED_URI = "urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported"
19
+ REQUEST_VERSION_DEPRECATED_URI = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated"
20
+ REQUEST_VERSION_TOO_HIGH_URI = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooHigh"
21
+ REQUEST_VERSION_TOO_LOW_URI = "urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow"
22
+ RESOURCE_NOT_RECOGNIZED_URI = "urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized"
23
+ TOO_MANY_RESPONSES = "urn:oasis:names:tc:SAML:2.0:status:TooManyResponses"
24
+ UNKNOWN_ATTR_PROFILE_URI = "urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile"
25
+ UNKNOWN_PRINCIPAL_URI = "urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal"
26
+ UNSUPPORTED_BINDING_URI = "urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding"
27
+ end
28
+ end
data/lib/xml_sec.rb ADDED
@@ -0,0 +1,91 @@
1
+ # The contents of this file are subject to the terms
2
+ # of the Common Development and Distribution License
3
+ # (the License). You may not use this file except in
4
+ # compliance with the License.
5
+ #
6
+ # You can obtain a copy of the License at
7
+ # https://opensso.dev.java.net/public/CDDLv1.0.html or
8
+ # opensso/legal/CDDLv1.0.txt
9
+ # See the License for the specific language governing
10
+ # permission and limitations under the License.
11
+ #
12
+ # When distributing Covered Code, include this CDDL
13
+ # Header Notice in each file and include the License file
14
+ # at opensso/legal/CDDLv1.0.txt.
15
+ # If applicable, add the following below the CDDL Header,
16
+ # with the fields enclosed by brackets [] replaced by
17
+ # your own identifying information:
18
+ # "Portions Copyrighted [year] [name of copyright owner]"
19
+ #
20
+ # $Id: xml_sec.rb,v 1.6 2007/10/24 00:28:41 todddd Exp $
21
+ #
22
+ # Copyright 2007 Sun Microsystems Inc. All Rights Reserved
23
+ # Portions Copyrighted 2007 Todd W Saxton.
24
+
25
+ require 'rubygems'
26
+ require "rexml/document"
27
+ require "rexml/xpath"
28
+ require "openssl"
29
+ require "xmlcanonicalizer"
30
+ require "digest/sha1"
31
+
32
+ module XMLSecurity
33
+
34
+ class SignedDocument < REXML::Document
35
+
36
+ def validate (idp_cert_fingerprint, logger = nil)
37
+ # get cert from response
38
+ base64_cert = self.elements["//ds:X509Certificate"].text
39
+ cert_text = Base64.decode64(base64_cert)
40
+ cert = OpenSSL::X509::Certificate.new(cert_text)
41
+
42
+ # check cert matches registered idp cert
43
+ fingerprint = Digest::SHA1.hexdigest(cert.to_der)
44
+ valid_flag = fingerprint == idp_cert_fingerprint.gsub(":", "").downcase
45
+
46
+ return valid_flag if !valid_flag
47
+
48
+ validate_doc(base64_cert, logger)
49
+ end
50
+
51
+ def validate_doc(base64_cert, logger)
52
+ # validate references
53
+
54
+ # remove signature node
55
+ sig_element = REXML::XPath.first(self, "//ds:Signature", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"})
56
+ sig_element.remove
57
+
58
+ #check digests
59
+ REXML::XPath.each(sig_element, "//ds:Reference", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}) do | ref |
60
+
61
+ uri = ref.attributes.get_attribute("URI").value
62
+ hashed_element = REXML::XPath.first(self, "//[@ID='#{uri[1,uri.size]}']")
63
+ canoner = XML::Util::XmlCanonicalizer.new(false, true)
64
+ canon_hashed_element = canoner.canonicalize(hashed_element)
65
+ hash = Base64.encode64(Digest::SHA1.digest(canon_hashed_element)).chomp
66
+ digest_value = REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}).text
67
+
68
+ valid_flag = hash == digest_value
69
+
70
+ return valid_flag if !valid_flag
71
+ end
72
+
73
+ # verify signature
74
+ canoner = XML::Util::XmlCanonicalizer.new(false, true)
75
+ signed_info_element = REXML::XPath.first(sig_element, "//ds:SignedInfo", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"})
76
+ canon_string = canoner.canonicalize(signed_info_element)
77
+
78
+ base64_signature = REXML::XPath.first(sig_element, "//ds:SignatureValue", {"ds"=>"http://www.w3.org/2000/09/xmldsig#"}).text
79
+ signature = Base64.decode64(base64_signature)
80
+
81
+ # get certificate object
82
+ cert_text = Base64.decode64(base64_cert)
83
+ cert = OpenSSL::X509::Certificate.new(cert_text)
84
+
85
+ valid_flag = cert.public_key.verify(OpenSSL::Digest::SHA1.new, signature, canon_string)
86
+
87
+ return valid_flag
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,28 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{ruby-saml-mod}
3
+ s.version = "0.1.0"
4
+
5
+ s.authors = ["OneLogin LLC", "Bracken"]
6
+ s.date = %q{2011-01-26}
7
+ s.extra_rdoc_files = [
8
+ "LICENSE"
9
+ ]
10
+ s.files = [
11
+ "LICENSE",
12
+ "README",
13
+ "lib/onelogin/saml.rb",
14
+ "lib/onelogin/saml/auth_request.rb",
15
+ "lib/onelogin/saml/log_out_request.rb",
16
+ "lib/onelogin/saml/meta_data.rb",
17
+ "lib/onelogin/saml/name_identifiers.rb",
18
+ "lib/onelogin/saml/response.rb",
19
+ "lib/onelogin/saml/settings.rb",
20
+ "lib/onelogin/saml/status_codes.rb",
21
+ "lib/xml_sec.rb",
22
+ "ruby-saml-mod.gemspec"
23
+ ]
24
+ s.homepage = %q{http://github.com/bracken/ruby-saml}
25
+ s.require_paths = ["lib"]
26
+ s.summary = %q{Ruby library for SAML service providers}
27
+ s.description = %q{This is an early fork from https://github.com/onelogin/ruby-saml - I plan to "rebase" these changes ontop of their current version eventually. }
28
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-saml-mod
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - OneLogin LLC
13
+ - Bracken
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-26 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: "This is an early fork from https://github.com/onelogin/ruby-saml - I plan to \"rebase\" these changes ontop of their current version eventually. "
23
+ email:
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - LICENSE
30
+ files:
31
+ - LICENSE
32
+ - README
33
+ - lib/onelogin/saml.rb
34
+ - lib/onelogin/saml/auth_request.rb
35
+ - lib/onelogin/saml/log_out_request.rb
36
+ - lib/onelogin/saml/meta_data.rb
37
+ - lib/onelogin/saml/name_identifiers.rb
38
+ - lib/onelogin/saml/response.rb
39
+ - lib/onelogin/saml/settings.rb
40
+ - lib/onelogin/saml/status_codes.rb
41
+ - lib/xml_sec.rb
42
+ - ruby-saml-mod.gemspec
43
+ has_rdoc: true
44
+ homepage: http://github.com/bracken/ruby-saml
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options: []
49
+
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.3.6
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Ruby library for SAML service providers
73
+ test_files: []
74
+