stub_saml_idp 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|