Skip to content

Commit

Permalink
code cleanups, fixing paths and files to follow best practices/standa…
Browse files Browse the repository at this point in the history
…rds, removing unneeded deps (jeweler)
  • Loading branch information
Joe Rozner committed Apr 13, 2012
1 parent bafc52d commit 21538be
Show file tree
Hide file tree
Showing 17 changed files with 324 additions and 369 deletions.
23 changes: 1 addition & 22 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,27 +1,6 @@
require 'rubygems'
require 'rake'

begin
require 'jeweler'
Jeweler::Tasks.new do |gem|
gem.name = "ruby-saml"
gem.summary = %Q{SAML Ruby Tookit}
gem.description = %Q{SAML toolkit for Ruby on Rails}
gem.email = "[email protected]"
gem.homepage = "http://github.com/onelogin/ruby-saml"
gem.authors = ["OneLogin LLC"]
gem.add_dependency("canonix","~> 0.1")
gem.add_dependency("uuid","~> 2.3")
gem.add_development_dependency "shoulda"
gem.add_development_dependency "ruby-debug"
gem.add_development_dependency "mocha"
#gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
end
Jeweler::GemcutterTasks.new
rescue LoadError
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
end

#not being used yet.
require 'rake/testtask'
Rake::TestTask.new(:test) do |test|
Expand All @@ -43,7 +22,7 @@ rescue LoadError
end
end

task :test => :check_dependencies
task :test

task :default => :test

Expand Down
74 changes: 74 additions & 0 deletions lib/onelogin/ruby-saml/authrequest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
require "base64"
require "uuid"
require "zlib"
require "cgi"
require "rexml/document"
require "rexml/xpath"

module Onelogin
module Saml
include REXML
class Authrequest
def create(settings, params = {})
uuid = "_" + UUID.new.generate
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
# Create AuthnRequest root element using REXML
request_doc = REXML::Document.new

root = request_doc.add_element "samlp:AuthnRequest", { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol" }
root.attributes['ID'] = uuid
root.attributes['IssueInstant'] = time
root.attributes['Version'] = "2.0"

# Conditionally defined elements based on settings
if settings.assertion_consumer_service_url != nil
root.attributes["AssertionConsumerServiceURL"] = settings.assertion_consumer_service_url
end
if settings.issuer != nil
issuer = root.add_element "saml:Issuer", { "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion" }
issuer.text = settings.issuer
end
if settings.name_identifier_format != nil
root.add_element "samlp:NameIDPolicy", {
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
# Might want to make AllowCreate a setting?
"AllowCreate" => "true",
"Format" => settings.name_identifier_format
}
end

# BUG fix here -- if an authn_context is defined, add the tags with an "exact"
# match required for authentication to succeed. If this is not defined,
# the IdP will choose default rules for authentication. (Shibboleth IdP)
if settings.authn_context != nil
requested_context = root.add_element "samlp:RequestedAuthnContext", {
"xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
"Comparison" => "exact",
}
class_ref = requested_context.add_element "saml:AuthnContextClassRef", {
"xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
}
class_ref.text = settings.authn_context
end

request = ""
request_doc.write(request)

Logging.debug "Created AuthnRequest: #{request}"

deflated_request = Zlib::Deflate.deflate(request, 9)[2..-5]
base64_request = Base64.encode64(deflated_request)
encoded_request = CGI.escape(base64_request)
params_prefix = (settings.idp_sso_target_url =~ /\?/) ? '&' : '?'
request_params = "#{params_prefix}SAMLRequest=#{encoded_request}"

params.each_pair do |key, value|
request_params << "&#{key}=#{CGI.escape(value.to_s)}"
end

settings.idp_sso_target_url + request_params
end

end
end
end
22 changes: 22 additions & 0 deletions lib/onelogin/ruby-saml/logging.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Simplistic log class when we're running in Rails
module Onelogin
module Saml
class Logging
def self.debug(message)
if defined? Rails
Rails.logger.debug message
else
puts message
end
end

def self.info(message)
if defined? Rails
Rails.logger.info message
else
puts message
end
end
end
end
end
47 changes: 47 additions & 0 deletions lib/onelogin/ruby-saml/metadata.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
require "rexml/document"
require "rexml/xpath"
require "uri"

# Class to return SP metadata based on the settings requested.
# Return this XML in a controller, then give that URL to the the
# IdP administrator. The IdP will poll the URL and your settings
# will be updated automatically
module Onelogin
module Saml
include REXML
class Metadata
def generate(settings)
meta_doc = REXML::Document.new
root = meta_doc.add_element "md:EntityDescriptor", {
"xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata"
}
sp_sso = root.add_element "md:SPSSODescriptor", {
"protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol"
}
if settings.issuer != nil
root.attributes["entityID"] = settings.issuer
end
if settings.name_identifier_format != nil
name_id = sp_sso.add_element "md:NameIDFormat"
name_id.text = settings.name_identifier_format
end
if settings.assertion_consumer_service_url != nil
sp_sso.add_element "md:AssertionConsumerService", {
# Add this as a setting to create different bindings?
"Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
"Location" => settings.assertion_consumer_service_url
}
end
meta_doc << REXML::XMLDecl.new
ret = ""
# pretty print the XML so IdP administrators can easily see what the SP supports
meta_doc.write(ret, 1)

Logging.debug "Generated metadata:\n#{ret}"

return ret

end
end
end
end
146 changes: 146 additions & 0 deletions lib/onelogin/ruby-saml/response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
require "xml_security"
require "time"

module Onelogin
module Saml

class Response
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
DSIG = "http://www.w3.org/2000/09/xmldsig#"

attr_accessor :options, :response, :document, :settings

def initialize(response, options = {})
raise ArgumentError.new("Response cannot be nil") if response.nil?
self.options = options
self.response = response
self.document = XMLSecurity::SignedDocument.new(Base64.decode64(response))
end

def is_valid?
validate(soft = true)
end

def validate!
validate(soft = false)
end

# The value of the user identifier as designated by the initialization request response
def name_id
@name_id ||= begin
node = REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id[1,document.signed_element_id.size]}']/a:Subject/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
node ||= REXML::XPath.first(document, "/p:Response[@ID='#{document.signed_element_id[1,document.signed_element_id.size]}']/a:Assertion/a:Subject/a:NameID", { "p" => PROTOCOL, "a" => ASSERTION })
node.nil? ? nil : node.text
end
end

# A hash of alle the attributes with the response. Assuming there is only one value for each key
def attributes
@attr_statements ||= begin
result = {}

stmt_element = REXML::XPath.first(document, "/p:Response/a:Assertion/a:AttributeStatement", { "p" => PROTOCOL, "a" => ASSERTION })
return {} if stmt_element.nil?

stmt_element.elements.each do |attr_element|
name = attr_element.attributes["Name"]
value = attr_element.elements.first.text

result[name] = value
end

result.keys.each do |key|
result[key.intern] = result[key]
end

result
end
end

# When this user session should expire at latest
def session_expires_at
@expires_at ||= begin
node = REXML::XPath.first(document, "/p:Response/a:Assertion/a:AuthnStatement", { "p" => PROTOCOL, "a" => ASSERTION })
parse_time(node, "SessionNotOnOrAfter")
end
end

# Conditions (if any) for the assertion to run
def conditions
@conditions ||= begin
REXML::XPath.first(document, "/p:Response/a:Assertion[@ID='#{document.signed_element_id[1,document.signed_element_id.size]}']/a:Conditions", { "p" => PROTOCOL, "a" => ASSERTION })
end
end

def issuer
@issuer ||= begin
node = REXML::XPath.first(document, "/p:Response/a:Issuer", { "p" => PROTOCOL, "a" => ASSERTION })
node.nil? ? nil : node.text
end
end

private

def validation_error(message)
raise ValidationError.new(message)
end

def validate(soft = true)
validate_response_state(soft) &&
validate_conditions(soft) &&
document.validate(get_fingerprint, soft)
end

def validate_response_state(soft = true)
if response.empty?
return soft ? false : validation_error("Blank response")
end

if settings.nil?
return soft ? false : validation_error("No settings on response")
end

if settings.idp_cert_fingerprint.nil? && settings.idp_cert.nil?
return soft ? false : validation_error("No fingerprint or certificate on settings")
end

true
end

def get_fingerprint
if settings.idp_cert
cert = OpenSSL::X509::Certificate.new(settings.idp_cert)
Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")
else
settings.idp_cert_fingerprint
end
end

def validate_conditions(soft = true)
return true if conditions.nil?
return true if options[:skip_conditions]

if not_before = parse_time(conditions, "NotBefore")
if Time.now.utc < not_before
return soft ? false : validation_error("Current time is earlier than NotBefore condition")
end
end

if not_on_or_after = parse_time(conditions, "NotOnOrAfter")
if Time.now.utc >= not_on_or_after
return soft ? false : validation_error("Current time is on or after NotOnOrAfter condition")
end
end

true
end

def parse_time(node, attribute)
if node && node.attributes[attribute]
Time.parse(node.attributes[attribute])
end
end
end
end
end
9 changes: 9 additions & 0 deletions lib/onelogin/ruby-saml/settings.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Onelogin
module Saml
class Settings
attr_accessor :assertion_consumer_service_url, :issuer, :sp_name_qualifier
attr_accessor :idp_sso_target_url, :idp_cert_fingerprint, :idp_cert, :name_identifier_format
attr_accessor :authn_context
end
end
end
File renamed without changes.
5 changes: 5 additions & 0 deletions lib/onelogin/ruby-saml/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Onelogin
module Saml
VERSION = '0.4.8'
end
end
8 changes: 0 additions & 8 deletions lib/onelogin/saml.rb

This file was deleted.

Loading

0 comments on commit 21538be

Please sign in to comment.