omniauth-dice 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +41 -0
- data/.rubocop.yml +8 -0
- data/.travis.yml +15 -0
- data/.yardopts +1 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +88 -0
- data/Rakefile +21 -0
- data/certs/stevenhaddox.pem +21 -0
- data/config/gem_sources.yml.example +1 -0
- data/lib/omniauth/dice/version.rb +5 -0
- data/lib/omniauth/strategies/dice.rb +422 -0
- data/lib/omniauth-dice.rb +3 -0
- data/lib/string.rb +17 -0
- data/omniauth-dice.gemspec +50 -0
- data/spec/certs/create_spec_cert.rb +41 -0
- data/spec/certs/ruby_user.crt +20 -0
- data/spec/certs/ruby_user.pub +9 -0
- data/spec/fixtures/valid_auth.json +27 -0
- data/spec/fixtures/valid_auth.xml +20 -0
- data/spec/omniauth/strategies/dice_integrations_spec.rb +201 -0
- data/spec/omniauth/strategies/dice_spec.rb +161 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/test_apps/test_rack_app.rb +17 -0
- data.tar.gz.sig +3 -0
- metadata +424 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 136390a70c4e465698843d744e9328e4a4b63131
|
4
|
+
data.tar.gz: f9822f9f4742f6a7c6c918fbf3a89b4ff9512c7c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6d2dd8b1326fc262540a5491854bf5536947964872a111a75db27f3b297979b2880f20150bf8e888b1ed89d7cb21ac5809a0bbc25d003439b76799f4369d5ad7
|
7
|
+
data.tar.gz: 6cc4801fe4420043826cfc27169b1e524460971cb826132e75a11ce6b734f036da268b9e80a25d6431b5fe49cf66596f7865c91a33011a5f99c3f0707a972d4c
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data/.coveralls.yml
ADDED
data/.gitignore
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
*.yml.a*
|
4
|
+
/.config
|
5
|
+
/coverage/
|
6
|
+
/InstalledFiles
|
7
|
+
/pkg/
|
8
|
+
/spec/certs/*.pem
|
9
|
+
/spec/reports/
|
10
|
+
/test/tmp/
|
11
|
+
/test/version_tmp/
|
12
|
+
/tmp/
|
13
|
+
|
14
|
+
## Specific to RubyMotion:
|
15
|
+
.dat*
|
16
|
+
.repl_history
|
17
|
+
build/
|
18
|
+
|
19
|
+
## Documentation cache and generated files:
|
20
|
+
/.yardoc/
|
21
|
+
/_yardoc/
|
22
|
+
/doc/
|
23
|
+
/rdoc/
|
24
|
+
|
25
|
+
## Environment normalisation:
|
26
|
+
/.bundle/
|
27
|
+
/lib/bundler/man/
|
28
|
+
.rvmrc
|
29
|
+
|
30
|
+
## Gem ignores
|
31
|
+
/Gemfile.lock
|
32
|
+
/.ruby-version
|
33
|
+
/.ruby-gemset
|
34
|
+
|
35
|
+
*.so
|
36
|
+
*.o
|
37
|
+
*.a
|
38
|
+
mkmf.log
|
39
|
+
|
40
|
+
# Custom
|
41
|
+
config/gem_sources.yml
|
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'psych' # Fix double-load bug when requiring yaml
|
2
|
+
require 'yaml'
|
3
|
+
if File.exist?('config/gem_sources.yml')
|
4
|
+
YAML.load_file('config/gem_sources.yml').each do |gem_source|
|
5
|
+
puts "Loading gem source: #{gem_source}"
|
6
|
+
source gem_source
|
7
|
+
end
|
8
|
+
else
|
9
|
+
source 'https://rubygems.org'
|
10
|
+
end
|
11
|
+
|
12
|
+
# Specify your gem's dependencies in omniauth-dice.gemspec
|
13
|
+
gemspec
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Steven Haddox
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# Omniauth::Dice [](http://badge.fury.io/rb/omniauth-dice)
|
2
|
+
|
3
|
+
[](https://travis-ci.org/stevenhaddox/omniauth-dice) [](https://gemnasium.com/stevenhaddox/omniauth-dice) [](https://coveralls.io/r/stevenhaddox/omniauth-dice) [](https://codeclimate.com/github/stevenhaddox/omniauth-dice) [](http://inch-ci.org/github/stevenhaddox/omniauth-dice)
|
4
|
+
|
5
|
+
# **D**N **I**nteroperable **C**onversion **E**xpert
|
6
|
+
|
7
|
+
omniauth-dice is an internal authentication strategy that authenticates via
|
8
|
+
a user's X509 certificate DN string to an Enterprise CAS server via REST.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
```ruby
|
14
|
+
gem 'omniauth-dice'
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
$ bundle
|
19
|
+
|
20
|
+
Or install it yourself with:
|
21
|
+
|
22
|
+
$ gem install omniauth-dice
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Setup your OmniAuth::Dice builder like so:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
{
|
31
|
+
cas_server: 'https://example.org:3000',
|
32
|
+
authentication_path: '/dn',
|
33
|
+
format_header: 'application/xml', # default is 'application/json'
|
34
|
+
format: 'xml', # default is 'json'
|
35
|
+
dnc_options: { transformation: 'downcase' }, # see `dnc` gem for all options
|
36
|
+
ssl_config: {
|
37
|
+
ca_file: 'spec/certs/CA.pem',
|
38
|
+
client_cert: 'spec/certs/client.pem',
|
39
|
+
client_key: 'spec/certs/key.np.pem'
|
40
|
+
} # See OmniAuth::Strategies::Dice.ssl_hash for all options
|
41
|
+
}
|
42
|
+
```
|
43
|
+
|
44
|
+
Full configuration options are as follows:
|
45
|
+
|
46
|
+
```
|
47
|
+
cas_server [String] Required base URL for CAS server
|
48
|
+
authentication_path [String] URL path for endpoint, e.g. '/users'
|
49
|
+
return_field [String] Optional path to append after DN string
|
50
|
+
ssl_config [Hash] Configuration hash for `Faraday` SSL options
|
51
|
+
format_header [String] 'application/json', 'application/xml', etc
|
52
|
+
Defaults to 'application/json'
|
53
|
+
format [String] 'json', 'xml', etc.
|
54
|
+
Defaults to 'json'
|
55
|
+
client_cert_header [String] ENV string to access user's X509 cert
|
56
|
+
Defaults to 'HTTP_SSL_CLIENT_CERT'
|
57
|
+
subject_dn_header [String] ENV string to access user's subject_dn
|
58
|
+
Defaults to 'HTTP_SSLC_LIENT_S_DN'
|
59
|
+
issuer_dn_header [String] ENV string to access user's issuer_dn
|
60
|
+
Defaults to 'HTTP_SSL_CLIENT_I_DN'
|
61
|
+
name_format [Symbol] Format for auth_hash['info']['name']
|
62
|
+
Defaults to attempting DN common name -> full name -> first & last name
|
63
|
+
Valid options are: :cn, :full_name, :first_last_name to override
|
64
|
+
```
|
65
|
+
|
66
|
+
### SSL Client Certificate Notes
|
67
|
+
|
68
|
+
`Faraday` (the HTTP library used by OmniAuth) can accept certificate paths:
|
69
|
+
|
70
|
+
```
|
71
|
+
client_cert: 'spec/certs/client.pem',
|
72
|
+
client_key: 'spec/certs/key.np.pem'
|
73
|
+
```
|
74
|
+
|
75
|
+
Or it also works with actual certificates (such as to pass a passphrase in):
|
76
|
+
```
|
77
|
+
client_cert: File.read('spec/certs/client.pem').to_cert,
|
78
|
+
client_key: OpenSSL::PKey::RSA.new(File.read('spec/certs/key.pem'), 'PASSW0RD')
|
79
|
+
```
|
80
|
+
|
81
|
+
## Contributing
|
82
|
+
|
83
|
+
1. Fork it ( https://github.com/[my-github-username]/omniauth-dice/fork )
|
84
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
85
|
+
3. **Add specs!**
|
86
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
87
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
88
|
+
6. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'rubocop/rake_task'
|
4
|
+
require 'coveralls/rake/task'
|
5
|
+
Coveralls::RakeTask.new
|
6
|
+
|
7
|
+
#task default: [:spec, :rubocop, 'coveralls:push']
|
8
|
+
task default: [:spec, 'coveralls:push']
|
9
|
+
|
10
|
+
desc 'Run specs'
|
11
|
+
RSpec::Core::RakeTask.new(:spec)
|
12
|
+
|
13
|
+
desc 'Run rubocop'
|
14
|
+
task :rubocop do
|
15
|
+
RuboCop::RakeTask.new
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'Display TODOs, FIXMEs, and OPTIMIZEs'
|
19
|
+
task :notes do
|
20
|
+
system("grep -r 'OPTIMIZE:\\|FIXME:\\|TODO:' #{Dir.pwd}")
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIDhTCCAm2gAwIBAgIBATANBgkqhkiG9w0BAQUFADBEMRYwFAYDVQQDDA1zdGV2
|
3
|
+
ZW4uaGFkZG94MRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJk/IsZAEZ
|
4
|
+
FgNjb20wHhcNMTQxMTE4MDIxMzIwWhcNMTUxMTE4MDIxMzIwWjBEMRYwFAYDVQQD
|
5
|
+
DA1zdGV2ZW4uaGFkZG94MRUwEwYKCZImiZPyLGQBGRYFZ21haWwxEzARBgoJkiaJ
|
6
|
+
k/IsZAEZFgNjb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDanmKr
|
7
|
+
vJDcVGMeDbDouLfKvU5ugOcHTXP04QDYSshaMTeuWSm4OXakxk2rxnR7Laq86R+8
|
8
|
+
h1NbHMdiZdwlHcpZm9/YD6qjbQhnLNGsezMrNpfZwfy9VnUQY4e0OCAca9vQXKTL
|
9
|
+
qC4fiuRD6sQQpyXkiIno0KlJOA4sKtH8vFucPGmhO0FUdlQY5FarDvCvZrtteO6L
|
10
|
+
6/GQFjupFBd9X6zt1XBs28IC+YUw33SN0UJ5JHB45ig0BmeWMXdd4SKWe4ve/2UY
|
11
|
+
asgs2miI3HP0wCPs0EF64/8LbuEUyMjHDr3a7+7KIRxYn2H/yUH5Ndqz6yL5G0sf
|
12
|
+
jUsC32JuE7VlJwFNAgMBAAGjgYEwfzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAd
|
13
|
+
BgNVHQ4EFgQUC8HywyOMPJFCsH7uGW+CeKcZ8+8wIgYDVR0RBBswGYEXc3RldmVu
|
14
|
+
LmhhZGRveEBnbWFpbC5jb20wIgYDVR0SBBswGYEXc3RldmVuLmhhZGRveEBnbWFp
|
15
|
+
bC5jb20wDQYJKoZIhvcNAQEFBQADggEBAFoac9ZKc20ZXw2R2mWUz7FaJJdUvb7o
|
16
|
+
4rKVzFQkJwvAX+NEdP32yCDViGoEqlA13el5fllllmG3E7Qrw+0JA5B3wrZbVfQA
|
17
|
+
v4eX0ZohhW3CXLSz65pd3zfrwPAw0pXs1QKP+IioTuLQoBsGUiIqCPulZvzn/xN2
|
18
|
+
KG7SexyfUEXyJRMMigA/mE8h6bYfgKKUmLQVs1uRaXmOI7dKUF6HZJpda51zJH3v
|
19
|
+
42qdwEXvvkODZAD6KAIXPdmbMfBgPbcd+B/4eUA0PyKo+4dgL1NuqX4MPWToevIZ
|
20
|
+
O8EKLF2X7NmC6FY1bOsSj/J8r1SOkx0rxgF+geRvY1P+hfNjDfxTsjU=
|
21
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1 @@
|
|
1
|
+
- https://geminabox.example.com
|
@@ -0,0 +1,422 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday_middleware'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'omniauth'
|
5
|
+
require 'cert_munger'
|
6
|
+
require 'dnc'
|
7
|
+
|
8
|
+
class RequiredCustomParamError < StandardError; end
|
9
|
+
|
10
|
+
module OmniAuth
|
11
|
+
module Strategies
|
12
|
+
|
13
|
+
#
|
14
|
+
# Provides omniauth authentication integration with a CAS server
|
15
|
+
#
|
16
|
+
# @option cas_server [String] Required base URL for CAS server
|
17
|
+
# @option authentication_path [String] URL path for endpoint, e.g. '/users'
|
18
|
+
# @option return_field [String] Optional path to append after DN string
|
19
|
+
# @option ssl_config [Hash] Configuration hash for `Faraday` SSL options
|
20
|
+
# @option format_header [String] 'application/json', 'application/xml', etc
|
21
|
+
# Defaults to 'application/json'
|
22
|
+
# @option format [String] 'json', 'xml', etc.
|
23
|
+
# Defaults to 'json'
|
24
|
+
# @option client_cert_header [String] ENV string to access user's X509 cert
|
25
|
+
# Defaults to 'HTTP_SSL_CLIENT_CERT'
|
26
|
+
# @option subject_dn_header [String] ENV string to access user's subject_dn
|
27
|
+
# Defaults to 'HTTP_SSLC_LIENT_S_DN'
|
28
|
+
# @option issuer_dn_header [String] ENV string to access user's issuer_dn
|
29
|
+
# Defaults to 'HTTP_SSL_CLIENT_I_DN'
|
30
|
+
# @option name_format [Symbol] Format for auth_hash['info']['name']
|
31
|
+
# Defaults to attempting DN common name -> full name -> first & last name
|
32
|
+
# Valid options are: :cn, :full_name, :first_last_name to override
|
33
|
+
# @option primary_visa_str [String] String to trigger primary visa boolean
|
34
|
+
class Dice
|
35
|
+
include OmniAuth::Strategy
|
36
|
+
attr_accessor :dn, :raw_dn, :data
|
37
|
+
args [:cas_server, :authentication_path]
|
38
|
+
|
39
|
+
def initialize(*args, &block)
|
40
|
+
validate_required_params(args)
|
41
|
+
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
option :dnc_options, {}
|
46
|
+
option :cas_server, nil
|
47
|
+
option :authentication_path, nil
|
48
|
+
option :return_field, 'info'
|
49
|
+
option :ssl_config, {}
|
50
|
+
option :format_header, 'application/json'
|
51
|
+
option :format, 'json'
|
52
|
+
option :client_cert_header, 'HTTP_SSL_CLIENT_CERT'
|
53
|
+
option :subject_dn_header, 'HTTP_SSL_CLIENT_S_DN'
|
54
|
+
option :issuer_dn_header, 'HTTP_SSL_CLIENT_I_DN'
|
55
|
+
option :name_format
|
56
|
+
option :primary_visa_str
|
57
|
+
|
58
|
+
# Reformat DN to expected element order for CAS DN server (via dnc gem).
|
59
|
+
def format_dn(dn_str)
|
60
|
+
get_dn(dn_str).to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
# Change Hashie indifferent access keys back to symbols
|
66
|
+
def unhashie(hash)
|
67
|
+
tmp_hash = {}
|
68
|
+
hash.each do |key, value|
|
69
|
+
tmp_hash[key.to_sym] = value
|
70
|
+
end
|
71
|
+
|
72
|
+
tmp_hash
|
73
|
+
end
|
74
|
+
|
75
|
+
def setup_phase(*args)
|
76
|
+
log :debug, 'setup_phase'
|
77
|
+
super
|
78
|
+
end
|
79
|
+
|
80
|
+
def request_phase
|
81
|
+
subject_dn = get_dn_by_type('subject')
|
82
|
+
return fail!('You need a valid DN to authenticate.') unless subject_dn
|
83
|
+
user_dn = format_dn(subject_dn)
|
84
|
+
log :debug, "Formatted user_dn: #{user_dn}"
|
85
|
+
return fail!('You need a valid DN to authenticate.') unless user_dn
|
86
|
+
set_session_dn(user_dn, 'subject')
|
87
|
+
issuer_dn = get_dn_by_type('issuer')
|
88
|
+
issuer_dn = format_dn(issuer_dn) if issuer_dn
|
89
|
+
log :debug, "Formatted issuer_dn: #{issuer_dn}"
|
90
|
+
set_session_dn(issuer_dn, 'issuer') if issuer_dn
|
91
|
+
|
92
|
+
redirect callback_url
|
93
|
+
end
|
94
|
+
|
95
|
+
def callback_phase
|
96
|
+
issuer_dn = env['omniauth.params']['issuer_dn']
|
97
|
+
if issuer_dn
|
98
|
+
response = connection.get query_url, { issuerDN: issuer_dn }
|
99
|
+
else
|
100
|
+
response = connection.get query_url
|
101
|
+
end
|
102
|
+
if !response || response.status.to_i >= 400
|
103
|
+
log :error, response.inspect
|
104
|
+
return fail!(:invalid_credentials)
|
105
|
+
end
|
106
|
+
@data = response.body
|
107
|
+
create_auth_hash
|
108
|
+
|
109
|
+
redirect request.env['omniauth.origin'] || '/'
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
# Coordinate building out the auth_hash
|
115
|
+
def create_auth_hash
|
116
|
+
log :debug, '.create_auth_hash'
|
117
|
+
init_auth_hash
|
118
|
+
set_auth_uid
|
119
|
+
parse_response_data
|
120
|
+
create_auth_info
|
121
|
+
end
|
122
|
+
|
123
|
+
# Initialize the auth_hash expected fields
|
124
|
+
def init_auth_hash
|
125
|
+
log :debug, '.init_auth_hash'
|
126
|
+
session['omniauth.auth'] ||= {
|
127
|
+
'provider' => 'Dice',
|
128
|
+
'uid' => nil,
|
129
|
+
'info' => nil,
|
130
|
+
'extra' => {
|
131
|
+
'raw_info' => nil
|
132
|
+
}
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
# Set the user's uid field for the auth_hash
|
137
|
+
def set_auth_uid
|
138
|
+
log :debug, '.set_auth_uid'
|
139
|
+
session['omniauth.auth']['uid'] = env['omniauth.params']['user_dn']
|
140
|
+
end
|
141
|
+
|
142
|
+
# Detect data format, parse with appropriate library
|
143
|
+
def parse_response_data
|
144
|
+
log :debug, '.parse_response_data'
|
145
|
+
session['omniauth.auth']['extra']['raw_info'] = @data
|
146
|
+
log :debug, "cas_server response.body:\r\n#{@data}"
|
147
|
+
unless @data.class == Hash # Webmock hack
|
148
|
+
case options.format.to_sym
|
149
|
+
when :json
|
150
|
+
@data = JSON.parse(@data, symbolize_names: true)
|
151
|
+
when :xml
|
152
|
+
@data = MultiXml.parse(@data)['userinfo']
|
153
|
+
end
|
154
|
+
log :debug, "Formatted response.body data: #{@data}"
|
155
|
+
end
|
156
|
+
|
157
|
+
@data
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
# Parse CAS server response and assign values as appropriate
|
162
|
+
def create_auth_info
|
163
|
+
log :debug, '.create_auth_info'
|
164
|
+
info = {}
|
165
|
+
info = auth_info_defaults(info)
|
166
|
+
info = auth_info_dynamic(info)
|
167
|
+
info = auth_info_custom(info)
|
168
|
+
|
169
|
+
session['omniauth.auth']['info'] = info
|
170
|
+
end
|
171
|
+
|
172
|
+
def info_defaults
|
173
|
+
[:dn, :email, :firstName, :lastName, :fullName, :citizenshipStatus,
|
174
|
+
:country, :grantBy, :organizations, :uid, :dutyorg, :visas,
|
175
|
+
:affiliations]
|
176
|
+
end
|
177
|
+
|
178
|
+
# Defualt auth_info fields
|
179
|
+
def auth_info_defaults(info)
|
180
|
+
info_defaults.each do |key_name|
|
181
|
+
info[key_name.to_s.to_snake] = @data[key_name]
|
182
|
+
end
|
183
|
+
|
184
|
+
info
|
185
|
+
end
|
186
|
+
|
187
|
+
# Dynamic auth_info fields
|
188
|
+
def auth_info_dynamic(info)
|
189
|
+
@data.each do |key, value|
|
190
|
+
info[key.to_s.to_snake] = value unless info_defaults.include?(key)
|
191
|
+
end
|
192
|
+
|
193
|
+
info
|
194
|
+
end
|
195
|
+
|
196
|
+
# Custom auth_info fields
|
197
|
+
def auth_info_custom(info)
|
198
|
+
info['common_name'] = get_dn(info['dn']).cn
|
199
|
+
set_name(info)
|
200
|
+
has_primary_visa?(info)
|
201
|
+
info['likely_npe?'] = identify_npe(info)
|
202
|
+
|
203
|
+
info
|
204
|
+
end
|
205
|
+
|
206
|
+
# Allow for a custom field for the name, or use a best guess default
|
207
|
+
def set_name(info)
|
208
|
+
# Do NOT override the value if it's returned from the CAS server
|
209
|
+
return info['name'] if info['name']
|
210
|
+
info['name'] = case options.name_format
|
211
|
+
when :cn
|
212
|
+
info['common_name']
|
213
|
+
when :full_name
|
214
|
+
info['full_name']
|
215
|
+
when :first_last_name
|
216
|
+
"#{info['first_name']} #{info['last_name']}"
|
217
|
+
end
|
218
|
+
info['name'] ||= info['common_name'] || info['full_name'] ||
|
219
|
+
"#{info['first_name']} #{info['last_name']}"
|
220
|
+
end
|
221
|
+
|
222
|
+
# Determine if client has the primary visa
|
223
|
+
def has_primary_visa?(info)
|
224
|
+
return info['primary_visa?'] = nil unless info['visas']
|
225
|
+
return info['primary_visa?'] = nil unless options.primary_visa
|
226
|
+
info['primary_visa?'] = info['visas'].include?(options.primary_visa)
|
227
|
+
end
|
228
|
+
|
229
|
+
# Determine if a client is likely a non-person entity
|
230
|
+
def identify_npe(info)
|
231
|
+
info['likely_npe?'] = nil
|
232
|
+
return true if auth_cn_with_tld?(info['common_name']) == true
|
233
|
+
return true if auth_info_missing_email?(info) == true
|
234
|
+
return true if auth_has_email_without_names?(info) == true
|
235
|
+
return false if auth_has_email_with_any_name?(info) == true
|
236
|
+
end
|
237
|
+
|
238
|
+
# Identify if there's a domain w/ TLD in the common_name
|
239
|
+
def auth_cn_with_tld?(common_name)
|
240
|
+
!!( common_name =~ /\w{3}\.\w+(\.\w{3,}+)?/ )
|
241
|
+
end
|
242
|
+
|
243
|
+
# Determine if the auth_hash does not have an email address
|
244
|
+
def auth_info_missing_email?(info)
|
245
|
+
!( info['email'] ) # !! returns false if no email, ! returns true
|
246
|
+
end
|
247
|
+
|
248
|
+
# Determine if the auth_hash has an email but no name fields
|
249
|
+
def auth_has_email_without_names?(info)
|
250
|
+
return false unless info['email']
|
251
|
+
return true if auth_info_has_any_name?(info) == false
|
252
|
+
end
|
253
|
+
|
254
|
+
# Determine if the auth_hash has an email with ANY name field
|
255
|
+
def auth_has_email_with_any_name?(info)
|
256
|
+
return false unless info['email']
|
257
|
+
return true if auth_info_has_any_name?(info) == true
|
258
|
+
end
|
259
|
+
|
260
|
+
# Determine if any name fields are present in the auth_hash['info']
|
261
|
+
def auth_info_has_any_name?(info)
|
262
|
+
name = info['full_name']
|
263
|
+
name ||= info['first_name']
|
264
|
+
name ||= info['last_name']
|
265
|
+
!!(name)
|
266
|
+
end
|
267
|
+
|
268
|
+
# Coordinate getting DN from cert, fallback to header
|
269
|
+
def get_dn_by_type(type='subject')
|
270
|
+
raw_dn = get_dn_from_certificate(type)
|
271
|
+
raw_dn ||= get_dn_from_header(type)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Reads the DN from headers
|
275
|
+
def get_dn_from_header(type)
|
276
|
+
headers = request.env
|
277
|
+
if type == 'issuer'
|
278
|
+
raw_dn = headers["#{options.issuer_dn_header}"]
|
279
|
+
else
|
280
|
+
raw_dn = headers["#{options.subject_dn_header}"]
|
281
|
+
end
|
282
|
+
log :debug, "raw_dn (#{type}) from headers: #{raw_dn}"
|
283
|
+
|
284
|
+
raw_dn
|
285
|
+
end
|
286
|
+
|
287
|
+
# Gets the DN from X509 certificate
|
288
|
+
def get_dn_from_certificate(type)
|
289
|
+
cert_str = request.env["#{options.client_cert_header}"]
|
290
|
+
if cert_str
|
291
|
+
client_cert = cert_str.to_cert
|
292
|
+
log :debug, "Client certificate:\r\n#{client_cert}"
|
293
|
+
raw_dn ||= parse_dn_from_certificate(client_cert, type)
|
294
|
+
log :debug, "raw_dn (#{type}) from cert: #{raw_dn}"
|
295
|
+
end
|
296
|
+
|
297
|
+
raw_dn
|
298
|
+
end
|
299
|
+
|
300
|
+
# Parse the DN out of an SSL X509 Client Certificate
|
301
|
+
def parse_dn_from_certificate(certificate, type='subject')
|
302
|
+
certificate.send(type.to_sym).to_s
|
303
|
+
end
|
304
|
+
|
305
|
+
# Create a Faraday instance with the cas_server & appropriate SSL config
|
306
|
+
def connection
|
307
|
+
log :debug, '.connection'
|
308
|
+
|
309
|
+
@conn ||= Faraday.new(url: options.cas_server, ssl: ssl_hash) do |conn|
|
310
|
+
conn.headers = headers
|
311
|
+
conn.response :logger # log requests to STDOUT
|
312
|
+
conn.response :xml, :content_type => /\bxml$/
|
313
|
+
conn.response :json, :content_type => /\bjson$/
|
314
|
+
conn.adapter :excon
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def headers
|
319
|
+
{
|
320
|
+
'Accept' => options.format_header,
|
321
|
+
'Content-Type' => options.format_header,
|
322
|
+
'X-XSRF-UseProtection' => ('false' if options.format_header),
|
323
|
+
'user-agent' => "Faraday via Ruby #{RUBY_VERSION}"
|
324
|
+
}
|
325
|
+
end
|
326
|
+
|
327
|
+
# Build out the query URL for CAS server with DN params
|
328
|
+
def query_url
|
329
|
+
user_dn = env['omniauth.params']['user_dn']
|
330
|
+
build_query = "#{options.cas_server}#{options.authentication_path}"
|
331
|
+
build_query += "/#{user_dn}"
|
332
|
+
build_query += "/#{options.return_field}.#{options.format}"
|
333
|
+
URI::encode(build_query)
|
334
|
+
end
|
335
|
+
|
336
|
+
# Specifies which attributes are required arguments to initialize
|
337
|
+
def required_params
|
338
|
+
[:cas_server, :authentication_path]
|
339
|
+
end
|
340
|
+
|
341
|
+
# Verify that arguments required to properly run are present or fail hard
|
342
|
+
# NOTE: CANNOT call "log" method from initialize block hooks
|
343
|
+
def validate_required_params(args)
|
344
|
+
required_params.each do |param|
|
345
|
+
param_present = nil
|
346
|
+
args.each do |arg|
|
347
|
+
param_present = true if param_in_arg?(param, arg) == true
|
348
|
+
end
|
349
|
+
|
350
|
+
if param_present.nil?
|
351
|
+
error_msg = "omniauth-dice error: #{param} is required"
|
352
|
+
fail RequiredCustomParamError, error_msg
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
# Determine if a specified param symbol exists in the passed argument
|
358
|
+
# NOTE: CANNOT call "log" method from initialize block hooks
|
359
|
+
def param_in_arg?(param, arg)
|
360
|
+
if arg.class == Hash
|
361
|
+
if arg.key?(param.to_sym)
|
362
|
+
true
|
363
|
+
else
|
364
|
+
false
|
365
|
+
end
|
366
|
+
else
|
367
|
+
false
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
def set_session_dn(dn_string, type='subject')
|
372
|
+
dn_type = case type
|
373
|
+
when 'subject'
|
374
|
+
'user_dn'
|
375
|
+
when 'issuer'
|
376
|
+
'issuer_dn'
|
377
|
+
else
|
378
|
+
fail "Invalid DN string type"
|
379
|
+
end
|
380
|
+
session['omniauth.params'] ||= {}
|
381
|
+
session['omniauth.params'][dn_type] = dn_string
|
382
|
+
end
|
383
|
+
|
384
|
+
# Dynamically builds out Faraday's SSL config hash by merging passed
|
385
|
+
# options hash with the default options.
|
386
|
+
#
|
387
|
+
# Available Faraday config options include:
|
388
|
+
# ca_file (e.g., /usr/lib/ssl/certs/ca-certificates.crt)
|
389
|
+
# ca_path (e.g., /usr/lib/ssl/certs)
|
390
|
+
# cert_store
|
391
|
+
# client_cert
|
392
|
+
# client_key
|
393
|
+
# certificate
|
394
|
+
# private_key
|
395
|
+
# verify
|
396
|
+
# verify_mode
|
397
|
+
# verify_depth
|
398
|
+
# version
|
399
|
+
def ssl_hash
|
400
|
+
ssl_defaults = {
|
401
|
+
verify: true,
|
402
|
+
verify_depth: 3,
|
403
|
+
version: 'TLSv1'
|
404
|
+
}
|
405
|
+
|
406
|
+
custom_config = unhashie(options.ssl_config)
|
407
|
+
ssl_defaults.merge(custom_config)
|
408
|
+
end
|
409
|
+
|
410
|
+
# Retrieve DNC default & custom configs
|
411
|
+
#
|
412
|
+
# @param dn_str [String] The string of text you wish to parse into a DN
|
413
|
+
# @return [DN]
|
414
|
+
def get_dn(dn_str)
|
415
|
+
custom_order = %w(cn l st ou o c street dc uid)
|
416
|
+
default_opts = { dn_string: dn_str, string_order: custom_order }
|
417
|
+
dnc_config = unhashie(options.dnc_options)
|
418
|
+
DN.new( default_opts.merge(dnc_config) )
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
end
|