saml-kit 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/exe/saml-kit-decode-http-post +8 -0
- data/lib/saml/kit.rb +4 -4
- data/lib/saml/kit/authentication_request.rb +3 -13
- data/lib/saml/kit/bindings.rb +45 -0
- data/lib/saml/kit/bindings/binding.rb +42 -0
- data/lib/saml/kit/bindings/http_post.rb +29 -0
- data/lib/saml/kit/bindings/http_redirect.rb +61 -0
- data/lib/saml/kit/bindings/url_builder.rb +40 -0
- data/lib/saml/kit/configuration.rb +22 -1
- data/lib/saml/kit/crypto.rb +16 -0
- data/lib/saml/kit/crypto/oaep_cipher.rb +22 -0
- data/lib/saml/kit/crypto/rsa_cipher.rb +23 -0
- data/lib/saml/kit/crypto/simple_cipher.rb +38 -0
- data/lib/saml/kit/crypto/unknown_cipher.rb +18 -0
- data/lib/saml/kit/cryptography.rb +30 -0
- data/lib/saml/kit/default_registry.rb +1 -0
- data/lib/saml/kit/document.rb +6 -2
- data/lib/saml/kit/identity_provider_metadata.rb +9 -9
- data/lib/saml/kit/locales/en.yml +4 -3
- data/lib/saml/kit/logout_request.rb +2 -2
- data/lib/saml/kit/logout_response.rb +3 -3
- data/lib/saml/kit/metadata.rb +12 -37
- data/lib/saml/kit/namespaces.rb +1 -13
- data/lib/saml/kit/respondable.rb +4 -0
- data/lib/saml/kit/response.rb +120 -37
- data/lib/saml/kit/service_provider_metadata.rb +16 -7
- data/lib/saml/kit/signature.rb +16 -13
- data/lib/saml/kit/trustable.rb +14 -6
- data/lib/saml/kit/version.rb +1 -1
- data/lib/saml/kit/xml.rb +19 -3
- data/saml-kit.gemspec +2 -2
- metadata +23 -14
- data/lib/saml/kit/binding.rb +0 -40
- data/lib/saml/kit/http_post_binding.rb +0 -27
- data/lib/saml/kit/http_redirect_binding.rb +0 -58
- data/lib/saml/kit/url_builder.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ebef3199d5a8a66f49c3c57a2aafb1a4f54149c
|
4
|
+
data.tar.gz: 9dce4bab8931cd0fc1aa8b147a461bdaf198328f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6f4922d39dd247dd91caa3262ed5cb0119c2f2d245c1609a5fa41bee27742a47845fea3dfe6605d7124030f9e7f844c2029e75d2cafbbf57288ded697fa14fa
|
7
|
+
data.tar.gz: 99d7c34ce9fae83d81ce16eec10fd6ef9fd1d70f8d67cfbb412d4be45e59b2f28f2db5818de8c7520756a508c9e973c6598571054bb52cda6b5d57fcf8af2c77
|
data/lib/saml/kit.rb
CHANGED
@@ -3,6 +3,7 @@ require "saml/kit/version"
|
|
3
3
|
require "active_model"
|
4
4
|
require "active_support/core_ext/date/calculations"
|
5
5
|
require "active_support/core_ext/hash/conversions"
|
6
|
+
require "active_support/core_ext/hash/indifferent_access"
|
6
7
|
require "active_support/core_ext/numeric/time"
|
7
8
|
require "active_support/duration"
|
8
9
|
require "builder"
|
@@ -21,14 +22,14 @@ require "saml/kit/trustable"
|
|
21
22
|
require "saml/kit/document"
|
22
23
|
|
23
24
|
require "saml/kit/authentication_request"
|
24
|
-
require "saml/kit/
|
25
|
+
require "saml/kit/bindings"
|
25
26
|
require "saml/kit/configuration"
|
27
|
+
require "saml/kit/crypto"
|
28
|
+
require "saml/kit/cryptography"
|
26
29
|
require "saml/kit/default_registry"
|
27
30
|
require "saml/kit/fingerprint"
|
28
31
|
require "saml/kit/logout_response"
|
29
32
|
require "saml/kit/logout_request"
|
30
|
-
require "saml/kit/http_post_binding"
|
31
|
-
require "saml/kit/http_redirect_binding"
|
32
33
|
require "saml/kit/metadata"
|
33
34
|
require "saml/kit/response"
|
34
35
|
require "saml/kit/identity_provider_metadata"
|
@@ -36,7 +37,6 @@ require "saml/kit/invalid_document"
|
|
36
37
|
require "saml/kit/self_signed_certificate"
|
37
38
|
require "saml/kit/service_provider_metadata"
|
38
39
|
require "saml/kit/signature"
|
39
|
-
require "saml/kit/url_builder"
|
40
40
|
require "saml/kit/xml"
|
41
41
|
|
42
42
|
I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
|
@@ -2,18 +2,13 @@ module Saml
|
|
2
2
|
module Kit
|
3
3
|
class AuthenticationRequest < Document
|
4
4
|
include Requestable
|
5
|
-
validates_presence_of :acs_url, if: :expected_type?
|
6
5
|
|
7
6
|
def initialize(xml)
|
8
7
|
super(xml, name: "AuthnRequest")
|
9
8
|
end
|
10
9
|
|
11
10
|
def acs_url
|
12
|
-
|
13
|
-
to_h[name]['AssertionConsumerServiceURL'] || registered_acs_url(binding: :post)
|
14
|
-
#else
|
15
|
-
#registered_acs_url
|
16
|
-
#end
|
11
|
+
to_h[name]['AssertionConsumerServiceURL']
|
17
12
|
end
|
18
13
|
|
19
14
|
def name_id_format
|
@@ -26,11 +21,6 @@ module Saml
|
|
26
21
|
|
27
22
|
private
|
28
23
|
|
29
|
-
def registered_acs_url(binding:)
|
30
|
-
return if provider.nil?
|
31
|
-
provider.assertion_consumer_service_for(binding: binding).try(:location)
|
32
|
-
end
|
33
|
-
|
34
24
|
class Builder
|
35
25
|
attr_accessor :id, :now, :issuer, :acs_url, :name_id_format, :sign, :destination
|
36
26
|
attr_accessor :version
|
@@ -45,10 +35,10 @@ module Saml
|
|
45
35
|
end
|
46
36
|
|
47
37
|
def to_xml
|
48
|
-
Signature.sign(
|
38
|
+
Signature.sign(sign: sign) do |xml, signature|
|
49
39
|
xml.tag!('samlp:AuthnRequest', request_options) do
|
50
40
|
xml.tag!('saml:Issuer', issuer)
|
51
|
-
signature.template(
|
41
|
+
signature.template(id)
|
52
42
|
xml.tag!('samlp:NameIDPolicy', Format: name_id_format)
|
53
43
|
end
|
54
44
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "saml/kit/bindings/binding"
|
2
|
+
require "saml/kit/bindings/http_post"
|
3
|
+
require "saml/kit/bindings/http_redirect"
|
4
|
+
require "saml/kit/bindings/url_builder"
|
5
|
+
|
6
|
+
module Saml
|
7
|
+
module Kit
|
8
|
+
module Bindings
|
9
|
+
HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'
|
10
|
+
HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
11
|
+
HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
12
|
+
ALL = {
|
13
|
+
http_post: HTTP_POST,
|
14
|
+
http_redirect: HTTP_REDIRECT,
|
15
|
+
http_artifact: HTTP_ARTIFACT,
|
16
|
+
}
|
17
|
+
|
18
|
+
def self.binding_for(binding)
|
19
|
+
ALL[binding]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.to_symbol(binding)
|
23
|
+
case binding
|
24
|
+
when HTTP_REDIRECT
|
25
|
+
:http_redirect
|
26
|
+
when HTTP_POST
|
27
|
+
:http_post
|
28
|
+
else
|
29
|
+
binding
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.create_for(binding, location)
|
34
|
+
case binding
|
35
|
+
when HTTP_REDIRECT
|
36
|
+
HttpRedirect.new(location: location)
|
37
|
+
when HTTP_POST
|
38
|
+
HttpPost.new(location: location)
|
39
|
+
else
|
40
|
+
Binding.new(binding: binding, location: location)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Saml
|
2
|
+
module Kit
|
3
|
+
module Bindings
|
4
|
+
class Binding
|
5
|
+
attr_reader :binding, :location
|
6
|
+
|
7
|
+
def initialize(binding:, location:)
|
8
|
+
@binding = binding
|
9
|
+
@location = location
|
10
|
+
end
|
11
|
+
|
12
|
+
def binding?(other)
|
13
|
+
binding == other
|
14
|
+
end
|
15
|
+
|
16
|
+
def serialize(builder, relay_state: nil)
|
17
|
+
[]
|
18
|
+
end
|
19
|
+
|
20
|
+
def deserialize(params)
|
21
|
+
raise ArgumentError.new("Unsupported binding")
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_h
|
25
|
+
{ binding: binding, location: location }
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def saml_param_from(params)
|
31
|
+
if params['SAMLRequest'].present?
|
32
|
+
params['SAMLRequest']
|
33
|
+
elsif params['SAMLResponse'].present?
|
34
|
+
params['SAMLResponse']
|
35
|
+
else
|
36
|
+
raise ArgumentError.new("SAMLRequest or SAMLResponse parameter is required.")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Saml
|
2
|
+
module Kit
|
3
|
+
module Bindings
|
4
|
+
class HttpPost < Binding
|
5
|
+
include Serializable
|
6
|
+
|
7
|
+
def initialize(location:)
|
8
|
+
super(binding: Saml::Kit::Bindings::HTTP_POST, location: location)
|
9
|
+
end
|
10
|
+
|
11
|
+
def serialize(builder, relay_state: nil)
|
12
|
+
builder.sign = true
|
13
|
+
builder.destination = location
|
14
|
+
document = builder.build
|
15
|
+
saml_params = {
|
16
|
+
document.query_string_parameter => Base64.strict_encode64(document.to_xml),
|
17
|
+
}
|
18
|
+
saml_params['RelayState'] = relay_state if relay_state.present?
|
19
|
+
[location, saml_params]
|
20
|
+
end
|
21
|
+
|
22
|
+
def deserialize(params)
|
23
|
+
xml = decode(saml_param_from(params))
|
24
|
+
Saml::Kit::Document.to_saml_document(xml)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Saml
|
2
|
+
module Kit
|
3
|
+
module Bindings
|
4
|
+
class HttpRedirect < Binding
|
5
|
+
include Serializable
|
6
|
+
|
7
|
+
def initialize(location:)
|
8
|
+
super(binding: Saml::Kit::Bindings::HTTP_REDIRECT, location: location)
|
9
|
+
end
|
10
|
+
|
11
|
+
def serialize(builder, relay_state: nil)
|
12
|
+
builder.sign = false
|
13
|
+
builder.destination = location
|
14
|
+
document = builder.build
|
15
|
+
[UrlBuilder.new.build(document, relay_state: relay_state), {}]
|
16
|
+
end
|
17
|
+
|
18
|
+
def deserialize(params)
|
19
|
+
document = deserialize_document_from!(params)
|
20
|
+
ensure_valid_signature!(params, document)
|
21
|
+
document.signature_verified!
|
22
|
+
document
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def deserialize_document_from!(params)
|
28
|
+
xml = inflate(decode(unescape(saml_param_from(params))))
|
29
|
+
Saml::Kit.logger.debug(xml)
|
30
|
+
Saml::Kit::Document.to_saml_document(xml)
|
31
|
+
end
|
32
|
+
|
33
|
+
def ensure_valid_signature!(params, document)
|
34
|
+
return if params['Signature'].blank? || params['SigAlg'].blank?
|
35
|
+
|
36
|
+
signature = decode(params['Signature'])
|
37
|
+
canonical_form = ['SAMLRequest', 'SAMLResponse', 'RelayState', 'SigAlg'].map do |key|
|
38
|
+
value = params[key]
|
39
|
+
value.present? ? "#{key}=#{value}" : nil
|
40
|
+
end.compact.join('&')
|
41
|
+
|
42
|
+
valid = document.provider.verify(algorithm_for(params['SigAlg']), signature, canonical_form)
|
43
|
+
raise ArgumentError.new("Invalid Signature") unless valid
|
44
|
+
end
|
45
|
+
|
46
|
+
def algorithm_for(algorithm)
|
47
|
+
case algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
|
48
|
+
when 256
|
49
|
+
OpenSSL::Digest::SHA256.new
|
50
|
+
when 384
|
51
|
+
OpenSSL::Digest::SHA384.new
|
52
|
+
when 512
|
53
|
+
OpenSSL::Digest::SHA512.new
|
54
|
+
else
|
55
|
+
OpenSSL::Digest::SHA1.new
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Saml
|
2
|
+
module Kit
|
3
|
+
module Bindings
|
4
|
+
class UrlBuilder
|
5
|
+
include Serializable
|
6
|
+
|
7
|
+
def initialize(private_key: Saml::Kit.configuration.signing_private_key)
|
8
|
+
@private_key = private_key
|
9
|
+
end
|
10
|
+
|
11
|
+
def build(saml_document, relay_state: nil)
|
12
|
+
payload = canonicalize(saml_document, relay_state)
|
13
|
+
"#{saml_document.destination}?#{payload}&Signature=#{signature_for(payload)}"
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :private_key
|
19
|
+
|
20
|
+
def signature_for(payload)
|
21
|
+
encode(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
|
22
|
+
end
|
23
|
+
|
24
|
+
def canonicalize(saml_document, relay_state)
|
25
|
+
{
|
26
|
+
saml_document.query_string_parameter => serialize(saml_document.to_xml),
|
27
|
+
'RelayState' => relay_state,
|
28
|
+
'SigAlg' => Saml::Kit::Namespaces::SHA256,
|
29
|
+
}.map do |(key, value)|
|
30
|
+
value.present? ? "#{key}=#{escape(value)}" : nil
|
31
|
+
end.compact.join('&')
|
32
|
+
end
|
33
|
+
|
34
|
+
def serialize(value)
|
35
|
+
encode(deflate(value))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -7,6 +7,7 @@ module Saml
|
|
7
7
|
attr_accessor :issuer
|
8
8
|
attr_accessor :signature_method, :digest_method
|
9
9
|
attr_accessor :signing_certificate_pem, :signing_private_key_pem, :signing_private_key_password
|
10
|
+
attr_accessor :encryption_certificate_pem, :encryption_private_key_pem, :encryption_private_key_password
|
10
11
|
attr_accessor :registry, :session_timeout
|
11
12
|
attr_accessor :logger
|
12
13
|
|
@@ -14,23 +15,43 @@ module Saml
|
|
14
15
|
@signature_method = :SHA256
|
15
16
|
@digest_method = :SHA256
|
16
17
|
@signing_private_key_password = SecureRandom.uuid
|
18
|
+
@encryption_private_key_password = SecureRandom.uuid
|
17
19
|
@signing_certificate_pem, @signing_private_key_pem = SelfSignedCertificate.new(@signing_private_key_password).create
|
20
|
+
@encryption_certificate_pem, @encryption_private_key_pem = SelfSignedCertificate.new(@encryption_private_key_password).create
|
18
21
|
@registry = DefaultRegistry.new
|
19
22
|
@session_timeout = 3.hours
|
20
23
|
@logger = Logger.new(STDOUT)
|
21
24
|
end
|
22
25
|
|
23
26
|
def stripped_signing_certificate
|
24
|
-
signing_certificate_pem
|
27
|
+
normalize(signing_certificate_pem)
|
28
|
+
end
|
29
|
+
|
30
|
+
def stripped_encryption_certificate
|
31
|
+
normalize(encryption_certificate_pem)
|
25
32
|
end
|
26
33
|
|
27
34
|
def signing_x509
|
28
35
|
OpenSSL::X509::Certificate.new(signing_certificate_pem)
|
29
36
|
end
|
30
37
|
|
38
|
+
def encryption_x509
|
39
|
+
OpenSSL::X509::Certificate.new(encryption_certificate_pem)
|
40
|
+
end
|
41
|
+
|
31
42
|
def signing_private_key
|
32
43
|
OpenSSL::PKey::RSA.new(signing_private_key_pem, signing_private_key_password)
|
33
44
|
end
|
45
|
+
|
46
|
+
def encryption_private_key
|
47
|
+
OpenSSL::PKey::RSA.new(encryption_private_key_pem, encryption_private_key_password)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def normalize(certificate)
|
53
|
+
certificate.to_s.gsub(BEGIN_CERT, '').gsub(END_CERT, '').gsub(/\n/, '')
|
54
|
+
end
|
34
55
|
end
|
35
56
|
end
|
36
57
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'saml/kit/crypto/oaep_cipher'
|
2
|
+
require 'saml/kit/crypto/rsa_cipher'
|
3
|
+
require 'saml/kit/crypto/simple_cipher'
|
4
|
+
require 'saml/kit/crypto/unknown_cipher'
|
5
|
+
|
6
|
+
module Saml
|
7
|
+
module Kit
|
8
|
+
module Crypto
|
9
|
+
DECRYPTORS = [ SimpleCipher, RsaCipher, OaepCipher, UnknownCipher ]
|
10
|
+
|
11
|
+
def self.decryptor_for(algorithm, key)
|
12
|
+
DECRYPTORS.find { |x| x.matches?(algorithm) }.new(algorithm, key)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Saml
|
2
|
+
module Kit
|
3
|
+
module Crypto
|
4
|
+
class OaepCipher
|
5
|
+
ALGORITHMS = {
|
6
|
+
'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' => true,
|
7
|
+
}
|
8
|
+
def initialize(algorithm, key)
|
9
|
+
@key = key
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.matches?(algorithm)
|
13
|
+
ALGORITHMS[algorithm]
|
14
|
+
end
|
15
|
+
|
16
|
+
def decrypt(cipher_text)
|
17
|
+
@key.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Saml
|
2
|
+
module Kit
|
3
|
+
module Crypto
|
4
|
+
class RsaCipher
|
5
|
+
ALGORITHMS = {
|
6
|
+
'http://www.w3.org/2001/04/xmlenc#rsa-1_5' => true,
|
7
|
+
}
|
8
|
+
|
9
|
+
def initialize(algorithm, key)
|
10
|
+
@key = key
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.matches?(algorithm)
|
14
|
+
ALGORITHMS[algorithm]
|
15
|
+
end
|
16
|
+
|
17
|
+
def decrypt(cipher_text)
|
18
|
+
@key.private_decrypt(cipher_text)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|