saml_idp_rails 0.0.1
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/MIT-LICENSE +20 -0
- data/README.md +150 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/saml_idp_rails/application.css +15 -0
- data/app/controllers/saml_idp_rails/application_controller.rb +4 -0
- data/app/controllers/saml_idp_rails/saml_idp_controller.rb +143 -0
- data/app/helpers/saml_idp_rails/application_helper.rb +4 -0
- data/app/jobs/saml_idp_rails/application_job.rb +4 -0
- data/app/mailers/saml_idp_rails/application_mailer.rb +6 -0
- data/app/models/saml_idp_rails/application_record.rb +5 -0
- data/app/models/saml_idp_rails/saml_sp_config.rb +50 -0
- data/app/views/layouts/saml_idp_rails/application.html.erb +17 -0
- data/app/views/saml_idp_rails/saml_idp/slo_request.html.erb +18 -0
- data/app/views/saml_idp_rails/saml_idp/slo_response.html.erb +22 -0
- data/app/views/saml_idp_rails/saml_idp/sso_response.html.erb +14 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20250120165545_create_saml_idp_rails_saml_sp_configs.rb +62 -0
- data/lib/saml_idp_rails/config.rb +27 -0
- data/lib/saml_idp_rails/engine.rb +5 -0
- data/lib/saml_idp_rails/railtie.rb +4 -0
- data/lib/saml_idp_rails/saml_config.rb +240 -0
- data/lib/saml_idp_rails/version.rb +3 -0
- data/lib/saml_idp_rails.rb +16 -0
- data/lib/tasks/saml_idp_rails_tasks.rake +4 -0
- metadata +137 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ee42ec55e4011bf7db613fd7ac82ae6357505f95cf19892f1203a659cb88a2fb
|
4
|
+
data.tar.gz: 204c561da47fc51ab53de0bb9b34d6a6d519febbfe638dd05b44a501176fe251
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 01c41f507dc9fe3b896382d9009ebafb2096e7b68b0c6d9c85413c167f9a9da7889cc0c9cd4b33b14e7cdc764ebe2dbe31d42352276336af04fbcecb7cc205e7
|
7
|
+
data.tar.gz: cfc3728394769efa8cff0370a500db391fa237379a60f12bfe3a82ab29ecd81869d6c5e0e65de2030766d303e4da1ac3e9ce2a2752e18473b25ce73ca1b29ce0
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright zogoo
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# SamlIdpRails
|
2
|
+
A Ruby gem that implements SAML Identity Provider (IdP) functionality for Rails applications.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
This gem allows you to add SAML IdP capabilities to your Rails application.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
1. Add this line to your Rails application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem "saml_idp_rails"
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
```bash
|
17
|
+
$ bundle
|
18
|
+
```
|
19
|
+
|
20
|
+
Or install it yourself as:
|
21
|
+
```bash
|
22
|
+
$ gem install saml_idp_rails
|
23
|
+
```
|
24
|
+
|
25
|
+
2. Generate the necessary migrations for Active Record:
|
26
|
+
```bash
|
27
|
+
bin/rails saml_idp_rails:install:migrations
|
28
|
+
```
|
29
|
+
|
30
|
+
3. Migrate the Service Provider settings to your database:
|
31
|
+
```bash
|
32
|
+
bin/rails db:migrate
|
33
|
+
```
|
34
|
+
|
35
|
+
4. Configure your IdP service:
|
36
|
+
Create a configuration file in your Rails initializers directory:
|
37
|
+
|
38
|
+
```rb
|
39
|
+
# config/initializers/saml_idp_config.rb
|
40
|
+
|
41
|
+
SamlIdpRails.configure do |config|
|
42
|
+
# Base URL of your application
|
43
|
+
config.base_url = "http://localhost:3000"
|
44
|
+
|
45
|
+
# URL where users will be redirected to sign in
|
46
|
+
config.sign_in_url = "/users/sign_in"
|
47
|
+
|
48
|
+
# Default URL to redirect after successful authentication
|
49
|
+
config.relay_state_url = "/home"
|
50
|
+
|
51
|
+
# Hook to validate user session
|
52
|
+
config.session_validation_hook = ->(session) { true }
|
53
|
+
|
54
|
+
# Lambda to find SAML Service Provider configuration
|
55
|
+
config.saml_config_finder = lambda do |_request|
|
56
|
+
SamlIdpRails::SamlSpConfig.find_by(uuid: params.require(:uuid))
|
57
|
+
end
|
58
|
+
|
59
|
+
# Lambda to find and return user information for SAML response
|
60
|
+
config.saml_user_finder = lambda do |_request|
|
61
|
+
User = Struct.new(:name_id_attribute, :email, keyword_init: true)
|
62
|
+
User.new(
|
63
|
+
name_id_attribute: "email",
|
64
|
+
email: "user@example.com"
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
5. Create a Service Provider (SP) configuration:
|
71
|
+
After running migrations, you'll find a migration file in your Rails app under:
|
72
|
+
`db/migrate/YYYYMMDDHHMMSS_create_saml_idp_rails_saml_sp_configs.saml_idp_rails.rb`
|
73
|
+
|
74
|
+
Example of creating an SP configuration:
|
75
|
+
|
76
|
+
```rb
|
77
|
+
SamlIdpRails::SamlSpConfig.create(
|
78
|
+
# Basic SP Information
|
79
|
+
name: "Test SP Config One", # Unique identifier for the SP configuration
|
80
|
+
display_name: "Test SP One", # Human-readable name for the SP
|
81
|
+
entity_id: "http://test-sp-one.com", # Entity ID provided by the SP
|
82
|
+
|
83
|
+
# Certificate and Key Configuration
|
84
|
+
signing_certificate: "Your IdP public key", # Base64 encoded IdP public key
|
85
|
+
encryption_certificate: nil, # Optional: SP encryption certificate
|
86
|
+
private_key: "Your IdP private key", # IdP private key (keep secure)
|
87
|
+
pv_key_password: nil, # Optional: Private key password if encrypted
|
88
|
+
|
89
|
+
# SAML Settings
|
90
|
+
sign_assertions: true, # Whether to sign SAML assertions
|
91
|
+
sign_authn_request: false, # Whether SP requires signed authentication requests
|
92
|
+
certificate: "SP X509 certificate", # SP's public certificate for signature validation
|
93
|
+
relay_state: "sample_relay_state", # Post-SSO redirect URL
|
94
|
+
name_id_attribute: "email", # Attribute to use as NameID
|
95
|
+
raw_metadata: nil, # Optional: Raw SP metadata XML
|
96
|
+
|
97
|
+
# SAML Format Settings
|
98
|
+
name_id_formats: ["email_address"], # Supported NameID formats
|
99
|
+
|
100
|
+
# Service Endpoints
|
101
|
+
assertion_consumer_services: [{
|
102
|
+
"binding" => "HTTP-POST",
|
103
|
+
"default" => "true",
|
104
|
+
"location" => "http://test-sp-one.com/acs"
|
105
|
+
}],
|
106
|
+
single_logout_services: {
|
107
|
+
"HTTP-Redirect" => "http://test-sp-one.com/slo"
|
108
|
+
},
|
109
|
+
|
110
|
+
# Contact Information
|
111
|
+
contact_person: {
|
112
|
+
"surname" => "Doe",
|
113
|
+
"given_name" => "John",
|
114
|
+
"email_address" => "john.doe@test-sp-one.com"
|
115
|
+
},
|
116
|
+
|
117
|
+
# SAML Attributes
|
118
|
+
saml_attributes: [{
|
119
|
+
"name" => "email",
|
120
|
+
"getter" => "email",
|
121
|
+
"nameFormat" => "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
|
122
|
+
"name_format" => "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
|
123
|
+
"friendlyName" => "Email address"
|
124
|
+
}],
|
125
|
+
|
126
|
+
# Public Identifier
|
127
|
+
uuid: "8ad17d2a-f870-4796-8212-7487411b8578" # Unique identifier for SP configuration
|
128
|
+
)
|
129
|
+
```
|
130
|
+
|
131
|
+
Once you successfully create the correct `SamlSpConfig`.
|
132
|
+
Then you can test it in your development environment to accessing following url
|
133
|
+
|
134
|
+
1. Metadata endpoint
|
135
|
+
```bash
|
136
|
+
curl http://localhost:3000/saml_idp/8ad17d2a-f870-4796-8212-7487411b8578/metadata
|
137
|
+
```
|
138
|
+
|
139
|
+
2. IdP initiated SAML SSO
|
140
|
+
You will see SAML response posted `http://test-sp-one.com/acs` in the Network tab of Browser dev tool.
|
141
|
+
|
142
|
+
```url
|
143
|
+
http://localhost:3000/saml_idp/8ad17d2a-f870-4796-8212-7487411b8578/sso
|
144
|
+
```
|
145
|
+
|
146
|
+
## Contributing
|
147
|
+
To contribute, fork the repository and create a pull request with your changes.
|
148
|
+
|
149
|
+
## License
|
150
|
+
This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require_relative "../../../lib/saml_idp_rails/saml_config"
|
2
|
+
|
3
|
+
module SamlIdpRails
|
4
|
+
class SamlIdpController < ApplicationController
|
5
|
+
layout false
|
6
|
+
include SamlIdp::Controller
|
7
|
+
|
8
|
+
before_action :store_authn_request, only: %i[sso_request], unless: :user_signed_in?
|
9
|
+
before_action :validate_session
|
10
|
+
before_action :load_config, only: %i[sso_request slo_request initiate_slo metadata]
|
11
|
+
before_action :validate_saml_request, only: %i[sso_request slo_request], if: :sp_initiated_request?
|
12
|
+
|
13
|
+
def sso_request
|
14
|
+
saml_response
|
15
|
+
render :sso_response
|
16
|
+
end
|
17
|
+
|
18
|
+
def slo_request
|
19
|
+
return redirect_to SamlIdpRails.config.relay_state_url, allow_other_host: true unless sp_initiated_request?
|
20
|
+
|
21
|
+
saml_slo_response = encode_logout_response(
|
22
|
+
current_saml_user,
|
23
|
+
@saml_config.append_request_config(saml_request).merge!(
|
24
|
+
public_cert: current_sp_config.certificate,
|
25
|
+
private_key: current_sp_config.private_key,
|
26
|
+
pv_key_password: current_sp_config.pv_key_password
|
27
|
+
)
|
28
|
+
)
|
29
|
+
|
30
|
+
# TODO: move this part to gem
|
31
|
+
# If SLO request doesn't contain the SLO endpoint then use SP config default SLO url
|
32
|
+
@sp_slo_endpoint = saml_request&.logout_url || current_sp_config.single_logout_services&.values&.first
|
33
|
+
@sp_slo_binding = current_sp_config.single_logout_services&.keys&.first == "HTTP-Redirect" ? :redirect : :post
|
34
|
+
saml_slo_response = Zlib::Deflate.deflate(saml_slo_response, 9)[2..-5] if @sp_slo_binding == :redirect
|
35
|
+
@saml_slo_response = Base64.strict_encode64(saml_slo_response)
|
36
|
+
@sp_slo_url = generate_url(host: @sp_slo_endpoint, SAMLResponse: @saml_slo_response, RelayState: SamlIdpRails.config.relay_state_url)
|
37
|
+
render :slo_response
|
38
|
+
end
|
39
|
+
|
40
|
+
def initiate_slo
|
41
|
+
# TODO: move it out to "saml_idp" gem
|
42
|
+
slo_endpoint = current_sp_config.single_logout_services
|
43
|
+
binding = slo_endpoint&.keys&.first == "HTTP-Redirect" ? :get : :post
|
44
|
+
slo_location = slo_endpoint&.values&.first
|
45
|
+
|
46
|
+
logout_request = SamlIdp::LogoutRequestBuilder.new(
|
47
|
+
response_id: SecureRandom.uuid,
|
48
|
+
issuer_uri: SamlIdpRails.config.base_url,
|
49
|
+
saml_slo_url: slo_location,
|
50
|
+
name_id: @saml_config.name_id_value,
|
51
|
+
algorithm: OpenSSL::Digest::SHA256, # TODO: Update this to use the SP's digest method
|
52
|
+
public_cert: current_sp_config.certificate,
|
53
|
+
private_key: current_sp_config.private_key,
|
54
|
+
pv_key_password: current_sp_config.pv_key_password
|
55
|
+
).signed
|
56
|
+
|
57
|
+
@slo_request_params = {
|
58
|
+
name: current_sp_config.name,
|
59
|
+
location: slo_location,
|
60
|
+
params: {
|
61
|
+
SAMLRequest: binding == :get ? Base64.encode64(logout_request) : logout_request,
|
62
|
+
RelayState: SamlIdpRails.config.relay_state_url
|
63
|
+
},
|
64
|
+
method: binding
|
65
|
+
}
|
66
|
+
render :slo_request
|
67
|
+
end
|
68
|
+
|
69
|
+
def metadata
|
70
|
+
render xml: @saml_config.idp_metadata
|
71
|
+
end
|
72
|
+
|
73
|
+
def attribute
|
74
|
+
# TODO: Remove this endpoint from the saml_idp gem
|
75
|
+
render json: @saml_config.saml_attributes
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def saml_response
|
81
|
+
@saml_response = encode_authn_response(
|
82
|
+
current_saml_user,
|
83
|
+
@saml_config.append_request_config(saml_request).merge!(
|
84
|
+
public_cert: current_sp_config.certificate,
|
85
|
+
private_key: current_sp_config.private_key,
|
86
|
+
pv_key_password: current_sp_config.pv_key_password
|
87
|
+
)
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
def saml_request
|
92
|
+
# GEM will decode SP initiated request which is mean @saml_request set by gem
|
93
|
+
sp_initiated_request? ? @saml_request : @saml_config.saml_request
|
94
|
+
end
|
95
|
+
|
96
|
+
def sp_initiated_request?
|
97
|
+
params[:SAMLRequest].present?
|
98
|
+
end
|
99
|
+
|
100
|
+
def current_sp_config
|
101
|
+
@current_sp_config ||= begin
|
102
|
+
instance_exec(request, &SamlIdpRails.config.saml_config_finder)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def current_saml_user
|
107
|
+
@current_saml_user ||= begin
|
108
|
+
instance_exec(request, &SamlIdpRails.config.saml_user_finder)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def store_authn_request
|
113
|
+
saml_authn_request = if request.post?
|
114
|
+
saml_post_req = { SAMLRequest: params[:SAMLRequest], RelayState: params[:RelayState] }
|
115
|
+
"#{request.fullpath}?#{saml_post_req.to_query}"
|
116
|
+
else
|
117
|
+
request.fullpath
|
118
|
+
end
|
119
|
+
|
120
|
+
session[:sp_config_id] = current_sp_config.id
|
121
|
+
session[:saml_auth_request] = saml_authn_request
|
122
|
+
|
123
|
+
redirect_to SamlIdpRails.config.sign_in_url, allow_other_host: true
|
124
|
+
end
|
125
|
+
|
126
|
+
def user_signed_in?
|
127
|
+
current_saml_user.present?
|
128
|
+
end
|
129
|
+
|
130
|
+
def load_config
|
131
|
+
@saml_config = SamlIdpRails::SamlConfig.new(current_sp_config, current_saml_user)
|
132
|
+
@saml_config.configure_saml_idp
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate_session
|
136
|
+
SamlIdpRails.config.session_validation_hook.call(session) if SamlIdpRails.config.session_validation_hook.present?
|
137
|
+
end
|
138
|
+
|
139
|
+
def generate_url(host:, **params)
|
140
|
+
"#{host}?#{params.to_query}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require "saml_idp"
|
2
|
+
|
3
|
+
module SamlIdpRails
|
4
|
+
class SamlSpConfig < ApplicationRecord
|
5
|
+
before_create :generate_uuid
|
6
|
+
|
7
|
+
if connection.adapter_name.downcase.starts_with?("sqlite")
|
8
|
+
serialize :name_id_formats, coder: JSON
|
9
|
+
serialize :assertion_consumer_services, coder: JSON
|
10
|
+
serialize :single_logout_services, type: Hash, coder: JSON
|
11
|
+
serialize :contact_person, type: Hash, coder: JSON
|
12
|
+
serialize :saml_attributes, coder: JSON
|
13
|
+
end
|
14
|
+
|
15
|
+
def parsed_metadata
|
16
|
+
metadata_attr = ::SamlIdp::IncomingMetadata.new(raw_metadata).to_h
|
17
|
+
# TODO: Move this logic to GEM
|
18
|
+
metadata_attr[:name_id_formats] = metadata_attr[:name_id_formats].to_a
|
19
|
+
if raw_metadata.present?
|
20
|
+
assign_attributes(metadata_attr.except(:unspecified_certificate))
|
21
|
+
# When SP metadata contains a <KeyDescriptor> that is not specified as signing or encryption,
|
22
|
+
# this method assigns the certificate to signing only.
|
23
|
+
self.signing_certificate = metadata_attr[:unspecified_certificate] if metadata_attr[:unspecified_certificate].present?
|
24
|
+
encoded_certificates
|
25
|
+
end
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# SP metadata removes header and footer of the certificate
|
30
|
+
# This method adds them back
|
31
|
+
def encoded_certificates
|
32
|
+
self.signing_certificate = format_with_pem(signing_certificate) if signing_certificate.present? && !pem_formatted?(signing_certificate)
|
33
|
+
self.encryption_certificate = format_with_pem(encryption_certificate) if encryption_certificate.present? && !pem_formatted?(encryption_certificate)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def generate_uuid
|
39
|
+
self.uuid = SecureRandom.uuid
|
40
|
+
end
|
41
|
+
|
42
|
+
def pem_formatted?(cert)
|
43
|
+
cert.scan(/(-----BEGIN CERTIFICATE-----)(.+?)(-----END CERTIFICATE-----)/m).any?
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_with_pem(cert)
|
47
|
+
"-----BEGIN CERTIFICATE-----\n#{cert.strip}\n-----END CERTIFICATE-----"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>SamlIdpRails</title>
|
5
|
+
<%= csrf_meta_tags %>
|
6
|
+
<%= csp_meta_tag %>
|
7
|
+
|
8
|
+
<%= yield :head %>
|
9
|
+
|
10
|
+
<%= stylesheet_link_tag "saml_idp_rails/application", media: "all" %>
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
|
14
|
+
<%= yield %>
|
15
|
+
|
16
|
+
</body>
|
17
|
+
</html>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Logging out from <%= @slo_request_params[:name] %></title>
|
5
|
+
</head>
|
6
|
+
<body>
|
7
|
+
<p>Logging out from <%= @slo_request_params[:name] %>...</p>
|
8
|
+
<form id="slo_form" action="<%= @slo_request_params[:location] %>" method="<%= @slo_request_params[:method] %>">
|
9
|
+
<% @slo_request_params[:params].each do |key, value| %>
|
10
|
+
<input type="hidden" name="<%= key %>" value="<%= value %>">
|
11
|
+
<% end %>
|
12
|
+
</form>
|
13
|
+
<script type="text/javascript">
|
14
|
+
// Automatically submit the form
|
15
|
+
document.getElementById('slo_form').submit();
|
16
|
+
</script>
|
17
|
+
</body>
|
18
|
+
</html>
|
@@ -0,0 +1,22 @@
|
|
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
|
+
<% if @sp_slo_binding == :redirect %>
|
7
|
+
<meta http-equiv="refresh" content="0;url=<%=@sp_slo_url%>">
|
8
|
+
<title>Redirecting...</title>
|
9
|
+
<% end %>
|
10
|
+
</head>
|
11
|
+
<% if @sp_slo_binding == :redirect %>
|
12
|
+
<p>This page has moved. If you are not redirected automatically, <a href="<%=@sp_slo_url%>">click here</a>.</p>
|
13
|
+
<% else %>
|
14
|
+
<body onload="document.forms[0].submit();" style="visibility:hidden;">
|
15
|
+
<%= form_tag(@sp_slo_endpoint) do %>
|
16
|
+
<%= hidden_field_tag("SAMLResponse", @saml_slo_response) %>
|
17
|
+
<%= hidden_field_tag("RelayState", SamlIdpRails.config.relay_state_url) %>
|
18
|
+
<%= submit_tag "Submit" %>
|
19
|
+
<% end %>
|
20
|
+
</body>
|
21
|
+
<% end %>
|
22
|
+
</html>
|
@@ -0,0 +1,14 @@
|
|
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
|
+
<%= hidden_field_tag("RelayState", params[:RelayState]) %>
|
11
|
+
<%= submit_tag "Submit" %>
|
12
|
+
<% end %>
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
SamlIdpRails::Engine.routes.draw do
|
2
|
+
get ":uuid/metadata", to: "saml_idp#metadata", as: "metadata"
|
3
|
+
post ":uuid/sso", to: "saml_idp#sso_request", as: "sso_post"
|
4
|
+
get ":uuid/sso", to: "saml_idp#sso_request", as: "sso_redirect"
|
5
|
+
post ":uuid/logout", to: "saml_idp#slo_request", as: "slo_post"
|
6
|
+
get ":uuid/logout", to: "saml_idp#slo_request", as: "slo_redirect"
|
7
|
+
post ":uuid/slo_request", to: "saml_idp#initiate_slo", as: "initiate_slo"
|
8
|
+
get ":uuid/attribute", to: "saml_idp#attribute", as: "attribute"
|
9
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
class CreateSamlIdpRailsSamlSpConfigs < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
create_table :saml_idp_rails_saml_sp_configs do |t|
|
4
|
+
# For identification
|
5
|
+
t.string :name
|
6
|
+
t.string :display_name
|
7
|
+
# SP attributes
|
8
|
+
t.string :entity_id
|
9
|
+
t.string :signing_certificate
|
10
|
+
t.string :encryption_certificate
|
11
|
+
t.boolean :sign_assertions
|
12
|
+
t.boolean :sign_authn_request
|
13
|
+
# IdP attributes
|
14
|
+
t.string :certificate
|
15
|
+
t.string :private_key
|
16
|
+
t.string :pv_key_password
|
17
|
+
t.string :relay_state
|
18
|
+
t.string :name_id_attribute
|
19
|
+
t.text :raw_metadata
|
20
|
+
|
21
|
+
if connection.adapter_name.downcase.starts_with?('postgresql')
|
22
|
+
# SP attributes
|
23
|
+
t.text :name_id_formats, array: true, default: []
|
24
|
+
t.jsonb :assertion_consumer_services, default: []
|
25
|
+
t.jsonb :single_logout_services, default: {}
|
26
|
+
# IdP attributes
|
27
|
+
t.jsonb :contact_person, default: {}
|
28
|
+
t.jsonb :saml_attributes, default: {}
|
29
|
+
elsif connection.adapter_name.downcase.starts_with?('mysql')
|
30
|
+
# SP attributes
|
31
|
+
t.text :name_id_formats, array: true, default: []
|
32
|
+
t.json :assertion_consumer_services, default: []
|
33
|
+
t.json :single_logout_services, default: {}
|
34
|
+
# IdP attributes
|
35
|
+
t.json :contact_person, default: {}
|
36
|
+
t.json :saml_attributes, default: {}
|
37
|
+
else
|
38
|
+
# SP attributes
|
39
|
+
t.text :name_id_formats, default: '[]'
|
40
|
+
t.text :assertion_consumer_services, default: '[]'
|
41
|
+
t.text :single_logout_services, default: '{}'
|
42
|
+
# IdP attributes
|
43
|
+
t.text :contact_person, default: '{}'
|
44
|
+
t.text :saml_attributes, default: '{}'
|
45
|
+
end
|
46
|
+
|
47
|
+
# Public UUID for SP
|
48
|
+
if connection.adapter_name.downcase.starts_with?('mysql')
|
49
|
+
t.string :uuid, null: false, default: -> { "UUID()" }
|
50
|
+
elsif connection.adapter_name.downcase.starts_with?('postgresql')
|
51
|
+
t.uuid :uuid, null: false, default: -> { "gen_random_uuid()" }
|
52
|
+
else
|
53
|
+
t.string :uuid, null: false
|
54
|
+
end
|
55
|
+
|
56
|
+
t.timestamps
|
57
|
+
end
|
58
|
+
|
59
|
+
add_index :saml_idp_rails_saml_sp_configs, :name, unique: true
|
60
|
+
add_index :saml_idp_rails_saml_sp_configs, :entity_id, unique: true
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "saml_idp"
|
2
|
+
|
3
|
+
module SamlIdpRails
|
4
|
+
class Config
|
5
|
+
ATTRIBUTES = %i[
|
6
|
+
base_url
|
7
|
+
sign_in_url
|
8
|
+
relay_state_url
|
9
|
+
session_validation_hook
|
10
|
+
saml_config_finder
|
11
|
+
saml_user_finder
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
attr_accessor *ATTRIBUTES
|
15
|
+
|
16
|
+
def configure(&block)
|
17
|
+
yield self if block_given?
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate!
|
22
|
+
ATTRIBUTES.each do |attribute|
|
23
|
+
raise("SamlIdpRails: #{attribute} is not set") if self.public_send(attribute).nil?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
require "saml_idp"
|
2
|
+
|
3
|
+
module SamlIdpRails
|
4
|
+
class SamlConfig
|
5
|
+
include SamlIdpRails::Engine.routes.url_helpers
|
6
|
+
|
7
|
+
attr_accessor :config
|
8
|
+
|
9
|
+
def initialize(sp_config, saml_user)
|
10
|
+
@config = {}
|
11
|
+
@config[:base_url] = SamlIdpRails.config.base_url
|
12
|
+
@config[:saml_config] = sp_config
|
13
|
+
@config[:saml_user] = saml_user
|
14
|
+
end
|
15
|
+
|
16
|
+
def configure_saml_idp
|
17
|
+
::SamlIdp.configure do |config|
|
18
|
+
config.x509_certificate = saml_config.certificate
|
19
|
+
config.secret_key = saml_config.private_key
|
20
|
+
config.password = saml_config.pv_key_password
|
21
|
+
config.algorithm = :sha256
|
22
|
+
config.organization_name = base_url
|
23
|
+
config.organization_url = base_url
|
24
|
+
# URL configuration
|
25
|
+
config.base_saml_location = base_url # TODO: Read from gem configuration
|
26
|
+
config.single_logout_service_post_location = slo_post_endpoint
|
27
|
+
config.single_logout_service_redirect_location = slo_redirect_endpoint
|
28
|
+
config.attribute_service_location = attribute_endpoint
|
29
|
+
config.single_service_post_location = sso_post_endpoint
|
30
|
+
config.single_service_redirect_location = sso_redirect_endpoint
|
31
|
+
# Name ID format
|
32
|
+
config.name_id.formats = name_id_format
|
33
|
+
config.attributes = saml_attributes_as_hash
|
34
|
+
config.service_provider.metadata_persister = metadata_persister
|
35
|
+
config.service_provider.persisted_metadata_getter = persisted_matadata
|
36
|
+
config.service_provider.finder = service_providers
|
37
|
+
config.logger = Rails.logger
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def append_request_config(saml_request)
|
42
|
+
config = {}
|
43
|
+
if saml_config.encryption_certificate.present?
|
44
|
+
config = {
|
45
|
+
encryption: {
|
46
|
+
cert: saml_config.encryption_certificate,
|
47
|
+
block_encryption: "aes256-cbc",
|
48
|
+
key_transport: "rsa-oaep-mgf1p"
|
49
|
+
}
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
config[:signed_assertion] = saml_config.sign_assertions
|
54
|
+
config[:signed_message] = true
|
55
|
+
|
56
|
+
# SP initiated SAML
|
57
|
+
if saml_request.present? && !saml_request.try(:idp_initiated?)
|
58
|
+
config[:acs_url] = saml_request.request["AssertionConsumerServiceURL"] if saml_request.authn_request?
|
59
|
+
return config
|
60
|
+
end
|
61
|
+
|
62
|
+
config.merge!(audience_uri: saml_config.entity_id)
|
63
|
+
end
|
64
|
+
|
65
|
+
def idp_metadata
|
66
|
+
SamlIdp.metadata.signed
|
67
|
+
end
|
68
|
+
|
69
|
+
def saml_request
|
70
|
+
@saml_request ||= Struct.new(
|
71
|
+
:request_id,
|
72
|
+
:issue_url,
|
73
|
+
:acs_url
|
74
|
+
) do
|
75
|
+
def authn_request?
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
79
|
+
def idp_initiated?
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
def issuer
|
84
|
+
url = URI(issue_url)
|
85
|
+
url.query = nil
|
86
|
+
url.to_s
|
87
|
+
end
|
88
|
+
end.new(nil, base_url, default_acs_config[:location])
|
89
|
+
end
|
90
|
+
|
91
|
+
def name_id_value(attribute_name = nil)
|
92
|
+
attr = attribute_name.presence || saml_user.name_id_attribute
|
93
|
+
val = saml_user.public_send(attr) if saml_user.respond_to?(attr)
|
94
|
+
raise("SamlIdpRails: Name ID attribute #{attr} is not set") if val.blank?
|
95
|
+
val
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def service_providers
|
101
|
+
lambda { |_issuer_or_entity_id|
|
102
|
+
{
|
103
|
+
response_hosts: saml_config.assertion_consumer_services.map do |acs|
|
104
|
+
url = acs["location"] || acs[:location]
|
105
|
+
URI(url).host
|
106
|
+
end,
|
107
|
+
acs_url: default_acs_config[:location],
|
108
|
+
cert: (saml_config.signing_certificate.present? ? saml_config.signing_certificate : nil),
|
109
|
+
fingerprint: (saml_config.signing_certificate.present? ? SamlIdp::Fingerprint.certificate_digest(saml_config.signing_certificate, :sha256) : nil),
|
110
|
+
assertion_consumer_logout_service_url: saml_config.single_logout_services.values.first,
|
111
|
+
sign_authn_request: saml_config.sign_authn_request
|
112
|
+
}
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def persisted_matadata
|
117
|
+
lambda { |_identifier, _|
|
118
|
+
# TODO: eliminate raw metadata usage
|
119
|
+
SamlIdp::IncomingMetadata.new(saml_config.raw_metadata)
|
120
|
+
}
|
121
|
+
end
|
122
|
+
|
123
|
+
# We don't need support it because, scary XML can be there
|
124
|
+
# TODO: Remove this method from GEM
|
125
|
+
def metadata_persister
|
126
|
+
lambda { |_identifier, _service_provider|
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
def default_name_idp_format
|
131
|
+
{
|
132
|
+
"1.1" => {
|
133
|
+
email_address: lambda { |_principal|
|
134
|
+
name_id_value
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
def name_id_format
|
141
|
+
first_name_format = saml_config.name_id_formats.first.to_s
|
142
|
+
first_name_format = "unspecified" if first_name_format.blank?
|
143
|
+
{
|
144
|
+
name_id_format_version(first_name_format).to_s => {
|
145
|
+
# TODO: Remove lambdas from GEM
|
146
|
+
first_name_format => lambda { |_principal|
|
147
|
+
name_id_value
|
148
|
+
}
|
149
|
+
}
|
150
|
+
}
|
151
|
+
end
|
152
|
+
|
153
|
+
def name_id_format_version(parsed_format)
|
154
|
+
return "1.1" if %w[email_address unspecified].include?(parsed_format.underscore)
|
155
|
+
|
156
|
+
"2.0"
|
157
|
+
end
|
158
|
+
|
159
|
+
def default_acs_config
|
160
|
+
(saml_config.assertion_consumer_services.first || {}).with_indifferent_access
|
161
|
+
end
|
162
|
+
|
163
|
+
def saml_attributes_as_hash
|
164
|
+
config_attribute = {}
|
165
|
+
saml_config.saml_attributes.each do |attribute|
|
166
|
+
# TODO: Resolve this issue on GEM side
|
167
|
+
attribute["name_format"] = attribute["nameFormat"] if attribute["nameFormat"].present?
|
168
|
+
|
169
|
+
config_attribute[attribute["friendlyName"]] = attribute.except("friendlyName")
|
170
|
+
# TODO: Remove lambdas from GEM
|
171
|
+
config_attribute[attribute["friendlyName"]]["getter"] = lambda { |_principal|
|
172
|
+
saml_attribute_getters(attribute["getter"])
|
173
|
+
}
|
174
|
+
end
|
175
|
+
config_attribute
|
176
|
+
end
|
177
|
+
|
178
|
+
def saml_attribute_getters(config_value)
|
179
|
+
attribute_value = user_attribute(config_value)
|
180
|
+
attribute_type = attribute_value.class.to_s
|
181
|
+
# Fixed string value
|
182
|
+
return config_value.to_s if attribute_value.blank?
|
183
|
+
|
184
|
+
case attribute_type
|
185
|
+
when "Array"
|
186
|
+
attribute_value.map(&:to_s)
|
187
|
+
when "Hash"
|
188
|
+
attribute_value.to_json
|
189
|
+
when "Integer"
|
190
|
+
attribute_value.to_s
|
191
|
+
when "String"
|
192
|
+
attribute_value
|
193
|
+
else
|
194
|
+
""
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def user_attribute(key)
|
199
|
+
saml_user.respond_to?(key) ? saml_user.public_send(key) : saml_user[key]
|
200
|
+
end
|
201
|
+
|
202
|
+
def metadata_endpoint
|
203
|
+
@config[:metadata_url] || metadata_url(uuid: saml_config.uuid, host: base_url)
|
204
|
+
end
|
205
|
+
|
206
|
+
def slo_post_endpoint
|
207
|
+
@config[:slo_post_url] || slo_post_url(uuid: saml_config.uuid, host: base_url)
|
208
|
+
end
|
209
|
+
|
210
|
+
# Form converts REDIRECT to POST
|
211
|
+
def slo_redirect_endpoint
|
212
|
+
@config[:metadata_url] || slo_redirect_url(uuid: saml_config.uuid, host: base_url)
|
213
|
+
end
|
214
|
+
|
215
|
+
def attribute_endpoint
|
216
|
+
@config[:attribute_url] || attribute_url(uuid: saml_config.uuid, host: base_url)
|
217
|
+
end
|
218
|
+
|
219
|
+
def sso_post_endpoint
|
220
|
+
@config[:sso_post_url] || sso_post_url(uuid: saml_config.uuid, host: base_url)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Form converts REDIRECT to POST
|
224
|
+
def sso_redirect_endpoint
|
225
|
+
@config[:sso_redirect_url] || sso_redirect_url(uuid: saml_config.uuid, host: base_url)
|
226
|
+
end
|
227
|
+
|
228
|
+
def saml_user
|
229
|
+
@config[:saml_user] || raise("SamlIdpRails: saml_user is not set")
|
230
|
+
end
|
231
|
+
|
232
|
+
def saml_config
|
233
|
+
@config[:saml_config] || raise("SamlIdpRails: saml_config is not set")
|
234
|
+
end
|
235
|
+
|
236
|
+
def base_url
|
237
|
+
@config[:base_url] || raise("SamlIdpRails: base_url is not set")
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "saml_idp_rails/version"
|
2
|
+
require "saml_idp_rails/engine"
|
3
|
+
require "saml_idp_rails/config"
|
4
|
+
|
5
|
+
module SamlIdpRails
|
6
|
+
class << self
|
7
|
+
def configure(&block)
|
8
|
+
@config = Config.new.configure(&block)
|
9
|
+
@config.validate!
|
10
|
+
end
|
11
|
+
|
12
|
+
def config
|
13
|
+
@config
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: saml_idp_rails
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- zogoo
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-03-23 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rails
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: 8.0.1
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 8.0.1
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: saml_idp
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rubocop
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: debug
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: ruby-saml
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
description: SamlIdpRails is open source Idp controller for Rails.
|
83
|
+
email:
|
84
|
+
- ch.zogoo@gmail.com
|
85
|
+
executables: []
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files: []
|
88
|
+
files:
|
89
|
+
- MIT-LICENSE
|
90
|
+
- README.md
|
91
|
+
- Rakefile
|
92
|
+
- app/assets/stylesheets/saml_idp_rails/application.css
|
93
|
+
- app/controllers/saml_idp_rails/application_controller.rb
|
94
|
+
- app/controllers/saml_idp_rails/saml_idp_controller.rb
|
95
|
+
- app/helpers/saml_idp_rails/application_helper.rb
|
96
|
+
- app/jobs/saml_idp_rails/application_job.rb
|
97
|
+
- app/mailers/saml_idp_rails/application_mailer.rb
|
98
|
+
- app/models/saml_idp_rails/application_record.rb
|
99
|
+
- app/models/saml_idp_rails/saml_sp_config.rb
|
100
|
+
- app/views/layouts/saml_idp_rails/application.html.erb
|
101
|
+
- app/views/saml_idp_rails/saml_idp/slo_request.html.erb
|
102
|
+
- app/views/saml_idp_rails/saml_idp/slo_response.html.erb
|
103
|
+
- app/views/saml_idp_rails/saml_idp/sso_response.html.erb
|
104
|
+
- config/routes.rb
|
105
|
+
- db/migrate/20250120165545_create_saml_idp_rails_saml_sp_configs.rb
|
106
|
+
- lib/saml_idp_rails.rb
|
107
|
+
- lib/saml_idp_rails/config.rb
|
108
|
+
- lib/saml_idp_rails/engine.rb
|
109
|
+
- lib/saml_idp_rails/railtie.rb
|
110
|
+
- lib/saml_idp_rails/saml_config.rb
|
111
|
+
- lib/saml_idp_rails/version.rb
|
112
|
+
- lib/tasks/saml_idp_rails_tasks.rake
|
113
|
+
homepage: https://github.com/zogoo/saml_idp_rails
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata:
|
117
|
+
homepage_uri: https://github.com/zogoo/saml_idp_rails
|
118
|
+
source_code_uri: https://github.com/zogoo/saml_idp_rails
|
119
|
+
changelog_uri: https://github.com/Zogoo/saml_idp_rails/blob/master/CHANGELOG.md
|
120
|
+
rdoc_options: []
|
121
|
+
require_paths:
|
122
|
+
- lib
|
123
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: 3.4.1
|
128
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
133
|
+
requirements: []
|
134
|
+
rubygems_version: 3.6.3
|
135
|
+
specification_version: 4
|
136
|
+
summary: Idp controller for Rails
|
137
|
+
test_files: []
|