-
-
Notifications
You must be signed in to change notification settings - Fork 975
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #445 from raecoo/master
Add SAML strategy
- Loading branch information
Showing
9 changed files
with
439 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
require 'omniauth/enterprise' | ||
|
||
module OmniAuth | ||
module Strategies | ||
class SAML | ||
include OmniAuth::Strategy | ||
autoload :AuthRequest, 'omniauth/strategies/saml/auth_request' | ||
autoload :AuthResponse, 'omniauth/strategies/saml/auth_response' | ||
autoload :ValidationError, 'omniauth/strategies/saml/validation_error' | ||
autoload :XMLSecurity, 'omniauth/strategies/saml/xml_security' | ||
|
||
@@settings = {} | ||
|
||
def initialize(app, options={}) | ||
super(app, :saml) | ||
@@settings = { | ||
:assertion_consumer_service_url => options[:assertion_consumer_service_url], | ||
:issuer => options[:issuer], | ||
:idp_sso_target_url => options[:idp_sso_target_url], | ||
:idp_cert_fingerprint => options[:idp_cert_fingerprint], | ||
:name_identifier_format => options[:name_identifier_format] || "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" | ||
} | ||
end | ||
|
||
def request_phase | ||
request = OmniAuth::Strategies::SAML::AuthRequest.new | ||
redirect(request.create(@@settings)) | ||
end | ||
|
||
def callback_phase | ||
begin | ||
response = OmniAuth::Strategies::SAML::AuthResponse.new(request.params['SAMLResponse']) | ||
response.settings = @@settings | ||
@name_id = response.name_id | ||
return fail!(:invalid_ticket, 'Invalid SAML Ticket') if @name_id.nil? || @name_id.empty? | ||
super | ||
rescue ArgumentError => e | ||
fail!(:invalid_ticket, 'Invalid SAML Response') | ||
end | ||
end | ||
|
||
def auth_hash | ||
OmniAuth::Utils.deep_merge(super, { | ||
'uid' => @name_id | ||
}) | ||
end | ||
|
||
end | ||
end | ||
end |
38 changes: 38 additions & 0 deletions
38
oa-enterprise/lib/omniauth/strategies/saml/auth_request.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
require "base64" | ||
require "uuid" | ||
require "zlib" | ||
require "cgi" | ||
|
||
module OmniAuth | ||
module Strategies | ||
class SAML | ||
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>" | ||
|
||
deflated_request = Zlib::Deflate.deflate(request, 9)[2..-5] | ||
base64_request = Base64.encode64(deflated_request) | ||
encoded_request = CGI.escape(base64_request) | ||
request_params = "?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 | ||
end |
141 changes: 141 additions & 0 deletions
141
oa-enterprise/lib/omniauth/strategies/saml/auth_response.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
require "time" | ||
|
||
module OmniAuth | ||
module Strategies | ||
class SAML | ||
class AuthResponse | ||
|
||
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 = OmniAuth::Strategies::SAML::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 | ||
|
||
private | ||
|
||
def validation_error(message) | ||
raise OmniAuth::Strategies::SAML::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 | ||
end |
8 changes: 8 additions & 0 deletions
8
oa-enterprise/lib/omniauth/strategies/saml/validation_error.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
module OmniAuth | ||
module Strategies | ||
class SAML | ||
class ValidationError < Exception | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.