saml_camel 0.2.2 → 0.2.6

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: 28b08cae52108c58df6749ee001140b88ed5f783
4
- data.tar.gz: 286d17661c59fbe7cd0b588979adb0abdbc09fcb
3
+ metadata.gz: b7fd883c9fb712daac3a0eec2ceecf41158452b8
4
+ data.tar.gz: a54c29f80e07049a3cae26a5ac2661fe458ca10e
5
5
  SHA512:
6
- metadata.gz: 0a400c238fb98ebfa7951e11b1ced46e095b4c0e08dbef17cb6e11ec04c68de04c73bf5f6dea9f9234187a695077b20588c9d18ba9e1f2292a7e38f72f2ddf46
7
- data.tar.gz: 7ef2bbe8e90b127740a6ccbe44f140aab5a5014c51876e85e31743e9e6ce576173177f0aff5a1e4e9304412dcc7c42a91d696e159019e0c6ec5dbe2efff6af4d
6
+ metadata.gz: 605cb590546df8667225f53750abc9ebd08a8e6e3884182a7f22464a916de928470955f24b04e6ccee3d564382057e88c078f7f48b9901288b54d0c2bfea2fe3
7
+ data.tar.gz: 4238d45a9998662d3abebea3b41a9dffe300561847f94871f70ed5eef979f9a980e45cf3cd47ad72023f7c110cc8ac3d41034e3c6093001ecbd8e7c10dd3cb10
data/README.md CHANGED
@@ -31,24 +31,26 @@ $ bundle
31
31
  ### IMPORTANT: This step enables security features and is required to use the gem!
32
32
  1. in your environments config (`config/development.rb` for example) ensure that you have caching configured as follows
33
33
 
34
- **NOTE:** use the cache_store most appropriate for your situation. It may make more sense to use a file store, or a redis server
34
+
35
35
  ```ruby
36
36
  config.action_controller.perform_caching = true
37
37
  config.cache_store = :memory_store
38
38
  ```
39
39
 
40
+ **NOTE:** use the cache_store most appropriate for your situation. It may make more sense to use a file store, or a redis server. For example it may not make sense to cache in memory in production. You can read more about rails caching behavior here http://guides.rubyonrails.org/caching_with_rails.html
41
+
40
42
  2. run `rake saml_camel:generate_saml` to generate metadata files for each environment. you can also specify a custom environment like this `rake saml_camel:generate_saml environment=acceptance`
41
43
 
42
44
  **Note: these steps will use development as an example, if you use separate metadata per environment, you will repeat each step for your chosen environment**
43
45
 
44
- 3. from the root of your app open `saml/development/settings.json` and specify an entity ID of your choice. this is a unique identifier used by the
46
+ 3. from the root of your app open `config/saml/development/settings.json` and specify an entity ID of your choice. this is a unique identifier used by the
45
47
  Identity Provider(idp) to recognize your app. Typically it should take the form of a url, however note that it is just an identifier and does not have to resolve (e.g. https://my-app-name/not/a/real/route)
46
48
 
47
- 4. Go to https://authentication.oit.duke.edu/manager/register/sp and register your metadata with the identity provider. You will need the values from `saml/development/settings.json` in addition to the `saml/development/saml_certificate.crt`
49
+ 4. Go to https://authentication.oit.duke.edu/manager/register/sp and register your metadata with the identity provider. You will need the values from `config/saml/development/settings.json` in addition to the `config/saml/development/saml_certificate.crt`
48
50
 
49
51
  - copy the entity_id you chose in the `settings.json` file and paste it into the "Entity Field"
50
52
  - fill out functional purpose, responsible dept, function owner dept, and audience with information relevant to your application
51
- - copy the cert from `saml/development/saml_certificate.crt` and paste it into the Certificate Field
53
+ - copy the cert from `config/saml/development/saml_certificate.crt` and paste it into the Certificate Field
52
54
  - copy the acs value and paste it into the Location field in the Assertion Consumer Service box
53
55
  - 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`
54
56
 
@@ -78,7 +80,7 @@ Identity Provider(idp) to recognize your app. Typically it should take the form
78
80
 
79
81
  8. It is recommended to set `config.force_ssl = true` in the `config/environments/production.rb` file for security
80
82
 
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
83
+ 9. Logging is turned on by default. Logging is configured in `config/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
82
84
 
83
85
 
84
86
  10. Users can go to http://localhost:3000/saml/attributes to view attributes being passed through
@@ -92,7 +94,7 @@ Identity Provider(idp) to recognize your app. Typically it should take the form
92
94
  "acs": "http://localhost:3000/saml/consumeSaml",
93
95
  "entity_id": "https://samlCamel.com/doesNotResolve",
94
96
  "sso_url": "https://shib.oit.duke.edu/idp/profile/SAML2/Redirect/SSO",
95
- "logout_return_url": "http://localhost:3000",
97
+ "logout_url": "https://shib.oit.duke.edu/cgi-bin/logout.pl",
96
98
  "primary_id": "eduPersonPrincipalName",
97
99
  "sp_session_timeout": 1,
98
100
  "sp_session_lifetime": 8,
@@ -0,0 +1,35 @@
1
+ require_dependency "saml_camel/application_controller"
2
+
3
+ module SamlCamel::SamlService
4
+ extend ActiveSupport::Concern
5
+
6
+ def cache_available?(app_cache)
7
+ if app_cache
8
+ true
9
+ else
10
+ session[:sp_session] = nil
11
+ false
12
+ end
13
+ end
14
+
15
+ def saml_protect
16
+ user_cache = cache_available?(Rails.cache.fetch(session[:saml_session_id])) if session[:saml_session_id]
17
+ if session[:saml_session_id] && user_cache
18
+ sp = SamlCamel::ServiceProvider.new(cache_permit_key: session[:saml_session_id].to_sym, saml_attributes: session[:saml_attributes])
19
+ session[:sp_session] = sp.validate_sp_session(session[:sp_session],request.remote_ip)
20
+ unless (session[:saml_response_success] || session[:sp_session])
21
+ saml_request_url = sp.generate_saml_request(request)
22
+ redirect_to(saml_request_url)
23
+ end
24
+
25
+ else
26
+ session[:saml_session_id] = SamlCamel::ServiceProvider.generate_permit_key
27
+ saml_request_url = SamlCamel::ServiceProvider.new(cache_permit_key: session[:saml_session_id].to_sym).generate_saml_request(request)
28
+ redirect_to(saml_request_url)
29
+ end
30
+ session[:saml_response_success] = nil #keeps us from looping
31
+ end
32
+
33
+
34
+
35
+ end
@@ -2,7 +2,7 @@ require_dependency "saml_camel/application_controller"
2
2
 
3
3
  module SamlCamel
4
4
  class SamlController < ApplicationController
5
- include SamlCamel::SamlHelpers
5
+ include SamlCamel::SamlService
6
6
  skip_before_action :verify_authenticity_token ,only: [:consume,:logout]
7
7
  before_action :saml_protect, only: [:attr_check]
8
8
 
@@ -19,31 +19,18 @@ module SamlCamel
19
19
  permit_key = session[:saml_session_id].to_sym
20
20
  user_cache = Rails.cache.fetch(permit_key)
21
21
  raise "Unable to access cache. Ensure cache is configrued according to documentation." unless cache_available?(user_cache)
22
-
23
22
  redirect_path = user_cache[:redirect_url]
24
- response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], :settings => saml_settings)
25
- response.settings = saml_settings
26
-
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)
23
+ sp = SamlCamel::ServiceProvider.new(cache_permit_key: permit_key, saml_attributes: session[:saml_attributes])
24
+ ol_response = SamlCamel::ServiceProvider.ol_response(params[:SAMLResponse])
39
25
 
26
+ if sp.validate_idp_response(ol_response,request.remote_ip)
40
27
  # authorize_success, log the user
41
28
  session[:saml_response_success] = true
42
- set_saml_session_lifetime(permit_key)
29
+ sp.set_saml_session_lifetime
43
30
  session[:sp_session] = Time.now
44
31
 
45
- session[:saml_attributes] = SamlCamel::Transaction.map_attributes(response.attributes)
46
- SamlCamel::Logging.successfull_auth(session[:saml_attributes])
32
+ session[:saml_attributes] = SamlCamel::Transaction.map_attributes(ol_response.attributes)
33
+ SamlCamel::Logging.successful_auth(session[:saml_attributes])
47
34
 
48
35
  redirect_to redirect_path
49
36
  else # otherwise list out the errors in the response
@@ -55,9 +42,9 @@ module SamlCamel
55
42
  session[:sp_session] = nil
56
43
  session[:saml_response_success] = false
57
44
  response.errors
58
- SamlCamel::Logging.auth_failure(response.errors)
45
+ SamlCamel::Logging.auth_failure(ol_response.errors)
59
46
 
60
- redirect_to action: "failure", locals:{errors: response.errors}
47
+ redirect_to action: "failure", locals:{errors: ol_response.errors}
61
48
  end
62
49
  rescue => e
63
50
  if session[:saml_session_id]
@@ -86,7 +73,7 @@ module SamlCamel
86
73
  session[:sp_session] = nil
87
74
 
88
75
  # return_url = SamlCamel::Transaction.logout #this methods logs the user out of the IDP, and returns a url to be redirected to
89
- redirect_to "https://shib.oit.duke.edu/cgi-bin/logout.pl"
76
+ redirect_to SP_SETTINGS["settings"]["logout_url"]
90
77
  end
91
78
 
92
79
 
@@ -1,5 +1,5 @@
1
1
  module SamlCamel
2
2
  class ApplicationRecord
3
-
3
+
4
4
  end
5
5
  end
@@ -0,0 +1,161 @@
1
+ module SamlCamel
2
+ class ServiceProvider
3
+ attr_reader :cache_permit_key, :user_cache, :saml_attributes
4
+
5
+ def initialize(cache_permit_key: nil, user_cache: nil, saml_attributes: nil)
6
+ @cache_permit_key = cache_permit_key.to_sym
7
+ @saml_attributes = saml_attributes
8
+ @user_cache = Rails.cache.fetch(@cache_permit_key)
9
+ end
10
+
11
+
12
+ def self.generate_permit_key
13
+ secure_random = SecureRandom.base64.chomp.gsub( /\W/, '' )
14
+ "samlCamel#{secure_random}"
15
+ end
16
+
17
+ def self.ol_response(idp_response)
18
+ response = OneLogin::RubySaml::Response.new(idp_response, :settings => SamlCamel::Transaction.saml_settings)
19
+ response.settings = SamlCamel::Transaction.saml_settings
20
+
21
+ response
22
+ end
23
+
24
+
25
+ def check_expired_session(sp_session)
26
+ # cache_available?(user_cache) TODO come back to this
27
+ sp_timeout = SP_SETTINGS["settings"]["sp_session_timeout"]
28
+ sp_lifetime = SP_SETTINGS['settings']["sp_session_lifetime"]
29
+
30
+ set_saml_session_lifetime if @user_cache[:session_start_time].nil?
31
+ sp_session_init_time = @user_cache[:session_start_time]
32
+
33
+
34
+ ######## set session[:sp_session] maybe a seperate method
35
+ if sp_session
36
+
37
+ #if the session has timed out remove session, otherwise refresh
38
+ if (Time.now - Time.parse(sp_session)) < sp_timeout.hour
39
+ return Time.now
40
+ else
41
+ SamlCamel::Logging.expired_session(@saml_attributes)
42
+ return nil
43
+ end
44
+
45
+ #if the session has exceeded the allowed lifetime, remove session
46
+ if (Time.now - sp_session_init_time) > sp_lifetime.hour
47
+ SamlCamel::Logging.expired_session(@saml_attributes)
48
+ return nil
49
+ end
50
+ else #if no sp session return nil
51
+ return nil
52
+ end
53
+ end
54
+
55
+
56
+ def duplicate_response_id?(response_id, count: 3)
57
+ #use semaphore to only allow 1 thread at a time to access
58
+ @semaphore ||= Mutex.new
59
+ @semaphore.synchronize {
60
+ ids = Rails.cache.fetch("saml_camel_response_ids")
61
+ if ids
62
+ if ids.include?(response_id)
63
+ session[:sp_session] = nil
64
+ raise "SAML response ID already issued."
65
+ else
66
+ ids << response_id
67
+ Rails.cache.fetch("saml_camel_response_ids", expires_in: 1.hours) do
68
+ ids
69
+ end
70
+ end
71
+ else
72
+ Rails.cache.fetch("saml_camel_response_ids", expires_in: 1.hours) do
73
+ [response_id]
74
+ end
75
+ end
76
+ }
77
+ rescue ThreadError
78
+ puts "locked "* 50
79
+ if count > 0
80
+ sleep(0.1)
81
+ duplicate_response_id?(response_id, count: count - 1)
82
+ else raise "Resposne ID Validation Error"
83
+ end
84
+ end
85
+
86
+
87
+
88
+ #generates a saml requests and establishes a cache for the user
89
+ def generate_saml_request(host_request)
90
+ request = OneLogin::RubySaml::Authrequest.new
91
+ lifetime = SamlCamel::SP_SETTINGS['settings']["sp_session_lifetime"]
92
+
93
+ #store ip address and original url request in memory to be used for verification and redirect after response
94
+ Rails.cache.fetch(@cache_permit_key, expires_in: lifetime.hours) do
95
+ {ip_address: host_request.remote_ip, redirect_url: host_request.url }
96
+ end
97
+ request.create(SamlCamel::Transaction.saml_settings)
98
+ end
99
+
100
+ #set saml_session lifetime, called if none set
101
+ def set_saml_session_lifetime
102
+ user_saml_cache = Rails.cache.fetch(@cache_permit_key)
103
+ user_saml_cache[:session_start_time] = Time.now
104
+ sp_lifetime = SP_SETTINGS['settings']["sp_session_lifetime"]
105
+ Rails.cache.fetch(@cache_permit_key, expires_in: sp_lifetime.hours) do
106
+ user_saml_cache #not sure if this can use @user_cache
107
+ end
108
+ end
109
+
110
+ #NOTE these methods will raise errors if not a valid response_id
111
+ # which in turn will trigger a resuce that kills the sp session
112
+ def validate_idp_response(response,remote_ip)
113
+ if response.is_valid?
114
+ #validate not sha1
115
+ verify_sha_type(response)
116
+
117
+ #validate IP address
118
+ raise "IP mismatch error" unless validate_ip(remote_ip)
119
+
120
+ response_id = response.id(response.document)
121
+ duplicate_response_id?(response_id)
122
+
123
+ response
124
+ else
125
+ false
126
+ end
127
+ end
128
+
129
+ #validate that ip address has not changed
130
+ def validate_ip(remote_ip)
131
+ # cache_available?(saml_cache) TODO come back to this
132
+ return true if remote_ip == @user_cache[:ip_address]
133
+ SamlCamel::Logging.bad_ip(@saml_attributes, @user_cache[:ip_address], remote_ip)
134
+ return nil
135
+ end
136
+
137
+ #validates a the sp timestamps and ip. returns a new timestamp if everything is good
138
+ # otherwise returns nil
139
+ def validate_sp_session(sp_session,remote_ip)
140
+ new_session = check_expired_session(sp_session)
141
+ if new_session
142
+ has_valid_ip = validate_ip(remote_ip)
143
+ else
144
+ return nil
145
+ end
146
+ return new_session if has_valid_ip
147
+ end
148
+
149
+
150
+ def verify_sha_type(response)
151
+ #was suggested to use an xml parser, however was having great difficulyt with nokogiri
152
+ #open to trying a different parser or advice on nokogiri as it's not reacting as I would typically expect it to
153
+ raw_xml_string = response.decrypted_document.to_s
154
+ attr_scan = raw_xml_string.scan(/<ds:SignatureMethod.*\/>/)
155
+ is_sha1 = attr_scan[0].match("sha1")
156
+
157
+ raise "SHA1 algorithm not supported " if is_sha1
158
+ end
159
+
160
+ end
161
+ end
@@ -0,0 +1,26 @@
1
+ module SamlCamel
2
+ class Shib
3
+
4
+ ATTRIBUTE_MAP = JSON.parse(File.read("config/saml/shibboleth.json"))
5
+
6
+ def self.attributes(request)
7
+ attrs = {}
8
+ ATTRIBUTE_MAP.each do |header_key, new_key|
9
+ attrs[new_key] = request.env.dig(header_key)
10
+ end
11
+ attrs
12
+ end
13
+
14
+ def self.create_identity(values_hash)
15
+ identity = {}
16
+ values_hash.each {|k,v| identity[k] = v}
17
+ identity
18
+ end
19
+
20
+ #sets shib headers directly
21
+ def self.set_headers(request: nil,identity: nil)
22
+ identity.each {|k,v| request.env[k] = v }
23
+ end
24
+
25
+ end
26
+ end
@@ -2,8 +2,6 @@
2
2
  <html>
3
3
  <head>
4
4
  <title>Saml camel</title>
5
- <%= stylesheet_link_tag "saml_camel/application", media: "all" %>
6
- <%= javascript_include_tag "saml_camel/application" %>
7
5
  <%= csrf_meta_tags %>
8
6
  </head>
9
7
  <body>
data/lib/saml_camel.rb CHANGED
@@ -1,5 +1,102 @@
1
1
  require "saml_camel/engine"
2
2
 
3
3
  module SamlCamel
4
- # Your code goes here...
4
+ begin
5
+ SP_SETTINGS = JSON.parse(File.read("config/saml/#{Rails.env}/settings.json"))
6
+ rescue
7
+ #rescue othewise the generator fails
8
+ end
9
+
10
+
11
+ module Transaction
12
+ begin
13
+ IDP_CERT = File.read("config/saml/#{Rails.env}/idp_certificate.crt")
14
+ SP_CERT = File.read("config/saml/#{Rails.env}/saml_certificate.crt")
15
+ SP_KEY = File.read("config/saml/#{Rails.env}/saml_key.key")
16
+ rescue
17
+ #rescue othewise the generator fails
18
+ end
19
+
20
+ def self.map_attributes(sp_attributes)
21
+ attr_map = SP_SETTINGS["attribute_map"]
22
+ mapped_attributes = {}
23
+
24
+ sp_attributes.each do |sp_attribute,value|
25
+ sp_attribute = attr_map[sp_attribute] || value
26
+ mapped_attributes[sp_attribute] = value
27
+ end
28
+ mapped_attributes
29
+ end
30
+
31
+ def self.saml_settings
32
+ sp_settings = SP_SETTINGS["settings"]
33
+
34
+ settings = OneLogin::RubySaml::Settings.new
35
+ settings.assertion_consumer_service_url = sp_settings["acs"]
36
+
37
+ settings.issuer = sp_settings["entity_id"]
38
+ settings.idp_sso_target_url = sp_settings["sso_url"]
39
+
40
+ # certificate to register with IDP and key to decrypt
41
+ settings.certificate = SP_CERT
42
+
43
+ # certificate to decrypt SAML response
44
+ settings.private_key = SP_KEY
45
+
46
+ # certificate to verify IDP signature
47
+ settings.idp_cert = IDP_CERT
48
+
49
+ settings
50
+ end
51
+ end
52
+
53
+
54
+ module Logging
55
+ begin
56
+ PRIMARY_ID = SP_SETTINGS["settings"]["primary_id"]
57
+ SHOULD_LOG = SP_SETTINGS["settings"]["saml_logging"]
58
+ LOGGER = Logger.new("log/saml.log")
59
+ rescue
60
+ #rescue othewise the generator fails
61
+ end
62
+
63
+
64
+ def self.auth_failure(error_context)
65
+ LOGGER.error("An error occured during authentication. #{error_context}") if SHOULD_LOG
66
+ LOGGER.error("Backtrace: \n\t\t#{error_context.backtrace.join("\n\t\t")}") if SHOULD_LOG
67
+ rescue
68
+ LOGGER.debug("Unknown Error During auth_failure logging.") if SHOULD_LOG
69
+ end
70
+
71
+ def self.bad_ip(saml_attrs,request_ip,current_ip)
72
+ LOGGER.info("Bad IP address for #{saml_attrs[PRIMARY_ID]}. IP at SAML request #{request_ip} | IP presented #{current_ip}") if SHOULD_LOG
73
+ rescue
74
+ LOGGER.debug("Unknown Error During relay state logging. IP check") if SHOULD_LOG
75
+ end
76
+
77
+ def self.expired_session(saml_attrs)
78
+ LOGGER.info("Session Expired for #{saml_attrs[PRIMARY_ID]}") if SHOULD_LOG
79
+ rescue
80
+ LOGGER.debug("Unknown Error During relay state logging. Expired session check") if SHOULD_LOG
81
+ end
82
+
83
+ def self.logout(saml_attrs)
84
+ LOGGER.info("#{saml_attrs[PRIMARY_ID]} has succesfully logged out.") if SHOULD_LOG
85
+ rescue
86
+ LOGGER.debug("Unknown error logging user logout. Most likely anonymous user clicked a logout button.") if SHOULD_LOG
87
+ end
88
+
89
+ def self.saml_state(data)
90
+ LOGGER.info("Stored Relay: #{data[:stored_relay]} | RequestRelay: #{data[:request_relay]} | Stored IP: #{data[:stored_ip]} RemoteIP: #{data[:remote_ip]}") if SHOULD_LOG
91
+ rescue
92
+ LOGGER.debug("Unknown Error During relay state logging. Saml state check") if SHOULD_LOG
93
+ end
94
+
95
+ def self.successful_auth(saml_attrs)
96
+ LOGGER.info("#{saml_attrs[PRIMARY_ID]} has succesfully authenticated.") if SHOULD_LOG
97
+ rescue
98
+ LOGGER.debug("Unknown Error During successful_auth logging. Check PRIMARY_ID configured in settings.json and that user has attribute.") if SHOULD_LOG
99
+ end
100
+ end
101
+
5
102
  end
@@ -6,7 +6,7 @@ module SamlCamel
6
6
  isolate_namespace SamlCamel
7
7
 
8
8
  config.to_prepare do
9
- ActionController::Base.include SamlCamel::SamlHelpers
9
+ ActionController::Base.include SamlCamel::SamlService
10
10
  end
11
11
 
12
12
  end
File without changes
@@ -1,3 +1,3 @@
1
1
  module SamlCamel
2
- VERSION = '0.2.2'
2
+ VERSION = '0.2.6'
3
3
  end
@@ -1,7 +1,7 @@
1
1
  namespace :saml_camel do
2
2
  desc "Generate Files for Saml"
3
3
  task :generate_saml do
4
- dir = "#{Rails.root}/saml/"
4
+ dir = "#{Rails.root}/config/saml/"
5
5
  FileUtils.mkdir(dir) unless Dir.exists?(dir)
6
6
 
7
7
  specified_env = ENV['environment']
@@ -12,7 +12,6 @@ namespace :saml_camel do
12
12
 
13
13
 
14
14
  #TODO pull in specified idp certificate
15
- # idp_cert = File.read("saml/idp_certs/#{ENV['idp']}.crt") if ENV['idp']
16
15
  idp_cert = """MIIEWjCCA0KgAwIBAgIJAP1rB/FjRgy6MA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNV
17
16
  BAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEPMA0GA1UEBxMGRHVyaGFt
18
17
  MRgwFgYDVQQKEw9EdWtlIFVuaXZlcnNpdHkxDDAKBgNVBAsTA09JVDEaMBgGA1UE
@@ -41,22 +40,22 @@ tA6SX0infqNRyPRNJK+bnQd1yOP4++tjD/lAPE+5tiD/waI3fArt43ZE/qp7pYMS
41
40
 
42
41
  unless specified_env
43
42
  default_envs.each do |e|
44
- dir = "#{Rails.root}/saml/#{e}"
43
+ dir = "#{Rails.root}/config/saml/#{e}"
45
44
  FileUtils.mkdir(dir) unless Dir.exists?(dir)
46
- File.open("#{Rails.root}/saml/#{e}/saml_certificate.crt","w+") {|f| f.write(cert) }
47
- File.open("#{Rails.root}/saml/#{e}/saml_key.key","w+") {|f| f.write(key) }
48
- File.open("#{Rails.root}/saml/#{e}/idp_certificate.crt","w+") {|f| f.write(idp_cert) }
49
- File.open("#{Rails.root}/saml/#{e}/settings.json","w+") {|f| f.write(settings) }
50
- File.open('.gitignore', 'a') { |f| f.write("saml/#{e}/saml_key.key\n") }
45
+ File.open("#{Rails.root}/config/saml/#{e}/saml_certificate.crt","w+") {|f| f.write(cert) }
46
+ File.open("#{Rails.root}/config/saml/#{e}/saml_key.key","w+") {|f| f.write(key) }
47
+ File.open("#{Rails.root}/config/saml/#{e}/idp_certificate.crt","w+") {|f| f.write(idp_cert) }
48
+ File.open("#{Rails.root}/config/saml/#{e}/settings.json","w+") {|f| f.write(settings) }
49
+ File.open('.gitignore', 'a') { |f| f.write("config/saml/#{e}/saml_key.key\n") }
51
50
  end
52
51
  else
53
- dir = "#{Rails.root}/saml/#{specified_env}"
52
+ dir = "#{Rails.root}/config/saml/#{specified_env}"
54
53
  FileUtils.mkdir(dir) unless Dir.exists?(dir)
55
- File.open("#{Rails.root}/saml/#{specified_env}/saml_certificate.crt","w+") {|f| f.write(cert) }
56
- File.open("#{Rails.root}/saml/#{specified_env}/saml_key.key","w+") {|f| f.write(key) }
57
- File.open("#{Rails.root}/saml/#{specified_env}/idp_certificate.crt","w+") {|f| f.write(idp_cert) }
58
- File.open("#{Rails.root}/saml/#{specified_env}/settings.json","w+") {|f| f.write(settings) }
59
- File.open('.gitignore', 'a') { |f| f.write("saml/#{specified_env}/saml_key.key") }
54
+ File.open("#{Rails.root}/config/saml/#{specified_env}/saml_certificate.crt","w+") {|f| f.write(cert) }
55
+ File.open("#{Rails.root}/config/saml/#{specified_env}/saml_key.key","w+") {|f| f.write(key) }
56
+ File.open("#{Rails.root}/config/saml/#{specified_env}/idp_certificate.crt","w+") {|f| f.write(idp_cert) }
57
+ File.open("#{Rails.root}/config/saml/#{specified_env}/settings.json","w+") {|f| f.write(settings) }
58
+ File.open('.gitignore', 'a') { |f| f.write("config/saml/#{specified_env}/saml_key.key") }
60
59
  end
61
60
  end
62
61
 
@@ -68,7 +67,7 @@ tA6SX0infqNRyPRNJK+bnQd1yOP4++tjD/lAPE+5tiD/waI3fArt43ZE/qp7pYMS
68
67
  acs: "http://localhost:3000/saml/consumeSaml" ,
69
68
  entity_id: "https://your-entity-id.com",
70
69
  sso_url: "https://shib.oit.duke.edu/idp/profile/SAML2/Redirect/SSO",
71
- logout_return_url: "http://localhost:3000",
70
+ logout_url: "https://shib.oit.duke.edu/cgi-bin/logout.pl",
72
71
  primary_id: "eduPersonPrincipalName",
73
72
  sp_session_timeout: 1,
74
73
  sp_session_lifetime: 8,
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.2
4
+ version: 0.2.6
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-26 00:00:00.000000000 Z
11
+ date: 2018-05-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -76,25 +76,19 @@ files:
76
76
  - MIT-LICENSE
77
77
  - README.md
78
78
  - Rakefile
79
- - app/assets/config/saml_camel_manifest.js
80
- - app/assets/javascripts/saml_camel/application.js
81
- - app/assets/stylesheets/saml_camel/application.css
82
- - app/assets/stylesheets/scaffold.css
83
- - app/controllers/concerns/saml_camel/saml_helpers.rb
79
+ - app/controllers/concerns/saml_camel/saml_service.rb
84
80
  - app/controllers/saml_camel/application_controller.rb
85
81
  - app/controllers/saml_camel/saml_controller.rb
86
- - app/helpers/saml_camel/application_helper.rb
87
- - app/jobs/saml_camel/application_job.rb
88
- - app/mailers/saml_camel/application_mailer.rb
89
82
  - app/models/saml_camel/application_record.rb
90
- - app/models/saml_camel/logging.rb
91
- - app/models/saml_camel/transaction.rb
83
+ - app/models/saml_camel/service_provider.rb
84
+ - app/models/saml_camel/shib.rb
92
85
  - app/views/layouts/saml_camel/application.html.erb
93
86
  - app/views/saml_camel/saml/attr_check.html.erb
94
87
  - app/views/saml_camel/saml/failure.html.erb
95
88
  - config/routes.rb
96
89
  - lib/saml_camel.rb
97
90
  - lib/saml_camel/engine.rb
91
+ - lib/saml_camel/transaction.rb
98
92
  - lib/saml_camel/version.rb
99
93
  - lib/tasks/saml_camel_tasks.rake
100
94
  homepage: https://idms-git.oit.duke.edu/da129/saml_camel
@@ -1,2 +0,0 @@
1
- //= link_directory ../javascripts/saml_camel .js
2
- //= link_directory ../stylesheets/saml_camel .css
@@ -1,13 +0,0 @@
1
- // This is a manifest file that'll be compiled into application.js, which will include all the files
2
- // listed below.
3
- //
4
- // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
- // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
6
- //
7
- // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
- // compiled file. JavaScript code in this file should be added after the last require_* statement.
9
- //
10
- // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
11
- // about supported directives.
12
- //
13
- //= require_tree .
@@ -1,15 +0,0 @@
1
- /*
2
- * This is a manifest file that'll be compiled into application.css, which will include all the files
3
- * listed below.
4
- *
5
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
- * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
- *
8
- * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
- * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
- * files in this directory. Styles in this file should be added after the last require_* statement.
11
- * It is generally better to create a new file per style scope.
12
- *
13
- *= require_tree .
14
- *= require_self
15
- */
@@ -1,80 +0,0 @@
1
- body {
2
- background-color: #fff;
3
- color: #333;
4
- margin: 33px;
5
- }
6
-
7
- body, p, ol, ul, td {
8
- font-family: verdana, arial, helvetica, sans-serif;
9
- font-size: 13px;
10
- line-height: 18px;
11
- }
12
-
13
- pre {
14
- background-color: #eee;
15
- padding: 10px;
16
- font-size: 11px;
17
- }
18
-
19
- a {
20
- color: #000;
21
- }
22
-
23
- a:visited {
24
- color: #666;
25
- }
26
-
27
- a:hover {
28
- color: #fff;
29
- background-color: #000;
30
- }
31
-
32
- th {
33
- padding-bottom: 5px;
34
- }
35
-
36
- td {
37
- padding: 0 5px 7px;
38
- }
39
-
40
- div.field,
41
- div.actions {
42
- margin-bottom: 10px;
43
- }
44
-
45
- #notice {
46
- color: green;
47
- }
48
-
49
- .field_with_errors {
50
- padding: 2px;
51
- background-color: red;
52
- display: table;
53
- }
54
-
55
- #error_explanation {
56
- width: 450px;
57
- border: 2px solid red;
58
- padding: 7px 7px 0;
59
- margin-bottom: 20px;
60
- background-color: #f0f0f0;
61
- }
62
-
63
- #error_explanation h2 {
64
- text-align: left;
65
- font-weight: bold;
66
- padding: 5px 5px 5px 15px;
67
- font-size: 12px;
68
- margin: -7px -7px 0;
69
- background-color: #c00;
70
- color: #fff;
71
- }
72
-
73
- #error_explanation ul li {
74
- font-size: 12px;
75
- list-style: square;
76
- }
77
-
78
- label {
79
- display: block;
80
- }
@@ -1,134 +0,0 @@
1
- require_dependency "saml_camel/application_controller"
2
-
3
- module SamlCamel::SamlHelpers
4
- extend ActiveSupport::Concern
5
- SP_SETTINGS = JSON.parse(File.read("saml/#{Rails.env}/settings.json"))
6
-
7
- #this generates a call to the idp, which will then be returned to the consume action the in saml_contorller
8
- def saml_request(host_request)
9
- request = OneLogin::RubySaml::Authrequest.new
10
- assign_permit_key
11
- lifetime = SP_SETTINGS['settings']["sp_session_lifetime"]
12
- permit_key = session[:saml_session_id].to_sym
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 }
17
- end
18
-
19
- saml_request_url = request.create(SamlCamel::Transaction.saml_settings)
20
- redirect_to(saml_request_url)
21
- end
22
-
23
-
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
38
- else
39
- Rails.cache.fetch("response_ids", expires_in: 1.hours) do
40
- []
41
- end
42
- end
43
- end
44
-
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
51
- end
52
-
53
-
54
- #Make it so sp sessions only last 1 hour, sp_session is set on a succesfull saml response.
55
- #We check that the session time is less than in hour if so we refresh, otherwise we delete the session
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
-
67
- if session[:sp_session]
68
- #if the session has timed out remove session, otherwise refresh
69
- if (Time.now - Time.parse(session[:sp_session])) < sp_timeout.hour
70
- session[:sp_session] = Time.now
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
80
- end
81
- end
82
- end
83
-
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
-
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
97
- def saml_protect
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
128
- end
129
-
130
- def assign_permit_key
131
- session[:saml_session_id] = SecureRandom.base64.chomp.gsub( /\W/, '' )
132
- end
133
-
134
- end
@@ -1,4 +0,0 @@
1
- module SamlCamel
2
- module ApplicationHelper
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- module SamlCamel
2
- class ApplicationJob < ActiveJob::Base
3
- end
4
- end
@@ -1,6 +0,0 @@
1
- module SamlCamel
2
- class ApplicationMailer < ActionMailer::Base
3
- default from: 'from@example.com'
4
- layout 'mailer'
5
- end
6
- end
@@ -1,51 +0,0 @@
1
- module SamlCamel
2
- class Logging
3
- SP_SETTINGS = JSON.parse(File.read("saml/#{Rails.env}/settings.json"))
4
- PRIMARY_ID = SP_SETTINGS["settings"]["primary_id"]
5
- SHOULD_LOG = SP_SETTINGS["settings"]["saml_logging"]
6
-
7
- def self.successfull_auth(saml_attrs)
8
- logger = Logger.new("log/saml.log")
9
- logger.info("#{saml_attrs[PRIMARY_ID]} has succesfully authenticated.")
10
- rescue
11
- logger.debug("Unknown Error During successfull_auth logging. Check PRIMARY_ID configured in settings.json and that user has attribute.")
12
- end
13
-
14
- def self.auth_failure(error_context)
15
- logger = Logger.new("log/saml.log")
16
- logger.error("An error occured during authentication. #{error_context}")
17
- logger.error("Backtrace: \n\t\t#{error_context.backtrace.join("\n\t\t")}")
18
- rescue
19
- logger.debug("Unknown Error During auth_failure logging.")
20
- end
21
-
22
- def self.logout(saml_attrs)
23
- logger = Logger.new("log/saml.log")
24
- logger.info("#{saml_attrs[PRIMARY_ID]} has succesfully logged out.")
25
- rescue
26
- logger.debug("Unknown error logging user logout. Most likely anonymous user clicked a logout button.")
27
- end
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
-
43
- def self.saml_state(data)
44
- logger = Logger.new("log/saml.log")
45
- logger.info("Stored Relay: #{data[:stored_relay]} | RequestRelay: #{data[:request_relay]} | Stored IP: #{data[:stored_ip]} RemoteIP: #{data[:remote_ip]}")
46
- rescue
47
- logger.debug("Unknown Error During relay state logging.")
48
- end
49
-
50
- end
51
- end
@@ -1,56 +0,0 @@
1
- module SamlCamel
2
- class Transaction
3
- SP_SETTINGS = JSON.parse(File.read("saml/#{Rails.env}/settings.json"))
4
-
5
- def self.saml_settings
6
- sp_settings = SP_SETTINGS["settings"]
7
- settings = OneLogin::RubySaml::Settings.new
8
- settings.assertion_consumer_service_url = sp_settings["acs"]
9
-
10
-
11
- settings.issuer = sp_settings["entity_id"]
12
- settings.idp_sso_target_url = sp_settings["sso_url"]
13
-
14
-
15
- # certificate to register with IDP and key to decrypt
16
- settings.certificate = File.read("saml/#{Rails.env}/saml_certificate.crt")
17
-
18
- # certificate to decrypt SAML response
19
- settings.private_key = File.read("saml/#{Rails.env}/saml_key.key")
20
-
21
- # certificate to verify IDP signature
22
- settings.idp_cert = File.read("saml/#{Rails.env}/idp_certificate.crt")
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
-
29
- # Optional for most SAML IdPs
30
- settings.authn_context = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
31
- settings.attribute_consuming_service.configure do
32
- service_name "Service"
33
- service_index 5
34
- add_attribute :redirect_path => "root_path"
35
- end
36
- settings
37
- end
38
-
39
- def self.map_attributes(attrs)
40
- attr_map = SP_SETTINGS["attribute_map"]
41
- mapped_attributes = {}
42
-
43
- attrs.each do |attr,value|
44
- mapped_name = attr_map[attr]
45
- if mapped_name.nil? #handles attributes not in map
46
- mapped_attributes[attr] = value
47
- else
48
- mapped_attributes[mapped_name] = value
49
- end
50
- end
51
- mapped_attributes
52
- end
53
-
54
-
55
- end
56
- end