saml-kit 0.1.0 → 0.2.0
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 +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
|