Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

Add dynamically generated SP metadata #20

Merged
merged 10 commits into from Nov 7, 2011
View
@@ -36,7 +36,9 @@ In the above there are a few assumptions in place, one being that the response.n
settings.idp_sso_target_url = "https://app.onelogin.com/saml/signon/#{OneLoginAppId}"
settings.idp_cert_fingerprint = OneLoginAppCertFingerPrint
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
-
+ # Optional for most SAML IdPs
+ settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
+
settings
end
@@ -70,7 +72,9 @@ What's left at this point, is to wrap it all up in a controller and point the in
settings.idp_sso_target_url = "https://app.onelogin.com/saml/signon/#{OneLoginAppId}"
settings.idp_cert_fingerprint = OneLoginAppCertFingerPrint
settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
-
+ # Optional for most SAML IdPs
+ settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
+
settings
end
end
@@ -83,6 +87,25 @@ contains all the saml:AttributeStatement with its 'Name' as a indifferent key an
response.attributes[:username]
+== Service Provider Metadata
+
+To form a trusted pair relationship with the IdP, the SP (you) need to provide metadata XML
+to the IdP for various good reasons. (Caching, certificate lookups, relying party permissions, etc)
+
+The class Onelogin::Saml::Metdata takes care of this by reading the Settings and returning XML. All
+you have to do is add a controller to return the data, then give this URL to the IdP administrator.
+The metdata will be polled by the IdP every few minutes, so updating your settings should propagate
+to the IdP settings.
+
+ class SamlController < ApplicationController
+ # ... the rest of your controller definitions ...
+ def metadata
+ settings = Account.get_saml_settings
+ meta = Onelogin::Saml::Metadata.new
+ render :xml => meta.create(settings)
+ end
+ end
+
= Full Example
View
@@ -1,5 +1,8 @@
+require 'onelogin/saml/logging'
require 'onelogin/saml/authrequest'
require 'onelogin/saml/response'
require 'onelogin/saml/settings'
require 'onelogin/saml/validation_error'
+require 'onelogin/saml/metadata'
+
@@ -2,21 +2,60 @@
require "uuid"
require "zlib"
require "cgi"
+require "rexml/document"
+require "rexml/xpath"
module Onelogin::Saml
+include REXML
class Authrequest
def create(settings, params = {})
+
uuid = "_" + UUID.new.generate
time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
-
- request =
- "<samlp:AuthnRequest xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"#{uuid}\" Version=\"2.0\" IssueInstant=\"#{time}\" ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" AssertionConsumerServiceURL=\"#{settings.assertion_consumer_service_url}\">" +
- "<saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{settings.issuer}</saml:Issuer>\n" +
- "<samlp:NameIDPolicy xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Format=\"#{settings.name_identifier_format}\" AllowCreate=\"true\"></samlp:NameIDPolicy>\n" +
- "<samlp:RequestedAuthnContext xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" Comparison=\"exact\">" +
- "<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" +
- "</samlp:AuthnRequest>"
-
+ # 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)
@@ -0,0 +1,20 @@
+# Simplistic log class when we're running in Rails
+module Onelogin::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
@@ -0,0 +1,46 @@
+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::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
+
@@ -2,5 +2,6 @@ module Onelogin::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