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 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