Commit 56167d4

mo <mo.khan@gmail.com>
2017-10-22 17:25:47
start to build a response for a authnrequest.
1 parent 23c6f85
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"