saml_camel 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +16 -4
- data/app/controllers/concerns/saml_camel/saml_service.rb +17 -12
- data/app/controllers/saml_camel/application_controller.rb +3 -0
- data/app/controllers/saml_camel/saml_controller.rb +47 -29
- data/app/models/saml_camel/application_record.rb +2 -1
- data/app/models/saml_camel/service_provider.rb +99 -71
- data/app/models/saml_camel/shib.rb +7 -12
- data/config/routes.rb +8 -2
- data/config/saml/development/idp_certificate.crt +25 -0
- data/config/saml/development/saml_certificate.crt +28 -0
- data/config/saml/development/saml_key.key +27 -0
- data/config/saml/development/settings.json +36 -0
- data/config/saml/production/idp_certificate.crt +25 -0
- data/config/saml/production/saml_certificate.crt +28 -0
- data/config/saml/production/saml_key.key +27 -0
- data/config/saml/production/settings.json +1 -0
- data/config/saml/test/idp_certificate.crt +25 -0
- data/config/saml/test/saml_certificate.crt +28 -0
- data/config/saml/test/saml_key.key +27 -0
- data/config/saml/test/settings.json +36 -0
- data/lib/saml_camel/engine.rb +3 -3
- data/lib/saml_camel/version.rb +3 -1
- data/lib/saml_camel.rb +81 -53
- data/lib/tasks/saml_camel_tasks.rake +73 -73
- metadata +33 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 10d5430e20fe2c208e8f6b5c23eb6619cab0c2a1
|
4
|
+
data.tar.gz: c41a7aca05f8d811e970d49cca9b46dafdbbe3a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9ce2f9753fd6fe3dfc8ef4e3bd71b51d507ddc3b488f5c8a69312bc0ee739c71212e5d43e374bc603e1f0294a21a908bade01758e2609695a5550e64d4ebcd1
|
7
|
+
data.tar.gz: 710161acccafad9bd900433440881c96d6bca2f392cc0320a91e3225dfd85144d9a912ece2c084608f5d23d78b661870b5343469472073f9f6820537db6b3196
|
data/README.md
CHANGED
@@ -83,8 +83,10 @@ Identity Provider(idp) to recognize your app. Typically it should take the form
|
|
83
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
|
84
84
|
|
85
85
|
|
86
|
-
10.
|
87
|
-
|
86
|
+
10. Convenience Endpoints (assuming enginte is mounted to `saml` path):
|
87
|
+
- `/saml/attributes` view attributes being passed through
|
88
|
+
- `/saml/metadata` generate metadata for your sp
|
89
|
+
- `/saml/testAuthn` forces authentication and returns decrypted saml response, test auth path must be set to `true` in settings
|
88
90
|
|
89
91
|
## Example settings.json
|
90
92
|
```json
|
@@ -92,13 +94,16 @@ Identity Provider(idp) to recognize your app. Typically it should take the form
|
|
92
94
|
"_comment": "note you will need to restart the application when you make changes to this file",
|
93
95
|
"settings": {
|
94
96
|
"acs": "http://localhost:3000/saml/consumeSaml",
|
95
|
-
"
|
97
|
+
"raw_response_acs": "http://localhost:3000/saml/consumeSaml/rawResponse",
|
98
|
+
"entity_id": "https://samlCamel.com/doesNotHaveToResolve",
|
96
99
|
"sso_url": "https://shib.oit.duke.edu/idp/profile/SAML2/Redirect/SSO",
|
97
100
|
"logout_url": "https://shib.oit.duke.edu/cgi-bin/logout.pl",
|
98
101
|
"primary_id": "eduPersonPrincipalName",
|
99
102
|
"sp_session_timeout": 1,
|
100
103
|
"sp_session_lifetime": 8,
|
101
|
-
"
|
104
|
+
"test_auth_path": true,
|
105
|
+
"saml_logging": true,
|
106
|
+
"debug": false
|
102
107
|
},
|
103
108
|
"attribute_map": {
|
104
109
|
"urn:oid:1.3.6.1.4.1.5923.1.1.1.9": "eduPersonScopedAffiliation",
|
@@ -123,6 +128,13 @@ Identity Provider(idp) to recognize your app. Typically it should take the form
|
|
123
128
|
}
|
124
129
|
```
|
125
130
|
|
131
|
+
## Testing
|
132
|
+
If saml_protect is called during testing the app will more than likely hang as it is
|
133
|
+
waiting for a user to authenticate at the idp. To get around this you have two options:
|
134
|
+
|
135
|
+
1. You can not call saml_protect in the test environment and mock your users
|
136
|
+
2. Use `SamlCaml::ServiceProvider.mock_saml_cache(permit_key: 'some permit key', ip_address: 'enter your host ip here')`. This will setup a cache(note caching for test must be enabled). In addition you will need to mock to session variables. set `session[:sp_session]` to `Time.now` and set mock attributes in `session[:saml_attributes]`
|
137
|
+
|
126
138
|
|
127
139
|
## License
|
128
140
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -1,6 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_dependency 'saml_camel/application_controller'
|
4
|
+
|
5
|
+
# Helper Methods for SamlController
|
6
|
+
module SamlCamel::SamlService # rubocop:disable Style/ClassAndModuleChildren
|
4
7
|
extend ActiveSupport::Concern
|
5
8
|
|
6
9
|
def cache_available?(app_cache)
|
@@ -12,24 +15,26 @@ module SamlCamel::SamlService
|
|
12
15
|
end
|
13
16
|
end
|
14
17
|
|
15
|
-
|
18
|
+
# TODO: refactor
|
19
|
+
def saml_protect # rubocop:disable Metrics/MethodLength, Metrics/AbcSize:
|
16
20
|
user_cache = cache_available?(Rails.cache.fetch(session[:saml_session_id])) if session[:saml_session_id]
|
17
21
|
if session[:saml_session_id] && user_cache
|
18
|
-
sp = SamlCamel::ServiceProvider.new(
|
19
|
-
|
20
|
-
|
22
|
+
sp = SamlCamel::ServiceProvider.new(
|
23
|
+
cache_permit_key: session[:saml_session_id].to_sym,
|
24
|
+
saml_attributes: session[:saml_attributes]
|
25
|
+
)
|
26
|
+
session[:sp_session] = sp.validate_sp_session(session[:sp_session], request.remote_ip)
|
27
|
+
unless session[:saml_response_success] || session[:sp_session]
|
21
28
|
saml_request_url = sp.generate_saml_request(request)
|
22
29
|
redirect_to(saml_request_url)
|
23
30
|
end
|
24
|
-
|
25
31
|
else
|
26
32
|
session[:saml_session_id] = SamlCamel::ServiceProvider.generate_permit_key
|
27
|
-
saml_request_url = SamlCamel::ServiceProvider.new(
|
33
|
+
saml_request_url = SamlCamel::ServiceProvider.new(
|
34
|
+
cache_permit_key: session[:saml_session_id].to_sym
|
35
|
+
).generate_saml_request(request)
|
28
36
|
redirect_to(saml_request_url)
|
29
37
|
end
|
30
|
-
session[:saml_response_success] = nil #keeps us from looping
|
38
|
+
session[:saml_response_success] = nil # keeps us from looping
|
31
39
|
end
|
32
|
-
|
33
|
-
|
34
|
-
|
35
40
|
end
|
@@ -1,53 +1,54 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_dependency 'saml_camel/application_controller'
|
3
4
|
module SamlCamel
|
5
|
+
# handles SamlCamel SP
|
4
6
|
class SamlController < ApplicationController
|
5
7
|
include SamlCamel::SamlService
|
6
|
-
skip_before_action :verify_authenticity_token
|
8
|
+
skip_before_action :verify_authenticity_token, only: %i[consume logout raw_response]
|
7
9
|
before_action :saml_protect, only: [:attr_check]
|
8
10
|
|
11
|
+
def attr_check; end
|
12
|
+
# consumes the saml response from the IDP
|
13
|
+
# TODO break out into separate methods if possible
|
14
|
+
# TODO rubocop also suggests to many assignments going on in consume
|
9
15
|
|
10
|
-
|
11
|
-
#convinence route to see attributes that are coming through
|
12
|
-
def index
|
13
|
-
@attributes = session[:saml_attributes]
|
14
|
-
end
|
15
|
-
|
16
|
-
|
17
|
-
#consumes the saml response from the IDP
|
18
|
-
def consume
|
16
|
+
def consume # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
19
17
|
permit_key = session[:saml_session_id].to_sym
|
20
|
-
user_cache =
|
21
|
-
|
18
|
+
user_cache = Rails.cache.fetch(permit_key)
|
19
|
+
unless cache_available?(user_cache)
|
20
|
+
raise 'Unable to access cache. Ensure cache is configured according to documentation.'
|
21
|
+
end
|
22
22
|
redirect_path = user_cache[:redirect_url]
|
23
|
-
sp = SamlCamel::ServiceProvider.new(
|
23
|
+
sp = SamlCamel::ServiceProvider.new(
|
24
|
+
cache_permit_key: permit_key, saml_attributes: session[:saml_attributes]
|
25
|
+
)
|
24
26
|
ol_response = SamlCamel::ServiceProvider.ol_response(params[:SAMLResponse])
|
25
27
|
|
26
|
-
if sp.validate_idp_response(ol_response,request.remote_ip)
|
28
|
+
if sp.validate_idp_response(ol_response, request.remote_ip)
|
27
29
|
# authorize_success, log the user
|
28
30
|
session[:saml_response_success] = true
|
29
31
|
sp.set_saml_session_lifetime
|
30
32
|
session[:sp_session] = Time.now
|
31
|
-
|
32
33
|
session[:saml_attributes] = SamlCamel::Transaction.map_attributes(ol_response.attributes)
|
33
34
|
SamlCamel::Logging.successful_auth(session[:saml_attributes])
|
34
35
|
|
35
36
|
redirect_to redirect_path
|
36
37
|
else # otherwise list out the errors in the response
|
37
|
-
if
|
38
|
+
if session[:saml_session_id]
|
38
39
|
permit_key = session[:saml_session_id].to_sym
|
39
40
|
Rails.cache.delete(permit_key)
|
40
41
|
session[:saml_session_id] = nil
|
41
42
|
end
|
42
43
|
session[:sp_session] = nil
|
43
44
|
session[:saml_response_success] = false
|
44
|
-
response.errors
|
45
|
+
# response.errors
|
45
46
|
SamlCamel::Logging.auth_failure(ol_response.errors)
|
46
47
|
|
47
|
-
redirect_to action:
|
48
|
+
redirect_to action: 'failure', locals: { errors: ol_response.errors }
|
48
49
|
end
|
49
|
-
rescue => e
|
50
|
-
if
|
50
|
+
rescue StandardError => e
|
51
|
+
if session[:saml_session_id]
|
51
52
|
permit_key = session[:saml_session_id].to_sym
|
52
53
|
Rails.cache.delete(permit_key)
|
53
54
|
end
|
@@ -56,35 +57,52 @@ module SamlCamel
|
|
56
57
|
session[:sp_session] = nil
|
57
58
|
|
58
59
|
SamlCamel::Logging.auth_failure(e)
|
59
|
-
redirect_to action:
|
60
|
+
redirect_to action: 'failure', locals: { errors: e }
|
60
61
|
end
|
61
62
|
|
62
|
-
|
63
|
-
#route to show saml failures
|
63
|
+
# route to show saml failures
|
64
64
|
def failure
|
65
65
|
@error = params[:locals][:errors]
|
66
66
|
end
|
67
67
|
|
68
|
+
# convinence route to see attributes that are coming through
|
69
|
+
def index
|
70
|
+
@attributes = session[:saml_attributes]
|
71
|
+
end
|
68
72
|
|
69
|
-
#kills SP session and redirects to IDP to kill idp session
|
73
|
+
# kills SP session and redirects to IDP to kill idp session
|
70
74
|
def logout
|
71
75
|
SamlCamel::Logging.logout(session[:saml_attributes])
|
72
76
|
session[:saml_attributes] = nil
|
73
77
|
session[:sp_session] = nil
|
74
78
|
|
75
|
-
# return_url = SamlCamel::Transaction.logout #this methods logs the user
|
76
|
-
|
79
|
+
# return_url = SamlCamel::Transaction.logout #this methods logs the user
|
80
|
+
# out of the IDP, and returns a url to be redirected to
|
81
|
+
redirect_to SP_SETTINGS['settings']['logout_url']
|
77
82
|
end
|
78
83
|
|
84
|
+
# generate a metadata page that you may need to share with an IDP
|
85
|
+
def metadata
|
86
|
+
settings = SamlCamel::Transaction.saml_settings
|
87
|
+
meta = OneLogin::RubySaml::Metadata.new
|
88
|
+
render xml: meta.generate(settings)
|
89
|
+
end
|
79
90
|
|
80
|
-
|
91
|
+
# meant to force authn and then be redirected to raw response to view raw decrypted response
|
92
|
+
def force_authn
|
93
|
+
saml_request_url = SamlCamel::ServiceProvider.new.generate_saml_request(request, force_authn: true)
|
94
|
+
redirect_to(saml_request_url)
|
81
95
|
end
|
82
96
|
|
97
|
+
def raw_response
|
98
|
+
ol_response = SamlCamel::ServiceProvider.ol_response(params[:SAMLResponse], raw_response: true)
|
99
|
+
render xml: ol_response.decrypted_document.to_s if ol_response.is_valid?
|
100
|
+
end
|
83
101
|
|
84
102
|
private
|
103
|
+
|
85
104
|
def saml_settings
|
86
105
|
SamlCamel::Transaction.saml_settings
|
87
106
|
end
|
88
|
-
|
89
107
|
end
|
90
108
|
end
|
@@ -1,121 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SamlCamel
|
4
|
+
# class serves as core buisness logic for SamlCamle SP
|
2
5
|
class ServiceProvider
|
3
|
-
attr_reader :cache_permit_key, :
|
6
|
+
attr_reader :cache_permit_key, :saml_attributes
|
4
7
|
|
5
|
-
def initialize(cache_permit_key: nil,
|
6
|
-
@cache_permit_key = cache_permit_key.to_sym
|
8
|
+
def initialize(cache_permit_key: nil, saml_attributes: nil)
|
9
|
+
@cache_permit_key = cache_permit_key.try(:to_sym)
|
7
10
|
@saml_attributes = saml_attributes
|
8
11
|
@user_cache = Rails.cache.fetch(@cache_permit_key)
|
9
12
|
end
|
10
13
|
|
11
|
-
|
12
14
|
def self.generate_permit_key
|
13
|
-
secure_random = SecureRandom.base64.chomp.gsub(
|
15
|
+
secure_random = SecureRandom.base64.chomp.gsub(/\W/, '')
|
14
16
|
"samlCamel#{secure_random}"
|
15
17
|
end
|
16
18
|
|
17
|
-
def self.
|
18
|
-
|
19
|
-
|
19
|
+
def self.mock_saml_cache(permit_key: nil, ip_address: nil)
|
20
|
+
lifetime = SP_SETTINGS['settings']['sp_session_lifetime']
|
21
|
+
Rails.cache.fetch(@cache_permit_key, expires_in: lifetime.hours) do
|
22
|
+
{ ip_address: host_request.remote_ip }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# ol OneLogin
|
27
|
+
def self.ol_response(idp_response, raw_response: false)
|
28
|
+
settings = SamlCamel::Transaction.saml_settings(raw_response: raw_response)
|
29
|
+
response = OneLogin::RubySaml::Response.new(idp_response, settings: settings)
|
30
|
+
response.settings = settings
|
20
31
|
|
21
32
|
response
|
22
33
|
end
|
23
34
|
|
24
|
-
|
25
|
-
def check_expired_session(sp_session)
|
26
|
-
|
27
|
-
|
28
|
-
sp_lifetime = SP_SETTINGS['settings']["sp_session_lifetime"]
|
35
|
+
# TODO: method too complex
|
36
|
+
def check_expired_session(sp_session) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/LineLength
|
37
|
+
sp_timeout = SP_SETTINGS['settings']['sp_session_timeout']
|
38
|
+
sp_lifetime = SP_SETTINGS['settings']['sp_session_lifetime']
|
29
39
|
|
30
40
|
set_saml_session_lifetime if @user_cache[:session_start_time].nil?
|
31
41
|
sp_session_init_time = @user_cache[:session_start_time]
|
32
42
|
|
33
|
-
|
43
|
+
SamlCamel::Logging.debug('Checking if session expired') if SP_DEBUG
|
34
44
|
######## set session[:sp_session] maybe a seperate method
|
35
45
|
if sp_session
|
36
|
-
|
37
|
-
|
38
|
-
if (Time.now - Time.parse(sp_session)) < sp_timeout.hour
|
39
|
-
return Time.now
|
40
|
-
else
|
46
|
+
# if the session has exceeded the allowed lifetime, remove session
|
47
|
+
if (Time.now - sp_session_init_time) > sp_lifetime.hour
|
41
48
|
SamlCamel::Logging.expired_session(@saml_attributes)
|
42
49
|
return nil
|
43
50
|
end
|
44
51
|
|
45
|
-
#if the session has
|
46
|
-
if (Time.now -
|
52
|
+
# if the session has timed out remove session, otherwise refresh
|
53
|
+
if (Time.now - Time.parse(sp_session)) < sp_timeout.hour
|
54
|
+
SamlCamel::Logging.debug('Session within timeout, session renewed') if SP_DEBUG
|
55
|
+
Time.now
|
56
|
+
else
|
47
57
|
SamlCamel::Logging.expired_session(@saml_attributes)
|
48
|
-
return
|
58
|
+
return nil
|
49
59
|
end
|
50
|
-
else #if no sp session return nil
|
60
|
+
else # if no sp session return nil
|
61
|
+
SamlCamel::Logging.debug('No session found when checking expiration') if SP_DEBUG
|
51
62
|
return nil
|
52
63
|
end
|
53
64
|
end
|
54
65
|
|
66
|
+
# TODO: method too complex
|
67
|
+
def duplicate_response_id?(response_id, count: 3) # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/LineLength
|
68
|
+
SamlCamel::Logging.debug('Checking uniqueness of response id') if SP_DEBUG
|
55
69
|
|
56
|
-
|
57
|
-
#use semaphore to only allow 1 thread at a time to access
|
70
|
+
# use semaphore to only allow 1 thread at a time to access
|
58
71
|
@semaphore ||= Mutex.new
|
59
|
-
@semaphore.synchronize {
|
60
|
-
ids = Rails.cache.fetch(
|
72
|
+
@semaphore.synchronize { # rubocop:disable Style/BlockDelimiters
|
73
|
+
ids = Rails.cache.fetch('saml_camel_response_ids')
|
61
74
|
if ids
|
62
75
|
if ids.include?(response_id)
|
63
|
-
|
64
|
-
raise
|
76
|
+
SamlCamel::Logging.debug('Response id has already been used') if SP_DEBUG
|
77
|
+
raise 'SAML response ID already issued.'
|
65
78
|
else
|
79
|
+
SamlCamel::Logging.debug("Unique Response ID #{response_id}") if SP_DEBUG
|
66
80
|
ids << response_id
|
67
|
-
Rails.cache.fetch(
|
81
|
+
Rails.cache.fetch('saml_camel_response_ids', expires_in: 1.hours) do
|
68
82
|
ids
|
69
83
|
end
|
70
84
|
end
|
71
85
|
else
|
72
|
-
|
86
|
+
SamlCamel::Logging.debug("Unique Response ID #{response_id}") if SP_DEBUG
|
87
|
+
Rails.cache.fetch('saml_camel_response_ids', expires_in: 1.hours) do
|
73
88
|
[response_id]
|
74
89
|
end
|
75
90
|
end
|
76
91
|
}
|
77
92
|
rescue ThreadError
|
78
|
-
|
79
|
-
|
93
|
+
puts 'locked ' * 50
|
94
|
+
# SamlCamel::Logging.debug('Response ID check locked, trying again') if SP_DEBUG
|
95
|
+
if count.positive? # rubocop:disable Style/GuardClause
|
80
96
|
sleep(0.1)
|
81
97
|
duplicate_response_id?(response_id, count: count - 1)
|
82
|
-
else raise
|
98
|
+
else raise 'Resposne ID Validation Error'
|
83
99
|
end
|
84
100
|
end
|
85
101
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
def generate_saml_request(host_request)
|
102
|
+
# generates a saml requests and establishes a cache for the user
|
103
|
+
def generate_saml_request(host_request, force_authn: false)
|
104
|
+
SamlCamel::Logging.debug("Creating request for #{host_request.remote_ip}") if SP_DEBUG
|
90
105
|
request = OneLogin::RubySaml::Authrequest.new
|
91
|
-
lifetime =
|
106
|
+
lifetime = SP_SETTINGS['settings']['sp_session_lifetime']
|
92
107
|
|
93
|
-
#store ip address and original url request in memory to be used for
|
108
|
+
# store ip address and original url request in memory to be used for
|
109
|
+
# verification and redirect after response
|
94
110
|
Rails.cache.fetch(@cache_permit_key, expires_in: lifetime.hours) do
|
95
|
-
{ip_address: host_request.remote_ip,
|
111
|
+
{ ip_address: host_request.remote_ip, redirect_url: host_request.url }
|
96
112
|
end
|
97
|
-
request.create(SamlCamel::Transaction.saml_settings)
|
113
|
+
request.create(SamlCamel::Transaction.saml_settings(raw_response: force_authn))
|
98
114
|
end
|
99
115
|
|
100
|
-
|
116
|
+
|
117
|
+
# set saml_session lifetime, called if none set
|
118
|
+
# TODO: this may need to be renamed, it's not really setting the lifetime
|
119
|
+
# it's refreshing the last time a user authenticated
|
101
120
|
def set_saml_session_lifetime
|
102
121
|
user_saml_cache = Rails.cache.fetch(@cache_permit_key)
|
103
122
|
user_saml_cache[:session_start_time] = Time.now
|
104
|
-
sp_lifetime = SP_SETTINGS['settings'][
|
123
|
+
sp_lifetime = SP_SETTINGS['settings']['sp_session_lifetime']
|
124
|
+
|
125
|
+
SamlCamel::Logging.debug("Setting lifetime of session. Lifetime of #{sp_lifetime} hours") if SP_DEBUG
|
105
126
|
Rails.cache.fetch(@cache_permit_key, expires_in: sp_lifetime.hours) do
|
106
|
-
user_saml_cache
|
127
|
+
user_saml_cache
|
107
128
|
end
|
108
129
|
end
|
109
130
|
|
110
|
-
#NOTE these methods will raise errors if not a valid response_id
|
111
|
-
|
112
|
-
def validate_idp_response(response,remote_ip)
|
131
|
+
# NOTE these methods will raise errors if not a valid response_id
|
132
|
+
# which in turn will trigger a resuce that kills the sp session
|
133
|
+
def validate_idp_response(response, remote_ip)
|
134
|
+
SamlCamel::Logging.debug('Validating IDP response') if SP_DEBUG
|
113
135
|
if response.is_valid?
|
114
|
-
#validate not sha1
|
136
|
+
# validate not sha1
|
115
137
|
verify_sha_type(response)
|
116
138
|
|
117
|
-
#validate IP address
|
118
|
-
raise
|
139
|
+
# validate IP address
|
140
|
+
raise 'IP mismatch error' unless validate_ip(remote_ip)
|
119
141
|
|
120
142
|
response_id = response.id(response.document)
|
121
143
|
duplicate_response_id?(response_id)
|
@@ -126,36 +148,42 @@ module SamlCamel
|
|
126
148
|
end
|
127
149
|
end
|
128
150
|
|
129
|
-
#validate that ip address has not changed
|
151
|
+
# validate that ip address has not changed
|
130
152
|
def validate_ip(remote_ip)
|
131
|
-
|
153
|
+
if SP_DEBUG
|
154
|
+
SamlCamel::Logging.debug(
|
155
|
+
"Validating IP consistancy. IP @ request = #{@user_cache[:ip_address]}
|
156
|
+
| current IP = #{remote_ip}"
|
157
|
+
)
|
158
|
+
end
|
159
|
+
|
132
160
|
return true if remote_ip == @user_cache[:ip_address]
|
133
|
-
SamlCamel::Logging.bad_ip(@saml_attributes,
|
134
|
-
|
161
|
+
SamlCamel::Logging.bad_ip(@saml_attributes,
|
162
|
+
@user_cache[:ip_address],
|
163
|
+
remote_ip)
|
164
|
+
nil
|
135
165
|
end
|
136
166
|
|
137
|
-
#validates a the sp timestamps and ip. returns a new timestamp if
|
138
|
-
# otherwise returns nil
|
139
|
-
|
167
|
+
# validates a the sp timestamps and ip. returns a new timestamp if
|
168
|
+
# everything is good otherwise returns nil
|
169
|
+
|
170
|
+
def validate_sp_session(sp_session, remote_ip)
|
140
171
|
new_session = check_expired_session(sp_session)
|
141
|
-
if new_session
|
142
|
-
|
143
|
-
else
|
144
|
-
return nil
|
145
|
-
end
|
146
|
-
return new_session if has_valid_ip
|
172
|
+
valid_ip = validate_ip(remote_ip) if new_session
|
173
|
+
return new_session if new_session && valid_ip
|
147
174
|
end
|
148
175
|
|
149
|
-
|
150
176
|
def verify_sha_type(response)
|
151
|
-
|
152
|
-
|
177
|
+
SamlCamel::Logging.debug('Verify response is not SHA1') if SP_DEBUG
|
178
|
+
|
179
|
+
# was suggested to use an xml parser, however was having great difficulyt
|
180
|
+
# with nokogiri open to trying a different parser or advice on nokogiri as
|
181
|
+
# it's not reacting as I would typically expect it to
|
153
182
|
raw_xml_string = response.decrypted_document.to_s
|
154
|
-
attr_scan = raw_xml_string.scan(
|
155
|
-
is_sha1 = attr_scan[0].match(
|
183
|
+
attr_scan = raw_xml_string.scan(%r{<ds:SignatureMethod.*\/>})
|
184
|
+
is_sha1 = attr_scan[0].match('sha1')
|
156
185
|
|
157
|
-
raise
|
186
|
+
raise 'SHA1 algorithm not supported' if is_sha1
|
158
187
|
end
|
159
|
-
|
160
188
|
end
|
161
189
|
end
|
@@ -1,7 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module SamlCamel
|
4
|
+
# handle shib attributes
|
2
5
|
class Shib
|
3
|
-
|
4
|
-
ATTRIBUTE_MAP = JSON.parse(File.read("config/saml/shibboleth.json"))
|
6
|
+
ATTRIBUTE_MAP = JSON.parse(File.read('config/saml/shibboleth.json'))
|
5
7
|
|
6
8
|
def self.attributes(request)
|
7
9
|
attrs = {}
|
@@ -11,16 +13,9 @@ module SamlCamel
|
|
11
13
|
attrs
|
12
14
|
end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
identity
|
16
|
+
# sets shib headers directly
|
17
|
+
def self.set_headers(request: nil, identity: nil)
|
18
|
+
identity.each { |k, v| request.env[k] = v }
|
18
19
|
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
20
|
end
|
26
21
|
end
|
data/config/routes.rb
CHANGED
@@ -1,9 +1,15 @@
|
|
1
|
+
settings = JSON.parse(File.read("config/saml/#{Rails.env}/settings.json"))
|
2
|
+
# NOTE there are two saml dirs, 1 in config and 1 in dummy. They need to be in
|
3
|
+
# both places so users can run their tests, but the gem can develop with it's
|
4
|
+
# own tests as well
|
5
|
+
|
1
6
|
SamlCamel::Engine.routes.draw do
|
2
7
|
get "/" => "saml#index"
|
3
8
|
get "/attributes" => 'saml#attr_check'
|
4
9
|
get "/failure" => 'saml#failure'
|
10
|
+
get "/metadata" => 'saml#metadata'
|
11
|
+
get "/testAuthn" => 'saml#force_authn' if settings.dig('settings', 'test_auth_path')
|
5
12
|
post "/consumeSaml" => "saml#consume"
|
13
|
+
post "/consumeSaml/rawResponse" => "saml#raw_response"
|
6
14
|
post "/logout" => "saml#logout"
|
7
15
|
end
|
8
|
-
#TODO check ip_address stored by the IDP
|
9
|
-
#TODO chceck ip address on every check ra
|
@@ -0,0 +1,25 @@
|
|
1
|
+
MIIEWjCCA0KgAwIBAgIJAP1rB/FjRgy6MA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNV
|
2
|
+
BAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEPMA0GA1UEBxMGRHVyaGFt
|
3
|
+
MRgwFgYDVQQKEw9EdWtlIFVuaXZlcnNpdHkxDDAKBgNVBAsTA09JVDEaMBgGA1UE
|
4
|
+
AxMRc2hpYi5vaXQuZHVrZS5lZHUwHhcNMTAwOTA5MTI0NDU1WhcNMjgwOTA0MTI0
|
5
|
+
NDU1WjB7MQswCQYDVQQGEwJVUzEXMBUGA1UECBMOTm9ydGggQ2Fyb2xpbmExDzAN
|
6
|
+
BgNVBAcTBkR1cmhhbTEYMBYGA1UEChMPRHVrZSBVbml2ZXJzaXR5MQwwCgYDVQQL
|
7
|
+
EwNPSVQxGjAYBgNVBAMTEXNoaWIub2l0LmR1a2UuZWR1MIIBIjANBgkqhkiG9w0B
|
8
|
+
AQEFAAOCAQ8AMIIBCgKCAQEAt+hnl6gSRi0Y8VuNl6PCPYejj7VfVs/y8bRa5zAY
|
9
|
+
RHwb75+vBSs2j1yeUcSore9Ba5Ni7v947V34afRMGRPOqr4TEDZxU+1Bg0zAvSrR
|
10
|
+
n4Y8B+zyJuhtOpmOZzTwE9o/Oc+CB4kYV/K0woKZdcoxHJm8TbqBqdxU4fFYUlNU
|
11
|
+
o4Dr5jRdCSr9MHBOqGWXtQMg16qYNB7StNk4twY29FNnpZwkVTfsE76uVsRMkG8i
|
12
|
+
6/RiHpXZ/ioOOqndptbEGdsOIE3ivAJOZdvYwnDe5NnTH06P01HsxH3OOnYqhuG2
|
13
|
+
J6qdhqoelGeHRG+jfl8YkYXCcKQvja2tJ5G+6iqSN7DP6QIDAQABo4HgMIHdMB0G
|
14
|
+
A1UdDgQWBBQHYXwB6otkfyMOmUI59j8823hFRDCBrQYDVR0jBIGlMIGigBQHYXwB
|
15
|
+
6otkfyMOmUI59j8823hFRKF/pH0wezELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5v
|
16
|
+
cnRoIENhcm9saW5hMQ8wDQYDVQQHEwZEdXJoYW0xGDAWBgNVBAoTD0R1a2UgVW5p
|
17
|
+
dmVyc2l0eTEMMAoGA1UECxMDT0lUMRowGAYDVQQDExFzaGliLm9pdC5kdWtlLmVk
|
18
|
+
dYIJAP1rB/FjRgy6MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAG7q
|
19
|
+
wJpiSLJbx2gj/cGDYeuBW/CeRGNghjQ/mb076P3WXsRNPAimcXulSUbQkS6eDH4t
|
20
|
+
Ifvsa0jf4FRsEOwH/x8354/0wyv4RwuavX25kjpmoFn3O+eKokyzsc7/Q2gsm0mv
|
21
|
+
V8XQo+5b+4we8AFYlAVp26nLeIqAiJM8xZJ9yHuzVL1O4yxIWIKECWHLqY5+1nas
|
22
|
+
XNiLURrHhsK5pZUPLuhzJFgZuJT62TtnrjJXlrRhJ389VSkh6R64C6ncjNkg6/Cu
|
23
|
+
tA6SX0infqNRyPRNJK+bnQd1yOP4++tjD/lAPE+5tiD/waI3fArt43ZE/qp7pYMS
|
24
|
+
9TEfyQ5QpfRYAUFWXBc=
|
25
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIEuTCCA6GgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBnTELMAkGA1UEBhMCVVMx
|
3
|
+
FzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMQ8wDQYDVQQHDAZEdXJoYW0xGDAWBgNV
|
4
|
+
BAoMD0R1a2UgVW5pdmVyc2l0eTEMMAoGA1UECwwDT0lUMR0wGwYDVQQDDBRzYW1s
|
5
|
+
IGNhbWVsIGR1bW15IGFwcDEdMBsGCSqGSIb3DQEJARYOZGExMjlAZHVrZS5lZHUw
|
6
|
+
HhcNMTgwNTIyMTcxMDMwWhcNMTkwNTIyMTcxMDMwWjCBnTELMAkGA1UEBhMCVVMx
|
7
|
+
FzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMQ8wDQYDVQQHDAZEdXJoYW0xGDAWBgNV
|
8
|
+
BAoMD0R1a2UgVW5pdmVyc2l0eTEMMAoGA1UECwwDT0lUMR0wGwYDVQQDDBRzYW1s
|
9
|
+
IGNhbWVsIGR1bW15IGFwcDEdMBsGCSqGSIb3DQEJARYOZGExMjlAZHVrZS5lZHUw
|
10
|
+
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+OHs74gT5AmdSsLgHETvX
|
11
|
+
50+S0NgWp5dcovfuMYFV+1CFX1MhgjhBQSwkA9U/0pfKf/eoU18O2gI2y46OK8j2
|
12
|
+
e5oyUuKv1UQWe2RHKvxvNrwvvUVcLY4mJDZf0d4q6EyTVo2aWHwoskxnQpjbusgp
|
13
|
+
Vq178Jfaeu/QaiBtq82vPlu0tfCeOXIyEdyRiOyc2bQvS5MW6FvzWtgatiNUnJJe
|
14
|
+
sBM/JUiFOvf3qG7LHEzpaIBmoHBwxG5b3yjrGgGTdw+5gyXdPEwEeiTddMvYlXWM
|
15
|
+
t+VMoTmsaBxrXRJBvpLxGWHZRb0VcoVTqWjcKVD/hR0A7H6ogaoOatHDWM41b3ZL
|
16
|
+
AgMBAAGjggEAMIH9MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFGh/Y36w7wcL
|
17
|
+
nLXFC0dUpboAAV+ZMIHKBgNVHSMEgcIwgb+AFGh/Y36w7wcLnLXFC0dUpboAAV+Z
|
18
|
+
oYGjpIGgMIGdMQswCQYDVQQGEwJVUzEXMBUGA1UECAwOTm9ydGggQ2Fyb2xpbmEx
|
19
|
+
DzANBgNVBAcMBkR1cmhhbTEYMBYGA1UECgwPRHVrZSBVbml2ZXJzaXR5MQwwCgYD
|
20
|
+
VQQLDANPSVQxHTAbBgNVBAMMFHNhbWwgY2FtZWwgZHVtbXkgYXBwMR0wGwYJKoZI
|
21
|
+
hvcNAQkBFg5kYTEyOUBkdWtlLmVkdYIBADANBgkqhkiG9w0BAQsFAAOCAQEAFE/X
|
22
|
+
DPipapLFDnu2jCMR4lhDeEF2Pm1DIibiy6ZvmzCstj++MYOI7gKkUgeUUhFTEQIV
|
23
|
+
fZIo5gIWkyoPVOwGALLTme01Tdk3Mul4pV0iqMn4k3F9NsC9wRy4WR2yPF9GYa/e
|
24
|
+
ktK+ZBYt/2SZA4vS5q63jsMC0TjkrTGJokXohwScWDc4kIFfvU6biWW7zBCVfpaa
|
25
|
+
YfsLYNBTbZ7VqEVFzcpYv8LBTOYoToAS5+yuAwrIdPEfqx3R4tIwGCik4tSByQFO
|
26
|
+
i/VvEL5rTWhmUrKPh1hriPVYZ9gW2Mk87Snlyswsqv5d8+ITVgF+RL+cutUA29C+
|
27
|
+
moSLPLaWINlhqvuRXw==
|
28
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,27 @@
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
2
|
+
MIIEogIBAAKCAQEAvjh7O+IE+QJnUrC4BxE71+dPktDYFqeXXKL37jGBVftQhV9T
|
3
|
+
IYI4QUEsJAPVP9KXyn/3qFNfDtoCNsuOjivI9nuaMlLir9VEFntkRyr8bza8L71F
|
4
|
+
XC2OJiQ2X9HeKuhMk1aNmlh8KLJMZ0KY27rIKVate/CX2nrv0GogbavNrz5btLXw
|
5
|
+
njlyMhHckYjsnNm0L0uTFuhb81rYGrYjVJySXrATPyVIhTr396huyxxM6WiAZqBw
|
6
|
+
cMRuW98o6xoBk3cPuYMl3TxMBHok3XTL2JV1jLflTKE5rGgca10SQb6S8Rlh2UW9
|
7
|
+
FXKFU6lo3ClQ/4UdAOx+qIGqDmrRw1jONW92SwIDAQABAoIBAHZpuKU9fPT5/xHl
|
8
|
+
upmDq+oqL0nowivQJhRfytE3dhjtOmHcRma8poJQrMa6sBxr31wKr0PUqn8XTXuI
|
9
|
+
2fQ843w003dyS3VD4H/STklTRBODUkCxpSTNowixUDvz7EZvl4O8xKeJX7kBzTgW
|
10
|
+
qAtYydOaBqL50b4K+5CVEBzVb1Qf/DKhCbBeYvnwAcUVT+t5lDGUh+54pLTHmeGZ
|
11
|
+
2as+1MeBWLMR/ynMDziVVR3XIM02+pHPEwiI9ZTazUAKRJnskb5gBpHqtGiZSijC
|
12
|
+
zQq+GSnnBPvvc0gtjqf+KF/6NLy/zDGmpF1e+blCnnLPUQGPTkClq59EHdn8jedO
|
13
|
+
YyRrWmkCgYEA9VqRMziTAi79yP2rLqE7cMKPDtrOilHK8fDk5N2xxzEsVoKUsotq
|
14
|
+
x384sfmrA3oVSNQsPi/DF16eH1cLaQL86rTaUKl4DqO6rLBPhQVjrmuwdWgnKKGn
|
15
|
+
9XMEp8lBC7KwAnaQKP7c83WarU/FbF08BbPkHob1wuAyMrD7wRv2XDcCgYEAxnl8
|
16
|
+
SuHwIooIyiW2/oDjoqCrdtgOLXzdOK2OSDcY+jARVkOA8N0ingPOb18RLOTmjGk5
|
17
|
+
KZDHa8xZzdd0Bt7xz3WV2FipYxnkkY7sJosJpMrY8k/QUip9i2D04uLypwVBfT7P
|
18
|
+
q3GOgOrP+nvRya8HLHKm0rf7+sU2mGIsSrVYtI0CgYBzQUIoL5FPW0e4XQFG/FJx
|
19
|
+
29NcBQk1DMsq8CB2KnZSvhS35st3O+rDIE4/vKrLDVRmS9UkuUcJ+VaKHler0s2A
|
20
|
+
a8iKT7GoHt2YNZKFSEzVKJ1R6cVLXvUJZihvsSivGBd6cLuzplWgwEQS2gBBsWJ6
|
21
|
+
w1CLzpYwHyU1jtIUmtAV7QKBgCtC3bnAx8PvjHzrfZi55WRUWyt7apO1rM6m3eWV
|
22
|
+
xOb7xTulWRynRt1kfQG/mhHMDwi6AtCxkxZHI6f/d3Xr8I9E1RWkNb+5LB4iJg08
|
23
|
+
ryxxXppqlUDjrBvOVXKC/1syhRTUtRVsmiA1joHNrWulsA2bLAuwOMdvZzgN5hOe
|
24
|
+
tagdAoGAP7kdbprmkT/7xX8puX6WD4MXQ+dgyb3FvpCIfQT8x0t/ndMI2wMc4keg
|
25
|
+
woD2L56tjtVyFH8LQz1sU7LroSc8XF2joZOdQePrnyTVUISoMiTqaXMPIO6l6pez
|
26
|
+
x7g1PP3ey5LOoX7LG5ule/6qNMtRhVOFok0vA9ZuuIIkkmYSo1c=
|
27
|
+
-----END RSA PRIVATE KEY-----
|