stub_saml_idp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +27 -0
- data/MIT-LICENSE +21 -0
- data/README.md +94 -0
- data/app/controllers/stub_saml_idp/idp_controller.rb +55 -0
- data/app/views/stub_saml_idp/idp/new.html.erb +21 -0
- data/app/views/stub_saml_idp/idp/saml_post.html.erb +13 -0
- data/lib/stub_saml_idp/configurator.rb +15 -0
- data/lib/stub_saml_idp/controller.rb +109 -0
- data/lib/stub_saml_idp/default.rb +29 -0
- data/lib/stub_saml_idp/engine.rb +6 -0
- data/lib/stub_saml_idp/version.rb +5 -0
- data/lib/stub_saml_idp.rb +17 -0
- data/spec/acceptance/acceptance_helper.rb +22 -0
- data/spec/acceptance/idp_controller_spec.rb +38 -0
- data/spec/rails_helper.rb +3 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/stub_saml_idp/controller_spec.rb +74 -0
- data/spec/support/idp_template.rb +28 -0
- data/spec/support/rails_app.rb +139 -0
- data/spec/support/saml_request_macros.rb +19 -0
- data/spec/support/sp_template.rb +27 -0
- data/stub_saml_idp.gemspec +36 -0
- metadata +159 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: a3f6ac356bfc0056773af0a5521b971242a78dcae4988d5dd1a11e7c185457fc
         | 
| 4 | 
            +
              data.tar.gz: 4e0cee21a7d101f93279a6c3692a5e632c9847ae55a0ab0be1d1cab6b8a603b8
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: a5719837b97827677c41461a987899847460ab0904092e98962320d19144b550df1426b8ea7be09bc1120e0ec0ee3c58a5879c31c95f8c954dd77177c072b82e
         | 
| 7 | 
            +
              data.tar.gz: 345e07b9e5da8a4f31c82954a9cf564402911d995e4da75d105ef3d5cefb1deb880820d91aed2ac61cdbdede20505a67cdd8fe2c10fb1cc9f097ac37657b6dc1
         | 
    
        data/Gemfile
    ADDED
    
    | @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            source 'https://rubygems.org'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            gemspec
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            group :development, :test do
         | 
| 8 | 
            +
              gem 'rails', '~> 7.0.0'
         | 
| 9 | 
            +
              gem 'rubocop'
         | 
| 10 | 
            +
              gem 'rubocop-rspec'
         | 
| 11 | 
            +
              gem 'ruby-saml'
         | 
| 12 | 
            +
            end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            group :test do
         | 
| 15 | 
            +
              gem 'capybara'
         | 
| 16 | 
            +
              gem 'rake'
         | 
| 17 | 
            +
              gem 'rspec', '~> 3.0'
         | 
| 18 | 
            +
              gem 'rspec-rails', '~> 5.0'
         | 
| 19 | 
            +
              gem 'selenium-webdriver'
         | 
| 20 | 
            +
              gem 'timecop'
         | 
| 21 | 
            +
            end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.1')
         | 
| 24 | 
            +
              gem 'net-imap', require: false
         | 
| 25 | 
            +
              gem 'net-pop', require: false
         | 
| 26 | 
            +
              gem 'net-smtp', require: false
         | 
| 27 | 
            +
            end
         | 
    
        data/MIT-LICENSE
    ADDED
    
    | @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            Copyright (c) 2022 Peter M. Goldstein (https://github.com/petergoldstein/stub_saml_idp)
         | 
| 2 | 
            +
            Copyright (c) 2012 Lawrence Pit (https://github.com/lawrencepit/ruby-saml-idp)
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining
         | 
| 5 | 
            +
            a copy of this software and associated documentation files (the
         | 
| 6 | 
            +
            "Software"), to deal in the Software without restriction, including
         | 
| 7 | 
            +
            without limitation the rights to use, copy, modify, merge, publish,
         | 
| 8 | 
            +
            distribute, sublicense, and/or sell copies of the Software, and to
         | 
| 9 | 
            +
            permit persons to whom the Software is furnished to do so, subject to
         | 
| 10 | 
            +
            the following conditions:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            The above copyright notice and this permission notice shall be
         | 
| 13 | 
            +
            included in all copies or substantial portions of the Software.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
         | 
| 16 | 
            +
            EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
         | 
| 17 | 
            +
            MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
         | 
| 18 | 
            +
            NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
         | 
| 19 | 
            +
            LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
         | 
| 20 | 
            +
            OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
         | 
| 21 | 
            +
            WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,94 @@ | |
| 1 | 
            +
            # Stub SAML Identity Provider (IdP)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            The Stub SAML Identity Provider library allows users to easily spin up stub SAML IdP
         | 
| 4 | 
            +
            servers in test environments.    
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            This is not a "real" IdP and should not be used in production environments.  It is intended
         | 
| 7 | 
            +
            only for use in testing environments.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
             | 
| 10 | 
            +
            Installation and Usage
         | 
| 11 | 
            +
            ----------------------
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            Add this to the Gemfile of your Rails app in your test environment:
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                gem 'stub_saml_idp'
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            Add to your `routes.rb` file, for example:
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            ``` ruby
         | 
| 20 | 
            +
            get '/saml/auth' => 'saml_idp#new'
         | 
| 21 | 
            +
            post '/saml/auth' => 'saml_idp#create'
         | 
| 22 | 
            +
            ```
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            Create a controller that looks like this, customize to your own situation:
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            ``` ruby
         | 
| 27 | 
            +
            class SamlIdpController < StubSamlIdp::IdpController
         | 
| 28 | 
            +
              before_action :find_account
         | 
| 29 | 
            +
              # layout 'saml_idp'
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              def idp_authenticate(email, password)
         | 
| 32 | 
            +
                user = @account.users.where(:email => params[:email]).first
         | 
| 33 | 
            +
                user && user.valid_password?(params[:password]) ? user : nil
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              def idp_make_saml_response(user)
         | 
| 37 | 
            +
                encode_SAMLResponse(user.email)
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
              private
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def find_account
         | 
| 43 | 
            +
                  @subdomain = saml_acs_url[/https?:\/\/(.+?)\.example.com/, 1]
         | 
| 44 | 
            +
                  @account = Account.find_by_subdomain(@subdomain)
         | 
| 45 | 
            +
                  render :status => :forbidden unless @account.saml_enabled?
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            end
         | 
| 49 | 
            +
            ```
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            The most minimal example controller would look like:
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            ``` ruby
         | 
| 54 | 
            +
            class SamlIdpController < StubSamlIdp::IdpController
         | 
| 55 | 
            +
             | 
| 56 | 
            +
              def idp_authenticate(email, password)
         | 
| 57 | 
            +
                true
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              def idp_make_saml_response(user)
         | 
| 61 | 
            +
                encode_SAMLResponse("you@example.com")
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
            end
         | 
| 65 | 
            +
            ```
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            Keys and Secrets
         | 
| 68 | 
            +
            ----------------
         | 
| 69 | 
            +
             | 
| 70 | 
            +
            To generate the SAML Response it uses a default X.509 certificate and secret key... which isn't so secret. You can find them in `SamlIdp::Default`. The X.509 certificate is valid until year 2032. You can customize these values by setting the properties `x509_certificate` and `secret_key` using a `prepend_before_action` callback within the current request context or setting them globally via the `SamlIdp.config.x509_certificate` and `SamlIdp.config.secret_key` properties.
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            The fingerprint to use, if you use the default X.509 certificate of this gem, is:
         | 
| 73 | 
            +
             | 
| 74 | 
            +
            ```
         | 
| 75 | 
            +
            9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D
         | 
| 76 | 
            +
            ```
         | 
| 77 | 
            +
             | 
| 78 | 
            +
             | 
| 79 | 
            +
            Service Providers
         | 
| 80 | 
            +
            -----------------
         | 
| 81 | 
            +
             | 
| 82 | 
            +
            To act as a Service Provider which generates SAML Requests and can react to SAML Responses use the excellent [ruby-saml](https://github.com/onelogin/ruby-saml) gem.
         | 
| 83 | 
            +
             | 
| 84 | 
            +
             | 
| 85 | 
            +
            Contributors
         | 
| 86 | 
            +
            -------------
         | 
| 87 | 
            +
             | 
| 88 | 
            +
            This is an updated version of the stub SAML IDP originally published by [Lawrence Pit](https://github.com/lawrencepit).  The updated gem would not have been possible without his contribution.
         | 
| 89 | 
            +
             | 
| 90 | 
            +
            Copyright
         | 
| 91 | 
            +
            -----------
         | 
| 92 | 
            +
             | 
| 93 | 
            +
            Copyright (c) 2022 Peter M. Goldstein See MIT-LICENSE for details.
         | 
| 94 | 
            +
            Copyright (c) 2012 Lawrence Pit. See MIT-LICENSE for details.
         | 
| @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module StubSamlIdp
         | 
| 4 | 
            +
              class IdpController < ActionController::Base
         | 
| 5 | 
            +
                include StubSamlIdp::Controller
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                protect_from_forgery
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                before_action :validate_saml_request
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def new
         | 
| 12 | 
            +
                  render template: 'stub_saml_idp/idp/new'
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def create
         | 
| 16 | 
            +
                  render_no_params && return unless auth_params?
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  if person.nil?
         | 
| 19 | 
            +
                    render_auth_failure
         | 
| 20 | 
            +
                  else
         | 
| 21 | 
            +
                    @saml_response = idp_make_saml_response(person)
         | 
| 22 | 
            +
                    render template: 'stub_saml_idp/idp/saml_post', layout: false
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def render_no_params
         | 
| 27 | 
            +
                  render template: 'stub_saml_idp/idp/new'
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def render_auth_failure
         | 
| 31 | 
            +
                  @saml_idp_fail_msg = 'Incorrect email or password.'
         | 
| 32 | 
            +
                  render template: 'stub_saml_idp/idp/new'
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def person
         | 
| 36 | 
            +
                  return nil unless auth_params?
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  @person ||= idp_authenticate(params[:email], params[:password])
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def auth_params?
         | 
| 42 | 
            +
                  !(params[:email].blank? || params[:password].blank?)
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                protected
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def idp_authenticate(_email, _password)
         | 
| 48 | 
            +
                  raise 'Not implemented'
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def idp_make_saml_response(_person)
         | 
| 52 | 
            +
                  raise 'Not implemented'
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            <% if @saml_idp_fail_msg %>
         | 
| 2 | 
            +
              <div id="saml_idp_fail_msg" class="flash error"><%= @saml_idp_fail_msg %></div>
         | 
| 3 | 
            +
            <% end %>
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            <%= form_tag do %>
         | 
| 6 | 
            +
              <%= hidden_field_tag("SAMLRequest", params[:SAMLRequest]) %>
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              <p>
         | 
| 9 | 
            +
                <%= label_tag :email %>
         | 
| 10 | 
            +
                <%= email_field_tag :email, params[:email], :autocapitalize => "off", :autocorrect => "off", :autofocus => "autofocus", :spellcheck => "false", :size => 30, :class => "email_pwd txt" %>
         | 
| 11 | 
            +
              </p>
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              <p>
         | 
| 14 | 
            +
                <%= label_tag :password %>
         | 
| 15 | 
            +
                <%= password_field_tag :password, params[:password], :autocapitalize => "off", :autocorrect => "off", :spellcheck => "false", :size => 30, :class => "email_pwd txt" %>
         | 
| 16 | 
            +
              </p>
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              <p>
         | 
| 19 | 
            +
                <%= submit_tag "Sign in", :class => "button big blueish" %>
         | 
| 20 | 
            +
              </p>
         | 
| 21 | 
            +
            <% end %>
         | 
| @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            <!DOCTYPE html>
         | 
| 2 | 
            +
            <html>
         | 
| 3 | 
            +
            <head>
         | 
| 4 | 
            +
            <meta charset="utf-8">
         | 
| 5 | 
            +
            <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
         | 
| 6 | 
            +
            </head>
         | 
| 7 | 
            +
            <body onload="document.forms[0].submit();" style="visibility:hidden;">
         | 
| 8 | 
            +
            <%= form_tag(@saml_acs_url) do %>
         | 
| 9 | 
            +
              <%= hidden_field_tag("SAMLResponse", @saml_response) %>
         | 
| 10 | 
            +
              <%= submit_tag "Submit" %>
         | 
| 11 | 
            +
            <% end %>
         | 
| 12 | 
            +
            </body>
         | 
| 13 | 
            +
            </html>
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module StubSamlIdp
         | 
| 4 | 
            +
              class Configurator
         | 
| 5 | 
            +
                attr_accessor :x509_certificate, :secret_key, :algorithm, :expires_in
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(config_file = nil)
         | 
| 8 | 
            +
                  self.x509_certificate = Default::X509_CERTIFICATE
         | 
| 9 | 
            +
                  self.secret_key = Default::SECRET_KEY
         | 
| 10 | 
            +
                  self.algorithm = :sha1
         | 
| 11 | 
            +
                  self.expires_in = nil
         | 
| 12 | 
            +
                  instance_eval(File.read(config_file), config_file) if config_file
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
            end
         | 
| @@ -0,0 +1,109 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'openssl'
         | 
| 4 | 
            +
            require 'base64'
         | 
| 5 | 
            +
            require 'time'
         | 
| 6 | 
            +
            require 'securerandom'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module StubSamlIdp
         | 
| 9 | 
            +
              module Controller
         | 
| 10 | 
            +
                attr_accessor :saml_acs_url
         | 
| 11 | 
            +
                attr_writer :expires_in, :secret_key, :x509_certificate
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def x509_certificate
         | 
| 14 | 
            +
                  @x509_certificate ||= StubSamlIdp.config.x509_certificate
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def secret_key
         | 
| 18 | 
            +
                  @secret_key ||= StubSamlIdp.config.secret_key
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def algorithm
         | 
| 22 | 
            +
                  @algorithm ||= algorithm_from_symbol(StubSamlIdp.config.algorithm)
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def algorithm=(alg)
         | 
| 26 | 
            +
                  @algorithm = if alg.is_a?(Symbol)
         | 
| 27 | 
            +
                                 algorithm_from_symbol(alg)
         | 
| 28 | 
            +
                               else
         | 
| 29 | 
            +
                                 alg
         | 
| 30 | 
            +
                               end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def algorithm_from_symbol(alg_sym = nil)
         | 
| 34 | 
            +
                  case alg_sym
         | 
| 35 | 
            +
                  when :sha256 then OpenSSL::Digest::SHA256
         | 
| 36 | 
            +
                  when :sha384 then OpenSSL::Digest::SHA384
         | 
| 37 | 
            +
                  when :sha512 then OpenSSL::Digest::SHA512
         | 
| 38 | 
            +
                  else
         | 
| 39 | 
            +
                    OpenSSL::Digest::SHA1
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def algorithm_name
         | 
| 44 | 
            +
                  algorithm.to_s.split('::').last.downcase
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def expires_in
         | 
| 48 | 
            +
                  return @expires_in if defined?(@expires_in)
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  @expires_in ||= StubSamlIdp.config.expires_in
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                protected
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def validate_saml_request(saml_request = params[:SAMLRequest])
         | 
| 56 | 
            +
                  decode_SAMLRequest(saml_request)
         | 
| 57 | 
            +
                rescue StandardError
         | 
| 58 | 
            +
                  false
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def decode_SAMLRequest(saml_request)
         | 
| 62 | 
            +
                  zstream = Zlib::Inflate.new(-Zlib::MAX_WBITS)
         | 
| 63 | 
            +
                  @saml_request = zstream.inflate(Base64.decode64(saml_request))
         | 
| 64 | 
            +
                  zstream.finish
         | 
| 65 | 
            +
                  zstream.close
         | 
| 66 | 
            +
                  @saml_request_id = @saml_request[/ID=['"](.+?)['"]/, 1]
         | 
| 67 | 
            +
                  @saml_acs_url = @saml_request[/AssertionConsumerServiceURL=['"](.+?)['"]/, 1]
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def encode_SAMLResponse(name_id, opts = {})
         | 
| 71 | 
            +
                  now = Time.now.utc
         | 
| 72 | 
            +
                  response_id = SecureRandom.uuid
         | 
| 73 | 
            +
                  reference_id = SecureRandom.uuid
         | 
| 74 | 
            +
                  audience_uri = opts[:audience_uri] || saml_acs_url[%r{^(.*?//.*?/)}, 1]
         | 
| 75 | 
            +
                  issuer_uri = opts[:issuer_uri] || (defined?(request) && request.url) || 'http://example.com'
         | 
| 76 | 
            +
                  attributes_statement = attributes(opts[:attributes_provider], name_id)
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  session_expiration = ''
         | 
| 79 | 
            +
                  session_expiration = %( SessionNotOnOrAfter="#{(now + expires_in).iso8601}") if expires_in
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  assertion = %(<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_#{reference_id}" IssueInstant="#{now.iso8601}" Version="2.0"><saml:Issuer Format="urn:oasis:names:SAML:2.0:nameid-format:entity">#{issuer_uri}</saml:Issuer><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">#{name_id}</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData#{@saml_request_id ? %( InResponseTo="#{@saml_request_id}") : ''} NotOnOrAfter="#{(now + (3 * 60)).iso8601}" Recipient="#{@saml_acs_url}"></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="#{(now - 5).iso8601}" NotOnOrAfter="#{(now + (60 * 60)).iso8601}"><saml:AudienceRestriction><saml:Audience>#{audience_uri}</saml:Audience></saml:AudienceRestriction></saml:Conditions>#{attributes_statement}<saml:AuthnStatement AuthnInstant="#{now.iso8601}" SessionIndex="_#{reference_id}"#{session_expiration}><saml:AuthnContext><saml:AuthnContextClassRef>urn:federation:authentication:windows</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement></saml:Assertion>)
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  digest_value = Base64.encode64(algorithm.digest(assertion)).gsub(/\n/, '')
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  signed_info = %(<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-#{algorithm_name}"></ds:SignatureMethod><ds:Reference URI="#_#{reference_id}"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig##{algorithm_name}"></ds:DigestMethod><ds:DigestValue>#{digest_value}</ds:DigestValue></ds:Reference></ds:SignedInfo>)
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  signature_value = sign(signed_info).gsub(/\n/, '')
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  signature = %(<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">#{signed_info}<ds:SignatureValue>#{signature_value}</ds:SignatureValue><KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><ds:X509Data><ds:X509Certificate>#{x509_certificate}</ds:X509Certificate></ds:X509Data></KeyInfo></ds:Signature>)
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  assertion_and_signature = assertion.sub(/Issuer><saml:Subject/, "Issuer>#{signature}<saml:Subject")
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  xml = %(<samlp:Response ID="_#{response_id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{@saml_acs_url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified"#{@saml_request_id ? %( InResponseTo="#{@saml_request_id}") : ''} xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"><saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">#{issuer_uri}</saml:Issuer><samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /></samlp:Status>#{assertion_and_signature}</samlp:Response>)
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  Base64.encode64(xml)
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                private
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def sign(data)
         | 
| 101 | 
            +
                  key = OpenSSL::PKey::RSA.new(secret_key)
         | 
| 102 | 
            +
                  Base64.encode64(key.sign(algorithm.new, data))
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def attributes(provider, name_id)
         | 
| 106 | 
            +
                  provider || %(<saml:AttributeStatement><saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"><saml:AttributeValue>#{name_id}</saml:AttributeValue></saml:Attribute></saml:AttributeStatement>)
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
              end
         | 
| 109 | 
            +
            end
         | 
| @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module StubSamlIdp
         | 
| 4 | 
            +
              module Default
         | 
| 5 | 
            +
                NAME_ID_FORMAT = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                X509_CERTIFICATE = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF5T0RBeU1qSXlPRm9YRFRNeU1EUXkKTXpBeU1qSXlPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXVCeXdQTmxDMUZvcEdMWWZGOTZTb3RpSwo4Tmo2L25XMDg0TzRvbVJNaWZ6eTd4OTU1UkxFeTY3M3EyYWlKTkIzTHZFNlh2a3Q5Y0d0eHROb09YdzFnMlV2CkhLcGxkUWJyNmJPRWpMTmVETlc3ajBvYitKclJ2QVVPSzlDUmdkeXc1TUM2bHdxVlFRNUMxRG5hVC8yZlNCRmoKYXNCRlRSMjRkRXBmVHk4SGZLRUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJRTkJHbW10M3l0S3BjSmFCYVlOYm55VTJ4a2F6QVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVEUVJwcHJkOHJTcVhDV2dXbURXNThsTnNaR3VoZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FBRQpjVlVQQlg3dVptenFaSmZ5K3RVUE9UNUltTlFqOFZFMmxlcmhuRmpuR1BIbUhJcWhwemdud0hRdWpKZnMvYTMwCjlXbTVxd2NDYUMxZU81Y1dqY0cweDNPamRsbHNnWURhdGw1R0F1bXRCeDhKM05oV1JxTlVnaXRDSWtRbHhISXcKVWZnUWFDdXNoWWdEREw1WWJJUWErK2VnQ2dwSVorVDBEajVvUmV3Ly9BPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo='
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                FINGERPRINT = '9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D'
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                SECRET_KEY = <<~PEM
         | 
| 12 | 
            +
                  -----BEGIN RSA PRIVATE KEY-----
         | 
| 13 | 
            +
                  MIICXAIBAAKBgQC4HLA82ULUWikYth8X3pKi2Irw2Pr+dbTzg7iiZEyJ/PLvH3nl
         | 
| 14 | 
            +
                  EsTLrverZqIk0Hcu8Tpe+S31wa3G02g5fDWDZS8cqmV1Buvps4SMs14M1buPShv4
         | 
| 15 | 
            +
                  mtG8BQ4r0JGB3LDkwLqXCpVBDkLUOdpP/Z9IEWNqwEVNHbh0Sl9PLwd8oQIDAQAB
         | 
| 16 | 
            +
                  AoGAQmUGIUtwUEgbXe//kooPc26H3IdDLJSiJtcvtFBbUb/Ik/dT7AoysgltA4DF
         | 
| 17 | 
            +
                  pGURNfqERE+0BVZNJtCCW4ixew4uEhk1XowYXHCzjkzyYoFuT9v5SP4cu4q3t1kK
         | 
| 18 | 
            +
                  51JF237F0eCY3qC3k96CzPGG67bwOu9EeXAu4ka/plLdsAECQQDkg0uhR/vsJffx
         | 
| 19 | 
            +
                  tiWxcDRNFoZpCpzpdWfQBnHBzj9ZC0xrdVilxBgBpupCljO2Scy4MeiY4S1Mleel
         | 
| 20 | 
            +
                  CWRqh7RBAkEAzkIjUnllEkr5sjVb7pNy+e/eakuDxvZck0Z8X3USUki/Nm3E/GPP
         | 
| 21 | 
            +
                  c+CwmXR4QlpMpJr3/Prf1j59l/LAK9AwYQJBAL9eRSQYCJ3HXlGKXR6v/NziFEY7
         | 
| 22 | 
            +
                  oRTSQdIw02ueseZ8U89aQpbwFbqsclq5Ny1duJg5E7WUPj94+rl3mCSu6QECQBVh
         | 
| 23 | 
            +
                  0duY7htpXl1VHsSq0H6MmVgXn/+eRpaV9grHTjDtjbUMyCEKD9WJc4VVB6qJRezC
         | 
| 24 | 
            +
                  i/bT4ySIsehwp+9i08ECQEH03lCpHpbwiWH4sD25l/z3g2jCbIZ+RTV6yHIz7Coh
         | 
| 25 | 
            +
                  gAbBqA04wh64JhhfG69oTBwqwj3imlWF8+jDzV9RNNw=
         | 
| 26 | 
            +
                  -----END RSA PRIVATE KEY-----
         | 
| 27 | 
            +
                PEM
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module StubSamlIdp
         | 
| 4 | 
            +
              require 'stub_saml_idp/configurator'
         | 
| 5 | 
            +
              require 'stub_saml_idp/controller'
         | 
| 6 | 
            +
              require 'stub_saml_idp/default'
         | 
| 7 | 
            +
              require 'stub_saml_idp/version'
         | 
| 8 | 
            +
              require 'stub_saml_idp/engine' if defined?(::Rails) && Rails::VERSION::MAJOR > 2
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def self.config=(config)
         | 
| 11 | 
            +
                @config = config
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def self.config
         | 
| 15 | 
            +
                @config ||= StubSamlIdp::Configurator.new
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative '../spec_helper'
         | 
| 4 | 
            +
            require_relative '../support/rails_app'
         | 
| 5 | 
            +
            require 'rails'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            require 'selenium-webdriver'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            Capybara.register_driver :chrome do |app|
         | 
| 10 | 
            +
              options = Selenium::WebDriver::Chrome::Options.new
         | 
| 11 | 
            +
              options.add_argument('--headless')
         | 
| 12 | 
            +
              options.add_argument('--allow-insecure-localhost')
         | 
| 13 | 
            +
              options.add_argument('--ignore-certificate-errors')
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              Capybara::Selenium::Driver.new(
         | 
| 16 | 
            +
                app,
         | 
| 17 | 
            +
                browser: :chrome,
         | 
| 18 | 
            +
                capabilities: [options]
         | 
| 19 | 
            +
              )
         | 
| 20 | 
            +
            end
         | 
| 21 | 
            +
            Capybara.default_driver = :chrome
         | 
| 22 | 
            +
            Capybara.server = :webrick
         | 
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'acceptance_helper'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            describe 'IdpController', type: :feature do
         | 
| 6 | 
            +
              let(:idp_port) { 8009 }
         | 
| 7 | 
            +
              let(:sp_port) { 8022 }
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              let(:idp_pid) do
         | 
| 10 | 
            +
                create_app('idp')
         | 
| 11 | 
            +
                start_app('idp', idp_port)
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              let(:sp_pid) do
         | 
| 15 | 
            +
                create_app('sp')
         | 
| 16 | 
            +
                start_app('sp', sp_port)
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              before do
         | 
| 20 | 
            +
                idp_pid
         | 
| 21 | 
            +
                sp_pid
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              after do
         | 
| 25 | 
            +
                stop_app('sp', sp_pid)
         | 
| 26 | 
            +
                stop_app('idp', idp_pid)
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              it 'Login via default signup page' do
         | 
| 30 | 
            +
                saml_request = make_saml_request("http://localhost:#{sp_port}/saml/consume")
         | 
| 31 | 
            +
                visit "http://localhost:#{idp_port}/saml/auth?SAMLRequest=#{CGI.escape(saml_request)}"
         | 
| 32 | 
            +
                fill_in 'Email', with: 'brad.copa@example.com'
         | 
| 33 | 
            +
                fill_in 'Password', with: 'okidoki'
         | 
| 34 | 
            +
                click_button 'Sign in'
         | 
| 35 | 
            +
                expect(current_url).to eq("http://localhost:#{sp_port}/saml/consume")
         | 
| 36 | 
            +
                expect(page).to have_content('brad.copa@example.com')
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
            end
         | 
    
        data/spec/spec_helper.rb
    ADDED
    
    | @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            warn("Running Specs under Ruby Version #{RUBY_VERSION}")
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            require 'rspec'
         | 
| 6 | 
            +
            require 'capybara/rspec'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            require 'ruby-saml'
         | 
| 9 | 
            +
            require 'stub_saml_idp'
         | 
| 10 | 
            +
            require 'support/saml_request_macros'
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            Capybara.default_host = 'https://app.example.com'
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            RSpec.configure do |config|
         | 
| 15 | 
            +
              config.mock_with :rspec
         | 
| 16 | 
            +
              config.include SamlRequestMacros
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,74 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'spec_helper'
         | 
| 4 | 
            +
            require 'timecop'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            describe StubSamlIdp::Controller do
         | 
| 7 | 
            +
              include StubSamlIdp::Controller
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              def params
         | 
| 10 | 
            +
                @params ||= {}
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              it 'finds the SAML ACS URL' do
         | 
| 14 | 
            +
                requested_saml_acs_url = 'https://example.com/saml/consume'
         | 
| 15 | 
            +
                params[:SAMLRequest] = make_saml_request(requested_saml_acs_url)
         | 
| 16 | 
            +
                validate_saml_request
         | 
| 17 | 
            +
                expect(saml_acs_url).to eq(requested_saml_acs_url)
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              context 'SAML Responses' do
         | 
| 21 | 
            +
                before do
         | 
| 22 | 
            +
                  params[:SAMLRequest] = make_saml_request
         | 
| 23 | 
            +
                  validate_saml_request
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                it 'creates a SAML Response' do
         | 
| 27 | 
            +
                  saml_response = encode_SAMLResponse('foo@example.com')
         | 
| 28 | 
            +
                  response = OneLogin::RubySaml::Response.new(saml_response)
         | 
| 29 | 
            +
                  expect(response.name_id).to eq('foo@example.com')
         | 
| 30 | 
            +
                  expect(response.issuers).to eq(['http://example.com'])
         | 
| 31 | 
            +
                  response.settings = saml_settings
         | 
| 32 | 
            +
                  expect(response.is_valid?).to be_truthy
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                it 'handles custom attribute objects' do
         | 
| 36 | 
            +
                  provider = double(to_s: %(<saml:AttributeStatement><saml:Attribute Name="organization"><saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">Organization name</saml:AttributeValue></saml:Attribute></saml:AttributeStatement>))
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  default_attributes = %(<saml:AttributeStatement><saml:Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"><saml:AttributeValue>foo@example.com</saml:AttributeValue></saml:Attribute></saml:AttributeStatement>)
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  saml_response = encode_SAMLResponse('foo@example.com', { attributes_provider: provider })
         | 
| 41 | 
            +
                  response = OneLogin::RubySaml::Response.new(saml_response)
         | 
| 42 | 
            +
                  expect(response.response).to include provider.to_s
         | 
| 43 | 
            +
                  expect(response.response).not_to include default_attributes
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                %i[sha1 sha256 sha384 sha512].each do |algorithm_name|
         | 
| 47 | 
            +
                  it "creates a SAML Response using the #{algorithm_name} algorithm" do
         | 
| 48 | 
            +
                    self.algorithm = algorithm_name
         | 
| 49 | 
            +
                    saml_response = encode_SAMLResponse('foo@example.com')
         | 
| 50 | 
            +
                    response = OneLogin::RubySaml::Response.new(saml_response)
         | 
| 51 | 
            +
                    expect(response.name_id).to eq('foo@example.com')
         | 
| 52 | 
            +
                    expect(response.issuers).to eq(['http://example.com'])
         | 
| 53 | 
            +
                    response.settings = saml_settings
         | 
| 54 | 
            +
                    expect(response.is_valid?).to be true
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                it 'does not set SessionNotOnOrAfter when expires_in is nil' do
         | 
| 59 | 
            +
                  Timecop.freeze
         | 
| 60 | 
            +
                  self.expires_in = nil
         | 
| 61 | 
            +
                  saml_response = encode_SAMLResponse('foo@example.com')
         | 
| 62 | 
            +
                  response = OneLogin::RubySaml::Response.new(saml_response)
         | 
| 63 | 
            +
                  expect(response.session_expires_at).to be_nil
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                it 'sets SessionNotOnOrAfter when expires_in is specified' do
         | 
| 67 | 
            +
                  self.expires_in = 86_400 # 1 day
         | 
| 68 | 
            +
                  now = Time.now.utc
         | 
| 69 | 
            +
                  saml_response = Timecop.freeze(now) { encode_SAMLResponse('foo@example.com') }
         | 
| 70 | 
            +
                  response = OneLogin::RubySaml::Response.new(saml_response)
         | 
| 71 | 
            +
                  expect(response.session_expires_at).to eq(Time.at(now.to_i + 86_400))
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
            end
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Set up a SAML IdP
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            gem 'stub_saml_idp', path: File.expand_path('../..', __dir__)
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.1')
         | 
| 8 | 
            +
              gem 'net-smtp', require: false
         | 
| 9 | 
            +
              gem 'net-imap', require: false
         | 
| 10 | 
            +
              gem 'net-pop', require: false
         | 
| 11 | 
            +
            end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            route "get '/saml/auth' => 'saml_idp#new'"
         | 
| 14 | 
            +
            route "post '/saml/auth' => 'saml_idp#create'"
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            file 'app/controllers/saml_idp_controller.rb', <<-CODE
         | 
| 17 | 
            +
              # frozen_string_literal: true
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              class SamlIdpController < StubSamlIdp::IdpController
         | 
| 20 | 
            +
                def idp_authenticate(email, _password)
         | 
| 21 | 
            +
                  { email: email }
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def idp_make_saml_response(user)
         | 
| 25 | 
            +
                  encode_SAMLResponse(user[:email])
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
            CODE
         | 
| @@ -0,0 +1,139 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'English'
         | 
| 4 | 
            +
            require 'open3'
         | 
| 5 | 
            +
            require 'socket'
         | 
| 6 | 
            +
            require 'tempfile'
         | 
| 7 | 
            +
            require 'timeout'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            APP_READY_TIMEOUT = 30
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            def sh!(cmd)
         | 
| 12 | 
            +
              raise "[#{cmd}] failed with exit code #{$CHILD_STATUS.exitstatus}" unless system(cmd)
         | 
| 13 | 
            +
            end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            def app_ready?(pid, port)
         | 
| 16 | 
            +
              Process.getpgid(pid) && port_open?(port)
         | 
| 17 | 
            +
            rescue Errno::ESRCH
         | 
| 18 | 
            +
              false
         | 
| 19 | 
            +
            end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            def create_app(name = 'idp', env = {})
         | 
| 22 | 
            +
              puts "[#{name}] Creating Rails app"
         | 
| 23 | 
            +
              rails_new_options = %w[-A -C -G -J -M -S -T --skip-keeps --skip-spring --skip-listen --skip-bootsnap --skip-action-mailbox --skip-action-text --skip-active-job --skip-active-storage --skip-hotwire --skip-jbuilder]
         | 
| 24 | 
            +
              env.merge!('RUBY_SAML_VERSION' => OneLogin::RubySaml::VERSION)
         | 
| 25 | 
            +
              Dir.chdir(working_directory) do
         | 
| 26 | 
            +
                FileUtils.rm_rf(name)
         | 
| 27 | 
            +
                puts("[#{working_directory}] rails _#{Rails.version}_ new #{name} #{rails_new_options.join(' ')} -m #{File.expand_path(
         | 
| 28 | 
            +
                  "../#{name}_template.rb", __FILE__
         | 
| 29 | 
            +
                )}")
         | 
| 30 | 
            +
                system(env, 'rails', "_#{Rails.version}_", 'new', name, *rails_new_options, '-m',
         | 
| 31 | 
            +
                       File.expand_path("../#{name}_template.rb", __FILE__))
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            def start_app(name, port, _options = {})
         | 
| 36 | 
            +
              puts "[#{name}] Starting Rails app"
         | 
| 37 | 
            +
              pid = nil
         | 
| 38 | 
            +
              app_bundle_install(name)
         | 
| 39 | 
            +
             | 
| 40 | 
            +
              with_clean_env do
         | 
| 41 | 
            +
                Dir.chdir(app_dir(name)) do
         | 
| 42 | 
            +
                  pid = Process.spawn(app_env(name), "bundle exec rails server -p #{port} -e production", chdir: app_dir(name),
         | 
| 43 | 
            +
                                                                                                          out: "log/#{name}.log", err: "log/#{name}.err.log")
         | 
| 44 | 
            +
                  begin
         | 
| 45 | 
            +
                    Timeout.timeout(APP_READY_TIMEOUT) do
         | 
| 46 | 
            +
                      sleep 1 until app_ready?(pid, port)
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                    raise "#{name} failed after starting" unless app_ready?(pid, port)
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    puts "[#{name}] Launched #{name} on port #{port} (pid #{pid})..."
         | 
| 51 | 
            +
                  rescue Timeout::Error
         | 
| 52 | 
            +
                    raise "#{name} failed to start"
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
              end
         | 
| 56 | 
            +
              pid
         | 
| 57 | 
            +
            rescue RuntimeError => e
         | 
| 58 | 
            +
              warn "=== #{name}"
         | 
| 59 | 
            +
              Dir.chdir(app_dir(name)) do
         | 
| 60 | 
            +
                warn File.read("log/#{name}.log") if File.exist?("log/#{name}.log")
         | 
| 61 | 
            +
                warn File.read("log/#{name}.err.log") if File.exist?("log/#{name}.err.log")
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
              raise e
         | 
| 64 | 
            +
            end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
            def stop_app(name, pid)
         | 
| 67 | 
            +
              if pid
         | 
| 68 | 
            +
                Process.kill(:INT, pid)
         | 
| 69 | 
            +
                Process.wait(pid)
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
              Dir.chdir(app_dir(name)) do
         | 
| 72 | 
            +
                if File.exist?("log/#{name}.log")
         | 
| 73 | 
            +
                  puts "=== [#{name}] stdout"
         | 
| 74 | 
            +
                  puts File.read("log/#{name}.log")
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
                if File.exist?("log/#{name}.err.log")
         | 
| 77 | 
            +
                  warn "=== [#{name}] stderr"
         | 
| 78 | 
            +
                  warn File.read("log/#{name}.err.log")
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
                if File.exist?('log/production.log')
         | 
| 81 | 
            +
                  puts "=== [#{name}] Rails logs"
         | 
| 82 | 
            +
                  puts File.read('log/production.log')
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
              end
         | 
| 85 | 
            +
            end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
            def port_open?(port)
         | 
| 88 | 
            +
              Timeout.timeout(1) do
         | 
| 89 | 
            +
                begin
         | 
| 90 | 
            +
                  s = TCPSocket.new('localhost', port)
         | 
| 91 | 
            +
                  s.close
         | 
| 92 | 
            +
                  return true
         | 
| 93 | 
            +
                rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::EADDRNOTAVAIL
         | 
| 94 | 
            +
                  # try 127.0.0.1
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
                begin
         | 
| 97 | 
            +
                  s = TCPSocket.new('127.0.0.1', port)
         | 
| 98 | 
            +
                  s.close
         | 
| 99 | 
            +
                  return true
         | 
| 100 | 
            +
                rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
         | 
| 101 | 
            +
                  return false
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
              end
         | 
| 104 | 
            +
            rescue Timeout::Error
         | 
| 105 | 
            +
              false
         | 
| 106 | 
            +
            end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
            def app_bundle_install(name)
         | 
| 109 | 
            +
              with_clean_env do
         | 
| 110 | 
            +
                Open3.popen3(app_env(name), 'bundle install', chdir: app_dir(name)) do |stdin, stdout, stderr, thread|
         | 
| 111 | 
            +
                  stdin.close
         | 
| 112 | 
            +
                  exit_status = thread.value
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                  puts stdout.read
         | 
| 115 | 
            +
                  warn stderr.read
         | 
| 116 | 
            +
                  raise 'bundle install failed' unless exit_status.success?
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
              end
         | 
| 119 | 
            +
            end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
            def app_dir(name)
         | 
| 122 | 
            +
              File.join(working_directory, name)
         | 
| 123 | 
            +
            end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
            def app_env(name)
         | 
| 126 | 
            +
              { 'BUNDLE_GEMFILE' => File.join(app_dir(name), 'Gemfile'), 'RAILS_ENV' => 'production' }
         | 
| 127 | 
            +
            end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
            def working_directory
         | 
| 130 | 
            +
              $working_directory ||= Dir.mktmpdir('idp_test')
         | 
| 131 | 
            +
            end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
            def with_clean_env(&blk)
         | 
| 134 | 
            +
              if Bundler.respond_to?(:with_original_env)
         | 
| 135 | 
            +
                Bundler.with_original_env(&blk)
         | 
| 136 | 
            +
              else
         | 
| 137 | 
            +
                Bundler.with_clean_env(&blk)
         | 
| 138 | 
            +
              end
         | 
| 139 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SamlRequestMacros
         | 
| 4 | 
            +
              def make_saml_request(requested_saml_acs_url = 'https://foo.example.com/saml/consume')
         | 
| 5 | 
            +
                auth_request = ::OneLogin::RubySaml::Authrequest.new
         | 
| 6 | 
            +
                auth_url = auth_request.create(saml_settings(saml_acs_url: requested_saml_acs_url))
         | 
| 7 | 
            +
                CGI.unescape(auth_url.split('=').last)
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def saml_settings(options = {})
         | 
| 11 | 
            +
                settings = ::OneLogin::RubySaml::Settings.new
         | 
| 12 | 
            +
                settings.assertion_consumer_service_url = options[:saml_acs_url] || 'https://foo.example.com/saml/consume'
         | 
| 13 | 
            +
                settings.issuer = options[:issuer] || 'https://foo.example.com/'
         | 
| 14 | 
            +
                settings.idp_sso_target_url = options[:idp_sso_target_url] || 'http://idp.com/saml/idp'
         | 
| 15 | 
            +
                settings.idp_cert_fingerprint = StubSamlIdp::Default::FINGERPRINT
         | 
| 16 | 
            +
                settings.name_identifier_format = StubSamlIdp::Default::NAME_ID_FORMAT
         | 
| 17 | 
            +
                settings
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Set up a SAML SP
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            gem 'ruby-saml'
         | 
| 6 | 
            +
            gem 'stub_saml_idp', path: File.expand_path('../..', __dir__)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.1')
         | 
| 9 | 
            +
              gem 'net-smtp', require: false
         | 
| 10 | 
            +
              gem 'net-imap', require: false
         | 
| 11 | 
            +
              gem 'net-pop', require: false
         | 
| 12 | 
            +
            end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            route "post '/saml/consume' => 'saml#consume'"
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            file 'app/controllers/saml_controller.rb', <<-CODE
         | 
| 17 | 
            +
              # frozen_string_literal: true
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              class SamlController < ApplicationController
         | 
| 20 | 
            +
                skip_before_action :verify_authenticity_token
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def consume
         | 
| 23 | 
            +
                  response = ::OneLogin::RubySaml::Response.new(params[:SAMLResponse])
         | 
| 24 | 
            +
                  render plain: response.name_id
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            CODE
         | 
| @@ -0,0 +1,36 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            $LOAD_PATH.push File.expand_path('lib', __dir__)
         | 
| 4 | 
            +
            require 'stub_saml_idp/version'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            Gem::Specification.new do |s|
         | 
| 7 | 
            +
              s.name = 'stub_saml_idp'
         | 
| 8 | 
            +
              s.version = StubSamlIdp::VERSION
         | 
| 9 | 
            +
              s.platform = Gem::Platform::RUBY
         | 
| 10 | 
            +
              s.authors = ['Peter M. Goldstein']
         | 
| 11 | 
            +
              s.email = 'peter.m.goldstein@gmail.com'
         | 
| 12 | 
            +
              s.homepage = 'http://github.com/petergoldstein/stub_saml_idp'
         | 
| 13 | 
            +
              s.summary = 'Stub SAML Identity Provider'
         | 
| 14 | 
            +
              s.description = 'Stub SAML IdP (Identity Provider) library'
         | 
| 15 | 
            +
              s.license = 'MIT'
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              s.required_ruby_version = '>= 2.5'
         | 
| 18 | 
            +
              s.metadata['rubygems_mfa_required'] = 'true'
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              s.files = Dir.glob('app/**/*') + Dir.glob('lib/**/*') + [
         | 
| 21 | 
            +
                'MIT-LICENSE',
         | 
| 22 | 
            +
                'README.md',
         | 
| 23 | 
            +
                'Gemfile',
         | 
| 24 | 
            +
                'stub_saml_idp.gemspec'
         | 
| 25 | 
            +
              ]
         | 
| 26 | 
            +
              s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
         | 
| 27 | 
            +
              s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
         | 
| 28 | 
            +
              s.require_paths = ['lib']
         | 
| 29 | 
            +
              s.rdoc_options = ['--charset=UTF-8']
         | 
| 30 | 
            +
              s.add_development_dependency('nokogiri')
         | 
| 31 | 
            +
              s.add_development_dependency('rails', '>= 5.2')
         | 
| 32 | 
            +
              s.add_development_dependency('rake')
         | 
| 33 | 
            +
              s.add_development_dependency('rspec', '~> 3.0')
         | 
| 34 | 
            +
              s.add_development_dependency('ruby-saml')
         | 
| 35 | 
            +
              s.add_development_dependency('timecop', '~> 0.9.0')
         | 
| 36 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,159 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: stub_saml_idp
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Peter M. Goldstein
         | 
| 8 | 
            +
            autorequire:
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2022-01-08 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: nokogiri
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - ">="
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: '0'
         | 
| 20 | 
            +
              type: :development
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - ">="
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: '0'
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: rails
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - ">="
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: '5.2'
         | 
| 34 | 
            +
              type: :development
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - ">="
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: '5.2'
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: rake
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - ">="
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '0'
         | 
| 48 | 
            +
              type: :development
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - ">="
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: rspec
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - "~>"
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: '3.0'
         | 
| 62 | 
            +
              type: :development
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - "~>"
         | 
| 67 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 68 | 
            +
                    version: '3.0'
         | 
| 69 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 70 | 
            +
              name: ruby-saml
         | 
| 71 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 72 | 
            +
                requirements:
         | 
| 73 | 
            +
                - - ">="
         | 
| 74 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 75 | 
            +
                    version: '0'
         | 
| 76 | 
            +
              type: :development
         | 
| 77 | 
            +
              prerelease: false
         | 
| 78 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 79 | 
            +
                requirements:
         | 
| 80 | 
            +
                - - ">="
         | 
| 81 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 82 | 
            +
                    version: '0'
         | 
| 83 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 84 | 
            +
              name: timecop
         | 
| 85 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 86 | 
            +
                requirements:
         | 
| 87 | 
            +
                - - "~>"
         | 
| 88 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 89 | 
            +
                    version: 0.9.0
         | 
| 90 | 
            +
              type: :development
         | 
| 91 | 
            +
              prerelease: false
         | 
| 92 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 93 | 
            +
                requirements:
         | 
| 94 | 
            +
                - - "~>"
         | 
| 95 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 96 | 
            +
                    version: 0.9.0
         | 
| 97 | 
            +
            description: Stub SAML IdP (Identity Provider) library
         | 
| 98 | 
            +
            email: peter.m.goldstein@gmail.com
         | 
| 99 | 
            +
            executables: []
         | 
| 100 | 
            +
            extensions: []
         | 
| 101 | 
            +
            extra_rdoc_files: []
         | 
| 102 | 
            +
            files:
         | 
| 103 | 
            +
            - Gemfile
         | 
| 104 | 
            +
            - MIT-LICENSE
         | 
| 105 | 
            +
            - README.md
         | 
| 106 | 
            +
            - app/controllers/stub_saml_idp/idp_controller.rb
         | 
| 107 | 
            +
            - app/views/stub_saml_idp/idp/new.html.erb
         | 
| 108 | 
            +
            - app/views/stub_saml_idp/idp/saml_post.html.erb
         | 
| 109 | 
            +
            - lib/stub_saml_idp.rb
         | 
| 110 | 
            +
            - lib/stub_saml_idp/configurator.rb
         | 
| 111 | 
            +
            - lib/stub_saml_idp/controller.rb
         | 
| 112 | 
            +
            - lib/stub_saml_idp/default.rb
         | 
| 113 | 
            +
            - lib/stub_saml_idp/engine.rb
         | 
| 114 | 
            +
            - lib/stub_saml_idp/version.rb
         | 
| 115 | 
            +
            - spec/acceptance/acceptance_helper.rb
         | 
| 116 | 
            +
            - spec/acceptance/idp_controller_spec.rb
         | 
| 117 | 
            +
            - spec/rails_helper.rb
         | 
| 118 | 
            +
            - spec/spec_helper.rb
         | 
| 119 | 
            +
            - spec/stub_saml_idp/controller_spec.rb
         | 
| 120 | 
            +
            - spec/support/idp_template.rb
         | 
| 121 | 
            +
            - spec/support/rails_app.rb
         | 
| 122 | 
            +
            - spec/support/saml_request_macros.rb
         | 
| 123 | 
            +
            - spec/support/sp_template.rb
         | 
| 124 | 
            +
            - stub_saml_idp.gemspec
         | 
| 125 | 
            +
            homepage: http://github.com/petergoldstein/stub_saml_idp
         | 
| 126 | 
            +
            licenses:
         | 
| 127 | 
            +
            - MIT
         | 
| 128 | 
            +
            metadata:
         | 
| 129 | 
            +
              rubygems_mfa_required: 'true'
         | 
| 130 | 
            +
            post_install_message:
         | 
| 131 | 
            +
            rdoc_options:
         | 
| 132 | 
            +
            - "--charset=UTF-8"
         | 
| 133 | 
            +
            require_paths:
         | 
| 134 | 
            +
            - lib
         | 
| 135 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 136 | 
            +
              requirements:
         | 
| 137 | 
            +
              - - ">="
         | 
| 138 | 
            +
                - !ruby/object:Gem::Version
         | 
| 139 | 
            +
                  version: '2.5'
         | 
| 140 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 141 | 
            +
              requirements:
         | 
| 142 | 
            +
              - - ">="
         | 
| 143 | 
            +
                - !ruby/object:Gem::Version
         | 
| 144 | 
            +
                  version: '0'
         | 
| 145 | 
            +
            requirements: []
         | 
| 146 | 
            +
            rubygems_version: 3.3.4
         | 
| 147 | 
            +
            signing_key:
         | 
| 148 | 
            +
            specification_version: 4
         | 
| 149 | 
            +
            summary: Stub SAML Identity Provider
         | 
| 150 | 
            +
            test_files:
         | 
| 151 | 
            +
            - spec/acceptance/acceptance_helper.rb
         | 
| 152 | 
            +
            - spec/acceptance/idp_controller_spec.rb
         | 
| 153 | 
            +
            - spec/rails_helper.rb
         | 
| 154 | 
            +
            - spec/spec_helper.rb
         | 
| 155 | 
            +
            - spec/stub_saml_idp/controller_spec.rb
         | 
| 156 | 
            +
            - spec/support/idp_template.rb
         | 
| 157 | 
            +
            - spec/support/rails_app.rb
         | 
| 158 | 
            +
            - spec/support/saml_request_macros.rb
         | 
| 159 | 
            +
            - spec/support/sp_template.rb
         |