Commit 76d83dd

mo <mo.khan@gmail.com>
2017-10-22 23:06:27
move classes to gem.
bin/console
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+
+require "bundler/setup"
+require "saml/kit"
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+# (If you use this, don't forget to add pry to your Gemfile!)
+# require "pry"
+# Pry.start
+
+require "irb"
+IRB.start(__FILE__)
bin/setup
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+set -vx
+
+bundle install
+
+# Do any other automated setup that you need to do here
lib/saml/kit/authentication_request.rb
@@ -0,0 +1,58 @@
+module Saml
+  module Kit
+    class AuthenticationRequest
+      def initialize(xml, registry = ServiceProviderRegistry.new)
+        @xml = xml
+        @registry = registry
+        @hash = Hash.from_xml(@xml)
+      end
+
+      def id
+        @hash['AuthnRequest']['ID']
+      end
+
+      def acs_url
+        @hash['AuthnRequest']['AssertionConsumerServiceURL']
+      end
+
+      def issuer
+        @hash['AuthnRequest']['Issuer']
+      end
+
+      def valid?
+        @registry.registered?(issuer)
+      end
+
+      def to_xml
+        @xml
+      end
+
+      def response_for(user)
+        SamlResponse::Builder.new(user, self).build
+      end
+
+      class Builder
+        attr_accessor :id, :issued_at, :issuer, :acs_url
+
+        def to_xml(xml = ::Builder::XmlMarkup.new)
+          xml.tag!('samlp:AuthnRequest',
+                   "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
+                   "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
+                   ID: id,
+                   Version: "2.0",
+                   IssueInstant: issued_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
+                   AssertionConsumerServiceURL: acs_url,
+                  ) do
+            xml.tag!('saml:Issuer', issuer)
+            xml.tag!('samlp:NameIDPolicy', Format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress")
+          end
+          xml.target!
+        end
+
+        def build
+          AuthenticationRequest.new(to_xml)
+        end
+      end
+    end
+  end
+end
lib/saml/kit/configuration.rb
@@ -0,0 +1,7 @@
+module Saml
+  module Kit
+    class Configuration
+      attr_accessor :issuer
+    end
+  end
+end
lib/saml/kit/namespaces.rb
@@ -0,0 +1,42 @@
+module Saml
+  module Kit
+    module Namespaces
+      METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
+      ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
+      SIGNATURE = "http://www.w3.org/2000/09/xmldsig#"
+      PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
+
+      module Statuses
+        SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
+      end
+
+      module Consents
+        UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:consent:unspecified"
+      end
+
+      module AuthnContext
+        module ClassRef
+          PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
+          PASSWORD_PROTECTED = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
+        end
+      end
+
+      module Methods
+        BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
+      end
+
+      module Formats
+        module Attr
+          URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
+          BASIC = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
+        end
+
+        module NameId
+          EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
+          TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
+          PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
+        end
+      end
+    end
+  end
+end
lib/saml/kit/saml_request.rb
@@ -0,0 +1,9 @@
+module Saml
+  module Kit
+    class SamlRequest
+      def self.decode(raw_request)
+        AuthenticationRequest.new(Base64.decode64(raw_request))
+      end
+    end
+  end
+end
lib/saml/kit/saml_response.rb
@@ -0,0 +1,127 @@
+module Saml
+  module Kit
+    class SamlResponse
+      def initialize(xml)
+        @xml = xml
+      end
+
+      def acs_url
+        Hash.from_xml(@xml)['Response']['Destination']
+      end
+
+      def to_xml
+        @xml
+      end
+
+      def encode
+        Base64.strict_encode64(to_xml)
+      end
+
+      class Builder
+        attr_reader :user, :request, :id, :reference_id, :now
+
+        def initialize(user, request)
+          @user = user
+          @request = request
+          @id = SecureRandom.uuid
+          @reference_id = SecureRandom.uuid
+          @now = Time.now.utc
+        end
+
+        def to_xml
+          xml = ::Builder::XmlMarkup.new
+          xml.tag!("samlp:Response", response_options) do
+            xml.Issuer(configuration.issuer, xmlns: Namespaces::ASSERTION)
+            xml.tag!("samlp:Status") do
+              xml.tag!('samlp:StatusCode', Value: Namespaces::Statuses::SUCCESS)
+            end
+            xml.Assertion(assertion_options) do
+              xml.Issuer configuration.issuer
+              xml.Subject do
+                xml.NameID user.uuid, Format: name_id_format
+                xml.SubjectConfirmation Method: Namespaces::Methods::BEARER do
+                  xml.SubjectConfirmationData "", subject_confirmation_data_options
+                end
+              end
+              xml.Conditions conditions_options do
+                xml.AudienceRestriction do
+                  xml.Audience request.issuer
+                end
+              end
+              xml.AuthnStatement authn_statement_options do
+                xml.AuthnContext do
+                  xml.AuthnContextClassRef Namespaces::AuthnContext::ClassRef::PASSWORD
+                end
+              end
+              xml.AttributeStatement do
+                user.assertion_attributes.each do |key, value|
+                  xml.Attribute Name: key, NameFormat: Namespaces::Formats::Attr::URI, FriendlyName: key do
+                    xml.AttributeValue value.to_s
+                  end
+                end
+              end
+            end
+          end
+          xml.target!
+        end
+
+        def build
+          SamlResponse.new(to_xml)
+        end
+
+        private
+
+        def configuration
+          Saml::Kit.configuration
+        end
+
+        def response_options
+          {
+            ID: "_#{id}",
+            Version: "2.0",
+            IssueInstant: now.iso8601,
+            Destination: request.acs_url,
+            Consent: Namespaces::Consents::UNSPECIFIED,
+            InResponseTo: request.id,
+            "xmlns:samlp" => Namespaces::PROTOCOL,
+          }
+        end
+
+        def assertion_options
+          {
+            ID: "_#{reference_id}",
+            IssueInstant: now.iso8601,
+            Version: "2.0",
+          }
+        end
+
+        def subject_confirmation_data_options
+          {
+            InResponseTo: request.id,
+            NotOnOrAfter: 3.hours.from_now.utc.iso8601,
+            Recipient: request.acs_url,
+          }
+        end
+
+        def conditions_options
+          {
+            NotBefore: 5.seconds.ago.utc.iso8601,
+            NotOnOrAfter: 3.hours.from_now.utc.iso8601,
+          }
+        end
+
+        def authn_statement_options
+          {
+            AuthnInstant: now.iso8601,
+            SessionIndex: assertion_options[:ID],
+            SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601,
+          }
+        end
+
+        def name_id_format
+          "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
+        end
+      end
+    end
+  end
+end
lib/saml/kit/service_provider_registry.rb
@@ -0,0 +1,9 @@
+module Saml
+  module Kit
+    class ServiceProviderRegistry
+      def registered?(issuer)
+        true
+      end
+    end
+  end
+end
lib/saml/kit/version.rb
@@ -0,0 +1,5 @@
+module Saml
+  module Kit
+    VERSION = "0.1.0"
+  end
+end
lib/saml/kit.rb
@@ -0,0 +1,25 @@
+require "saml/kit/version"
+
+require "builder"
+require "securerandom"
+require "active_support/duration"
+require "active_support/core_ext/numeric/time"
+require "active_support/core_ext/hash/conversions"
+require "saml/kit/authentication_request"
+require "saml/kit/configuration"
+require "saml/kit/namespaces"
+require "saml/kit/saml_request"
+require "saml/kit/saml_response"
+require "saml/kit/service_provider_registry"
+
+module Saml
+  module Kit
+    def self.configuration
+      @config ||= Saml::Kit::Configuration.new
+    end
+
+    def self.configure
+      yield configuration
+    end
+  end
+end
spec/saml/authentication_request_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::AuthenticationRequest do
+  subject { described_class.new(raw_xml, registry) }
+  let(:registry) { double }
+  let(:id) { SecureRandom.uuid }
+  let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+  let(:issuer) { FFaker::Movie.title }
+  let(:raw_xml) do
+    builder = described_class::Builder.new
+    builder.id = id
+    builder.issued_at = Time.now.utc
+    builder.issuer = issuer
+    builder.acs_url = acs_url
+    builder.to_xml
+  end
+
+  it { expect(subject.issuer).to eql(issuer) }
+  it { expect(subject.id).to eql(id) }
+  it { expect(subject.acs_url).to eql(acs_url) }
+
+  describe "#valid?" do
+    it 'returns false when the service provider is not known' do
+      allow(registry).to receive(:registered?).with(issuer).and_return(false)
+      expect(subject).to_not be_valid
+    end
+
+    it 'returns true when the service provider is registered' do
+      allow(registry).to receive(:registered?).with(issuer).and_return(true)
+      expect(subject).to be_valid
+    end
+  end
+end
spec/saml/kit_spec.rb
@@ -0,0 +1,7 @@
+require "spec_helper"
+
+RSpec.describe Saml::Kit do
+  it "has a version number" do
+    expect(Saml::Kit::VERSION).not_to be nil
+  end
+end
spec/saml/saml_response_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::SamlResponse do
+  describe "#acs_url" do
+    let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+    let(:user) { double(:user, uuid: SecureRandom.uuid, assertion_attributes: { }) }
+    let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title) }
+    subject { described_class::Builder.new(user, request).build }
+
+    it 'returns the acs_url' do
+      expect(subject.acs_url).to eql(acs_url)
+    end
+  end
+
+  describe "#to_xml" do
+    subject { described_class::Builder.new(user, request) }
+    let(:user) { double(:user, uuid: SecureRandom.uuid, assertion_attributes: { email: email, created_at: Time.now.utc.iso8601 }) }
+    let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title) }
+    let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+    let(:issuer) { FFaker::Movie.title }
+    let(:email) { FFaker::Internet.email }
+
+    <<-XML
+<samlp:Response
+  xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
+  xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+  ID="_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6"
+  Version="2.0"
+  IssueInstant="2014-07-17T01:01:48Z"
+  Destination="http://sp.example.com/demo1/index.php?acs"
+  InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685">
+  <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
+  <samlp:Status>
+    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+  </samlp:Status>
+  <saml:Assertion
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:xs="http://www.w3.org/2001/XMLSchema"
+    ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75"
+    Version="2.0"
+    IssueInstant="2014-07-17T01:01:48Z">
+    <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
+    <saml:Subject>
+      <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
+      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+        <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/>
+      </saml:SubjectConfirmation>
+    </saml:Subject>
+    <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
+      <saml:AudienceRestriction>
+        <saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
+      </saml:AudienceRestriction>
+    </saml:Conditions>
+    <saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
+      <saml:AuthnContext>
+        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
+      </saml:AuthnContext>
+    </saml:AuthnStatement>
+    <saml:AttributeStatement>
+      <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
+      </saml:Attribute>
+      <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
+        <saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue>
+      </saml:Attribute>
+    </saml:AttributeStatement>
+  </saml:Assertion>
+</samlp:Response>
+    XML
+    it 'returns a proper response for the user' do
+      travel_to 1.second.from_now
+      allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
+      result = subject.to_xml
+      hash = Hash.from_xml(result)
+
+      expect(hash['Response']['ID']).to be_present
+      expect(hash['Response']['Version']).to eql('2.0')
+      expect(hash['Response']['IssueInstant']).to eql(Time.now.utc.iso8601)
+      expect(hash['Response']['Destination']).to eql(acs_url)
+      expect(hash['Response']['InResponseTo']).to eql(request.id)
+      expect(hash['Response']['Issuer']).to eql(issuer)
+      expect(hash['Response']['Status']['StatusCode']['Value']).to eql("urn:oasis:names:tc:SAML:2.0:status:Success")
+
+      expect(hash['Response']['Assertion']['ID']).to be_present
+      expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
+      expect(hash['Response']['Assertion']['Version']).to eql("2.0")
+      expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
+
+      expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.uuid)
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql("urn:oasis:names:tc:SAML:2.0:cm:bearer")
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(acs_url)
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
+
+      expect(hash['Response']['Assertion']['Conditions']['NotBefore']).to eql(5.seconds.ago.utc.iso8601)
+      expect(hash['Response']['Assertion']['Conditions']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+      expect(hash['Response']['Assertion']['Conditions']['AudienceRestriction']['Audience']).to eql(request.issuer)
+
+      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnInstant']).to eql(Time.now.utc.iso8601)
+      expect(hash['Response']['Assertion']['AuthnStatement']['SessionNotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+      expect(hash['Response']['Assertion']['AuthnStatement']['SessionIndex']).to eql(hash['Response']['Assertion']['ID'])
+      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to eql('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
+
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['Name']).to eql('email')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['FriendlyName']).to eql('email')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql(email)
+
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['Name']).to eql('created_at')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['FriendlyName']).to eql('created_at')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['AttributeValue']).to be_present
+    end
+  end
+end
spec/spec_helper.rb
@@ -0,0 +1,17 @@
+require "bundler/setup"
+require "saml/kit"
+require "ffaker"
+require "active_support/testing/time_helpers"
+
+RSpec.configure do |config|
+  config.include ActiveSupport::Testing::TimeHelpers
+  # Enable flags like --only-failures and --next-failure
+  config.example_status_persistence_file_path = ".rspec_status"
+
+  # Disable RSpec exposing methods globally on `Module` and `main`
+  config.disable_monkey_patching!
+
+  config.expect_with :rspec do |c|
+    c.syntax = :expect
+  end
+end
.gitignore
@@ -0,0 +1,12 @@
+/.bundle/
+/.yardoc
+/Gemfile.lock
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+
+# rspec failure tracking
+.rspec_status
.rspec
@@ -0,0 +1,2 @@
+--format documentation
+--color
.travis.yml
@@ -0,0 +1,5 @@
+sudo: false
+language: ruby
+rvm:
+  - 2.4.2
+before_install: gem install bundler -v 1.15.4
Gemfile
@@ -0,0 +1,6 @@
+source "https://rubygems.org"
+
+git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
+
+# Specify your gem's dependencies in saml-kit.gemspec
+gemspec
LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 mo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
Rakefile
@@ -0,0 +1,6 @@
+require "bundler/gem_tasks"
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+task :default => :spec
README.md
@@ -0,0 +1,39 @@
+# Saml::Kit
+
+Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/saml/kit`. To experiment with that code, run `bin/console` for an interactive prompt.
+
+TODO: Delete this and the text above, and describe your gem
+
+## Installation
+
+Add this line to your application's Gemfile:
+
+```ruby
+gem 'saml-kit'
+```
+
+And then execute:
+
+    $ bundle
+
+Or install it yourself as:
+
+    $ gem install saml-kit
+
+## Usage
+
+TODO: Write usage instructions here
+
+## Development
+
+After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
+
+To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
+
+## Contributing
+
+Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/saml-kit.
+
+## License
+
+The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
saml-kit.gemspec
@@ -0,0 +1,39 @@
+# coding: utf-8
+lib = File.expand_path("../lib", __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+require "saml/kit/version"
+
+Gem::Specification.new do |spec|
+  spec.name          = "saml-kit"
+  spec.version       = Saml::Kit::VERSION
+  spec.authors       = ["mo"]
+  spec.email         = ["mo@mokhan.ca"]
+
+  spec.summary       = %q{A simple toolkit for working with SAML.}
+  spec.description   = %q{A simple toolkit for working with SAML.}
+  spec.homepage      = "http://www.mokhan.ca"
+  spec.license       = "MIT"
+
+  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
+  # to allow pushing to a single host or delete this section to allow pushing to any host.
+  if spec.respond_to?(:metadata)
+    spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
+  else
+    raise "RubyGems 2.0 or newer is required to protect against " \
+      "public gem pushes."
+  end
+
+  spec.files         = `git ls-files -z`.split("\x0").reject do |f|
+    f.match(%r{^(test|spec|features)/})
+  end
+  spec.bindir        = "exe"
+  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+  spec.require_paths = ["lib"]
+
+  spec.add_dependency "builder", "~> 3.2"
+  spec.add_dependency "activesupport", "~> 5.1"
+  spec.add_development_dependency "bundler", "~> 1.15"
+  spec.add_development_dependency "rake", "~> 10.0"
+  spec.add_development_dependency "rspec", "~> 3.0"
+  spec.add_development_dependency "ffaker", "~> 2.7"
+end