Commit 19c1e76

mo <mo.khan@gmail.com>
2017-10-22 21:36:59
add nameid and subject confirmation.
1 parent 7682669
app/models/namespaces.rb
@@ -0,0 +1,37 @@
+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"
+    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_response.rb
@@ -15,29 +15,30 @@ class SamlResponse
   end
 
   class Builder
-    attr_reader :user, :request, :id
+    attr_reader :user, :request, :id, :reference_id
 
     def initialize(user, request)
       @user = user
       @request = request
       @id = SecureRandom.uuid
+      @reference_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,
-        InResponseTo: request.id,
-      }
-      xml.tag!("samlp:Response", options) do
-        xml.tag!('saml:Issuer', configuration.issuer)
+      xml.tag!("samlp:Response", response_options) do
+        xml.Issuer(configuration.issuer, xmlns: Namespaces::ASSERTION)
         xml.tag!("samlp:Status") do
-          xml.tag!('samlp:StatusCode', Value: "urn:oasis:names:tc:SAML:2.0:status:Success")
+          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
         end
       end
       xml.target!
@@ -52,5 +53,37 @@ class SamlResponse
     def configuration
       Rails.configuration.x
     end
+
+    def response_options
+      {
+        ID: "_#{id}",
+        Version: "2.0",
+        IssueInstant: Time.now.utc.iso8601,
+        Destination: request.acs_url,
+        Consent: Namespaces::Consents::UNSPECIFIED,
+        InResponseTo: request.id,
+        "xmlns:samlp" => Namespaces::PROTOCOL,
+      }
+    end
+
+    def assertion_options
+      {
+        ID: "_#{reference_id}",
+        IssueInstant: Time.now.utc.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 name_id_format
+      "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
+    end
   end
 end
app/models/user.rb
@@ -1,3 +1,6 @@
 class User < ApplicationRecord
   has_secure_password
+  after_initialize do
+    self.uuid = SecureRandom.uuid unless self.uuid
+  end
 end
db/migrate/20171021193946_create_users.rb
@@ -2,7 +2,9 @@ class CreateUsers < ActiveRecord::Migration[5.1]
   def change
     create_table :users do |t|
       t.string :email
+      t.string :uuid, null: false, index: true
       t.string :password_digest
+      t.timestamps null: false
     end
   end
 end
db/schema.rb
@@ -14,7 +14,11 @@ ActiveRecord::Schema.define(version: 20171021193946) do
 
   create_table "users", force: :cascade do |t|
     t.string "email"
+    t.string "uuid", null: false
     t.string "password_digest"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["uuid"], name: "index_users_on_uuid"
   end
 
 end
spec/models/saml_response_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
 describe SamlResponse do
   describe ".for" do
     subject { described_class }
-    let(:user) { double(:user) }
+    let(:user) { double(:user, uuid: SecureRandom.uuid) }
     let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url) }
     let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
     let(:issuer) { FFaker::Movie.title }
@@ -21,10 +21,15 @@ describe SamlResponse do
   <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: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: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>
@@ -46,10 +51,6 @@ describe SamlResponse do
       <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>
@@ -67,6 +68,17 @@ describe SamlResponse do
       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)
     end
   end
 end