ruby-saml-mod 0.1.0

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