saml_camel 0.2.2 → 0.2.6

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