Commit 56167d4
Changed files (11)
app
spec
app/controllers/sessions_controller.rb
@@ -33,6 +33,7 @@ class SessionsController < ApplicationController
def post_to_service_provider(user)
@saml_response = @saml_request.response_for(user)
+ @relay_state = params[:RelayState]
render template: "sessions/saml_post", layout: nil
end
app/models/authentication_request.rb
@@ -1,5 +1,7 @@
+require 'builder'
+
class AuthenticationRequest
- def initialize(xml, registry = {})
+ def initialize(xml, registry = ServiceProviderRegistry.new)
@xml = xml
@registry = registry
@hash = Hash.from_xml(@xml)
@@ -16,4 +18,27 @@ class AuthenticationRequest
def to_xml
@xml
end
+
+ def response_for(user)
+ SamlResponse.for(user, self)
+ 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
+ end
end
app/models/authentication_request_builder.rb
@@ -1,20 +0,0 @@
-require 'builder'
-
-class AuthenticationRequestBuilder
- 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
-end
app/models/saml_request.rb
@@ -1,5 +1,5 @@
class SamlRequest
def self.decode(raw_request)
- new(Base64.decode64(raw_request))
+ AuthenticationRequest.new(Base64.decode64(raw_request))
end
end
app/models/saml_response.rb
@@ -0,0 +1,45 @@
+require 'builder'
+
+class SamlResponse
+ def initialize(xml)
+ @xml = xml
+ end
+
+ def to_xml
+ @xml
+ end
+
+ def self.for(user, authentication_request)
+ builder = Builder.new(user, authentication_request)
+ builder.build
+ end
+
+ class Builder
+ attr_reader :user, :request, :id
+
+ def initialize(user, request)
+ @user = user
+ @request = request
+ @id = SecureRandom.uuid
+ end
+
+ def to_xml
+ xml = ::Builder::XmlMarkup.new
+ options = {
+ "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: Time.now.utc.iso8601,
+ Destination: request.acs_url,
+ }
+ xml.tag! "samlp:Response", options do |response|
+ end
+ xml.target!
+ end
+
+ def build
+ SamlResponse.new(to_xml)
+ end
+ end
+end
app/models/service_provider_registry.rb
@@ -0,0 +1,5 @@
+class ServiceProviderRegistry
+ def registered?(issuer)
+ true
+ end
+end
app/views/sessions/new.html.erb
@@ -1,7 +1,8 @@
+<%= debug flash %>
<%= form_for :user, url: session_path, method: :post do |form| %>
<%= hidden_field_tag :SAMLRequest, params[:SAMLRequest] %>
<%= hidden_field_tag :RelayState, params[:RelayState] %>
- <%= form.email_field :email, autofocus: true, placeholder: t('email'), required: :required %>
- <%= form.password_field :password, placeholder: t('password'), required: :required %>
+ <%= form.email_field :email, autofocus: true, required: :required %>
+ <%= form.password_field :password, required: :required %>
<%= form.button t('log_in'), type: 'submit', data: { disable_with: t('loading') } %>
<% end %>
app/views/sessions/saml_post.html.erb
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <style type="text/css" media="all">
+ html,
+ body {
+ width: 100%;
+ height: 100%;
+ }
+
+ body {
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-align: center;
+ align-items: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ }
+
+ @keyframes spinner {
+ 0% { transform: rotate(0deg) }
+ 100% { transform: rotate(360deg) }
+ }
+
+ .spinner {
+ width: 48px;
+ height: 48px;
+ animation: spinner 0.65s infinite steps(12);
+ }
+
+ .spinner svg {
+ width: 48px;
+ height: 48px;
+ }
+ </style>
+ </head>
+ <body onload="document.forms[0].submit();">
+ <div class="spinner"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25"><defs><style>line{fill:none;stroke:#1a1a1a;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2px;}.o25{opacity:0.25;}.o85{opacity:0.85;}.o7{opacity:0.7;}.o55{opacity:0.55;}.o35{opacity:0.35;}</style></defs><line x1="12.5" y1="2" x2="12.5" y2="7.05463"/><line class="o25" x1="12.5" y1="23" x2="12.5" y2="17.94537"/><line class="o85" x1="7.25" y1="3.40674" x2="9.77942" y2="7.78778"/><line class="o25" x1="17.75" y1="21.59326" x2="15.22058" y2="17.21222"/><line class="o25" x1="17.21222" y1="15.22058" x2="21.59326" y2="17.75"/><line class="o7" x1="7.78778" y1="9.77942" x2="3.40674" y2="7.25"/><line class="o25" x1="23" y1="12.5" x2="17.94537" y2="12.5"/><line class="o55" x1="7.05463" y1="12.5" x2="2" y2="12.5"/><line class="o35" x1="7.78778" y1="15.22058" x2="3.40674" y2="17.75"/><line class="o25" x1="21.59326" y1="7.25" x2="17.21222" y2="9.77942"/><line class="o25" x1="9.77942" y1="17.21222" x2="7.25" y2="21.59326"/><line class="o25" x1="17.75" y1="3.40674" x2="15.22058" y2="7.78778"/></svg></div>
+ <%= form_tag(saml_acs_url, style: "position: absolute; left: -10000px; top: -10000px;") do %>
+ <%= hidden_field_tag("SAMLResponse", @saml_response) %>
+ <%= hidden_field_tag("RelayState", @relay_state) %>
+ <%= submit_tag "Submit" %>
+ <% end %>
+ </body>
+</html>
spec/models/saml_request_spec.rb
@@ -7,7 +7,7 @@ describe AuthenticationRequest do
describe "#valid?" do
let(:raw_xml) do
- builder = AuthenticationRequestBuilder.new
+ builder = AuthenticationRequest::Builder.new
builder.id = SecureRandom.uuid
builder.issued_at = Time.now.utc
builder.issuer = "my-issuer"
spec/models/saml_response_spec.rb
@@ -0,0 +1,67 @@
+require 'rails_helper'
+
+describe SamlResponse do
+ describe ".for" do
+ subject { described_class }
+ let(:user) { double(:user) }
+ let(:authentication_request) { double(acs_url: acs_url) }
+ let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+
+ <<-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 SPNameQualifier="http://sp.example.com/demo1/metadata.php" 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:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+ <saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
+ <saml:AttributeValue xsi:type="xs:string">examplerole1</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
+ result = subject.for(user, authentication_request).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)
+ end
+ end
+end
spec/rails_helper.rb
@@ -27,6 +27,7 @@ require 'rspec/rails'
ActiveRecord::Migration.maintain_test_schema!
RSpec.configure do |config|
+ config.include ActiveSupport::Testing::TimeHelpers
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"