rack-saml 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +7 -0
- data/README.md +171 -0
- data/Rakefile +7 -0
- data/bin/conv_metadata.rb +68 -0
- data/config/attribute-map.yml +8 -0
- data/config/attribute-map.yml.sample +15 -0
- data/config/metadata.yml +32 -0
- data/config/saml.yml +6 -0
- data/lib/rack-saml.rb +1 -0
- data/lib/rack-saml/version.rb +6 -0
- data/lib/rack/saml.rb +108 -0
- data/lib/rack/saml/metadata/abstract_metadata.rb +16 -0
- data/lib/rack/saml/metadata/onelogin_metadata.rb +18 -0
- data/lib/rack/saml/metadata/opensaml_metadata.rb +12 -0
- data/lib/rack/saml/metadata_handler.rb +19 -0
- data/lib/rack/saml/misc/onelogin_setting.rb +18 -0
- data/lib/rack/saml/request/abstract_request.rb +16 -0
- data/lib/rack/saml/request/onelogin_request.rb +18 -0
- data/lib/rack/saml/request/opensaml_request.rb +12 -0
- data/lib/rack/saml/request_handler.rb +19 -0
- data/lib/rack/saml/response/abstract_response.rb +16 -0
- data/lib/rack/saml/response/onelogin_response.rb +26 -0
- data/lib/rack/saml/response/opensaml_response.rb +12 -0
- data/lib/rack/saml/response_handler.rb +29 -0
- data/rack-saml.gemspec +20 -0
- metadata +82 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
# SAML (Shibboleth SP) middleware for Rack
|
2
|
+
|
3
|
+
This project is deeply inspired by rack-shibboleth and ruby-saml. It is recommended to use the defact SAML implementation such as OpenSAML from the security or the functional aspect. However, there are also requirements to use SAML for light weight applications implemented by Ruby. rack-shibboleth may be a candidate to support such kind of objective. However it lacks the configurability to fit OmniAuth and the upgrade path to secure and stable middleware as OpenSAML. So thus I just implemented a prototype to support SAML (Shibboleth SP) for Rack middleware.
|
4
|
+
|
5
|
+
OmniAuth Shibboleth Strategy
|
6
|
+
https://github.com/toyokazu/omniauth-shibboleth
|
7
|
+
|
8
|
+
## Limitations
|
9
|
+
|
10
|
+
### Signing AuthnRequest
|
11
|
+
|
12
|
+
Current implementation supports only Onelogin SAML assertion handler. It does not support to sign AuthnRequest.
|
13
|
+
|
14
|
+
### Encrypted Assertion
|
15
|
+
|
16
|
+
Current implementation does not support assertion encryption. So thus, assertion encription function should be disabled for rack-saml SPs.
|
17
|
+
|
18
|
+
### SP Session
|
19
|
+
|
20
|
+
Current implementation does not support keeping session at SP side.
|
21
|
+
|
22
|
+
## Getting Started
|
23
|
+
|
24
|
+
### Installation
|
25
|
+
|
26
|
+
% gem install rack-saml
|
27
|
+
|
28
|
+
### Setup Gemfile
|
29
|
+
|
30
|
+
% cd rails-app
|
31
|
+
% vi Gemfile
|
32
|
+
gem 'rack-saml'
|
33
|
+
|
34
|
+
### Setup Rack::Saml middleware
|
35
|
+
|
36
|
+
#### For Rack applicaitons
|
37
|
+
|
38
|
+
In the following example, config.ru is used to add Rack::Saml middleware into a Rails application.
|
39
|
+
|
40
|
+
% vi config.ru
|
41
|
+
use Rack::Saml, {:saml_config => "#{Rails.root}/config/saml.yml",
|
42
|
+
:metadata => "#{Rails.root}/config/metadata.yml",
|
43
|
+
:attribute_map => "#{Rails.root}/config/attribute-map.yml",
|
44
|
+
:shib_app_id => "default"}
|
45
|
+
|
46
|
+
#### For Ralis applications
|
47
|
+
|
48
|
+
In the following example, config/application.rb is used to Rack::Saml middleware into a Rails application.
|
49
|
+
|
50
|
+
% vi config/application.rb
|
51
|
+
module TestRackSaml
|
52
|
+
class Application < Rails::Application
|
53
|
+
config.middleware.use Rack::Saml, {:saml_config => "#{Rails.root}/config/saml.yml",
|
54
|
+
:metadata => "#{Rails.root}/config/metadata.yml",
|
55
|
+
:attribute_map => "#{Rails.root}/config/attribute-map.yml"
|
56
|
+
:shib_app_id => "default"}
|
57
|
+
...
|
58
|
+
|
59
|
+
If you just want to test Rack::Saml, you can ommit middleware options in the both example (config.ru or config/application.rb).
|
60
|
+
|
61
|
+
use Rack::Saml
|
62
|
+
|
63
|
+
However, you can not omit :shib_app_id option if you want to use this middleware with OmniAuth::Shibboleth Strategy.
|
64
|
+
|
65
|
+
Rack::Saml uses default configurations located in the rack-saml gem path.
|
66
|
+
|
67
|
+
$GEM_HOME/rack-saml-x.x.x/config/xxx.yml
|
68
|
+
|
69
|
+
#### Middleware options
|
70
|
+
|
71
|
+
* *:saml_config* path to saml.yml file
|
72
|
+
* *:metadata* path to metadata.yml file
|
73
|
+
* *:attribute_map* path to attribute-map.yml file
|
74
|
+
* *:shib_app_id* If you want to use the middleware as Shibboleth SP, you should specify an application ID. In the Shibboleth SP default configuration, 'default' is used as the application ID.
|
75
|
+
|
76
|
+
|
77
|
+
#### Configuration files
|
78
|
+
|
79
|
+
You can find default configuration files at
|
80
|
+
|
81
|
+
$GEM_HOME/rack-saml-x.x.x/config/
|
82
|
+
|
83
|
+
##### saml.yml
|
84
|
+
|
85
|
+
Configuration to set SAML parameters.
|
86
|
+
|
87
|
+
* *protected_path* string or regular expression of the path name where SAML protects, e.g. /auth/shibboleth/callback or '^\/secure\/[^\s]*'
|
88
|
+
* *protected_path_regexp* use regular expression to match protected_path or not e.g. true / false
|
89
|
+
* *metadata_path* the path name where SP's metadata is generated
|
90
|
+
* *assertion_handler* 'onelogin' / 'opensaml' (not implemented yet)
|
91
|
+
* *saml_idp* idp_entity_id
|
92
|
+
* *saml_sp* sp_entity_id (self id)
|
93
|
+
* *sp_cert* path to the SAML SP's certificate file, e.g. cert.pem (signing AuthnRequest is not supported yet)
|
94
|
+
* *sp_key* path to the SAML SP's key file, e.g. key.pem (signing AuthnRequest is not supported yet)
|
95
|
+
|
96
|
+
##### metadata.yml
|
97
|
+
|
98
|
+
To connect to an IdP, you must describe IdP's specification. In rack-saml, it should be written in metadata.yml. metadata.yml file include the following lists.
|
99
|
+
|
100
|
+
* *idp_lists* list of IdP metadata
|
101
|
+
* *sp_lists* list of SP metadata
|
102
|
+
|
103
|
+
idp_lists and sp_lists are hashes which have entity ids as key values.
|
104
|
+
|
105
|
+
parameters of the idp_lists:
|
106
|
+
|
107
|
+
* *certificate* base64 encoded certificate of IdP
|
108
|
+
* *saml2_http_redirect* Location attribute of the IdP's assertion handler uri with HTTP Redirect Binding
|
109
|
+
|
110
|
+
parameters of the sp_lists (currently not used):
|
111
|
+
|
112
|
+
* *certificate* base64 encoded certificate of SP
|
113
|
+
* *saml2_http_post* Location attribute of the SP's assertion consumer uri with HTTP POST Binding
|
114
|
+
|
115
|
+
These parameters are automatically extracted from SAML metadata. You can use conv_metadata.rb command for extraction.
|
116
|
+
|
117
|
+
% $GEM_HOME/rack-saml-x.x.x/bin/conv_metadata.rb metadata.xml > metadata.yml
|
118
|
+
|
119
|
+
##### attribute-map.yml
|
120
|
+
|
121
|
+
attribute-map.yml can extract attributes from SAML Response and put attributes on request environment variables. It is useful to pass attributes into applications. The configuration file format is as follows:
|
122
|
+
|
123
|
+
"Attribute Name": "Environment Variable Name"
|
124
|
+
"urn:oid:0.9.2342.19200300.100.1.1": "uid"
|
125
|
+
...
|
126
|
+
|
127
|
+
### Setup IdP to accept rack-saml SP
|
128
|
+
|
129
|
+
#### SP Metadata generation
|
130
|
+
|
131
|
+
To connect a new SP to the existing IdP, you need to import SP's metadata into the IdP. rack-saml provides metadata generation function. It is generated at '/Shibboleth.sso/Metadata' by default.
|
132
|
+
|
133
|
+
#### IdP configuration examples not to encrypt assertion
|
134
|
+
|
135
|
+
Current rack-saml implementation does not support assertion encryption because Onelogin::Saml does not support signature and encryption of assertion. So thus, in the followings, we would like to show sample configurations to disable encryption in IdP assertion processing. These are not recommended for sensitive applications.
|
136
|
+
|
137
|
+
##### Shibboleth IdP example
|
138
|
+
|
139
|
+
Add the following configuration after <rp:DefaultRelyingParty> in relying-party.xml. You should specify sp entity id at the 'id' and the 'provider' attributes.
|
140
|
+
|
141
|
+
% vi $IDP_HOME/conf/relying-party.xml
|
142
|
+
...
|
143
|
+
<rp:RelyingParty id="http://example.com:3000/auth/shibboleth/callback" provider="http://example.com:3000/auth/shibboleth/callback" defaultSigningCredentialRef="IdPCredential">
|
144
|
+
<rp:ProfileConfiguration xsi:type="saml:SAML2SSOProfile" includeAttributeStatement="true" assertionLifetime="PT5M" assertionProxyCount="0" signResponses="never" signAssertions="always" encryptAssertions="never" encryptNameIds="never"/>
|
145
|
+
</rp:RelyingParty>
|
146
|
+
|
147
|
+
## TODO
|
148
|
+
|
149
|
+
* ruby-opensaml (I hope someone implement it :)
|
150
|
+
|
151
|
+
## License
|
152
|
+
|
153
|
+
Copyright (C) 2011 by Toyokazu Akiyama.
|
154
|
+
|
155
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
156
|
+
of this software and associated documentation files (the "Software"), to deal
|
157
|
+
in the Software without restriction, including without limitation the rights
|
158
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
159
|
+
copies of the Software, and to permit persons to whom the Software is
|
160
|
+
furnished to do so, subject to the following conditions:
|
161
|
+
|
162
|
+
The above copyright notice and this permission notice shall be included in
|
163
|
+
all copies or substantial portions of the Software.
|
164
|
+
|
165
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
166
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
167
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
168
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
169
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
170
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
171
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rexml/document'
|
3
|
+
require 'uri'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
DS = 'http://www.w3.org/2000/09/xmldsig#'
|
7
|
+
|
8
|
+
if ARGV.size < 1
|
9
|
+
puts "outputs yaml format metadata file"
|
10
|
+
puts "usage: conv_metadata.rb metadata_file"
|
11
|
+
exit(1)
|
12
|
+
end
|
13
|
+
|
14
|
+
file = File.new(ARGV[0])
|
15
|
+
doc = REXML::Document.new(file)
|
16
|
+
|
17
|
+
def get_list_type(elem)
|
18
|
+
if !elem.elements["IDPSSODescriptor"].nil?
|
19
|
+
return "idp_lists"
|
20
|
+
end
|
21
|
+
"sp_lists"
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_entity_hash(elem, list_type)
|
25
|
+
case list_type
|
26
|
+
when "idp_lists"
|
27
|
+
idp_elem = elem.elements["IDPSSODescriptor"]
|
28
|
+
# the first certificate is used
|
29
|
+
certificate = "-----BEGIN CERTIFICATE-----#{REXML::XPath.first(idp_elem, './/ds:X509Certificate', 'ds' => DS).text.gsub(/\s*$/, "")}\n-----END CERTIFICATE-----"
|
30
|
+
saml2_http_redirect = nil
|
31
|
+
idp_elem.elements.each("SingleSignOnService") do |e|
|
32
|
+
if e.attributes["Binding"] == "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
33
|
+
saml2_http_redirect = e.attributes["Location"]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
return {"certificate" => certificate,
|
37
|
+
"saml2_http_redirect" => saml2_http_redirect}
|
38
|
+
when "sp_lists"
|
39
|
+
sp_elem = elem.elements["SPSSODescriptor"]
|
40
|
+
# the first certificate is used
|
41
|
+
certificate = REXML::XPath.first(sp_elem, './/ds:X509Certificate', 'ds' => DS).text
|
42
|
+
saml2_http_post = nil
|
43
|
+
sp_elem.elements.each("AssertionConsumerService") do |e|
|
44
|
+
if e.attributes["Binding"] == "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
45
|
+
saml2_http_post = e.attributes["Location"]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
return {"certificate" => certificate,
|
49
|
+
"saml2_http_post" => saml2_http_post}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_entities(entities, elem)
|
54
|
+
list_type = get_list_type(elem)
|
55
|
+
entity_id = elem.attributes["entityID"]
|
56
|
+
entities[list_type][entity_id] = create_entity_hash(elem, list_type)
|
57
|
+
end
|
58
|
+
|
59
|
+
entities = {"idp_lists" => {}, "sp_lists" => {}}
|
60
|
+
doc.elements.each("EntityDescriptor") do |elem|
|
61
|
+
add_entities(entities, elem)
|
62
|
+
end
|
63
|
+
|
64
|
+
doc.elements.each("EntitiesDescriptor/EntityDescriptor") do |elem|
|
65
|
+
add_entities(entities, elem)
|
66
|
+
end
|
67
|
+
|
68
|
+
puts entities.to_yaml
|
@@ -0,0 +1,8 @@
|
|
1
|
+
"urn:oid:1.3.6.1.4.1.5923.1.1.1.6": "eppn"
|
2
|
+
"urn:oid:1.3.6.1.4.1.5923.1.1.1.9": "affiliation"
|
3
|
+
"urn:oid:1.3.6.1.4.1.5923.1.1.1.1": "unscoped-affiliation"
|
4
|
+
"urn:oid:1.3.6.1.4.1.5923.1.1.1.7": "entitlement"
|
5
|
+
"urn:oid:0.9.2342.19200300.100.1.1": "uid"
|
6
|
+
"urn:oid:0.9.2342.19200300.100.1.3": "mail"
|
7
|
+
"urn:oid:2.16.840.1.113730.3.1.241": "displayName"
|
8
|
+
"urn:oid:1.3.6.1.4.1.5923.1.1.1.10": "persistent-id"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"urn:mace:dir:attribute-def:eduPersonPrincipalName": "eppn"
|
2
|
+
"urn:oid:1.3.6.1.4.1.5923.1.1.1.6": "eppn"
|
3
|
+
"urn:mace:dir:attribute-def:eduPersonScopedAffiliation": "affiliation"
|
4
|
+
"urn:oid:1.3.6.1.4.1.5923.1.1.1.9": "affiliation"
|
5
|
+
"urn:mace:dir:attribute-def:eduPersonAffiliation": "unscoped-affiliation"
|
6
|
+
"urn:oid:1.3.6.1.4.1.5923.1.1.1.1": "unscoped-affiliation"
|
7
|
+
"urn:mace:dir:attribute-def:eduPersonEntitlement": "entitlement"
|
8
|
+
"urn:oid:1.3.6.1.4.1.5923.1.1.1.7": "entitlement"
|
9
|
+
"urn:mace:dir:attribute-def:uid": "uid"
|
10
|
+
"urn:oid:0.9.2342.19200300.100.1.1": "uid"
|
11
|
+
"urn:mace:dir:attribute-def:mail": "mail"
|
12
|
+
"urn:oid:0.9.2342.19200300.100.1.3": "mail"
|
13
|
+
"urn:mace:dir:attribute-def:displayName": "displayName"
|
14
|
+
"urn:oid:2.16.840.1.113730.3.1.241": "displayName"
|
15
|
+
"urn:oid:1.3.6.1.4.1.5923.1.1.1.10": "persistent-id"
|
data/config/metadata.yml
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
---
|
2
|
+
idp_lists:
|
3
|
+
https://localhost/idp/shibboleth:
|
4
|
+
certificate: |-
|
5
|
+
-----BEGIN CERTIFICATE-----
|
6
|
+
MIIEKDCCAxCgAwIBAgIJALh5qhU6z0gMMA0GCSqGSIb3DQEBBQUAMIGKMQswCQYD
|
7
|
+
VQQGEwJKUDERMA8GA1UEBwwIQWNhZGVtZTIxIDAeBgNVBAoMF0t5b3RvIFNhbmd5
|
8
|
+
byBVbml2ZXJzaXR5MTQwMgYDVQQLDCtGYWN1bHR5IG9mIENvbXB1dGVyIFNjaWVu
|
9
|
+
Y2UgYW5kIEVuZ2luZWVyaW5nMRAwDgYDVQQDDAdUZXN0IENBMB4XDTExMDgxMDA3
|
10
|
+
MTY1OFoXDTEyMDgwOTA3MTY1OFowgZ4xCzAJBgNVBAYTAkpQMREwDwYDVQQHDAhB
|
11
|
+
Y2FkZW1lMjEgMB4GA1UECgwXS3lvdG8gU2FuZ3lvIFVuaXZlcnNpdHkxNDAyBgNV
|
12
|
+
BAsMK0ZhY3VsdHkgb2YgQ29tcHV0ZXIgU2NpZW5jZSBhbmQgRW5naW5lZXJpbmcx
|
13
|
+
JDAiBgNVBAMMG2lkcC50ZXN0LmNzZS5reW90by1zdS5hYy5qcDCCASIwDQYJKoZI
|
14
|
+
hvcNAQEBBQADggEPADCCAQoCggEBAN1/g5HoajtIFLrFGEt6z6vyWS7CNhz/nOw9
|
15
|
+
7Ei0R7TYg/iwz3CuJdowlz7VnVTi/Oi2kz+YxuLw3RDwl5r16AKRCePyc0F63xSv
|
16
|
+
5rc6kJkXBqGZlKtSn5OeSa+w18c4MOtu4zSZP7wHhS5bMABUL8UBX1P021bPd7Ad
|
17
|
+
6gMt9mCF+0giIyTWMP8PH8EoDTMPq+ko1QosxSxPSaERB2+OEmwBa4NnKyBmB9lU
|
18
|
+
NrWN5tB+BJ8qD9C+5EU+miWFzY59GC1JPc7TmQ28frobmgc22pUc6d/0+uBELEbx
|
19
|
+
v7G942PYDCdF1V0chRyGAP4/7IAH77B5t6wIqhLBffawb993mi8CAwEAAaN7MHkw
|
20
|
+
CQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2Vy
|
21
|
+
dGlmaWNhdGUwHQYDVR0OBBYEFDpB4W//bAwVauJd/pvUOfZl72eqMB8GA1UdIwQY
|
22
|
+
MBaAFINIlekZI9dBQNn6x8vCKxS3h6CSMA0GCSqGSIb3DQEBBQUAA4IBAQBZwuZp
|
23
|
+
TyczweI8L68yzkq//5ORzkoJtW29aftSfWrXIO4/ckydyqYNHW1H62J4QtMxljHG
|
24
|
+
ZK+GAALGKIAYQTD805Ha4tezY/bpXB1HTu+E2e2jL6AmYEP62WcFdCmnPS7DSQ78
|
25
|
+
LDLDDmrPBRfNTVxgjEq1GRS1JfQJb8JrNipG+YqCinNVKuEx4wsc7bIRbY0YZrVp
|
26
|
+
+sRk6BB3HrOY1p+F/83in45wyNxGfgZpmdAvk9yB8ubzBhDaaNSqeU33iOGsqSBH
|
27
|
+
jD2RyhOELbPlOMEHn+q2vSZdlo4hqRpamhahSBsuiQFbcwpTkWJ3SJyjYYn845Xw
|
28
|
+
Uq9SgXh2ssVUFkni
|
29
|
+
-----END CERTIFICATE-----
|
30
|
+
saml2_http_redirect: https://localhost/idp/profile/SAML2/Redirect/SSO
|
31
|
+
sp_lists: {}
|
32
|
+
|
data/config/saml.yml
ADDED
data/lib/rack-saml.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'rack/saml'
|
data/lib/rack/saml.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
# Rack::Saml
|
6
|
+
#
|
7
|
+
# As the Shibboleth SP, Rack::Saml::Base adopts :protected_path
|
8
|
+
# as an :assertion_consumer_path. It is easy to configure and
|
9
|
+
# support omniauth-shibboleth.
|
10
|
+
# To establish single path behavior, it currently supports only
|
11
|
+
# HTTP Redirect Binding from SP to Idp
|
12
|
+
# HTTP POST Binding from IdP to SP
|
13
|
+
class Saml
|
14
|
+
autoload "RequestHandler", 'rack/saml/request_handler'
|
15
|
+
autoload "MetadataHandler", 'rack/saml/metadata_handler'
|
16
|
+
autoload "ResponseHandler", 'rack/saml/response_handler'
|
17
|
+
|
18
|
+
class SamlAssertionError < StandardError
|
19
|
+
end
|
20
|
+
|
21
|
+
def default_config_path(config_file)
|
22
|
+
::File.expand_path("../../../config/#{config_file}", __FILE__)
|
23
|
+
end
|
24
|
+
|
25
|
+
def default_saml_config
|
26
|
+
default_config_path('saml.yml')
|
27
|
+
end
|
28
|
+
|
29
|
+
def default_metadata
|
30
|
+
default_config_path('metadata.yml')
|
31
|
+
end
|
32
|
+
|
33
|
+
def default_attribute_map
|
34
|
+
default_config_path('attribute-map.yml')
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize app, opts = {}
|
38
|
+
@app = app
|
39
|
+
@opts = opts
|
40
|
+
|
41
|
+
if @opts[:saml_config].nil? || !::File.exists?(@opts[:saml_config])
|
42
|
+
@opts[:saml_config] = default_saml_config
|
43
|
+
end
|
44
|
+
@saml_config = YAML.load_file(@opts[:saml_config])
|
45
|
+
if @saml_config['assertion_handler'].nil?
|
46
|
+
raise ArgumentError, "'assertion_handler' parameter should be specified in the :saml_config file"
|
47
|
+
end
|
48
|
+
if @opts[:metadata].nil? || !::File.exists?(@opts[:metadata])
|
49
|
+
@opts[:metadata] = default_metadata
|
50
|
+
end
|
51
|
+
@metadata = YAML.load_file(@opts[:metadata])
|
52
|
+
if @opts[:attribute_map].nil? || !::File.exists?(@opts[:attribute_map])
|
53
|
+
@opts[:attribute_map] = default_attribute_map
|
54
|
+
end
|
55
|
+
@attribute_map = YAML.load_file(@opts[:attribute_map])
|
56
|
+
end
|
57
|
+
|
58
|
+
def call env
|
59
|
+
request = Rack::Request.new env
|
60
|
+
#return [
|
61
|
+
# 403,
|
62
|
+
# {
|
63
|
+
# 'Content-Type' => 'text/plain'
|
64
|
+
# },
|
65
|
+
# ["Forbidden." + request.inspect]
|
66
|
+
# ["Forbidden." + env.to_a.map {|i| "#{i[0]}: #{i[1]}"}.join("\n")]
|
67
|
+
#]
|
68
|
+
if request.request_method == 'GET'
|
69
|
+
if match_protected_path?(request) # generate AuthnRequest
|
70
|
+
handler = RequestHandler.new(request, @saml_config, @metadata['idp_lists'][@saml_config['saml_idp']])
|
71
|
+
return Rack::Response.new.tap { |r|
|
72
|
+
r.redirect handler.authn_request.redirect_uri
|
73
|
+
}.finish
|
74
|
+
elsif match_metadata_path?(request) # generate Metadata
|
75
|
+
handler = MetadataHandler.new(request, @saml_config, @metadata['idp_lists'][@saml_config['saml_idp']])
|
76
|
+
return [
|
77
|
+
200,
|
78
|
+
{
|
79
|
+
'Content-Type' => 'application/samlmetadata+xml'
|
80
|
+
},
|
81
|
+
[handler.sp_metadata.generate]
|
82
|
+
]
|
83
|
+
end
|
84
|
+
elsif request.request_method == 'POST' && match_protected_path?(request) # process Response
|
85
|
+
handler = ResponseHandler.new(request, @saml_config, @metadata['idp_lists'][@saml_config['saml_idp']])
|
86
|
+
if handler.response.is_valid?
|
87
|
+
handler.extract_attrs(env, @attribute_map, @opts)
|
88
|
+
else
|
89
|
+
raise SamlAssertionError, "Invalid SAML response."
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
@app.call env
|
94
|
+
end
|
95
|
+
|
96
|
+
def match_protected_path?(request)
|
97
|
+
if @saml_config['protected_path_regexp']
|
98
|
+
# to be fixed (Regexp)
|
99
|
+
return (request.path_info =~ Regexp.new(@saml_config['protected_path']))
|
100
|
+
end
|
101
|
+
request.path_info == @saml_config['protected_path']
|
102
|
+
end
|
103
|
+
|
104
|
+
def match_metadata_path?(request)
|
105
|
+
request.path_info == @saml_config['metadata_path']
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rack
|
2
|
+
class Saml
|
3
|
+
class AbstractMetadata
|
4
|
+
attr_reader :request, :saml_config, :metadata
|
5
|
+
|
6
|
+
def initialize(request, saml_config, metadata)
|
7
|
+
@request = request
|
8
|
+
@saml_config = saml_config
|
9
|
+
@metadata = metadata
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rack
|
2
|
+
class Saml
|
3
|
+
require 'rack/saml/misc/onelogin_setting'
|
4
|
+
|
5
|
+
class OneloginMetadata < AbstractMetadata
|
6
|
+
include OneloginSetting
|
7
|
+
|
8
|
+
def initialize(request, saml_config, metadata)
|
9
|
+
super(request, saml_config, metadata)
|
10
|
+
@sp_metadata = Onelogin::Saml::Metadata.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate
|
14
|
+
@sp_metadata.generate(saml_settings)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rack
|
2
|
+
class Saml
|
3
|
+
require 'rack/saml/metadata/abstract_metadata'
|
4
|
+
autoload "OneloginMetadata", 'rack/saml/metadata/onelogin_metadata'
|
5
|
+
autoload "OpensamlMetadata", 'rack/saml/metadata/opensaml_metadata'
|
6
|
+
|
7
|
+
class MetadataHandler
|
8
|
+
attr_reader :sp_metadata
|
9
|
+
|
10
|
+
# Rack::Saml::MetadataHandler
|
11
|
+
# request: Rack current request instance
|
12
|
+
# saml_config: config/saml.yml
|
13
|
+
# metadata: specified idp entity in the config/metadata.yml
|
14
|
+
def initialize(request, saml_config, metadata)
|
15
|
+
@sp_metadata = (eval "Rack::Saml::#{saml_config['assertion_handler'].to_s.capitalize}Metadata").new(request, saml_config, metadata)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rack
|
2
|
+
class Saml
|
3
|
+
module OneloginSetting
|
4
|
+
require 'ruby-saml'
|
5
|
+
|
6
|
+
def saml_settings
|
7
|
+
settings = Onelogin::Saml::Settings.new
|
8
|
+
settings.assertion_consumer_service_url = "#{@request.scheme}://#{@request.host}#{":#{@request.port}" if @request.port}#{request.script_name}#{@saml_config['protected_path']}"
|
9
|
+
settings.issuer = @saml_config['saml_sp']
|
10
|
+
settings.idp_sso_target_url = @metadata['saml2_http_redirect']
|
11
|
+
settings.idp_cert = @metadata['certificate']
|
12
|
+
settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
|
13
|
+
#settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
|
14
|
+
settings
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rack
|
2
|
+
class Saml
|
3
|
+
class AbstractRequest
|
4
|
+
attr_reader :request, :saml_config, :metadata
|
5
|
+
|
6
|
+
def initialize(request, saml_config, metadata)
|
7
|
+
@request = request
|
8
|
+
@saml_config = saml_config
|
9
|
+
@metadata = metadata
|
10
|
+
end
|
11
|
+
|
12
|
+
def redirect_uri
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rack
|
2
|
+
class Saml
|
3
|
+
require 'rack/saml/misc/onelogin_setting'
|
4
|
+
|
5
|
+
class OneloginRequest < AbstractRequest
|
6
|
+
include OneloginSetting
|
7
|
+
|
8
|
+
def initialize(request, saml_config, metadata)
|
9
|
+
super(request, saml_config, metadata)
|
10
|
+
@authrequest = Onelogin::Saml::Authrequest.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def redirect_uri
|
14
|
+
@authrequest.create(saml_settings)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rack
|
2
|
+
class Saml
|
3
|
+
require 'rack/saml/request/abstract_request'
|
4
|
+
autoload "OneloginRequest", 'rack/saml/request/onelogin_request'
|
5
|
+
autoload "OpensamlRequest", 'rack/saml/request/opensaml_request'
|
6
|
+
|
7
|
+
class RequestHandler
|
8
|
+
attr_reader :authn_request
|
9
|
+
|
10
|
+
# Rack::Saml::RequestHandler
|
11
|
+
# request: Rack current request instance
|
12
|
+
# saml_config: config/saml.yml
|
13
|
+
# metadata: specified idp entity in the config/metadata.yml
|
14
|
+
def initialize(request, saml_config, metadata)
|
15
|
+
@authn_request = (eval "Rack::Saml::#{saml_config['assertion_handler'].to_s.capitalize}Request").new(request, saml_config, metadata)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rack
|
2
|
+
class Saml
|
3
|
+
class AbstractResponse
|
4
|
+
attr_reader :request, :saml_config, :metadata
|
5
|
+
|
6
|
+
def initialize(request, saml_config, metadata)
|
7
|
+
@request = request
|
8
|
+
@saml_config = saml_config
|
9
|
+
@metadata = metadata
|
10
|
+
end
|
11
|
+
|
12
|
+
def is_valid?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Rack
|
2
|
+
class Saml
|
3
|
+
require 'rack/saml/misc/onelogin_setting'
|
4
|
+
|
5
|
+
class OneloginResponse < AbstractResponse
|
6
|
+
include OneloginSetting
|
7
|
+
#extend Forwardable
|
8
|
+
|
9
|
+
def initialize(request, saml_config, metadata)
|
10
|
+
super(request, saml_config, metadata)
|
11
|
+
@response = Onelogin::Saml::Response.new(@request.params['SAMLResponse'])
|
12
|
+
@response.settings = saml_settings
|
13
|
+
end
|
14
|
+
|
15
|
+
def is_valid?
|
16
|
+
@response.is_valid?
|
17
|
+
end
|
18
|
+
|
19
|
+
def attributes
|
20
|
+
@response.attributes
|
21
|
+
end
|
22
|
+
|
23
|
+
#def_delegator :@response, :is_valid?, :attributes
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Rack
|
2
|
+
class Saml
|
3
|
+
require 'rack/saml/response/abstract_response'
|
4
|
+
autoload "OneloginResponse", 'rack/saml/response/onelogin_response'
|
5
|
+
autoload "OpensamlResponse", 'rack/saml/response/opensaml_response'
|
6
|
+
|
7
|
+
class ResponseHandler
|
8
|
+
attr_reader :response
|
9
|
+
|
10
|
+
# Rack::Saml::ResponseHandler
|
11
|
+
# request: Rack current request instance
|
12
|
+
# saml_config: config/saml.yml
|
13
|
+
# metadata: specified idp entity in the config/metadata.yml
|
14
|
+
def initialize(request, saml_config, metadata)
|
15
|
+
@response = (eval "Rack::Saml::#{saml_config['assertion_handler'].to_s.capitalize}Response").new(request, saml_config, metadata)
|
16
|
+
end
|
17
|
+
|
18
|
+
def extract_attrs(env, attribute_map, opts = {})
|
19
|
+
attribute_map.each do |attr_name, env_name|
|
20
|
+
attribute = @response.attributes[attr_name]
|
21
|
+
env[env_name] = attribute if !attribute.nil?
|
22
|
+
end
|
23
|
+
if !opts[:shib_app_id].nil?
|
24
|
+
env['Shib-Application-ID'] = opts[:shib_app_id]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/rack-saml.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/rack-saml/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.add_dependency 'ruby-saml', '~> 0.4.7'
|
6
|
+
|
7
|
+
gem.authors = ["Toyokazu Akiyama"]
|
8
|
+
gem.email = ["toyokazu@gmail.com"]
|
9
|
+
gem.description = %q{SAML middleware for Rack (using ruby-saml)}
|
10
|
+
gem.summary = %q{SAML middleware for Rack (using ruby-saml)}
|
11
|
+
gem.homepage = ""
|
12
|
+
|
13
|
+
gem.files = `find . -not \\( -regex ".*\\.git.*" -o -regex "\\./pkg.*" -o -regex "\\./spec.*" \\)`.split("\n").map{ |f| f.gsub(/^.\//, '') }
|
14
|
+
#gem.files = `find .`.split("\n").map{ |f| f.gsub(/^.\//, '') }
|
15
|
+
gem.test_files = `find spec/*`.split("\n")
|
16
|
+
#gem.test_files = `find test/* spec/* features/*`.split("\n")
|
17
|
+
gem.name = "rack-saml"
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
gem.version = Rack::Saml::VERSION
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-saml
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Toyokazu Akiyama
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-29 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: ruby-saml
|
16
|
+
requirement: &2153190660 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.4.7
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *2153190660
|
25
|
+
description: SAML middleware for Rack (using ruby-saml)
|
26
|
+
email:
|
27
|
+
- toyokazu@gmail.com
|
28
|
+
executables: []
|
29
|
+
extensions: []
|
30
|
+
extra_rdoc_files: []
|
31
|
+
files:
|
32
|
+
- bin/conv_metadata.rb
|
33
|
+
- config/attribute-map.yml
|
34
|
+
- config/attribute-map.yml.sample
|
35
|
+
- config/metadata.yml
|
36
|
+
- config/saml.yml
|
37
|
+
- Gemfile
|
38
|
+
- lib/rack/saml/metadata/abstract_metadata.rb
|
39
|
+
- lib/rack/saml/metadata/onelogin_metadata.rb
|
40
|
+
- lib/rack/saml/metadata/opensaml_metadata.rb
|
41
|
+
- lib/rack/saml/metadata_handler.rb
|
42
|
+
- lib/rack/saml/misc/onelogin_setting.rb
|
43
|
+
- lib/rack/saml/request/abstract_request.rb
|
44
|
+
- lib/rack/saml/request/onelogin_request.rb
|
45
|
+
- lib/rack/saml/request/opensaml_request.rb
|
46
|
+
- lib/rack/saml/request_handler.rb
|
47
|
+
- lib/rack/saml/response/abstract_response.rb
|
48
|
+
- lib/rack/saml/response/onelogin_response.rb
|
49
|
+
- lib/rack/saml/response/opensaml_response.rb
|
50
|
+
- lib/rack/saml/response_handler.rb
|
51
|
+
- lib/rack/saml.rb
|
52
|
+
- lib/rack-saml/version.rb
|
53
|
+
- lib/rack-saml.rb
|
54
|
+
- rack-saml.gemspec
|
55
|
+
- Rakefile
|
56
|
+
- README.md
|
57
|
+
homepage: ''
|
58
|
+
licenses: []
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 1.8.12
|
78
|
+
signing_key:
|
79
|
+
specification_version: 3
|
80
|
+
summary: SAML middleware for Rack (using ruby-saml)
|
81
|
+
test_files: []
|
82
|
+
has_rdoc:
|