saml_camel 0.2.0 → 0.2.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 +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
|