rails-auth 2.1.3 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +14 -1
- data/.travis.yml +8 -5
- data/BUG-BOUNTY.md +3 -3
- data/CHANGES.md +51 -2
- data/CONTRIBUTING.md +11 -10
- data/Gemfile +6 -5
- data/Guardfile +2 -0
- data/Rakefile +3 -1
- data/lib/rails/auth/acl.rb +4 -0
- data/lib/rails/auth/acl/matchers/allow_all.rb +3 -0
- data/lib/rails/auth/acl/middleware.rb +3 -0
- data/lib/rails/auth/acl/resource.rb +7 -5
- data/lib/rails/auth/config_builder.rb +5 -8
- data/lib/rails/auth/controller_methods.rb +4 -0
- data/lib/rails/auth/credentials.rb +3 -1
- data/lib/rails/auth/credentials/injector_middleware.rb +6 -2
- data/lib/rails/auth/env.rb +4 -3
- data/lib/rails/auth/error_page/debug_middleware.rb +1 -1
- data/lib/rails/auth/error_page/middleware.rb +3 -0
- data/lib/rails/auth/exceptions.rb +2 -0
- data/lib/rails/auth/helpers.rb +3 -1
- data/lib/rails/auth/installed_constraint.rb +2 -0
- data/lib/rails/auth/monitor/middleware.rb +2 -0
- data/lib/rails/auth/rack.rb +1 -0
- data/lib/rails/auth/rspec.rb +2 -0
- data/lib/rails/auth/rspec/helper_methods.rb +6 -5
- data/lib/rails/auth/rspec/matchers/acl_matchers.rb +4 -2
- data/lib/rails/auth/version.rb +1 -1
- data/lib/rails/auth/x509/certificate.rb +35 -5
- data/lib/rails/auth/x509/filter/java.rb +4 -12
- data/lib/rails/auth/x509/filter/pem.rb +2 -0
- data/lib/rails/auth/x509/matcher.rb +2 -0
- data/lib/rails/auth/x509/middleware.rb +11 -31
- data/lib/rails/auth/x509/subject_alt_name_extension.rb +29 -0
- data/rails-auth.gemspec +5 -4
- data/spec/rails/auth/acl/matchers/allow_all_spec.rb +2 -0
- data/spec/rails/auth/acl/middleware_spec.rb +2 -0
- data/spec/rails/auth/acl/resource_spec.rb +2 -0
- data/spec/rails/auth/acl_spec.rb +2 -0
- data/spec/rails/auth/controller_methods_spec.rb +2 -0
- data/spec/rails/auth/credentials/injector_middleware_spec.rb +15 -0
- data/spec/rails/auth/credentials_spec.rb +2 -0
- data/spec/rails/auth/env_spec.rb +2 -0
- data/spec/rails/auth/error_page/debug_middleware_spec.rb +2 -0
- data/spec/rails/auth/error_page/middleware_spec.rb +2 -0
- data/spec/rails/auth/monitor/middleware_spec.rb +2 -0
- data/spec/rails/auth/rspec/helper_methods_spec.rb +2 -0
- data/spec/rails/auth/rspec/matchers/acl_matchers_spec.rb +12 -1
- data/spec/rails/auth/x509/certificate_spec.rb +103 -20
- data/spec/rails/auth/x509/matcher_spec.rb +2 -0
- data/spec/rails/auth/x509/middleware_spec.rb +15 -37
- data/spec/rails/auth/x509/subject_alt_name_extension_spec.rb +39 -0
- data/spec/rails/auth_spec.rb +2 -0
- data/spec/spec_helper.rb +5 -3
- data/spec/support/claims_matcher.rb +2 -0
- data/spec/support/create_certs.rb +51 -13
- metadata +14 -7
data/lib/rails/auth/helpers.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rails
|
2
4
|
# Modular resource-based authentication and authorization for Rails/Rack
|
3
5
|
module Auth
|
@@ -24,7 +26,7 @@ module Rails
|
|
24
26
|
|
25
27
|
# Mark what authorized the request in the Rack environment
|
26
28
|
#
|
27
|
-
# @param [Hash] :
|
29
|
+
# @param [Hash] :rack_env Rack environment
|
28
30
|
# @param [String] :allowed_by what allowed this request
|
29
31
|
def set_allowed_by(rack_env, allowed_by)
|
30
32
|
Env.new(rack_env).tap do |env|
|
data/lib/rails/auth/rack.rb
CHANGED
data/lib/rails/auth/rspec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Rails
|
2
4
|
module Auth
|
3
5
|
module RSpec
|
@@ -12,6 +14,7 @@ module Rails
|
|
12
14
|
# NOTE: Credentials will be *cleared* after the block. Nesting is not allowed.
|
13
15
|
def with_credentials(credentials = {})
|
14
16
|
raise TypeError, "expected Hash of credentials, got #{credentials.class}" unless credentials.is_a?(Hash)
|
17
|
+
|
15
18
|
test_credentials.clear
|
16
19
|
|
17
20
|
credentials.each do |type, value|
|
@@ -24,8 +27,8 @@ module Rails
|
|
24
27
|
# Creates an Rails::Auth::X509::Certificate instance double
|
25
28
|
def x509_certificate(cn: nil, ou: nil)
|
26
29
|
subject = ""
|
27
|
-
subject
|
28
|
-
subject
|
30
|
+
subject += "CN=#{cn}" if cn
|
31
|
+
subject += "OU=#{ou}" if ou
|
29
32
|
|
30
33
|
instance_double(Rails::Auth::X509::Certificate, subject, cn: cn, ou: ou).tap do |certificate|
|
31
34
|
allow(certificate).to receive(:[]) do |key|
|
@@ -47,9 +50,7 @@ module Rails
|
|
47
50
|
path = self.class.description
|
48
51
|
|
49
52
|
# Warn if methods are improperly used
|
50
|
-
unless path.chars[0] == "/"
|
51
|
-
raise ArgumentError, "expected #{path} to start with '/'"
|
52
|
-
end
|
53
|
+
raise ArgumentError, "expected #{path} to start with '/'" unless path.chars[0] == "/"
|
53
54
|
|
54
55
|
env = {
|
55
56
|
"REQUEST_METHOD" => method,
|
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
RSpec::Matchers.define(:permit) do |env|
|
2
4
|
description do
|
3
5
|
method = env["REQUEST_METHOD"]
|
4
6
|
credentials = Rails::Auth.credentials(env)
|
5
7
|
message = "allow #{method}s by "
|
6
8
|
|
7
|
-
return message
|
9
|
+
return message + "unauthenticated clients" if credentials.count.zero?
|
8
10
|
|
9
|
-
message
|
11
|
+
message + credentials.values.map(&:inspect).join(", ")
|
10
12
|
end
|
11
13
|
|
12
14
|
match { |acl| acl.match(env) }
|
data/lib/rails/auth/version.rb
CHANGED
@@ -18,7 +18,8 @@ module Rails
|
|
18
18
|
@certificate.subject.to_a.each do |name, data, _type|
|
19
19
|
@subject[name.freeze] = data.freeze
|
20
20
|
end
|
21
|
-
|
21
|
+
@subject_alt_names = SubjectAltNameExtension.new(certificate)
|
22
|
+
@subject_alt_names.freeze
|
22
23
|
@subject.freeze
|
23
24
|
end
|
24
25
|
|
@@ -27,23 +28,52 @@ module Rails
|
|
27
28
|
end
|
28
29
|
|
29
30
|
def cn
|
30
|
-
@subject["CN"
|
31
|
+
@subject["CN"]
|
31
32
|
end
|
32
33
|
alias common_name cn
|
33
34
|
|
35
|
+
def dns_names
|
36
|
+
@subject_alt_names.dns_names
|
37
|
+
end
|
38
|
+
|
39
|
+
def ips
|
40
|
+
@subject_alt_names.ips
|
41
|
+
end
|
42
|
+
|
34
43
|
def ou
|
35
|
-
@subject["OU"
|
44
|
+
@subject["OU"]
|
36
45
|
end
|
37
46
|
alias organizational_unit ou
|
38
47
|
|
48
|
+
def uris
|
49
|
+
@subject_alt_names.uris
|
50
|
+
end
|
51
|
+
|
52
|
+
# According to the SPIFFE standard only one SPIFFE ID can exist in the URI
|
53
|
+
# SAN:
|
54
|
+
# (https://github.com/spiffe/spiffe/blob/master/standards/X509-SVID.md#2-spiffe-id)
|
55
|
+
#
|
56
|
+
# @return [String, nil] string containing SPIFFE ID if one is present
|
57
|
+
# in the certificate
|
58
|
+
def spiffe_id
|
59
|
+
uris.detect { |uri| uri.start_with?("spiffe://") }
|
60
|
+
end
|
61
|
+
|
39
62
|
# Generates inspectable attributes for debugging
|
40
63
|
#
|
41
64
|
# @return [Hash] hash containing parts of the certificate subject (cn, ou)
|
65
|
+
# and subject alternative name extension (uris, dns_names) as well
|
66
|
+
# as SPIFFE ID (spiffe_id), which is just a convenience since those
|
67
|
+
# are already included in the uris
|
42
68
|
def attributes
|
43
69
|
{
|
44
70
|
cn: cn,
|
45
|
-
|
46
|
-
|
71
|
+
dns_names: dns_names,
|
72
|
+
ips: ips,
|
73
|
+
ou: ou,
|
74
|
+
spiffe_id: spiffe_id,
|
75
|
+
uris: uris
|
76
|
+
}.reject { |_, v| v.nil? || v.empty? }
|
47
77
|
end
|
48
78
|
|
49
79
|
# Compare ourself to another object by ensuring that it has the same type
|
@@ -1,23 +1,15 @@
|
|
1
|
-
|
2
|
-
require "stringio"
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module Rails
|
5
4
|
module Auth
|
6
5
|
module X509
|
7
6
|
module Filter
|
8
|
-
# Extract OpenSSL::X509::Certificates from
|
7
|
+
# Extract OpenSSL::X509::Certificates from java.security.cert.Certificate
|
9
8
|
class Java
|
10
9
|
def call(certs)
|
11
|
-
return
|
12
|
-
OpenSSL::X509::Certificate.new(extract_der(certs[0])).freeze
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
10
|
+
return if certs.nil? || certs.empty?
|
16
11
|
|
17
|
-
|
18
|
-
stringio = StringIO.new
|
19
|
-
cert.derEncode(stringio.to_outputstream)
|
20
|
-
stringio.string
|
12
|
+
OpenSSL::X509::Certificate.new(certs[0].get_encoded).freeze
|
21
13
|
end
|
22
14
|
end
|
23
15
|
end
|
@@ -3,29 +3,20 @@
|
|
3
3
|
module Rails
|
4
4
|
module Auth
|
5
5
|
module X509
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
# Validates X.509 client certificates and adds credential objects for valid
|
10
|
-
# clients to the rack environment as env["rails-auth.credentials"]["x509"]
|
6
|
+
# Extracts X.509 client certificates and adds credential objects to the
|
7
|
+
# rack environment as env["rails-auth.credentials"]["x509"]
|
11
8
|
class Middleware
|
12
9
|
# Create a new X.509 Middleware object
|
13
10
|
#
|
14
|
-
# @param [Object]
|
15
|
-
# @param [Hash]
|
16
|
-
# @param [
|
17
|
-
# @param [OpenSSL::X509::Store] truststore (optional) provide your own truststore (for e.g. CRLs)
|
18
|
-
# @param [Boolean] require_cert causes middleware to raise if certs are unverified
|
11
|
+
# @param [Object] app next app in the Rack middleware chain
|
12
|
+
# @param [Hash] cert_filters maps Rack environment names to cert extractors
|
13
|
+
# @param [Logger] logger place to log certificate extraction issues
|
19
14
|
#
|
20
15
|
# @return [Rails::Auth::X509::Middleware] new X509 middleware instance
|
21
|
-
def initialize(app, cert_filters: {},
|
22
|
-
raise ArgumentError, "no ca_file given" unless ca_file
|
23
|
-
|
16
|
+
def initialize(app, cert_filters: {}, logger: nil)
|
24
17
|
@app = app
|
25
|
-
@logger = logger
|
26
|
-
@truststore = truststore || OpenSSL::X509::Store.new.add_file(ca_file)
|
27
|
-
@require_cert = require_cert
|
28
18
|
@cert_filters = cert_filters
|
19
|
+
@logger = logger
|
29
20
|
|
30
21
|
@cert_filters.each do |key, filter|
|
31
22
|
next unless filter.is_a?(Symbol)
|
@@ -40,7 +31,7 @@ module Rails
|
|
40
31
|
|
41
32
|
def call(env)
|
42
33
|
credential = extract_credential(env)
|
43
|
-
Rails::Auth.add_credential(env, "x509"
|
34
|
+
Rails::Auth.add_credential(env, "x509", credential.freeze) if credential
|
44
35
|
|
45
36
|
@app.call(env)
|
46
37
|
end
|
@@ -52,16 +43,9 @@ module Rails
|
|
52
43
|
cert = extract_certificate_with_filter(filter, env[key])
|
53
44
|
next unless cert
|
54
45
|
|
55
|
-
|
56
|
-
log("Verified", cert)
|
57
|
-
return Rails::Auth::X509::Certificate.new(cert)
|
58
|
-
else
|
59
|
-
log("Verify FAILED", cert)
|
60
|
-
raise CertificateVerifyFailed, "verify failed: #{subject(cert)}" if @require_cert
|
61
|
-
end
|
46
|
+
return Rails::Auth::X509::Certificate.new(cert)
|
62
47
|
end
|
63
48
|
|
64
|
-
raise CertificateVerifyFailed, "no client certificate in request" if @require_cert
|
65
49
|
nil
|
66
50
|
end
|
67
51
|
|
@@ -72,15 +56,11 @@ module Rails
|
|
72
56
|
end
|
73
57
|
|
74
58
|
filter.call(raw_cert)
|
75
|
-
rescue =>
|
76
|
-
@logger.debug("rails-auth: Certificate error: #{
|
59
|
+
rescue StandardError => e
|
60
|
+
@logger.debug("rails-auth: Certificate error: #{e.class}: #{e.message}") if @logger
|
77
61
|
nil
|
78
62
|
end
|
79
63
|
|
80
|
-
def log(message, cert)
|
81
|
-
@logger.debug("rails-auth: #{message} (#{subject(cert)})") if @logger
|
82
|
-
end
|
83
|
-
|
84
64
|
def subject(cert)
|
85
65
|
cert.subject.to_a.map { |attr, data| "#{attr}=#{data}" }.join(",")
|
86
66
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Auth
|
5
|
+
module X509
|
6
|
+
# Provides convenience methods for subjectAltName extension of X.509 certificates
|
7
|
+
class SubjectAltNameExtension
|
8
|
+
attr_reader :dns_names, :ips, :uris
|
9
|
+
|
10
|
+
DNS_REGEX = /^DNS:/i.freeze
|
11
|
+
IP_REGEX = /^IP( Address)?:/i.freeze
|
12
|
+
URI_REGEX = /^URI:/i.freeze
|
13
|
+
|
14
|
+
def initialize(certificate)
|
15
|
+
unless certificate.is_a?(OpenSSL::X509::Certificate)
|
16
|
+
raise TypeError, "expecting OpenSSL::X509::Certificate, got #{certificate.class}"
|
17
|
+
end
|
18
|
+
|
19
|
+
extension = certificate.extensions.detect { |ext| ext.oid == "subjectAltName" }
|
20
|
+
values = (extension&.value&.split(",") || []).map(&:strip)
|
21
|
+
|
22
|
+
@dns_names = values.grep(DNS_REGEX) { |v| v.sub(DNS_REGEX, "") }.freeze
|
23
|
+
@ips = values.grep(IP_REGEX) { |v| v.sub(IP_REGEX, "") }.freeze
|
24
|
+
@uris = values.grep(URI_REGEX) { |v| v.sub(URI_REGEX, "") }.freeze
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/rails-auth.gemspec
CHANGED
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require "rails/auth/version"
|
5
6
|
|
@@ -25,10 +26,10 @@ Gem::Specification.new do |spec|
|
|
25
26
|
spec.bindir = "exe"
|
26
27
|
spec.require_paths = ["lib"]
|
27
28
|
|
28
|
-
spec.required_ruby_version = ">= 2.
|
29
|
+
spec.required_ruby_version = ">= 2.3.0"
|
29
30
|
|
30
31
|
spec.add_runtime_dependency "rack"
|
31
32
|
|
32
|
-
spec.add_development_dependency "bundler", "
|
33
|
+
spec.add_development_dependency "bundler", ">= 1.10", "< 3"
|
33
34
|
spec.add_development_dependency "rake", "~> 10.0"
|
34
35
|
end
|
data/spec/rails/auth/acl_spec.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
RSpec.describe Rails::Auth::Credentials::InjectorMiddleware do
|
2
4
|
let(:request) { Rack::MockRequest.env_for("https://www.example.com") }
|
3
5
|
let(:app) { ->(env) { [200, env, "Hello, world!"] } }
|
@@ -8,4 +10,17 @@ RSpec.describe Rails::Auth::Credentials::InjectorMiddleware do
|
|
8
10
|
_response, env = middleware.call(request)
|
9
11
|
expect(env[Rails::Auth::Env::CREDENTIALS_ENV_KEY]).to eq credentials
|
10
12
|
end
|
13
|
+
|
14
|
+
context "with a proc for credentials" do
|
15
|
+
let(:credentials_proc) { instance_double(Proc) }
|
16
|
+
let(:middleware) { described_class.new(app, credentials_proc) }
|
17
|
+
|
18
|
+
it "overrides rails-auth credentials in the rack environment" do
|
19
|
+
expect(credentials_proc).to receive(:call).with(request).and_return(credentials)
|
20
|
+
|
21
|
+
_response, env = middleware.call(request)
|
22
|
+
|
23
|
+
expect(env[Rails::Auth::Env::CREDENTIALS_ENV_KEY]).to eq credentials
|
24
|
+
end
|
25
|
+
end
|
11
26
|
end
|
data/spec/rails/auth/env_spec.rb
CHANGED