Skip to content

Commit

Permalink
Merge pull request #108 from onelogin/namespacing-change
Browse files Browse the repository at this point in the history
Change namespacing from Onelogin::Saml to Onelogin::Rubysaml
  • Loading branch information
Lordnibbler committed Feb 24, 2014
2 parents 2016b77 + 14528d1 commit e9a91c1
Show file tree
Hide file tree
Showing 20 changed files with 209 additions and 197 deletions.
135 changes: 70 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Ruby SAML [![Build Status](https://secure.travis-ci.org/onelogin/ruby-saml.png)](http://travis-ci.org/onelogin/ruby-saml)

## Updating from 0.7.x to 0.8.x
Version `0.8.0` changes the namespace of the gem from `Onelogin::Saml` to `Onelogin::RubySaml`. Please update your implementations of the gem accordingly.

## Overview

The Ruby SAML library is for implementing the client side of a SAML authorization, i.e. it provides a means for managing authorization initialization and confirmation requests from identity providers.

SAML authorization is a two step process and you are expected to implement support for both.
Expand All @@ -9,113 +14,113 @@ SAML authorization is a two step process and you are expected to implement suppo
This is the first request you will get from the identity provider. It will hit your application at a specific URL (that you've announced as being your SAML initialization point). The response to this initialization, is a redirect back to the identity provider, which can look something like this (ignore the saml_settings method call for now):

```ruby
def init
request = Onelogin::Saml::Authrequest.new
redirect_to(request.create(saml_settings))
end
def init
request = Onelogin::RubySaml::Authrequest.new
redirect_to(request.create(saml_settings))
end
```

Once you've redirected back to the identity provider, it will ensure that the user has been authorized and redirect back to your application for final consumption, this is can look something like this (the authorize_success and authorize_failure methods are specific to your application):

```ruby
def consume
response = Onelogin::Saml::Response.new(params[:SAMLResponse])
response.settings = saml_settings

if response.is_valid? && user = current_account.users.find_by_email(response.name_id)
authorize_success(user)
else
authorize_failure(user)
end
end
def consume
response = Onelogin::RubySaml::Response.new(params[:SAMLResponse])
response.settings = saml_settings

if response.is_valid? && user = current_account.users.find_by_email(response.name_id)
authorize_success(user)
else
authorize_failure(user)
end
end
```

In the above there are a few assumptions in place, one being that the response.name_id is an email address. This is all handled with how you specify the settings that are in play via the saml_settings method. That could be implemented along the lines of this:

```ruby
def saml_settings
settings = Onelogin::Saml::Settings.new

settings.assertion_consumer_service_url = "http://#{request.host}/saml/finalize"
settings.issuer = request.host
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
def saml_settings
settings = Onelogin::RubySaml::Settings.new

settings.assertion_consumer_service_url = "http://#{request.host}/saml/finalize"
settings.issuer = request.host
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
```

What's left at this point, is to wrap it all up in a controller and point the initialization and consumption URLs in OneLogin at that. A full controller example could look like this:

```ruby
# This controller expects you to use the URLs /saml/init and /saml/consume in your OneLogin application.
class SamlController < ApplicationController
def init
request = Onelogin::Saml::Authrequest.new
redirect_to(request.create(saml_settings))
end
# This controller expects you to use the URLs /saml/init and /saml/consume in your OneLogin application.
class SamlController < ApplicationController
def init
request = Onelogin::RubySaml::Authrequest.new
redirect_to(request.create(saml_settings))
end

def consume
response = Onelogin::Saml::Response.new(params[:SAMLResponse])
response.settings = saml_settings
def consume
response = Onelogin::RubySaml::Response.new(params[:SAMLResponse])
response.settings = saml_settings

if response.is_valid? && user = current_account.users.find_by_email(response.name_id)
authorize_success(user)
else
authorize_failure(user)
end
if response.is_valid? && user = current_account.users.find_by_email(response.name_id)
authorize_success(user)
else
authorize_failure(user)
end
end

private
private

def saml_settings
settings = Onelogin::Saml::Settings.new
def saml_settings
settings = Onelogin::RubySaml::Settings.new

settings.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
settings.issuer = request.host
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.assertion_consumer_service_url = "http://#{request.host}/saml/consume"
settings.issuer = request.host
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
settings
end
end
```

If are using saml:AttributeStatement to transfare metadata, like the user name, you can access all the attributes through response.attributes. It
contains all the saml:AttributeStatement with its 'Name' as a indifferent key and the one saml:AttributeValue as value.

```ruby
response = Onelogin::Saml::Response.new(params[:SAMLResponse])
response.settings = saml_settings
response = Onelogin::RubySaml::Response.new(params[:SAMLResponse])
response.settings = saml_settings

response.attributes[:username]
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, relaying party permissions, etc)

The class Onelogin::Saml::Metadata takes care of this by reading the Settings and returning XML. All
The class Onelogin::RubySaml::Metadata 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.

```ruby
class SamlController < ApplicationController
# ... the rest of your controller definitions ...
def metadata
settings = Account.get_saml_settings
meta = Onelogin::Saml::Metadata.new
render :xml => meta.generate(settings)
end
class SamlController < ApplicationController
# ... the rest of your controller definitions ...
def metadata
settings = Account.get_saml_settings
meta = Onelogin::RubySaml::Metadata.new
render :xml => meta.generate(settings)
end
end
```

## Clock Drift
Expand All @@ -127,7 +132,7 @@ First, ensure that both systems synchronize their clocks, using for example the
Even then you may experience intermittent issues though, because the clock of the Identity Provider may drift slightly ahead of your system clocks. To allow for a small amount of clock drift you can initialize the response passing in an option named `:allowed_clock_drift`. Its value must be given in a number (and/or fraction) of seconds. The value given is added to the current time at which the response is validated before it's tested against the `NotBefore` assertion. For example:

```ruby
response = Onelogin::Saml::Response.new(params[:SAMLResponse], :allowed_clock_drift => 1)
response = Onelogin::RubySaml::Response.new(params[:SAMLResponse], :allowed_clock_drift => 1)
```

Make sure to keep the value as comfortably small as possible to keep security risks to a minimum.
Expand Down
7 changes: 7 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# RubySaml Changelog

### 0.8.0 (Feb 21, 2014)
Changed namespace of the gem from `Onelogin::Saml` to `Onelogin::RubySaml`. Please update your implementations of the gem accordingly.

### 0.7.3 (Feb 20, 2014)
Updated gem dependencies to be compatible with Ruby 1.8.7-p374 and 1.9.3-p448. Removed unnecessary `canonix` gem dependency.
2 changes: 1 addition & 1 deletion lib/onelogin/ruby-saml/authrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
require "rexml/xpath"

module Onelogin
module Saml
module RubySaml
include REXML
class Authrequest
def create(settings, params = {})
Expand Down
2 changes: 1 addition & 1 deletion lib/onelogin/ruby-saml/logging.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Simplistic log class when we're running in Rails
module Onelogin
module Saml
module RubySaml
class Logging
def self.debug(message)
return if !!ENV["ruby-saml/testing"]
Expand Down
2 changes: 1 addition & 1 deletion lib/onelogin/ruby-saml/logoutrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
require "cgi"

module Onelogin
module Saml
module RubySaml
include REXML
class Logoutrequest

Expand Down
2 changes: 1 addition & 1 deletion lib/onelogin/ruby-saml/logoutresponse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
require "zlib"

module Onelogin
module Saml
module RubySaml
class Logoutresponse

ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
Expand Down
4 changes: 2 additions & 2 deletions lib/onelogin/ruby-saml/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
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
# 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
module RubySaml
include REXML
class Metadata
def generate(settings)
Expand Down
2 changes: 1 addition & 1 deletion lib/onelogin/ruby-saml/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# Only supports SAML 2.0
module Onelogin
module Saml
module RubySaml

class Response
ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
Expand Down
2 changes: 1 addition & 1 deletion lib/onelogin/ruby-saml/settings.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Onelogin
module Saml
module RubySaml
class Settings
def initialize(overrides = {})
config = DEFAULTS.merge(overrides)
Expand Down
2 changes: 1 addition & 1 deletion lib/onelogin/ruby-saml/validation_error.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Onelogin
module Saml
module RubySaml
class ValidationError < StandardError
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/onelogin/ruby-saml/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Onelogin
module Saml
VERSION = '0.7.3'
module RubySaml
VERSION = '0.8.0'
end
end
8 changes: 4 additions & 4 deletions lib/xml_security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def initialize(response)
def validate_document(idp_cert_fingerprint, soft = true)
# get cert from response
cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
raise Onelogin::Saml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)") unless cert_element
raise Onelogin::RubySaml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)") unless cert_element
base64_cert = cert_element.text
cert_text = Base64.decode64(base64_cert)
cert = OpenSSL::X509::Certificate.new(cert_text)
Expand All @@ -56,7 +56,7 @@ def validate_document(idp_cert_fingerprint, soft = true)
fingerprint = Digest::SHA1.hexdigest(cert.to_der)

if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
return soft ? false : (raise Onelogin::Saml::ValidationError.new("Fingerprint mismatch"))
return soft ? false : (raise Onelogin::RubySaml::ValidationError.new("Fingerprint mismatch"))
end

validate_signature(base64_cert, soft)
Expand Down Expand Up @@ -102,7 +102,7 @@ def validate_signature(base64_cert, soft = true)
digest_value = Base64.decode64(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG}).text)

unless digests_match?(hash, digest_value)
return soft ? false : (raise Onelogin::Saml::ValidationError.new("Digest mismatch"))
return soft ? false : (raise Onelogin::RubySaml::ValidationError.new("Digest mismatch"))
end
end

Expand All @@ -117,7 +117,7 @@ def validate_signature(base64_cert, soft = true)
signature_algorithm = algorithm(REXML::XPath.first(signed_info_element, "//ds:SignatureMethod", {"ds"=>DSIG}))

unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
return soft ? false : (raise Onelogin::Saml::ValidationError.new("Key validation error"))
return soft ? false : (raise Onelogin::RubySaml::ValidationError.new("Key validation error"))
end

return true
Expand Down
2 changes: 1 addition & 1 deletion ruby-saml.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ require 'onelogin/ruby-saml/version'

Gem::Specification.new do |s|
s.name = 'ruby-saml'
s.version = Onelogin::Saml::VERSION
s.version = Onelogin::RubySaml::VERSION

s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["OneLogin LLC"]
Expand Down
Loading

0 comments on commit e9a91c1

Please # to comment.