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
|