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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d301a7a75f983e4a996abb0c7929c03918e0b9a0
4
- data.tar.gz: 80c7b4c2083c751a1f384f854af60d5e6e243e1f
3
+ metadata.gz: ae748230ac0230ac13ca645a14e0d5394a4a7ae0
4
+ data.tar.gz: 1cc2096cbc175ee27688750af186f897fb6f525a
5
5
  SHA512:
6
- metadata.gz: 232ceb4b900009acf4ba6df0b73b24dd0a87b726062e5fc6c5bbe9038ee72ddd3561edfaf68fde56d0f21a82721c811ce6051c900c5ba1e401af0b5ad65b0a97
7
- data.tar.gz: 8cbfe2f298adabff017463fa86fe062d3c7b0ca379167b37214b63714f41deefac2b9ad46c8872e757a4c060f5bda19c99fc3e4a08b14b7bce3e322052631bbe
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 differnet 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`
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": "http://my-entity-id.corgi",
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
- Rails.cache.fetch(permit_key, expires_in: 5.minutes) do
17
- {ip_address: host_request.remote_ip, relay_state: relay_state, redirect_url: host_request.url }
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
- saml_request_url = request.create(SamlCamel::Transaction.saml_settings) + "&RelayState=#{Rails.cache.fetch(permit_key)[:relay_state]}"
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
- #validates the user ip address and relay state given to IDP. Prevents Replay Attacks.
25
- def valid_state(param_relay_state, remote_ip)
26
- permit_key = session[:saml_session_id].to_sym
27
- saml_cache = Rails.cache.fetch(permit_key)
28
-
29
- if saml_cache
30
- stored_relay = saml_cache[:relay_state]
31
- stored_ip = saml_cache[:ip_address]
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
- raise "Unable to access cache. Ensure cache is configrued according to documentation."
39
+ Rails.cache.fetch("response_ids", expires_in: 1.hours) do
40
+ []
41
+ end
34
42
  end
43
+ end
35
44
 
36
- SamlCamel::Logging.saml_state({stored_relay: stored_relay, request_relay: param_relay_state, stored_ip: stored_ip, remote_ip: remote_ip})
37
- param_relay_state == stored_relay && remote_ip == stored_ip
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 (Time.now - Time.parse(session[:sp_session])) < 1.hour
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
- return true
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
- not_expired = expired_session?
57
- saml_request(request) unless (session[:saml_success] || session[:sp_session] || not_expired) #keeps us from looping, and maintains sp session
58
- session[:saml_success] = nil
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
@@ -1,6 +1,5 @@
1
1
  module SamlCamel
2
2
  class ApplicationController < ActionController::Base
3
3
  protect_from_forgery with: :exception
4
-
5
4
  end
6
5
  end
@@ -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
- redirect_path = Rails.cache.fetch(permit_key)[:redirect_url]
21
- Rails.cache.delete(permit_key) #we no longer need cache at this stage
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[:saml_success] = true
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[:saml_success] = false
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
- permit_key = session[:saml_session_id].to_sym
51
- Rails.cache.delete(permit_key)
52
- session[:saml_success] = false
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
@@ -1,3 +1,3 @@
1
1
  module SamlCamel
2
- VERSION = '0.2.0'
2
+ VERSION = '0.2.1'
3
3
  end
@@ -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.0
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-20 00:00:00.000000000 Z
11
+ date: 2018-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails