duo_security 0.0.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.
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/duo_security/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Marten Veldthuis"]
6
+ gem.email = ["marten@veldthuis.com"]
7
+ gem.description = %q{Perform 2-factor authentication using duosecurity.com}
8
+ gem.summary = %q{}
9
+ gem.homepage = "https://github.com/roqua/duo_security"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "duo_security"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = DuoSecurity::VERSION
17
+
18
+ gem.add_dependency "httparty", "~> 0.8.3"
19
+
20
+ gem.add_development_dependency "vcr", "~> 2.2.4"
21
+ gem.add_development_dependency "webmock", "~> 1.8.9"
22
+ end
@@ -0,0 +1,82 @@
1
+ require 'cgi'
2
+ require 'httparty'
3
+
4
+ module DuoSecurity
5
+ class API
6
+ class UnknownUser < StandardError; end
7
+
8
+ FACTORS = ["auto", "passcode", "phone", "sms", "push"]
9
+
10
+ include HTTParty
11
+ ssl_ca_file File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "data", "ca-bundle.crt"))
12
+
13
+ def initialize(host, secret_key, integration_key)
14
+ @host = host
15
+ @skey = secret_key
16
+ @ikey = integration_key
17
+
18
+ self.class.base_uri "https://#{@host}/rest/v1"
19
+ end
20
+
21
+ def ping
22
+ response = self.class.get("/ping")
23
+ response.parsed_response.fetch("response") == "pong"
24
+ end
25
+
26
+ def check
27
+ auth = sign("get", @host, "/rest/v1/check", {}, @skey, @ikey)
28
+ response = self.class.get("/check", headers: {"Authorization" => auth})
29
+
30
+ # TODO use parsed_response.fetch(...) when content-type is set correctly
31
+ response["response"] == "valid"
32
+ end
33
+
34
+ def preauth(user)
35
+ response = post("/preauth", {"user" => user})["response"]
36
+
37
+ raise UnknownUser, response.fetch("status") if response.fetch("result") == "enroll"
38
+
39
+ return response
40
+ end
41
+
42
+ def auth(user, factor, factor_params)
43
+ raise ArgumentError.new("Factor should be one of #{FACTORS.join(", ")}") unless FACTORS.include?(factor)
44
+
45
+ params = {"user" => user, "factor" => factor}.merge(factor_params)
46
+ response = post("/auth",params)
47
+
48
+ response["response"]["result"] == "allow"
49
+ end
50
+
51
+ protected
52
+
53
+ def post(path, params = {})
54
+ auth = sign("post", @host, "/rest/v1#{path}", params, @skey, @ikey)
55
+ self.class.post(path, headers: {"Authorization" => auth}, body: params)
56
+ end
57
+
58
+ def hmac_sha1(key, data)
59
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new('sha1'), key, data.to_s)
60
+ end
61
+
62
+ def sign(method, host, path, params, skey, ikey)
63
+ canon = [method.upcase, host.downcase, path]
64
+
65
+ args = []
66
+ for key in params.keys.sort
67
+ val = params[key]
68
+ args << "#{CGI.escape(key)}=#{CGI.escape(val)}"
69
+ end
70
+
71
+ canon << args.join("&")
72
+ canon = canon.join("\n")
73
+
74
+ sig = hmac_sha1(skey, canon)
75
+ auth = "#{ikey}:#{sig}"
76
+
77
+ encoded = Base64.encode64(auth).split("\n").join("")
78
+
79
+ return "Basic #{encoded}"
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,17 @@
1
+ module DuoSecurity
2
+ class Attempt
3
+ def initialize(api, username)
4
+ @api = api
5
+ @username = username
6
+ end
7
+
8
+ def login!
9
+ preauth = @api.preauth(@username)
10
+ factor = preauth["factors"].fetch("default")
11
+
12
+ @api.auth(@username, "auto", {"auto" => factor})
13
+ rescue API::UnknownUser => e
14
+ false
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module DuoSecurity
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,6 @@
1
+ require "duo_security/version"
2
+ require "duo_security/api"
3
+ require "duo_security/attempt"
4
+
5
+ module DuoSecurity
6
+ end
@@ -0,0 +1,99 @@
1
+ require 'minitest/autorun'
2
+ require 'vcr'
3
+ require_relative "../../lib/duo_security/api"
4
+
5
+ VCR.configure do |c|
6
+ c.cassette_library_dir = "fixtures/vcr"
7
+ c.hook_into :webmock
8
+ c.allow_http_connections_when_no_cassette = true
9
+ c.before_http_request(:real?) do |request|
10
+ puts "Cassette #{VCR.current_cassette.name} being recorded. Take appropriate actions on your phone."
11
+ end
12
+ end
13
+
14
+ module DuoSecurity
15
+ describe API do
16
+ let(:host) { ENV["DUO_HOST"] }
17
+ let(:skey) { ENV["DUO_SKEY"] }
18
+ let(:ikey) { ENV["DUO_IKEY"] }
19
+
20
+ describe '#ping' do
21
+ it 'succeeds' do
22
+ VCR.use_cassette("api_ping_success") do
23
+ duo = API.new(host, skey, ikey)
24
+ duo.ping.must_equal true
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#check' do
30
+ it 'succeeds with correct credentials' do
31
+ VCR.use_cassette("api_check_success") do
32
+ duo = API.new(host, skey, ikey)
33
+ duo.check.must_equal true
34
+ end
35
+ end
36
+
37
+ it 'fails with incorrect skey' do
38
+ VCR.use_cassette("api_check_wrong_skey") do
39
+ duo = API.new(host, "wrong", ikey)
40
+ duo.check.must_equal false
41
+ end
42
+ end
43
+
44
+ it 'fails with incorrect ikey' do
45
+ VCR.use_cassette("api_check_wrong_ikey") do
46
+ duo = API.new(host, skey, "wrong")
47
+ duo.check.must_equal false
48
+ end
49
+ end
50
+ end
51
+
52
+ describe '#preauth' do
53
+ it 'returns a list of possible factors' do
54
+ VCR.use_cassette("api_preauth") do
55
+ duo = API.new(host, skey, ikey)
56
+ result = duo.preauth("marten")
57
+ result["factors"].must_equal({"1"=>"push1", "2"=>"sms1", "default"=>"push1"})
58
+ result["result"].must_equal("auth")
59
+ end
60
+ end
61
+
62
+ it 'raises when user does not exist' do
63
+ VCR.use_cassette("api_preauth_unknown_user") do
64
+ duo = API.new(host, skey, ikey)
65
+ -> { duo.preauth("unknown") }.must_raise(API::UnknownUser)
66
+ end
67
+ end
68
+ end
69
+
70
+ describe '#auth' do
71
+ let(:duo) { API.new(host, skey, ikey) }
72
+
73
+ it 'returns true if user OKs the request' do
74
+ VCR.use_cassette("api_auth_user_accepts") do
75
+ result = duo.auth("marten", "push", "phone" => "phone1")
76
+ result.must_equal(true)
77
+ end
78
+ end
79
+
80
+ it 'returns false if the user denies the request as a mistake' do
81
+ VCR.use_cassette("api_auth_user_denies_mistake") do
82
+ result = duo.auth("marten", "push", "phone" => "phone1")
83
+ result.must_equal(false)
84
+ end
85
+ end
86
+
87
+ it 'returns false if the user denies the request as a fraudulent attack' do
88
+ VCR.use_cassette("api_auth_user_denies_fraud") do
89
+ result = duo.auth("marten", "push", "phone" => "phone1")
90
+ result.must_equal(false)
91
+ end
92
+ end
93
+
94
+ it 'raises an exception when factor is unknown' do
95
+ -> { duo.auth("marten", "something") }.must_raise(ArgumentError)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,28 @@
1
+ require 'minitest/autorun'
2
+ require_relative '../../lib/duo_security/attempt'
3
+
4
+ module DuoSecurity
5
+ describe Attempt do
6
+ let(:api) { API.new(ENV["DUO_HOST"], ENV["DUO_SKEY"], ENV["DUO_IKEY"]) }
7
+
8
+ describe 'when using push notifications' do
9
+ it 'returns true if the user accepts the login' do
10
+ VCR.use_cassette("attempt_allowed") do
11
+ Attempt.new(api, "marten").login!.must_equal true
12
+ end
13
+ end
14
+
15
+ it 'returns false if the user denies the login' do
16
+ VCR.use_cassette("attempt_disallowed") do
17
+ Attempt.new(api, "marten").login!.must_equal false
18
+ end
19
+ end
20
+
21
+ it 'returns false if the user is not known' do
22
+ VCR.use_cassette("attempt_user_unknown") do
23
+ Attempt.new(api, "unknown").login!.must_equal false
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: duo_security
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Marten Veldthuis
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: httparty
16
+ requirement: &70240733958400 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.8.3
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70240733958400
25
+ - !ruby/object:Gem::Dependency
26
+ name: vcr
27
+ requirement: &70240733957900 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 2.2.4
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70240733957900
36
+ - !ruby/object:Gem::Dependency
37
+ name: webmock
38
+ requirement: &70240733957380 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 1.8.9
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70240733957380
47
+ description: Perform 2-factor authentication using duosecurity.com
48
+ email:
49
+ - marten@veldthuis.com
50
+ executables:
51
+ - duo
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - Gemfile
57
+ - LICENSE
58
+ - README.md
59
+ - Rakefile
60
+ - bin/duo
61
+ - data/ca-bundle.crt
62
+ - duo_security.gemspec
63
+ - lib/duo_security.rb
64
+ - lib/duo_security/api.rb
65
+ - lib/duo_security/attempt.rb
66
+ - lib/duo_security/version.rb
67
+ - spec/duo_security/api_spec.rb
68
+ - spec/duo_security/attempt_spec.rb
69
+ homepage: https://github.com/roqua/duo_security
70
+ licenses: []
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 1.8.11
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: ''
93
+ test_files:
94
+ - spec/duo_security/api_spec.rb
95
+ - spec/duo_security/attempt_spec.rb
96
+ has_rdoc: