userbin 0.1.3

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 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