userbin 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4283d8611ee035f2141284b2c339d5244f2830fa
4
+ data.tar.gz: 0200a7e005412dc60e56fee0693fcea5a1f826bd
5
+ SHA512:
6
+ metadata.gz: 2980f02fb4669639ac88c121141959499e6e783dc23ca58f4dc41156838a3639f626e55537ec0a04b4d8f4016857400cee9e8cc69f7a10bb5184fa938a55a4c4
7
+ data.tar.gz: 4735bd77af93832204838b1776c0779c77056af02f31e19778e7b4bc92933f07a0fa1214dd2f592f86c49084d79d2e6fe767b56e22ae7b3959a684916148f6d4
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ Userbin Ruby gem
2
+ ================
3
+
4
+ Hold tight for more info.
@@ -0,0 +1,140 @@
1
+ module Userbin
2
+ class Authentication
3
+
4
+ CLOSING_HEAD_TAG = %r{</head>}
5
+ CLOSING_BODY_TAG = %r{</body>}
6
+
7
+ def initialize(app, options = {})
8
+ @app = app
9
+ @restricted_path = options[:restricted_path]
10
+ end
11
+
12
+ def call(env)
13
+ request = Rack::Request.new(env)
14
+
15
+ begin
16
+ if env["REQUEST_PATH"] == "/userbin" &&
17
+ env["REQUEST_METHOD"] == "POST"
18
+ signature, data = Userbin.authenticate_events!(request)
19
+
20
+ MultiJson.decode(data)['events'].each do |event|
21
+ Userbin.trigger(event)
22
+ end
23
+
24
+ [ 200, { 'Content-Type' => 'text/html',
25
+ 'Content-Length' => '2' }, ['OK'] ]
26
+ else
27
+ signature, data = Userbin.authenticate!(request)
28
+
29
+ if restrict && env["REQUEST_PATH"].start_with?(restrict) &&
30
+ !Userbin.authenticated?
31
+ return render_gateway(env["REQUEST_PATH"])
32
+ end
33
+
34
+ generate_response(env, signature, data)
35
+ end
36
+ rescue Userbin::SecurityError
37
+ message =
38
+ 'Userbin::SecurityError: Invalid signature. Refresh to try again.'
39
+ headers = {
40
+ 'Content-Type' => 'text/text',
41
+ 'Content-Length' => message.length.to_s
42
+ }
43
+
44
+ Rack::Utils.delete_cookie_header!(
45
+ headers, 'userbin_signature', value = {})
46
+ Rack::Utils.delete_cookie_header!(
47
+ headers, 'userbin_data', value = {})
48
+
49
+ [ 400, headers, [message] ]
50
+ end
51
+ end
52
+
53
+ def restrict
54
+ Userbin.restricted_path || @restricted_path
55
+ end
56
+
57
+ def link_tags(login_path)
58
+ <<-LINK_TAGS
59
+ <link rel="userbin:root" href="/">
60
+ <link rel="userbin:login" href="#{login_path}">
61
+ LINK_TAGS
62
+ end
63
+
64
+ def meta_tag
65
+ <<-META_TAG
66
+ <meta property="userbin:events" content="/userbin">
67
+ META_TAG
68
+ end
69
+
70
+ def script_tag
71
+ script_url = ENV.fetch('USERBIN_SCRIPT_URL') {
72
+ "https://userbin.com/js/v0"
73
+ }
74
+ str = <<-SCRIPT_TAG
75
+ <script src="#{script_url}?#{Userbin.app_id}"></script>
76
+ SCRIPT_TAG
77
+ end
78
+
79
+ def render_gateway(current_path)
80
+ login_page = <<-LOGIN_PAGE
81
+ <html>
82
+ <head>
83
+ <title>Log in</title>
84
+ </head>
85
+ <body style="background-color: #DBDBDB;">
86
+ <a class="ub-login-form"></a>
87
+ </body>
88
+ </html>
89
+ LOGIN_PAGE
90
+ login_page = inject_tags(login_page, current_path)
91
+ [ 200,
92
+ { 'Content-Type' => 'text/html',
93
+ 'Content-Length' => login_page.length.to_s },
94
+ [login_page]
95
+ ]
96
+ end
97
+
98
+ def inject_tags(body, login_path = restrict)
99
+ if body[CLOSING_HEAD_TAG]
100
+ body = body.gsub(CLOSING_HEAD_TAG, link_tags(login_path) + '\\0')
101
+ body = body.gsub(CLOSING_HEAD_TAG, meta_tag + '\\0')
102
+ end
103
+ if body[CLOSING_BODY_TAG]
104
+ body = body.gsub(CLOSING_BODY_TAG, script_tag + '\\0')
105
+ end
106
+ body
107
+ end
108
+
109
+ def generate_response(env, signature, data)
110
+ status, headers, response = @app.call(env)
111
+ if headers['Content-Type'] && headers['Content-Type']['text/html']
112
+ body = response.each.map do |chunk|
113
+ inject_tags(chunk)
114
+ end
115
+
116
+ if response.class.name.end_with?('::Response')
117
+ response.body = body
118
+ else
119
+ response = body
120
+ end
121
+
122
+ headers['Content-Length'] = body.join.length.to_s
123
+ end
124
+
125
+ if signature && data
126
+ Rack::Utils.set_cookie_header!(
127
+ headers, 'userbin_signature', value: signature, path: '/')
128
+ Rack::Utils.set_cookie_header!(
129
+ headers, 'userbin_data', value: data, path: '/')
130
+ else
131
+ Rack::Utils.delete_cookie_header!(
132
+ headers, 'userbin_signature', value = {})
133
+ Rack::Utils.delete_cookie_header!(
134
+ headers, 'userbin_data', value = {})
135
+ end
136
+
137
+ [status, headers, response]
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,33 @@
1
+ module Userbin
2
+ class BasicAuth < Faraday::Middleware
3
+ def call(env)
4
+ value = Base64.encode64([Userbin.app_id, Userbin.api_secret].join(':'))
5
+ value.gsub!("\n", '')
6
+ env[:request_headers]["Authorization"] = "Basic #{value}"
7
+ @app.call(env)
8
+ end
9
+ end
10
+
11
+ class VerifySignature < Faraday::Response::Middleware
12
+ def call(env)
13
+ @app.call(env).on_complete do
14
+ signature = env[:response_headers]['x-userbin-signature']
15
+ data = env[:body]
16
+ Userbin.valid_signature?(signature, data)
17
+ end
18
+ end
19
+ end
20
+
21
+ class ParseSignedJSON < Faraday::Response::Middleware
22
+ def on_complete(env)
23
+ json = MultiJson.load(env[:body], symbolize_keys: true)
24
+ signature = env[:response_headers]['x-userbin-signature']
25
+ json[:signature] = signature if signature
26
+ env[:body] = {
27
+ data: json,
28
+ errors: [],
29
+ metadata: {}
30
+ }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ module Userbin
2
+ class Current
3
+ attr_accessor :token, :expires_at, :user
4
+
5
+ def initialize(data)
6
+ if data
7
+ @token = data['id']
8
+ @expires_at = data['expires_at']
9
+ @user = Userbin::User.new(data['user'])
10
+ end
11
+ end
12
+
13
+ def authenticated?
14
+ !@user.nil?
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ require 'her'
2
+
3
+ module Userbin
4
+ class Model
5
+ include Her::Model
6
+ end
7
+
8
+ class User < Model
9
+ def update_local_id(local_id)
10
+ self.local_id = local_id
11
+ save
12
+ end
13
+ end
14
+
15
+ class Session < Model
16
+ has_one :user
17
+
18
+ # Hack to avoid loading a remote user
19
+ def user
20
+ return self['user'] if self['user'].is_a?(User)
21
+ User.new(self['user']) if self['user']
22
+ end
23
+
24
+ def authenticated?
25
+ !user.id.nil? rescue false
26
+ end
27
+ end
28
+
29
+ class Event < Model
30
+ end
31
+ end
@@ -0,0 +1,123 @@
1
+ module Userbin
2
+
3
+ Callback = Struct.new(:pattern, :block) do; end
4
+
5
+ class << self
6
+ attr_accessor :app_id, :api_secret, :restricted_path
7
+ end
8
+
9
+ def self.authenticate_events!(request, now = Time.now)
10
+ signature, data =
11
+ request.params.values_at('userbin_signature', 'userbin_data')
12
+
13
+ valid_signature?(signature, data)
14
+
15
+ [signature, data]
16
+ end
17
+
18
+ # Provide either a Rack::Request or a Hash containing :signature and :data.
19
+ #
20
+ def self.authenticate!(request, now = Time.now)
21
+ signature, data =
22
+ request.cookies.values_at('userbin_signature', 'userbin_data')
23
+
24
+ if signature && data && valid_signature?(signature, data)
25
+
26
+ current = Userbin::Session.new(MultiJson.decode(data))
27
+
28
+ if current.authenticated?
29
+ # FIXME: NoMethodError (undefined method `/' for nil:NilClass):
30
+ if now > Time.at(current.expires_at / 1000)
31
+ signature, data = refresh_session(current.user.id)
32
+ end
33
+ end
34
+ end
35
+
36
+ tmp = MultiJson.decode(data) if data
37
+
38
+ self.current = Userbin::Session.new(tmp)
39
+
40
+ [signature, data]
41
+ end
42
+
43
+ def self.refresh_session(user_id)
44
+ api_endpoint = ENV["USERBIN_API_ENDPOINT"] || 'https://userbin.com/api/v0'
45
+ uri = URI("#{api_endpoint}/users/#{user_id}/sessions")
46
+ uri.user = app_id
47
+ uri.password = api_secret
48
+ net = Net::HTTP.post_form(uri, {})
49
+ [net['X-Userbin-Signature'], net.body]
50
+ end
51
+
52
+ def self.current
53
+ Thread.current[:userbin]
54
+ end
55
+
56
+ def self.current=(value)
57
+ Thread.current[:userbin] = value
58
+ end
59
+
60
+ def self.authenticated?
61
+ current.authenticated? rescue false
62
+ end
63
+
64
+ def self.current_user
65
+ current.user if current
66
+ end
67
+
68
+ def self.user
69
+ current_user
70
+ end
71
+
72
+ # Event handling
73
+ #
74
+ class << self
75
+ def on(*names, &block)
76
+ pattern = Regexp.union(names.empty? ? TYPE_LIST.to_a : names)
77
+ callbacks.each do |callback|
78
+ if pattern == callback.pattern
79
+ callbacks.delete(callback)
80
+ callbacks << Userbin::Callback.new(pattern, block)
81
+ return
82
+ end
83
+ end
84
+ callbacks << Userbin::Callback.new(pattern, block)
85
+ end
86
+
87
+ def trigger(raw_event)
88
+ event = Userbin::Event.new(raw_event)
89
+ callbacks.each do |callback|
90
+ if event.type =~ callback.pattern
91
+ object = case event['type']
92
+ when /^user\./
93
+ Userbin::User.new(event.object)
94
+ else
95
+ event.object
96
+ end
97
+ model = event.instance_exec object, &callback.block
98
+
99
+ if event.type =~ /user\.created/ && model.respond_to?(:id)
100
+ object.update_local_id(model.id)
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def callbacks
109
+ @callbacks ||= []
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ # Checks signature against secret and returns boolean
116
+ #
117
+ def self.valid_signature?(signature, data)
118
+ digest = OpenSSL::Digest::SHA256.new
119
+ valid = signature == OpenSSL::HMAC.hexdigest(digest, api_secret, data)
120
+ raise SecurityError, "Invalid signature" unless valid
121
+ valid
122
+ end
123
+ end
data/lib/userbin.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'her'
2
+ require 'multi_json'
3
+ require 'openssl'
4
+ require 'net/http'
5
+
6
+ require "userbin/userbin"
7
+ require "userbin/basic_auth"
8
+
9
+ api_endpoint = ENV.fetch('USERBIN_API_ENDPOINT') {
10
+ "https://userbin.com/api/v0"
11
+ }
12
+
13
+ @api = Her::API.setup url: api_endpoint do |c|
14
+ c.use Userbin::BasicAuth
15
+ c.use Faraday::Request::UrlEncoded
16
+ c.use Userbin::ParseSignedJSON
17
+ c.use Faraday::Adapter::NetHttp
18
+ c.use Userbin::VerifySignature
19
+ end
20
+
21
+ require "userbin/current"
22
+ require "userbin/session"
23
+ require "userbin/authentication"
24
+
25
+ class Userbin::Error < Exception; end
26
+ class Userbin::SecurityError < Userbin::Error; end
27
+
28
+
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'multi_json'
3
+
4
+ describe 'Userbin::Session' do
5
+ before do
6
+ Userbin.app_id = '100000000000000'
7
+ Userbin.api_secret = 'test'
8
+ end
9
+
10
+ before do
11
+ data = {
12
+ id: "Prars5v7xz2xwWvF5LEqfEUHCoNNsV7V",
13
+ created_at: 1378978281000,
14
+ expires_at: 1378981881000,
15
+ user: {
16
+ confirmed_at: nil,
17
+ created_at: 1378978280000,
18
+ email: "johan@userbin.com",
19
+ id: "TF15JEy7HRxDYx6U435zzEwydKJcptUr",
20
+ last_sign_in_at: nil,
21
+ local_id: nil
22
+ }
23
+ }
24
+ stub_request(:post, /\/users\/.*\/sessions/).to_return(
25
+ status: 200,
26
+ headers: {'X-Userbin-Signature' => 'abcd'},
27
+ body: MultiJson.encode(data)
28
+ )
29
+ end
30
+
31
+ xit 'creates a session' do
32
+ allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
33
+ user = Userbin::User.new(id: 'guid1')
34
+ session = user.sessions.create
35
+ session.user.email.should == 'johan@userbin.com'
36
+ session.signature.should == 'abcd'
37
+ end
38
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rack'
4
+ require 'userbin'
5
+ require 'webmock/rspec'
6
+
7
+ RSpec.configure do |config|
8
+ end
@@ -0,0 +1,100 @@
1
+ require 'spec_helper'
2
+ require 'cgi'
3
+
4
+ describe Userbin do
5
+ before do
6
+ Userbin.app_id = '1000'
7
+ Userbin.api_secret = '1234'
8
+ end
9
+
10
+ let (:args) do
11
+ {
12
+ "HTTP_COOKIE" => "userbin_signature=abcd; userbin_data=#{CGI.escape(MultiJson.encode(session))} "
13
+ }
14
+ end
15
+
16
+ let (:session) do
17
+ {
18
+ "id" => 'xyz',
19
+ "expires_at" => 1478981881000,
20
+ "user" => {
21
+ "id" => 'abc'
22
+ }
23
+ }
24
+ end
25
+
26
+ let (:request) do
27
+ Rack::Request.new({
28
+ 'HTTP_X_USERBIN_SIGNATURE' => 'abcd',
29
+ 'CONTENT_TYPE' => 'application/json',
30
+ 'rack.input' => StringIO.new()
31
+ }.merge(args))
32
+ end
33
+
34
+ let (:response) do
35
+ Rack::Response.new("", 200, {})
36
+ end
37
+
38
+ context 'when session is created' do
39
+
40
+ it 'authenticates with class methods' do
41
+ allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
42
+ Userbin.authenticate!(request)
43
+ Userbin.should be_authenticated
44
+ Userbin.current_user.id.should == "abc"
45
+ end
46
+
47
+ it 'renews' do
48
+ stub_request(:post, /.*userbin\.com.*/).to_return(:status => 200, :body => "{\"id\":\"Prars5v7xz2xwWvF5LEqfEUHCoNNsV7V\",\"created_at\":1378978281000,\"expires_at\":1378981881000,\"user\":{\"confirmed_at\":null,\"created_at\":1378978280000,\"email\":\"admin@getapp6133.com\",\"id\":\"TF15JEy7HRxDYx6U435zzEwydKJcptUr\",\"last_sign_in_at\":null,\"local_id\":null}}", :headers => {'X-Userbin-Signature' => 'abcd'})
49
+ allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
50
+ Userbin.authenticate!(request, Time.at(1478981882)) # expired 1s
51
+ Userbin.current.id.should == 'Prars5v7xz2xwWvF5LEqfEUHCoNNsV7V'
52
+ end
53
+
54
+ it 'does not renew' do
55
+ stub_request(:post, /.*userbin\.com.*/).to_return(:status => 404)
56
+ allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
57
+ Userbin.authenticate!(request, Time.at(1478981882)) # expired 1s
58
+ end
59
+
60
+ xit 'authenticate with correct signature' do
61
+ expect {
62
+ Userbin.authenticate!(request)
63
+ }.to raise_error { Userbin::SecurityError }
64
+ end
65
+
66
+ it 'does not authenticate incorrect signature' do
67
+ expect {
68
+ Userbin.authenticate!(request)
69
+ }.to raise_error { Userbin::SecurityError }
70
+ end
71
+ end
72
+
73
+ context 'when session is deleted' do
74
+ let (:args) do
75
+ {
76
+ "HTTP_COOKIE" => "userbin_signature=abcd;"
77
+ }
78
+ end
79
+
80
+ it 'does not authenticate' do
81
+ allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
82
+ Userbin.authenticate!(request)
83
+ Userbin.should_not be_authenticated
84
+ Userbin.user.should be_nil
85
+ end
86
+ end
87
+
88
+ context 'when params are present' do
89
+ let (:args) do
90
+ {
91
+ "QUERY_STRING" => "userbin_signature=abcd&userbin_data=#{MultiJson.encode(session)}"
92
+ }
93
+ end
94
+
95
+ it 'authenticates with class methods' do
96
+ allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
97
+ Userbin.authenticate_events!(request)
98
+ end
99
+ end
100
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: userbin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Johan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-09-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: her
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: 0.6.8
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 0.6.8
27
+ - !ruby/object:Gem::Dependency
28
+ name: multi_json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rack
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Plug n’ play user accounts. The simplest way to integrate a full authentication
84
+ and user management stack into your web application.
85
+ email: johan@userbin.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - lib/userbin/authentication.rb
91
+ - lib/userbin/basic_auth.rb
92
+ - lib/userbin/current.rb
93
+ - lib/userbin/session.rb
94
+ - lib/userbin/userbin.rb
95
+ - lib/userbin.rb
96
+ - README.md
97
+ - spec/session_spec.rb
98
+ - spec/spec_helper.rb
99
+ - spec/userbin_spec.rb
100
+ homepage: https://userbin.com
101
+ licenses:
102
+ - MIT
103
+ metadata: {}
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubyforge_project:
120
+ rubygems_version: 2.0.3
121
+ signing_key:
122
+ specification_version: 4
123
+ summary: Userbin
124
+ test_files:
125
+ - spec/session_spec.rb
126
+ - spec/spec_helper.rb
127
+ - spec/userbin_spec.rb