Commit e9bf0d8
Changed files (7)
app/models/authentication_request.rb
@@ -1,56 +0,0 @@
-require 'builder'
-
-class AuthenticationRequest
- def initialize(xml, registry = ServiceProviderRegistry.new)
- @xml = xml
- @registry = registry
- @hash = Hash.from_xml(@xml)
- end
-
- def id
- @hash['AuthnRequest']['ID']
- end
-
- def acs_url
- @hash['AuthnRequest']['AssertionConsumerServiceURL']
- end
-
- def issuer
- @hash['AuthnRequest']['Issuer']
- end
-
- def valid?
- @registry.registered?(issuer)
- end
-
- def to_xml
- @xml
- end
-
- def response_for(user)
- SamlResponse::Builder.new(user, self).build
- end
-
- class Builder
- attr_accessor :id, :issued_at, :issuer, :acs_url
-
- def to_xml(xml = ::Builder::XmlMarkup.new)
- xml.tag!('samlp:AuthnRequest',
- "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
- "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
- ID: id,
- Version: "2.0",
- IssueInstant: issued_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
- AssertionConsumerServiceURL: acs_url,
- ) do
- xml.tag!('saml:Issuer', issuer)
- xml.tag!('samlp:NameIDPolicy', Format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
- end
- xml.target!
- end
-
- def build
- AuthenticationRequest.new(to_xml)
- end
- end
-end
app/models/namespaces.rb
@@ -1,38 +0,0 @@
-module Namespaces
- METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
- ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
- SIGNATURE = "http://www.w3.org/2000/09/xmldsig#"
- PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
-
- module Statuses
- SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
- end
-
- module Consents
- UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:consent:unspecified"
- end
-
- module AuthnContext
- module ClassRef
- PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
- PASSWORD_PROTECTED = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
- end
- end
-
- module Methods
- BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
- end
-
- module Formats
- module Attr
- URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
- BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
- end
-
- module NameId
- EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
- TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
- PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
- end
- end
-end
app/models/saml_request.rb
@@ -1,5 +0,0 @@
-class SamlRequest
- def self.decode(raw_request)
- AuthenticationRequest.new(Base64.decode64(raw_request))
- end
-end
app/models/saml_response.rb
@@ -1,125 +0,0 @@
-require 'builder'
-
-class SamlResponse
- def initialize(xml)
- @xml = xml
- end
-
- def acs_url
- Hash.from_xml(@xml)['Response']['Destination']
- end
-
- def to_xml
- @xml
- end
-
- def encode
- Base64.strict_encode64(to_xml)
- end
-
- class Builder
- attr_reader :user, :request, :id, :reference_id, :now
-
- def initialize(user, request)
- @user = user
- @request = request
- @id = SecureRandom.uuid
- @reference_id = SecureRandom.uuid
- @now = Time.now.utc
- end
-
- def to_xml
- xml = ::Builder::XmlMarkup.new
- xml.tag!("samlp:Response", response_options) do
- xml.Issuer(configuration.issuer, xmlns: Namespaces::ASSERTION)
- xml.tag!("samlp:Status") do
- xml.tag!('samlp:StatusCode', Value: Namespaces::Statuses::SUCCESS)
- end
- xml.Assertion(assertion_options) do
- xml.Issuer configuration.issuer
- xml.Subject do
- xml.NameID user.uuid, Format: name_id_format
- xml.SubjectConfirmation Method: Namespaces::Methods::BEARER do
- xml.SubjectConfirmationData "", subject_confirmation_data_options
- end
- end
- xml.Conditions conditions_options do
- xml.AudienceRestriction do
- xml.Audience request.issuer
- end
- end
- xml.AuthnStatement authn_statement_options do
- xml.AuthnContext do
- xml.AuthnContextClassRef Namespaces::AuthnContext::ClassRef::PASSWORD
- end
- end
- xml.AttributeStatement do
- user.assertion_attributes.each do |key, value|
- xml.Attribute Name: key, NameFormat: Namespaces::Formats::Attr::URI, FriendlyName: key do
- xml.AttributeValue value.to_s
- end
- end
- end
- end
- end
- xml.target!
- end
-
- def build
- SamlResponse.new(to_xml)
- end
-
- private
-
- def configuration
- Rails.configuration.x
- end
-
- def response_options
- {
- ID: "_#{id}",
- Version: "2.0",
- IssueInstant: now.iso8601,
- Destination: request.acs_url,
- Consent: Namespaces::Consents::UNSPECIFIED,
- InResponseTo: request.id,
- "xmlns:samlp" => Namespaces::PROTOCOL,
- }
- end
-
- def assertion_options
- {
- ID: "_#{reference_id}",
- IssueInstant: now.iso8601,
- Version: "2.0",
- }
- end
-
- def subject_confirmation_data_options
- {
- InResponseTo: request.id,
- NotOnOrAfter: 3.hours.from_now.utc.iso8601,
- Recipient: request.acs_url,
- }
- end
-
- def conditions_options
- {
- NotBefore: 5.seconds.ago.utc.iso8601,
- NotOnOrAfter: 3.hours.from_now.utc.iso8601,
- }
- end
-
- def authn_statement_options
- {
- AuthnInstant: now.iso8601,
- SessionIndex: assertion_options[:ID],
- SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601,
- }
- end
-
- def name_id_format
- "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
- end
- end
-end
app/models/service_provider_registry.rb
@@ -1,5 +0,0 @@
-class ServiceProviderRegistry
- def registered?(issuer)
- true
- end
-end
spec/models/authentication_request_spec.rb
@@ -1,33 +0,0 @@
-require 'rails_helper'
-
-describe AuthenticationRequest do
- subject { described_class.new(raw_xml, registry) }
- let(:registry) { double }
- let(:id) { SecureRandom.uuid }
- let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
- let(:issuer) { FFaker::Movie.title }
- let(:raw_xml) do
- builder = AuthenticationRequest::Builder.new
- builder.id = id
- builder.issued_at = Time.now.utc
- builder.issuer = issuer
- builder.acs_url = acs_url
- builder.to_xml
- end
-
- it { expect(subject.issuer).to eql(issuer) }
- it { expect(subject.id).to eql(id) }
- it { expect(subject.acs_url).to eql(acs_url) }
-
- describe "#valid?" do
- it 'returns false when the service provider is not known' do
- allow(registry).to receive(:registered?).with(issuer).and_return(false)
- expect(subject).to_not be_valid
- end
-
- it 'returns true when the service provider is registered' do
- allow(registry).to receive(:registered?).with(issuer).and_return(true)
- expect(subject).to be_valid
- end
- end
-end
spec/models/saml_response_spec.rb
@@ -1,115 +0,0 @@
-require 'rails_helper'
-
-describe SamlResponse do
- describe "#acs_url" do
- let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
- let(:user) { double(:user, uuid: SecureRandom.uuid, assertion_attributes: { }) }
- let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title) }
- subject { SamlResponse::Builder.new(user, request).build }
-
- it 'returns the acs_url' do
- expect(subject.acs_url).to eql(acs_url)
- end
- end
-
- describe "#to_xml" do
- subject { SamlResponse::Builder.new(user, request) }
- let(:user) { double(:user, uuid: SecureRandom.uuid, assertion_attributes: { email: email, created_at: Time.now.utc.iso8601 }) }
- let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title) }
- let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
- let(:issuer) { FFaker::Movie.title }
- let(:email) { FFaker::Internet.email }
-
- <<-XML
-<samlp:Response
- xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
- xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
- ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6"
- Version="2.0"
- IssueInstant="2014-07-17T01:01:48Z"
- Destination="http://sp.example.com/demo1/index.php?acs"
- InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
- <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
- <samlp:Status>
- <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
- </samlp:Status>
- <saml:Assertion
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:xs="http://www.w3.org/2001/XMLSchema"
- ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75"
- Version="2.0"
- IssueInstant="2014-07-17T01:01:48Z">
- <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
- <saml:Subject>
- <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
- <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
- <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
- </saml:SubjectConfirmation>
- </saml:Subject>
- <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
- <saml:AudienceRestriction>
- <saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
- </saml:AudienceRestriction>
- </saml:Conditions>
- <saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
- <saml:AuthnContext>
- <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
- </saml:AuthnContext>
- </saml:AuthnStatement>
- <saml:AttributeStatement>
- <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
- <saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
- </saml:Attribute>
- <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
- <saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
- </saml:Attribute>
- </saml:AttributeStatement>
- </saml:Assertion>
-</samlp:Response>
- XML
- it 'returns a proper response for the user' do
- travel_to 1.second.from_now
- allow(Rails.configuration.x).to receive(:issuer).and_return(issuer)
- result = subject.to_xml
- hash = Hash.from_xml(result)
-
- expect(hash['Response']['ID']).to be_present
- expect(hash['Response']['Version']).to eql('2.0')
- expect(hash['Response']['IssueInstant']).to eql(Time.now.utc.iso8601)
- expect(hash['Response']['Destination']).to eql(acs_url)
- expect(hash['Response']['InResponseTo']).to eql(request.id)
- expect(hash['Response']['Issuer']).to eql(issuer)
- expect(hash['Response']['Status']['StatusCode']['Value']).to eql("urn:oasis:names:tc:SAML:2.0:status:Success")
-
- expect(hash['Response']['Assertion']['ID']).to be_present
- expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
- expect(hash['Response']['Assertion']['Version']).to eql("2.0")
- expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
-
- expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.uuid)
- expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql("urn:oasis:names:tc:SAML:2.0:cm:bearer")
- expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
- expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(acs_url)
- expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
-
- expect(hash['Response']['Assertion']['Conditions']['NotBefore']).to eql(5.seconds.ago.utc.iso8601)
- expect(hash['Response']['Assertion']['Conditions']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
- expect(hash['Response']['Assertion']['Conditions']['AudienceRestriction']['Audience']).to eql(request.issuer)
-
- expect(hash['Response']['Assertion']['AuthnStatement']['AuthnInstant']).to eql(Time.now.utc.iso8601)
- expect(hash['Response']['Assertion']['AuthnStatement']['SessionNotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
- expect(hash['Response']['Assertion']['AuthnStatement']['SessionIndex']).to eql(hash['Response']['Assertion']['ID'])
- expect(hash['Response']['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to eql('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
-
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['Name']).to eql('email')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['FriendlyName']).to eql('email')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql(email)
-
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['Name']).to eql('created_at')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['FriendlyName']).to eql('created_at')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
- expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['AttributeValue']).to be_present
- end
- end
-end