saml_camel 0.2.0 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +20 -2
- data/app/controllers/concerns/saml_camel/saml_helpers.rb +93 -24
- data/app/controllers/saml_camel/application_controller.rb +0 -1
- data/app/controllers/saml_camel/saml_controller.rb +26 -10
- data/app/models/saml_camel/logging.rb +14 -0
- data/app/models/saml_camel/transaction.rb +5 -0
- data/lib/saml_camel/version.rb +1 -1
- data/lib/tasks/saml_camel_tasks.rake +2 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae748230ac0230ac13ca645a14e0d5394a4a7ae0
|
4
|
+
data.tar.gz: 1cc2096cbc175ee27688750af186f897fb6f525a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55c69cba9cee795e417709c02d4c8348bd657ee104ebefd65241a57a91b0dd2177548adbc1998f753d073646ef8346dd8850b3abde2bcff86650281b7d22c1df
|
7
|
+
data.tar.gz: 95d27ce112e6b95ce27f4f30af3af1a881888a7a67b39ef0a1c0771ae4f78f07497ce8d96d231674f9b0be02c8980e8f12cd11b5555323706bc407ff28dc58f4
|
data/README.md
CHANGED
@@ -1,4 +1,18 @@
|
|
1
1
|
# SamlCamel - (not production ready)
|
2
|
+
## About
|
3
|
+
SamlCamel is a SAML gem/engine built off of the OneLogin ruby-saml gem.
|
4
|
+
It is intended to provide Rails applications with capability to generate and consume
|
5
|
+
SAML requests and responses. The gem is optimized to integrate with the Duke University
|
6
|
+
Identity Provider, but can be used to integrate with other Identity Provider. Additional Information
|
7
|
+
about integrating with the Duke IDP can be found here https://authentication.oit.duke.edu/manager/docs
|
8
|
+
|
9
|
+
## Terminology within the context of the gem
|
10
|
+
- **Identity Provider (IDP)**: This is the system providing the authentication Service and provides user credentials.
|
11
|
+
- **Service Provider (SP)**: This is the system that sends authentication requests and consumes attributes from the response. SamlCamel is the SP in SAML integrations.
|
12
|
+
- **Entity ID**: A unique identifier for your SP. It usually takes the form of a url but does not have to resolve. The entity id is not used for routing in any way, only for identification to the IDP. Example: *https://my-site.com/does/not/resolve*
|
13
|
+
- **Assertion Consumer Service (ACS)**: the endpoint of the SP where the IDP should send the response. The is part of the SamlCamel gem. Example: */saml/consumeSaml*
|
14
|
+
|
15
|
+
|
2
16
|
## Installation
|
3
17
|
Add this line to your application's Gemfile:
|
4
18
|
|
@@ -36,7 +50,7 @@ Identity Provider(idp) to recognize your app. Typically it should take the form
|
|
36
50
|
- fill out functional purpose, responsible dept, function owner dept, and audience with information relevant to your application
|
37
51
|
- copy the cert from `saml/development/saml_certificate.crt` and paste it into the Certificate Field
|
38
52
|
- copy the acs value and paste it into the Location field in the Assertion Consumer Service box
|
39
|
-
- note that the default host value for ACS is `http://locahost:3000` which is the default `rails s` host. If you're using a
|
53
|
+
- note that the default host value for ACS is `http://locahost:3000` which is the default `rails s` host. If you're using a different host (such as in production or using docker) you will want to replace the host value with what is relevent for your situation(*e.g. https://my-app.duke.edu/saml/consumeSaml*), but keep the path `/saml/consumeSaml`
|
40
54
|
|
41
55
|
5. In your app mount the engine in config/routes.rb
|
42
56
|
```ruby
|
@@ -66,18 +80,22 @@ Identity Provider(idp) to recognize your app. Typically it should take the form
|
|
66
80
|
|
67
81
|
9. Logging is turned on by default. Logging is configured in `saml/development/settings.json`. To utilize logging saml_logging should be set to true (default), and primary_id must have a value. primary_id is the saml attribute you consider to be a primary identifier for a user
|
68
82
|
|
83
|
+
|
69
84
|
10. Users can go to http://localhost:3000/saml/attributes to view attributes being passed through
|
70
85
|
|
86
|
+
|
71
87
|
## Example settings.json
|
72
88
|
```json
|
73
89
|
{
|
74
90
|
"_comment": "note you will need to restart the application when you make changes to this file",
|
75
91
|
"settings": {
|
76
92
|
"acs": "http://localhost:3000/saml/consumeSaml",
|
77
|
-
"entity_id": "
|
93
|
+
"entity_id": "https://samlCamel.com/doesNotResolve",
|
78
94
|
"sso_url": "https://shib.oit.duke.edu/idp/profile/SAML2/Redirect/SSO",
|
79
95
|
"logout_return_url": "http://localhost:3000",
|
80
96
|
"primary_id": "eduPersonPrincipalName",
|
97
|
+
"sp_session_timeout": 1,
|
98
|
+
"sp_session_lifetime": 8,
|
81
99
|
"saml_logging": true
|
82
100
|
},
|
83
101
|
"attribute_map": {
|
@@ -4,58 +4,127 @@ module SamlCamel::SamlHelpers
|
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
SP_SETTINGS = JSON.parse(File.read("saml/#{Rails.env}/settings.json"))
|
6
6
|
|
7
|
-
|
8
7
|
#this generates a call to the idp, which will then be returned to the consume action the in saml_contorller
|
9
8
|
def saml_request(host_request)
|
10
|
-
relay_state = SecureRandom.base64.chomp.gsub( /\W/, '' ) #set relay state to secure against replay attack
|
11
9
|
request = OneLogin::RubySaml::Authrequest.new
|
12
|
-
|
13
|
-
#store relay state, ip address and original url request in memory to be used for verification and redirect after response
|
14
10
|
assign_permit_key
|
11
|
+
lifetime = SP_SETTINGS['settings']["sp_session_lifetime"]
|
15
12
|
permit_key = session[:saml_session_id].to_sym
|
16
|
-
|
17
|
-
|
13
|
+
|
14
|
+
#store ip address and original url request in memory to be used for verification and redirect after response
|
15
|
+
Rails.cache.fetch(permit_key, expires_in: lifetime.hours) do
|
16
|
+
{ip_address: host_request.remote_ip, redirect_url: host_request.url }
|
18
17
|
end
|
19
|
-
|
18
|
+
|
19
|
+
saml_request_url = request.create(SamlCamel::Transaction.saml_settings)
|
20
20
|
redirect_to(saml_request_url)
|
21
21
|
end
|
22
22
|
|
23
23
|
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
#ensures that a saml response can not be used more than once.
|
25
|
+
#stores response ids and checks to see if the response id has already been used.
|
26
|
+
def duplicate_response_id?(response_id)
|
27
|
+
ids = Rails.cache.fetch("response_ids")
|
28
|
+
if ids
|
29
|
+
if ids.include?(response_id)
|
30
|
+
session[:sp_session] = nil
|
31
|
+
raise "SAML response ID already issued."
|
32
|
+
else
|
33
|
+
ids << response_id
|
34
|
+
Rails.cache.fetch("response_ids", expires_in: 1.hours) do
|
35
|
+
ids
|
36
|
+
end
|
37
|
+
end
|
32
38
|
else
|
33
|
-
|
39
|
+
Rails.cache.fetch("response_ids", expires_in: 1.hours) do
|
40
|
+
[]
|
41
|
+
end
|
34
42
|
end
|
43
|
+
end
|
35
44
|
|
36
|
-
|
37
|
-
|
45
|
+
def set_saml_session_lifetime(permit_key)
|
46
|
+
user_saml_cache = Rails.cache.fetch(permit_key)
|
47
|
+
user_saml_cache[:session_start_time] = Time.now
|
48
|
+
Rails.cache.fetch(permit_key, expires_in: 8.hours) do
|
49
|
+
user_saml_cache
|
50
|
+
end
|
38
51
|
end
|
39
52
|
|
40
53
|
|
41
54
|
#Make it so sp sessions only last 1 hour, sp_session is set on a succesfull saml response.
|
42
55
|
#We check that the session time is less than in hour if so we refresh, otherwise we delete the session
|
43
56
|
def expired_session?
|
57
|
+
permit_key = session[:saml_session_id].to_sym
|
58
|
+
user_cache = Rails.cache.fetch(permit_key)
|
59
|
+
cache_available?(user_cache)
|
60
|
+
|
61
|
+
sp_timeout = SP_SETTINGS["settings"]["sp_session_timeout"]
|
62
|
+
sp_lifetime = SP_SETTINGS['settings']["sp_session_lifetime"]
|
63
|
+
|
64
|
+
set_saml_session_lifetime(permit_key) if user_cache[:session_start_time].nil?
|
65
|
+
sp_session_init_time = user_cache[:session_start_time]
|
66
|
+
|
44
67
|
if session[:sp_session]
|
45
|
-
if
|
68
|
+
#if the session has timed out remove session, otherwise refresh
|
69
|
+
if (Time.now - Time.parse(session[:sp_session])) < sp_timeout.hour
|
46
70
|
session[:sp_session] = Time.now
|
47
|
-
|
71
|
+
else
|
72
|
+
SamlCamel::Logging.expired_session(session[:saml_attributes])
|
73
|
+
session[:sp_session] = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
#if the session has exceeded the allowed lifetime, remove session
|
77
|
+
if (Time.now - sp_session_init_time) > sp_lifetime.hour
|
78
|
+
SamlCamel::Logging.expired_session(session[:saml_attributes])
|
79
|
+
session[:sp_session] = nil
|
48
80
|
end
|
49
81
|
end
|
50
82
|
end
|
51
83
|
|
52
84
|
|
85
|
+
def valid_ip?(remote_ip)
|
86
|
+
permit_key = session[:saml_session_id].to_sym
|
87
|
+
saml_cache = Rails.cache.fetch(permit_key)
|
88
|
+
cache_available?(saml_cache)
|
89
|
+
unless remote_ip == saml_cache[:ip_address]
|
90
|
+
SamlCamel::Logging.bad_ip(session[:saml_attributes], saml_cache[:ip_address], remote_ip)
|
91
|
+
session[:sp_session] = nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
53
96
|
#saml_protect is what is called in the app. it initiates the saml request if there is no active session, or if a user has been idle for over an hour
|
54
|
-
#TODO re-factor in future
|
55
97
|
def saml_protect
|
56
|
-
|
57
|
-
|
58
|
-
session[:
|
98
|
+
user_cache = cache_available?(Rails.cache.fetch(session[:saml_session_id]))
|
99
|
+
if session[:saml_session_id] && user_cache
|
100
|
+
#sets session[:sp_session] to nil if expired, of if the ip adress changes
|
101
|
+
|
102
|
+
expired_session?
|
103
|
+
valid_ip?(request.remote_ip)
|
104
|
+
saml_request(request) unless (session[:saml_response_success] || session[:sp_session])
|
105
|
+
else
|
106
|
+
saml_request(request)
|
107
|
+
end
|
108
|
+
session[:saml_response_success] = nil #keeps us from looping
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def cache_available?(app_cache)
|
113
|
+
if app_cache
|
114
|
+
true
|
115
|
+
else
|
116
|
+
session[:sp_session] = nil
|
117
|
+
false
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
|
122
|
+
def verify_sha_type(response)
|
123
|
+
raw_xml_string = response.decrypted_document.to_s
|
124
|
+
attr_scan = raw_xml_string.scan(/<ds:SignatureMethod.*\/>/)
|
125
|
+
is_sha1 = attr_scan[0].match?("sha1")
|
126
|
+
|
127
|
+
raise "SHA1 algorithm not supported " if is_sha1
|
59
128
|
end
|
60
129
|
|
61
130
|
def assign_permit_key
|
@@ -7,6 +7,7 @@ module SamlCamel
|
|
7
7
|
before_action :saml_protect, only: [:attr_check]
|
8
8
|
|
9
9
|
|
10
|
+
|
10
11
|
#convinence route to see attributes that are coming through
|
11
12
|
def index
|
12
13
|
@attributes = session[:saml_attributes]
|
@@ -15,18 +16,30 @@ module SamlCamel
|
|
15
16
|
|
16
17
|
#consumes the saml response from the IDP
|
17
18
|
def consume
|
18
|
-
raise "Invalid RelayState" unless valid_state(params[:RelayState], request.remote_ip)
|
19
19
|
permit_key = session[:saml_session_id].to_sym
|
20
|
-
|
21
|
-
|
22
|
-
session[:saml_session_id] = nil
|
20
|
+
user_cache = Rails.cache.fetch(permit_key)
|
21
|
+
raise "Unable to access cache. Ensure cache is configrued according to documentation." unless cache_available?(user_cache)
|
23
22
|
|
23
|
+
redirect_path = user_cache[:redirect_url]
|
24
24
|
response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :settings => saml_settings)
|
25
25
|
response.settings = saml_settings
|
26
26
|
|
27
27
|
if response.is_valid? # validate the SAML Response
|
28
|
+
|
29
|
+
#verify not sha1
|
30
|
+
verify_sha_type(response)
|
31
|
+
|
32
|
+
response_id = response.id(response.document)
|
33
|
+
|
34
|
+
#confirm that IP address from response matches that of original request
|
35
|
+
valid_ip?(request.remote_ip)
|
36
|
+
|
37
|
+
#check that response id has not already been used
|
38
|
+
duplicate_response_id?(response_id)
|
39
|
+
|
28
40
|
# authorize_success, log the user
|
29
|
-
session[:
|
41
|
+
session[:saml_response_success] = true
|
42
|
+
set_saml_session_lifetime(permit_key)
|
30
43
|
session[:sp_session] = Time.now
|
31
44
|
|
32
45
|
session[:saml_attributes] = SamlCamel::Transaction.map_attributes(response.attributes)
|
@@ -39,18 +52,21 @@ module SamlCamel
|
|
39
52
|
Rails.cache.delete(permit_key)
|
40
53
|
session[:saml_session_id] = nil
|
41
54
|
end
|
42
|
-
|
43
|
-
session[:
|
55
|
+
session[:sp_session] = nil
|
56
|
+
session[:saml_response_success] = false
|
44
57
|
response.errors
|
45
58
|
SamlCamel::Logging.auth_failure(response.errors)
|
46
59
|
|
47
60
|
redirect_to action: "failure", locals:{errors: response.errors}
|
48
61
|
end
|
49
62
|
rescue => e
|
50
|
-
|
51
|
-
|
52
|
-
|
63
|
+
if session[:saml_session_id]
|
64
|
+
permit_key = session[:saml_session_id].to_sym
|
65
|
+
Rails.cache.delete(permit_key)
|
66
|
+
end
|
67
|
+
session[:saml_response_success] = false
|
53
68
|
session[:saml_session_id] = nil
|
69
|
+
session[:sp_session] = nil
|
54
70
|
|
55
71
|
SamlCamel::Logging.auth_failure(e)
|
56
72
|
redirect_to action: "failure", locals:{errors: e}
|
@@ -26,6 +26,20 @@ module SamlCamel
|
|
26
26
|
logger.debug("Unknown error logging user logout. Most likely anonymous user clicked a logout button.")
|
27
27
|
end
|
28
28
|
|
29
|
+
def self.expired_session(saml_attrs)
|
30
|
+
logger = Logger.new("log/saml.log")
|
31
|
+
logger.info("Session Expired for #{saml_attrs[PRIMARY_ID]}")
|
32
|
+
rescue
|
33
|
+
logger.debug("Unknown Error During relay state logging.")
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.bad_ip(saml_attrs,request_ip,current_ip)
|
37
|
+
logger = Logger.new("log/saml.log")
|
38
|
+
logger.info("Bad IP address for #{saml_attrs[PRIMARY_ID]}. IP at SAML request #{request_ip} | IP presented #{current_ip}")
|
39
|
+
rescue
|
40
|
+
logger.debug("Unknown Error During relay state logging.")
|
41
|
+
end
|
42
|
+
|
29
43
|
def self.saml_state(data)
|
30
44
|
logger = Logger.new("log/saml.log")
|
31
45
|
logger.info("Stored Relay: #{data[:stored_relay]} | RequestRelay: #{data[:request_relay]} | Stored IP: #{data[:stored_ip]} RemoteIP: #{data[:remote_ip]}")
|
@@ -21,6 +21,11 @@ module SamlCamel
|
|
21
21
|
# certificate to verify IDP signature
|
22
22
|
settings.idp_cert = File.read("saml/#{Rails.env}/idp_certificate.crt")
|
23
23
|
|
24
|
+
|
25
|
+
#TODO test by modding relying party duke-coi-smart example
|
26
|
+
settings.security[:digest_method] = XMLSecurity::Document::SHA256
|
27
|
+
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
|
28
|
+
|
24
29
|
# Optional for most SAML IdPs
|
25
30
|
settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
|
26
31
|
settings.attribute_consuming_service.configure do
|
data/lib/saml_camel/version.rb
CHANGED
@@ -70,6 +70,8 @@ tA6SX0infqNRyPRNJK+bnQd1yOP4++tjD/lAPE+5tiD/waI3fArt43ZE/qp7pYMS
|
|
70
70
|
sso_url: "https://shib.oit.duke.edu/idp/profile/SAML2/Redirect/SSO",
|
71
71
|
logout_return_url: "http://localhost:3000",
|
72
72
|
primary_id: "eduPersonPrincipalName",
|
73
|
+
sp_session_timeout: 1,
|
74
|
+
sp_session_lifetime: 8,
|
73
75
|
saml_logging: true
|
74
76
|
},
|
75
77
|
"attribute_map": {
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: saml_camel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- 'Danai Adkisson '
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-04-
|
11
|
+
date: 2018-04-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|