fake_idp 1.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b50b9e2c4cd4e172edc611187bd17f3dac879078abe55d58eb8e6ecab18c9dba
4
+ data.tar.gz: 2339286b95f50fa05dc64cb3dcb2c49b2ee95927c79562a3b8484b95eb1a7ee1
5
+ SHA512:
6
+ metadata.gz: c3956511324619f761439951aa85bd40e0e107bf1b72047a1b9f03b7aa651c3040a7aa277e384e4349cd4908e7b7445b08bdf1e134d7704f78ba78a19d487714
7
+ data.tar.gz: 2b79b09ada2b3fca8c27e4c5dd1f3a35a303ff40bc30d37e740242a08f154282a1d1f8877daa74251035c456eb9a9b7ad5f4af03b5cc9d6014c5954893bacb41
@@ -0,0 +1,7 @@
1
+ SSO_UID=123456
2
+ CALLBACK_URL="http://localhost.dev:3000/auth/saml/devidp/callback"
3
+ USERNAME=bobthessouser
4
+ FIRST_NAME=Bob
5
+ LAST_NAME=TheSsoUser
6
+ EMAIL=bobthessouser@example.com
7
+ NAME_ID=bobthessouser@example.com
@@ -0,0 +1,2 @@
1
+ .idea
2
+ .env
@@ -0,0 +1 @@
1
+ 2.6.5
@@ -0,0 +1,19 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.7
5
+ - 2.6
6
+ - 2.5
7
+ - 2.4
8
+ - truffleruby
9
+ - jruby
10
+ sudo: false
11
+ gemfile: Gemfile
12
+ script: bundle exec rspec spec
13
+ before_install:
14
+ - gem update --system 3.1.2 --no-document
15
+ - gem install bundler -v 2.1.2 --no-document
16
+ jobs:
17
+ allow_failures:
18
+ - rvm: truffleruby
19
+ - rvm: jruby
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) do |repo_name|
4
+ repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
5
+ "https://github.com/#{repo_name}.git"
6
+ end
7
+
8
+ # overrides gem source in gemspec
9
+ # needed to enable attributes statement in SAMLRequest
10
+ gem 'ruby-saml-idp', github: 'lawrencepit/ruby-saml-idp'
11
+
12
+ gemspec
@@ -0,0 +1,102 @@
1
+ GIT
2
+ remote: https://github.com/lawrencepit/ruby-saml-idp.git
3
+ revision: ae53d76af08711affaeb182e7a877676ab554818
4
+ specs:
5
+ ruby-saml-idp (0.3.2)
6
+ uuid
7
+
8
+ PATH
9
+ remote: .
10
+ specs:
11
+ fake_idp (1.0.0)
12
+ activesupport (~> 5.2.4.3)
13
+ builder (>= 3.2.2)
14
+ nokogiri (>= 1.10.5)
15
+ ruby-saml (~> 1.11.0)
16
+ ruby-saml-idp
17
+ sinatra (~> 2.0.0)
18
+ xmlenc (>= 0.7.1)
19
+
20
+ GEM
21
+ remote: https://rubygems.org/
22
+ specs:
23
+ activemodel (5.2.4.3)
24
+ activesupport (= 5.2.4.3)
25
+ activesupport (5.2.4.3)
26
+ concurrent-ruby (~> 1.0, >= 1.0.2)
27
+ i18n (>= 0.7, < 2)
28
+ minitest (~> 5.1)
29
+ tzinfo (~> 1.1)
30
+ builder (3.2.4)
31
+ coderay (1.1.2)
32
+ concurrent-ruby (1.1.7)
33
+ diff-lcs (1.4.2)
34
+ dotenv (1.0.2)
35
+ i18n (1.8.5)
36
+ concurrent-ruby (~> 1.0)
37
+ macaddr (1.7.1)
38
+ systemu (~> 2.6.2)
39
+ method_source (0.9.2)
40
+ mini_portile2 (2.4.0)
41
+ minitest (5.14.1)
42
+ mustermann (1.1.1)
43
+ ruby2_keywords (~> 0.0.1)
44
+ nokogiri (1.10.10)
45
+ mini_portile2 (~> 2.4.0)
46
+ pry (0.12.2)
47
+ coderay (~> 1.1.0)
48
+ method_source (~> 0.9.0)
49
+ rack (2.2.3)
50
+ rack-protection (2.0.8.1)
51
+ rack
52
+ rake (13.0.1)
53
+ rspec (3.9.0)
54
+ rspec-core (~> 3.9.0)
55
+ rspec-expectations (~> 3.9.0)
56
+ rspec-mocks (~> 3.9.0)
57
+ rspec-core (3.9.2)
58
+ rspec-support (~> 3.9.3)
59
+ rspec-expectations (3.9.2)
60
+ diff-lcs (>= 1.2.0, < 2.0)
61
+ rspec-support (~> 3.9.0)
62
+ rspec-mocks (3.9.1)
63
+ diff-lcs (>= 1.2.0, < 2.0)
64
+ rspec-support (~> 3.9.0)
65
+ rspec-support (3.9.3)
66
+ ruby-saml (1.11.0)
67
+ nokogiri (>= 1.5.10)
68
+ ruby2_keywords (0.0.2)
69
+ sinatra (2.0.8.1)
70
+ mustermann (~> 1.0)
71
+ rack (~> 2.0)
72
+ rack-protection (= 2.0.8.1)
73
+ tilt (~> 2.0)
74
+ systemu (2.6.5)
75
+ thread_safe (0.3.6)
76
+ tilt (2.0.10)
77
+ tzinfo (1.2.7)
78
+ thread_safe (~> 0.1)
79
+ uuid (2.3.8)
80
+ macaddr (~> 1.0)
81
+ xmlenc (0.7.1)
82
+ activemodel (>= 3.0.0)
83
+ activesupport (>= 3.0.0)
84
+ nokogiri (>= 1.6.0, < 2.0.0)
85
+ xmlmapper (>= 0.7.3)
86
+ xmlmapper (0.7.3)
87
+ nokogiri (~> 1.5)
88
+
89
+ PLATFORMS
90
+ ruby
91
+
92
+ DEPENDENCIES
93
+ bundler (~> 2)
94
+ dotenv (~> 1.0)
95
+ fake_idp!
96
+ pry (~> 0.12.2)
97
+ rake (~> 13.0)
98
+ rspec (~> 3.9.0)
99
+ ruby-saml-idp!
100
+
101
+ BUNDLED WITH
102
+ 2.1.4
@@ -0,0 +1,128 @@
1
+ # Fake IdP
2
+
3
+ [![Build Status](https://travis-ci.com/healthify/fake_idp.svg?branch=master)](https://travis-ci.com/healthify/fake_idp)
4
+
5
+ ## About
6
+
7
+ This is an open source Ruby gem intended for developers needing to spin up a fake Identity Provider (IdP) for testing SAML authentication flows. It's made by the [Healthify](http://healthify.us) team. It's _not_ for setting up an IdP within Healthify—to do that, you'll need to reach out to integrations@healthify.us.
8
+
9
+ ## Installation
10
+
11
+ Clone the repo and `cd` into the project directory.
12
+
13
+ ```sh
14
+ bin/setup
15
+ ```
16
+
17
+ ## Running in Development
18
+
19
+ To run locally, you first need to set up the following environment variables:
20
+
21
+ |Variables|Description|
22
+ |---|---|
23
+ |CALLBACK_URL|The URL of the Healthify app to POST to for SAML authentication - required|
24
+ |NAME_ID|Name_id of the user you want to log in as - may be nil/blank|
25
+ |SSO_UID|Unique id of the user you want to log in as - may be nil/blank|
26
+ |USERNAME|Username of the user you want to log in as - may be nil/blank|
27
+ |ENCRYPTION_ENABLED| Set to 'true' to indicate that the generated assertion should be encrypted. Defaults to false|
28
+
29
+ The `.env.example` file has examples of what these env variables could look like.
30
+ You can copy that over to your own `.env` file to set these environment variables:
31
+
32
+ cp .env.example .env
33
+
34
+ Next, to start the server, you can run:
35
+
36
+ ```sh
37
+ bin/server
38
+ ```
39
+
40
+ Then navigate to `http://localhost:9292/saml/auth` to begin making your SAML requests.
41
+
42
+ ## Running in Test
43
+
44
+ ### Gemfile
45
+
46
+ If you are using this gem to provide a Fake IDP server in a test suite, add the gem to the Gemfile:
47
+
48
+ ```ruby
49
+ gem 'fake_idp', github: 'healthify/fake_idp'
50
+ ```
51
+
52
+ ### Configuration
53
+
54
+ You can set the relevant variables in a configuration block if they aren't provided as environment variables. For example:
55
+
56
+ ```ruby
57
+ FakeIdp.configure do |config|
58
+ config.callback_url = 'http://localhost.dev:3000/auth/saml/devidp/callback'
59
+ config.sso_uid = '12345'
60
+ config.name_id = 'user@example.com'
61
+ config.username = nil
62
+ config.certificate = "YOUR CERT HERE"
63
+ config.idp_certificate = "YOUR IDP CERT HERE"
64
+ config.idp_secret_key = "YOUR IDP SECRET KEY HERE"
65
+ config.algorithm = :sha512
66
+ config.additional_attributes = { custom_saml_attr: "DELIVERED_IN_ASSERTION" }
67
+ end
68
+ ```
69
+
70
+ #### Resetting Configuration
71
+
72
+ If you ever want to reset your FakeIdp configuration (e.g. between test specs), you can use the following:
73
+
74
+ ```ruby
75
+ FakeIdp.reset!
76
+ ```
77
+
78
+ ### Use
79
+
80
+ You can use Capybara Discoball to spin `FakeIdp::Application` up in a test:
81
+
82
+ ```ruby
83
+ require 'fake_idp'
84
+
85
+ before(:each) do
86
+ FakeIdp.configure do |config|
87
+ config.callback_url = callback_url
88
+ config.sso_uid = sso_uid
89
+ config.name_id = name_id
90
+ end
91
+ end
92
+
93
+ it 'logs the sso user in' do
94
+ Capybara::Discoball.spin(FakeIdp::Application) do |fake_idp_server|
95
+ # ...
96
+ end
97
+ end
98
+ ```
99
+
100
+ ### Generating a SAML Response
101
+
102
+ The gem provides a `SamlResponse` class used to generate a custom signed XML SAML response with an assertion that can be encrypted by setting `encryption_enabled` to `true`.
103
+
104
+ **Usage**
105
+
106
+ ```ruby
107
+ # Instantiate with your IDP settings, user attributes and service provider details
108
+ saml_response = FakeIdp::SamlResponse.new(
109
+ saml_acs_url: "http://localhost.dev:3000/auth/saml/devidp/callback",
110
+ saml_request_id: "_#{SecureRandom.uuid}",
111
+ name_id: "bob_builder@gmail.com",
112
+ issuer_uri: "http://publichost.dev:3000",
113
+ algorithm_name: :sha256,
114
+ certificate: "YOUR IDP CERTIFICATE HERE",
115
+ secret_key: "YOUR IDP SECRET KEY HERE",
116
+ encryption_enabled: false,
117
+ user_attributes: {
118
+ uuid: "12345",
119
+ username: "bob_builder",
120
+ first_name: "Bob",
121
+ last_name: "The Builder",
122
+ email: "bob_builder@gmail.com",
123
+ },
124
+ )
125
+
126
+ # Returns a signed XML SAML response document
127
+ saml_response.build
128
+ ```
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "fake_idp"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ set -vx
4
+
5
+ bundle exec rackup
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+ cp .env.example .env
8
+
9
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ $LOAD_PATH << File.expand_path("../lib", __FILE__)
2
+ require "fake_idp"
3
+ require 'dotenv'
4
+
5
+ Dotenv.load
6
+ run FakeIdp::Application
@@ -0,0 +1,39 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fake_idp/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fake_idp"
8
+ spec.version = FakeIdp::VERSION
9
+ spec.authors = [
10
+ "Shelby Switzer",
11
+ "Tyler Willingham",
12
+ "Robyn-Dale Samuda",
13
+ "Chet Bortz",
14
+ ]
15
+ spec.email = ["engineering@healthify.us"]
16
+
17
+ spec.summary = 'Fake IDP to test SAML authentication'
18
+ spec.license = "MIT"
19
+
20
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_dependency "nokogiri", ">= 1.10.5"
26
+ spec.add_dependency "builder", ">= 3.2.2"
27
+ spec.add_dependency "activesupport", "~> 5.2.4.3"
28
+ spec.add_dependency "xmlenc", ">= 0.7.1"
29
+
30
+ spec.add_development_dependency "bundler", "~> 2"
31
+ spec.add_development_dependency "rake", "~> 13.0"
32
+ spec.add_development_dependency "rspec", "~> 3.9.0"
33
+ spec.add_development_dependency "dotenv", "~> 1.0"
34
+ spec.add_development_dependency "pry", "~> 0.12.2"
35
+
36
+ spec.add_runtime_dependency "sinatra", "~> 2.0.0"
37
+ spec.add_runtime_dependency "ruby-saml-idp"
38
+ spec.add_runtime_dependency "ruby-saml", "~> 1.11.0"
39
+ end
@@ -0,0 +1,24 @@
1
+ require 'sinatra/base'
2
+ require 'ruby-saml-idp'
3
+ require 'zlib'
4
+ require 'tilt/erb'
5
+ require 'fake_idp/configuration'
6
+ require 'fake_idp/application'
7
+
8
+ module FakeIdp
9
+ class << self
10
+ attr_writer :configuration
11
+ end
12
+
13
+ def self.configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def self.configure
18
+ yield(configuration)
19
+ end
20
+
21
+ def self.reset!
22
+ @configuration = nil
23
+ end
24
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./saml_response"
4
+ require "ruby-saml"
5
+
6
+ module FakeIdp
7
+ class Application < Sinatra::Base
8
+ include SamlIdp::Controller
9
+
10
+ get "/saml/auth" do
11
+ begin
12
+ decode_SAMLRequest(generate_saml_request)
13
+ @saml_response = Base64.encode64(build_xml_saml_response).delete("\r\n")
14
+
15
+ erb :auth
16
+ rescue => e
17
+ puts e
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def configuration
24
+ FakeIdp.configuration
25
+ end
26
+
27
+ def build_xml_saml_response
28
+ FakeIdp::SamlResponse.new(
29
+ name_id: configuration.name_id,
30
+ issuer_uri: configuration.issuer,
31
+ saml_acs_url: @saml_acs_url, # Defined in #decode_SAMLRequest in ruby-saml-idp gem
32
+ saml_request_id: @saml_request_id, # Defined in #decode_SAMLRequest in ruby-saml-idp gem
33
+ user_attributes: user_attributes,
34
+ algorithm_name: configuration.algorithm,
35
+ certificate: configuration.idp_certificate,
36
+ secret_key: configuration.idp_secret_key,
37
+ encryption_enabled: configuration.encryption_enabled,
38
+ ).build
39
+ end
40
+
41
+ def user_attributes
42
+ {
43
+ uuid: configuration.sso_uid,
44
+ username: configuration.username,
45
+ first_name: configuration.first_name,
46
+ last_name: configuration.last_name,
47
+ email: configuration.email,
48
+ }.merge(configuration.additional_attributes)
49
+ end
50
+
51
+ # An AuthRequest is required by the ruby-saml-idp gem to begin the process of returning
52
+ # a SAMLResponse. We will likely remove the ruby-saml-idp dependency in a future update
53
+ def generate_saml_request
54
+ auth_request = OneLogin::RubySaml::Authrequest.new
55
+ auth_url = auth_request.create(saml_settings)
56
+ CGI.unescape(auth_url.split("=").last)
57
+ end
58
+
59
+ def saml_settings
60
+ OneLogin::RubySaml::Settings.new.tap do |setting|
61
+ setting.assertion_consumer_service_url = configuration.callback_url
62
+ setting.issuer = configuration.issuer
63
+ setting.idp_sso_target_url = configuration.idp_sso_target_url
64
+ setting.name_identifier_format = FakeIdp::SamlResponse::EMAIL_ADDRESS_FORMAT
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,65 @@
1
+ module FakeIdp
2
+ class Configuration
3
+ attr_accessor(
4
+ :callback_url,
5
+ :sso_uid,
6
+ :username,
7
+ :first_name,
8
+ :last_name,
9
+ :email,
10
+ :name_id,
11
+ :certificate,
12
+ :idp_certificate,
13
+ :idp_secret_key,
14
+ :idp_sso_target_url,
15
+ :issuer,
16
+ :algorithm,
17
+ :additional_attributes,
18
+ :encryption_enabled,
19
+ )
20
+
21
+ def initialize
22
+ @callback_url = ENV['CALLBACK_URL']
23
+ @sso_uid = ENV['SSO_UID']
24
+ @username = ENV['USERNAME']
25
+ @first_name = ENV['FIRST_NAME']
26
+ @last_name = ENV['LAST_NAME']
27
+ @email = ENV['EMAIL']
28
+ @name_id = ENV['NAME_ID']
29
+ @certificate = default_certificate
30
+ @idp_certificate = default_idp_certificate
31
+ @idp_secret_key = default_idp_secret_key
32
+ @idp_sso_target_url = idp_sso_target_url
33
+ @issuer = issuer
34
+ @algorithm = default_algorithm
35
+ @additional_attributes = {}
36
+ @encryption_enabled = default_encryption
37
+ end
38
+
39
+ private
40
+
41
+ def default_certificate
42
+ ENV["CERTIFICATE"] ||
43
+ SamlIdp::Default::X509_CERTIFICATE
44
+ end
45
+
46
+ def default_idp_certificate
47
+ ENV["IDP_CERTIFICATE"] ||
48
+ SamlIdp::Default::X509_CERTIFICATE
49
+ end
50
+
51
+ def default_idp_secret_key
52
+ ENV["IDP_SECRET_KEY"] ||
53
+ SamlIdp::Default::SECRET_KEY
54
+ end
55
+
56
+ def default_algorithm
57
+ ENV["ALGORITHM"]&.to_sym ||
58
+ :sha1
59
+ end
60
+
61
+ def default_encryption
62
+ ENV["ENCRYPTION_ENABLED"] == "true"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,90 @@
1
+ require "xmlenc"
2
+ require "builder"
3
+
4
+ module FakeIdp
5
+ class Encryptor
6
+ ENCRYPTION_STRATEGY = "aes256-cbc".freeze
7
+ KEY_TRANSPORT = "rsa-oaep-mgf1p".freeze
8
+
9
+ attr_reader :raw_xml, :certificate, :encryption_key
10
+
11
+ def initialize(raw_xml, certificate)
12
+ @raw_xml = raw_xml
13
+ @certificate = certificate
14
+ end
15
+
16
+ # Encryption approach borrowed from
17
+ # https://github.com/saml-idp/saml_idp/blob/master/lib/saml_idp/encryptor.rb
18
+ def encrypt
19
+ encryption_template = Nokogiri::XML::Document.parse(build_encryption_template).root
20
+ encrypted_data = Xmlenc::EncryptedData.new(encryption_template)
21
+ @encryption_key = encrypted_data.encrypt(raw_xml)
22
+ encrypted_key_node = encrypted_data.node.at_xpath(
23
+ "//xenc:EncryptedData/ds:KeyInfo/xenc:EncryptedKey",
24
+ Xmlenc::NAMESPACES,
25
+ )
26
+ encrypted_key = Xmlenc::EncryptedKey.new(encrypted_key_node)
27
+ encrypted_key.encrypt(openssl_cert.public_key, encryption_key)
28
+
29
+ xml = Builder::XmlMarkup.new
30
+ xml.EncryptedAssertion(xmlns: "urn:oasis:names:tc:SAML:2.0:assertion") do |enc_assert|
31
+ enc_assert << encrypted_data.node.to_s
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def openssl_cert
38
+ @_openssl_cert ||= if certificate.is_a?(String)
39
+ OpenSSL::X509::Certificate.new(certificate)
40
+ else
41
+ certificate
42
+ end
43
+ end
44
+
45
+ def encryption_strategy_ns
46
+ "http://www.w3.org/2001/04/xmlenc##{ENCRYPTION_STRATEGY}"
47
+ end
48
+
49
+ def key_transport_ns
50
+ "http://www.w3.org/2001/04/xmlenc##{KEY_TRANSPORT}"
51
+ end
52
+
53
+ def build_encryption_template
54
+ xml = Builder::XmlMarkup.new
55
+ xml.EncryptedData(
56
+ Id: "ED",
57
+ Type: "http://www.w3.org/2001/04/xmlenc#Element",
58
+ xmlns: "http://www.w3.org/2001/04/xmlenc#",
59
+ ) do |enc_data|
60
+ enc_data.EncryptionMethod(Algorithm: encryption_strategy_ns)
61
+ enc_data.tag!("ds:KeyInfo", "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#") do |key_info|
62
+ key_info.EncryptedKey(Id: "EK", xmlns: "http://www.w3.org/2001/04/xmlenc#") do |enc_key|
63
+ enc_key.EncryptionMethod(Algorithm: key_transport_ns)
64
+ enc_key.tag!("ds:KeyInfo", "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#") do |key_info_child|
65
+ key_info_child.tag!("ds:KeyName")
66
+ key_info_child.tag!("ds:X509Data") do |x509_data|
67
+ x509_data.tag!("ds:X509Certificate") do |x509_cert|
68
+ x509_cert << certificate.to_s.gsub(/-+(BEGIN|END) CERTIFICATE-+/, "")
69
+ end
70
+ end
71
+ end
72
+
73
+ enc_key.CipherData(&:CipherValue)
74
+ enc_key.ReferenceList { |ref_list| ref_list.DataReference(URI: "#ED") }
75
+ end
76
+ end
77
+
78
+ enc_data.CipherData(&:CipherValue)
79
+ end
80
+ end
81
+
82
+ def encrypted_data_namespace
83
+ {
84
+ Id: "ED",
85
+ Type: "http://www.w3.org/2001/04/xmlenc#Element",
86
+ xmlns: "http://www.w3.org/2001/04/xmlenc#",
87
+ }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "nokogiri"
5
+ require "openssl"
6
+ require_relative "./encryptor"
7
+
8
+ module FakeIdp
9
+ class SamlResponse
10
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
11
+ SAML_VERSION = "2.0"
12
+ ASSERTION_NAMESPACE = "urn:oasis:names:tc:SAML:2.0:assertion"
13
+ ENTITY_FORMAT = "urn:oasis:names:SAML:2.0:nameid-format:entity"
14
+ BEARER_FORMAT = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
15
+ ENVELOPE_SCHEMA = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
16
+ STATUS_CODE_VALUE = "urn:oasis:names:tc:SAML:2.0:status:Success"
17
+ FEDERATION_SOURCE = "urn:federation:authentication:windows"
18
+ EMAIL_ADDRESS_FORMAT = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
19
+
20
+ # For the time being we're only supporting a single canonical schema since
21
+ # supporting multiple is inconsequential for our immediate need.
22
+ CANONICAL_VALUE = 1
23
+ CANONICAL_SCHEMA = "http://www.w3.org/2001/10/xml-exc-c14n#"
24
+
25
+ def initialize(
26
+ name_id:,
27
+ issuer_uri:,
28
+ saml_acs_url:,
29
+ saml_request_id:,
30
+ user_attributes:,
31
+ algorithm_name:,
32
+ certificate:,
33
+ secret_key:,
34
+ encryption_enabled: false
35
+ )
36
+ @name_id = name_id
37
+ @issuer_uri = issuer_uri
38
+ @saml_acs_url = saml_acs_url
39
+ @saml_request_id = saml_request_id
40
+ @user_attributes = user_attributes
41
+ @algorithm_name = algorithm_name
42
+ @certificate = certificate
43
+ @secret_key = secret_key
44
+ @encryption_enabled = encryption_enabled
45
+ @builder = Nokogiri::XML::Builder.new
46
+ @timestamp = Time.now
47
+ end
48
+
49
+ def build
50
+ @builder[:samlp].Response(root_namespace_attributes) do |response|
51
+ build_issuer_segment(response)
52
+ build_status_segment(response)
53
+ build_assertion_segment(response)
54
+ end
55
+
56
+ document_with_digest = replace_digest_value(@builder.to_xml)
57
+ document = replace_signature_value(document_with_digest)
58
+ encrypt_assertion!(document)
59
+ end
60
+
61
+ private
62
+
63
+ def encrypt_assertion!(document)
64
+ return document unless @encryption_enabled
65
+
66
+ document_copy = document.dup
67
+ working_document = Nokogiri::XML(document)
68
+ assertion = working_document.at_xpath("//saml:Assertion", "saml" => ASSERTION_NAMESPACE)
69
+ encrypted_assertion_xml = FakeIdp::Encryptor.new(
70
+ assertion.to_xml,
71
+ @certificate,
72
+ ).encrypt
73
+
74
+ document_copy = Nokogiri::XML(document_copy)
75
+ target_assertion_node = document_copy.at_xpath(
76
+ "//saml:Assertion",
77
+ "saml" => ASSERTION_NAMESPACE,
78
+ )
79
+ # Replace Assertion node with encrypted assertion
80
+ target_assertion_node.replace(encrypted_assertion_xml)
81
+ document_copy.to_xml
82
+ end
83
+
84
+ def replace_digest_value(document)
85
+ document_copy = document.dup
86
+ working_document = Nokogiri::XML(document)
87
+
88
+ # The signature element needs to be removed from the assertion before creating a digest
89
+ signature_element = working_document.at_xpath("//ds:Signature", "ds" => DSIG)
90
+ signature_element.remove
91
+
92
+ assertion_without_signature = working_document.
93
+ at_xpath("//*[@ID=$id]", nil, "id" => assertion_reference_response_id)
94
+ canon_hashed_element = assertion_without_signature.canonicalize(CANONICAL_VALUE)
95
+
96
+ digest_value = Base64.encode64(algorithm.digest(canon_hashed_element)).strip
97
+
98
+ # Replace digest node with the generated value
99
+ document_copy = Nokogiri::XML(document_copy)
100
+ target_digest_node = document_copy.at_xpath("//ds:DigestValue", "ds" => DSIG)
101
+ target_digest_node.content = digest_value
102
+ document_copy
103
+ end
104
+
105
+ def replace_signature_value(document)
106
+ document_copy = document.dup
107
+ signature_element = document.at_xpath("//ds:Signature", "ds" => DSIG)
108
+
109
+ # The SignatureValue is a signed copy of the SignedInfo element
110
+ signed_info_element = signature_element.at_xpath("./ds:SignedInfo", "ds" => DSIG)
111
+ canon_string = signed_info_element.canonicalize(CANONICAL_VALUE)
112
+
113
+ signature_value = sign(canon_string)
114
+
115
+ target_signature_node = document_copy.at_xpath("//ds:SignatureValue", "ds" => DSIG)
116
+ target_signature_node.content = signature_value
117
+ document_copy.to_xml
118
+ end
119
+
120
+ def build_issuer_segment(parent_attribute)
121
+ parent_attribute[:saml].Issuer("xmlns:saml" => ASSERTION_NAMESPACE) do |issuer|
122
+ issuer << @issuer_uri
123
+ end
124
+ end
125
+
126
+ def build_status_segment(parent_attribute)
127
+ parent_attribute[:samlp].Status do |status|
128
+ status[:samlp].StatusCode("Value" => STATUS_CODE_VALUE)
129
+ end
130
+ end
131
+
132
+ def build_assertion_segment(parent_attribute)
133
+ parent_attribute[:saml].Assertion(assertion_namespace_attributes) do |assertion|
134
+ assertion[:saml].Issuer("Format" => ENTITY_FORMAT) do |issuer|
135
+ issuer << @issuer_uri
136
+ end
137
+
138
+ build_assertion_signature(assertion)
139
+
140
+ assertion[:saml].Subject do |subject|
141
+ subject[:saml].NameID("Format" => EMAIL_ADDRESS_FORMAT) do |name_id|
142
+ name_id << @name_id
143
+ end
144
+
145
+ subject[:saml].SubjectConfirmation("Method" => BEARER_FORMAT) do |subject_confirmation|
146
+ subject_confirmation[:saml].SubjectConfirmationData(subject_confirmation_data) { "" }
147
+ end
148
+ end
149
+
150
+ assertion[:saml].Conditions(saml_conditions) do |conditions|
151
+ conditions[:saml].AudienceRestriction do |restriction|
152
+ restriction[:saml].Audience { |audience| audience << @issuer_uri }
153
+ end
154
+ end
155
+
156
+ assertion[:saml].AttributeStatement do |attribute_statement|
157
+ @user_attributes.map do |name, value|
158
+ attribute_statement[:saml].Attribute("Name" => name) do |attribute|
159
+ attribute[:saml].AttributeValue { |attribute_value| attribute_value << value }
160
+ end
161
+ end
162
+ end
163
+
164
+ assertion[:saml].AuthnStatement(authn_statement) do |statement|
165
+ statement[:saml].AuthnContext do |authn_context|
166
+ authn_context[:saml].AuthnContextClassRef do |context_class_ref|
167
+ context_class_ref << FEDERATION_SOURCE
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ def build_assertion_signature(parent_attribute)
175
+ parent_attribute[:ds].Signature("xmlns:ds" => DSIG) do |signature|
176
+ signature[:ds].SignedInfo("xmlns:ds" => DSIG) do |signed_info|
177
+ signed_info[:ds].CanonicalizationMethod("Algorithm" => CANONICAL_SCHEMA)
178
+ signed_info[:ds].SignatureMethod("Algorithm" => "#{DSIG}#{@algorithm_name}")
179
+
180
+ signed_info[:ds].Reference("URI" => reference_uri) do |reference|
181
+ reference[:ds].Transforms do |transform|
182
+ transform[:ds].Transform("Algorithm" => ENVELOPE_SCHEMA)
183
+ transform[:ds].Transform("Algorithm" => CANONICAL_SCHEMA)
184
+ end
185
+
186
+ reference[:ds].DigestMethod("Algorithm" => "#{DSIG}#{@algorithm_name}")
187
+
188
+ # The digest_value is set and derived from creating a digest of the Assertion element
189
+ # without the signature element after the document is generated
190
+ reference[:ds].DigestValue("xmlns:ds" => DSIG) { |d| d << "" }
191
+ end
192
+ end
193
+
194
+ # The signature_value is set and derived from signing the SignedInfo element after the
195
+ # document is generated
196
+ signature[:ds].SignatureValue { |signature_value| signature_value << "" }
197
+
198
+ signature.KeyInfo("xmlns:ds" => DSIG) do |key_info|
199
+ key_info[:ds].X509Data do |x509_data|
200
+ x509_data[:ds].X509Certificate do |x509_certificate|
201
+ x509_certificate << Base64.encode64(@certificate)
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ def algorithm
209
+ raise "Algorithm name must be a Symbol" unless @algorithm_name.is_a?(Symbol)
210
+
211
+ case @algorithm_name
212
+ when :sha256 then OpenSSL::Digest::SHA256
213
+ when :sha384 then OpenSSL::Digest::SHA384
214
+ when :sha512 then OpenSSL::Digest::SHA512
215
+ else
216
+ OpenSSL::Digest::SHA1
217
+ end
218
+ end
219
+
220
+ def sign(data)
221
+ key = OpenSSL::PKey::RSA.new(@secret_key)
222
+ Base64.encode64(key.sign(algorithm.new, data)).gsub(/\n/, "")
223
+ end
224
+
225
+ def reference_response_id
226
+ @_reference_response_id ||= "_#{SecureRandom.uuid}"
227
+ end
228
+
229
+ def assertion_reference_response_id
230
+ @assertion_reference_response_id ||= "_#{SecureRandom.uuid}"
231
+ end
232
+
233
+ def reference_uri
234
+ "_#{assertion_reference_response_id}"
235
+ end
236
+
237
+ def root_namespace_attributes
238
+ {
239
+ "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
240
+ "Consent" => "urn:oasis:names:tc:SAML:2.0:consent:unspecified",
241
+ "Destination" => @saml_acs_url,
242
+ "ID" => reference_response_id,
243
+ "InResponseTo" => @saml_request_id,
244
+ "IssueInstant" => @timestamp.strftime("%Y-%m-%dT%H:%M:%S"),
245
+ "Version" => SAML_VERSION,
246
+ }
247
+ end
248
+
249
+ def assertion_namespace_attributes
250
+ {
251
+ "xmlns:saml" => ASSERTION_NAMESPACE,
252
+ "ID" => assertion_reference_response_id,
253
+ "IssueInstant" => @timestamp.strftime("%Y-%m-%dT%H:%M:%S"),
254
+ "Version" => SAML_VERSION,
255
+ }
256
+ end
257
+
258
+ def subject_confirmation_data
259
+ {
260
+ "InResponseTo" => @saml_request_id,
261
+ "NotOnOrAfter" => (@timestamp + 3 * 60).strftime("%Y-%m-%dT%H:%M:%S"),
262
+ "Recipient" => @saml_acs_url,
263
+ }
264
+ end
265
+
266
+ def saml_conditions
267
+ {
268
+ "NotBefore" => (@timestamp - 5).strftime("%Y-%m-%dT%H:%M:%S"),
269
+ "NotOnOrAfter" => (@timestamp + 60 * 60).strftime("%Y-%m-%dT%H:%M:%S"),
270
+ }
271
+ end
272
+
273
+ def authn_statement
274
+ {
275
+ "AuthnInstant" => @timestamp.strftime("%Y-%m-%dT%H:%M:%S"),
276
+ "SessionIndex" => reference_response_id,
277
+ }
278
+ end
279
+ end
280
+ end
@@ -0,0 +1,3 @@
1
+ module FakeIdp
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,18 @@
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>
8
+ <h1>Fake SAML SSO Authenticator</h1>
9
+
10
+ <form action="<%= @saml_acs_url %>" method="post">
11
+ <input name="SAMLResponse" type="hidden" value="<%= @saml_response %>">
12
+ <input type="submit" value="Login">
13
+ </form>
14
+
15
+ <h2>Encrypted Response</h2>
16
+ <pre><%= @saml_response.strip %></pre>
17
+ </body>
18
+ </html>
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Shelby Switzer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
metadata ADDED
@@ -0,0 +1,235 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fake_idp
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Shelby Switzer
8
+ - Tyler Willingham
9
+ - Robyn-Dale Samuda
10
+ - Chet Bortz
11
+ autorequire:
12
+ bindir: exe
13
+ cert_chain: []
14
+ date: 2020-08-13 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: nokogiri
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.10.5
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 1.10.5
30
+ - !ruby/object:Gem::Dependency
31
+ name: builder
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.2.2
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 3.2.2
44
+ - !ruby/object:Gem::Dependency
45
+ name: activesupport
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - "~>"
49
+ - !ruby/object:Gem::Version
50
+ version: 5.2.4.3
51
+ type: :runtime
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - "~>"
56
+ - !ruby/object:Gem::Version
57
+ version: 5.2.4.3
58
+ - !ruby/object:Gem::Dependency
59
+ name: xmlenc
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 0.7.1
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 0.7.1
72
+ - !ruby/object:Gem::Dependency
73
+ name: bundler
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - "~>"
77
+ - !ruby/object:Gem::Version
78
+ version: '2'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - "~>"
84
+ - !ruby/object:Gem::Version
85
+ version: '2'
86
+ - !ruby/object:Gem::Dependency
87
+ name: rake
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '13.0'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '13.0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: rspec
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - "~>"
105
+ - !ruby/object:Gem::Version
106
+ version: 3.9.0
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: 3.9.0
114
+ - !ruby/object:Gem::Dependency
115
+ name: dotenv
116
+ requirement: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - "~>"
119
+ - !ruby/object:Gem::Version
120
+ version: '1.0'
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - "~>"
126
+ - !ruby/object:Gem::Version
127
+ version: '1.0'
128
+ - !ruby/object:Gem::Dependency
129
+ name: pry
130
+ requirement: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - "~>"
133
+ - !ruby/object:Gem::Version
134
+ version: 0.12.2
135
+ type: :development
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - "~>"
140
+ - !ruby/object:Gem::Version
141
+ version: 0.12.2
142
+ - !ruby/object:Gem::Dependency
143
+ name: sinatra
144
+ requirement: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - "~>"
147
+ - !ruby/object:Gem::Version
148
+ version: 2.0.0
149
+ type: :runtime
150
+ prerelease: false
151
+ version_requirements: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - "~>"
154
+ - !ruby/object:Gem::Version
155
+ version: 2.0.0
156
+ - !ruby/object:Gem::Dependency
157
+ name: ruby-saml-idp
158
+ requirement: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ type: :runtime
164
+ prerelease: false
165
+ version_requirements: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ - !ruby/object:Gem::Dependency
171
+ name: ruby-saml
172
+ requirement: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - "~>"
175
+ - !ruby/object:Gem::Version
176
+ version: 1.11.0
177
+ type: :runtime
178
+ prerelease: false
179
+ version_requirements: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - "~>"
182
+ - !ruby/object:Gem::Version
183
+ version: 1.11.0
184
+ description:
185
+ email:
186
+ - engineering@healthify.us
187
+ executables: []
188
+ extensions: []
189
+ extra_rdoc_files: []
190
+ files:
191
+ - ".env.example"
192
+ - ".gitignore"
193
+ - ".ruby-version"
194
+ - ".travis.yml"
195
+ - Gemfile
196
+ - Gemfile.lock
197
+ - README.md
198
+ - Rakefile
199
+ - bin/console
200
+ - bin/server
201
+ - bin/setup
202
+ - config.ru
203
+ - fake_idp.gemspec
204
+ - lib/fake_idp.rb
205
+ - lib/fake_idp/application.rb
206
+ - lib/fake_idp/configuration.rb
207
+ - lib/fake_idp/encryptor.rb
208
+ - lib/fake_idp/saml_response.rb
209
+ - lib/fake_idp/version.rb
210
+ - lib/fake_idp/views/auth.erb
211
+ - license.txt
212
+ homepage:
213
+ licenses:
214
+ - MIT
215
+ metadata: {}
216
+ post_install_message:
217
+ rdoc_options: []
218
+ require_paths:
219
+ - lib
220
+ required_ruby_version: !ruby/object:Gem::Requirement
221
+ requirements:
222
+ - - ">="
223
+ - !ruby/object:Gem::Version
224
+ version: '0'
225
+ required_rubygems_version: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ requirements: []
231
+ rubygems_version: 3.0.6
232
+ signing_key:
233
+ specification_version: 4
234
+ summary: Fake IDP to test SAML authentication
235
+ test_files: []