userbin 0.4.5 → 1.0.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +87 -111
  3. data/lib/userbin.rb +18 -39
  4. data/lib/userbin/configuration.rb +14 -16
  5. data/lib/userbin/errors.rb +5 -0
  6. data/lib/userbin/helpers.rb +83 -0
  7. data/lib/userbin/jwt.rb +40 -0
  8. data/lib/userbin/models/base.rb +24 -0
  9. data/lib/userbin/models/challenge.rb +4 -0
  10. data/lib/userbin/models/session.rb +11 -0
  11. data/lib/userbin/models/user.rb +9 -0
  12. data/lib/userbin/request.rb +99 -0
  13. data/lib/userbin/utils.rb +28 -0
  14. data/lib/userbin/version.rb +1 -1
  15. data/spec/configuration_spec.rb +8 -0
  16. data/spec/fixtures/vcr_cassettes/session_create.yml +47 -0
  17. data/spec/fixtures/vcr_cassettes/session_refresh.yml +47 -0
  18. data/spec/fixtures/vcr_cassettes/session_verify.yml +47 -0
  19. data/spec/fixtures/vcr_cassettes/user_find.yml +44 -0
  20. data/spec/fixtures/vcr_cassettes/user_find_non_existing.yml +42 -0
  21. data/spec/fixtures/vcr_cassettes/user_import.yml +46 -0
  22. data/spec/fixtures/vcr_cassettes/user_update.yml +47 -0
  23. data/spec/helpers_spec.rb +38 -0
  24. data/spec/jwt_spec.rb +67 -0
  25. data/spec/models/session_spec.rb +33 -0
  26. data/spec/models/user_spec.rb +43 -0
  27. data/spec/spec_helper.rb +11 -0
  28. data/spec/utils_spec.rb +36 -0
  29. metadata +128 -36
  30. data/lib/userbin/authentication.rb +0 -132
  31. data/lib/userbin/basic_auth.rb +0 -31
  32. data/lib/userbin/current.rb +0 -17
  33. data/lib/userbin/events.rb +0 -40
  34. data/lib/userbin/rails/auth_helpers.rb +0 -22
  35. data/lib/userbin/railtie.rb +0 -14
  36. data/lib/userbin/session.rb +0 -26
  37. data/lib/userbin/userbin.rb +0 -104
  38. data/spec/session_spec.rb +0 -40
  39. data/spec/userbin_spec.rb +0 -102
@@ -1,132 +0,0 @@
1
- module Userbin
2
- class Authentication
3
-
4
- CLOSING_BODY_TAG = %r{</body>}
5
-
6
- def initialize(app, options = {})
7
- @app = app
8
- end
9
-
10
- def call(env)
11
- if !Userbin.config.app_id || !Userbin.config.api_secret
12
- raise ConfigurationError, "app_id and api_secret must be present"
13
- end
14
-
15
- Thread.current[:userbin] = nil
16
- env['userbin.unauthenticated'] = false
17
-
18
- request = Rack::Request.new(env)
19
-
20
- begin
21
- jwt = Userbin.authenticate!(request)
22
-
23
- if !Userbin.authenticated? && Userbin.config.protected_path &&
24
- env["PATH_INFO"].start_with?(Userbin.config.protected_path)
25
-
26
- return render_gateway(env["PATH_INFO"])
27
- end
28
-
29
- generate_response(env, jwt)
30
- rescue Userbin::SecurityError
31
- message =
32
- 'Userbin::SecurityError: Invalid session. Refresh to try again.'
33
- headers = {
34
- 'Content-Type' => 'text/text'
35
- }
36
-
37
- Rack::Utils.delete_cookie_header!(
38
- headers, '_ubt', value = {})
39
-
40
- [ 400, headers, [message] ]
41
- end
42
- end
43
-
44
- def script_tag(login_path)
45
- script_url = ENV.fetch('USERBIN_SCRIPT_URL') {
46
- "//js.userbin.com"
47
- }
48
- path = login_path || Userbin.config.protected_path
49
-
50
- tag = "<script src='#{script_url}?#{Userbin.config.app_id}'></script>\n"
51
- tag += "<script type='text/javascript'>\n"
52
- tag += " Userbin.config({\n"
53
- if Userbin.config.root_path
54
- tag += " logoutRedirectUrl: '#{Userbin.config.root_path}',\n"
55
- end
56
- tag += " loginRedirectUrl: '#{path}',\n" if path
57
- tag += " reloadOnSuccess: true\n"
58
- tag += " });\n"
59
- tag += "</script>\n"
60
- end
61
-
62
- def inject_tags(body, login_path = nil)
63
- if body[CLOSING_BODY_TAG]
64
- body = body.gsub(CLOSING_BODY_TAG, script_tag(login_path) + '\\0')
65
- end
66
- body
67
- end
68
-
69
- def render_gateway(current_path)
70
- script_url = ENV["USERBIN_SCRIPT_URL"] || '//js.userbin.com'
71
-
72
- login_page = <<-LOGIN_PAGE
73
- <!DOCTYPE html>
74
- <html>
75
- <head>
76
- <meta charset="utf-8">
77
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
78
- <title>Log in</title>
79
- <meta name="viewport" content="width=device-width, initial-scale=1">
80
- </head>
81
- <body>
82
- <a class="ub-login-form"></a>
83
- </body>
84
- </html>
85
- LOGIN_PAGE
86
-
87
- login_page = inject_tags(login_page, current_path)
88
-
89
- headers = { 'Content-Type' => 'text/html' }
90
-
91
- [403, headers, [login_page]]
92
- end
93
-
94
- def generate_response(env, jwt)
95
- status, headers, response = @app.call(env)
96
-
97
- # application stack is responsible for setting userbin.authenticated
98
- return render_gateway(env["PATH_INFO"]) if env['userbin.unauthenticated']
99
-
100
- if headers['Content-Type'] && headers['Content-Type']['text/html']
101
- if response.respond_to?(:body)
102
- body = [*response.body]
103
- else
104
- body = response
105
- end
106
- if !Userbin.config.skip_script_injection
107
- body = body.each.map do |chunk|
108
- inject_tags(chunk)
109
- end
110
- end
111
- if response.respond_to?(:body)
112
- response.body = body
113
- else
114
- response = body
115
- end
116
- unless body.empty?
117
- headers['Content-Length'] = Rack::Utils.bytesize(body.flatten[0]).to_s
118
- end
119
- end
120
-
121
- if jwt
122
- Rack::Utils.set_cookie_header!(
123
- headers, '_ubt', value: jwt, path: '/')
124
- else
125
- Rack::Utils.delete_cookie_header!(
126
- headers, '_ubt', value = {})
127
- end
128
-
129
- [status, headers, response]
130
- end
131
- end
132
- end
@@ -1,31 +0,0 @@
1
- module Userbin
2
- class BasicAuth < Faraday::Middleware
3
- def call(env)
4
- value = Base64.encode64([Userbin.config.app_id, Userbin.config.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
- Userbin.decode_jwt(env[:body])
15
- end
16
- end
17
- end
18
-
19
- class ParseSignedJSON < Faraday::Response::Middleware
20
- def on_complete(env)
21
- json = MultiJson.load(env[:body], symbolize_keys: true)
22
- signature = env[:response_headers]['x-userbin-signature']
23
- json[:signature] = signature if signature
24
- env[:body] = {
25
- data: json,
26
- errors: [],
27
- metadata: {}
28
- }
29
- end
30
- end
31
- end
@@ -1,17 +0,0 @@
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
@@ -1,40 +0,0 @@
1
- module Userbin
2
-
3
- Callback = Struct.new(:pattern, :block) do; end
4
-
5
- class Events
6
- def self.on(*names, &block)
7
- pattern = Regexp.union(names.empty? ? TYPE_LIST.to_a : names)
8
- callbacks.each do |callback|
9
- if pattern == callback.pattern
10
- callbacks.delete(callback)
11
- callbacks << Userbin::Callback.new(pattern, block)
12
- return
13
- end
14
- end
15
- callbacks << Userbin::Callback.new(pattern, block)
16
- end
17
-
18
- def self.trigger(raw_event)
19
- event = Userbin::Event.new(raw_event)
20
- callbacks.each do |callback|
21
- if event.type =~ callback.pattern
22
- object = case event['type']
23
- when /^user\./
24
- Userbin::User.new(event.object)
25
- else
26
- event.object
27
- end
28
- model = event.instance_exec object, &callback.block
29
- end
30
- end
31
- end
32
-
33
- private
34
-
35
- def self.callbacks
36
- @callbacks ||= []
37
- end
38
- end
39
-
40
- end
@@ -1,22 +0,0 @@
1
- module Userbin
2
- module AuthHelpers
3
- def authenticate_user!
4
- unless user_logged_in?
5
- env['userbin.unauthenticated'] = true
6
- render nothing: true
7
- end
8
- end
9
-
10
- def current_user
11
- Userbin.current_user
12
- end
13
-
14
- def user_logged_in?
15
- Userbin.user_logged_in?
16
- end
17
-
18
- def user_signed_in?
19
- Userbin.user_signed_in?
20
- end
21
- end
22
- end
@@ -1,14 +0,0 @@
1
- require 'userbin/rails/auth_helpers'
2
-
3
- module Userbin
4
- ActiveSupport.on_load(:action_controller) do
5
- include AuthHelpers
6
- end
7
-
8
- class Railtie < Rails::Railtie
9
- initializer "userbin" do |app|
10
- ActionView::Base.send :include, AuthHelpers
11
- app.config.middleware.use "Userbin::Authentication"
12
- end
13
- end
14
- end
@@ -1,26 +0,0 @@
1
- require 'her'
2
-
3
- module Userbin
4
- class Model
5
- include Her::Model
6
- end
7
-
8
- class User < Model; end
9
-
10
- class Session < Model
11
- has_one :user
12
-
13
- # Hack to avoid loading a remote user
14
- def user
15
- return self['user'] if self['user'].is_a?(User)
16
- User.new(self['user']) if self['user']
17
- end
18
-
19
- def authenticated?
20
- !user.id.nil? rescue false
21
- end
22
- end
23
-
24
- class Event < Model
25
- end
26
- end
@@ -1,104 +0,0 @@
1
- require 'jwt'
2
-
3
- module Userbin
4
- def self.decode_jwt(jwt)
5
- JWT.decode(jwt, Userbin.config.api_secret)
6
- end
7
-
8
- def self.authenticate!(request)
9
- jwt = request.cookies['_ubt']
10
- return unless jwt
11
-
12
- decoded = Userbin.decode_jwt(jwt)
13
-
14
- if Time.now > Time.at(decoded['expires_at'] / 1000)
15
- jwt = refresh_session(decoded['id'])
16
- return unless jwt
17
-
18
- decoded = Userbin.decode_jwt(jwt)
19
-
20
- if Time.now > Time.at(decoded['expires_at'] / 1000)
21
- raise Userbin::SecurityError
22
- end
23
- end
24
-
25
- self.current = Userbin::Session.new(decoded)
26
-
27
- return jwt
28
- end
29
-
30
- def self.refresh_session(session_id)
31
- api_endpoint = ENV["USERBIN_API_ENDPOINT"] || 'https://api.userbin.com'
32
- uri = URI("#{api_endpoint}/sessions/#{session_id}/refresh.jwt")
33
- uri.user = config.app_id
34
- uri.password = config.api_secret
35
- net = Net::HTTP.post_form(uri, {})
36
- net.body
37
- end
38
-
39
- def self.current
40
- Thread.current[:userbin]
41
- end
42
-
43
- def self.current=(value)
44
- Thread.current[:userbin] = value
45
- end
46
-
47
- def self.authenticated?
48
- current.authenticated? rescue false
49
- end
50
-
51
- def self.logged_in?
52
- authenticated?
53
- end
54
-
55
- def self.user_logged_in?
56
- authenticated?
57
- end
58
-
59
- def self.user_signed_in?
60
- authenticated?
61
- end
62
-
63
- def self._current_user
64
- current.user if current
65
- end
66
-
67
- def self.current_profile
68
- _current_user
69
- end
70
-
71
- def self.current_user
72
- if _current_user
73
- if Userbin.config.find_user
74
- u = Userbin.config.find_user.call(_current_user.id)
75
- if u
76
- u
77
- else
78
- if Userbin.config.create_user
79
-
80
- # Fetch a full profile from the API. This way we can get more
81
- # sensitive details than those stored in the cookie. It also checks
82
- # that the user still exists in Userbin.
83
- profile = User.find(_current_user.id)
84
-
85
- u = Userbin.config.create_user.call(profile)
86
- if u
87
- u
88
- else
89
- _current_user
90
- end
91
- else
92
- raise ConfigurationError, "You need to implement create_user"
93
- end
94
- end
95
- else
96
- _current_user
97
- end
98
- end
99
- end
100
-
101
- def self.user
102
- current_user
103
- end
104
- end
data/spec/session_spec.rb DELETED
@@ -1,40 +0,0 @@
1
- require 'spec_helper'
2
- require 'multi_json'
3
-
4
- describe 'Userbin::Session' do
5
- before do
6
- Userbin.configure do |config|
7
- config.app_id = '100000000000000'
8
- config.api_secret = 'test'
9
- end
10
- end
11
-
12
- before do
13
- data = {
14
- id: "Prars5v7xz2xwWvF5LEqfEUHCoNNsV7V",
15
- created_at: 1378978281000,
16
- expires_at: 1378981881000,
17
- user: {
18
- confirmed_at: nil,
19
- created_at: 1378978280000,
20
- email: "johan@userbin.com",
21
- id: "TF15JEy7HRxDYx6U435zzEwydKJcptUr",
22
- last_sign_in_at: nil,
23
- local_id: nil
24
- }
25
- }
26
- stub_request(:post, /\/users\/.*\/sessions/).to_return(
27
- status: 200,
28
- headers: {'X-Userbin-Signature' => 'abcd'},
29
- body: MultiJson.encode(data)
30
- )
31
- end
32
-
33
- xit 'creates a session' do
34
- allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
35
- user = Userbin::User.new(id: 'guid1')
36
- session = user.sessions.create
37
- session.user.email.should == 'johan@userbin.com'
38
- session.signature.should == 'abcd'
39
- end
40
- end
data/spec/userbin_spec.rb DELETED
@@ -1,102 +0,0 @@
1
- require 'spec_helper'
2
- require 'cgi'
3
-
4
- describe Userbin do
5
- before do
6
- Userbin.configure do |config|
7
- config.app_id = '1000'
8
- config.api_secret = '1234'
9
- end
10
- end
11
-
12
- let (:args) do
13
- {
14
- "HTTP_COOKIE" => "_ubs=abcd; _ubd=#{CGI.escape(MultiJson.encode(session))} "
15
- }
16
- end
17
-
18
- let (:session) do
19
- {
20
- "id" => 'xyz',
21
- "expires_at" => 1478981881000,
22
- "user" => {
23
- "id" => 'abc'
24
- }
25
- }
26
- end
27
-
28
- let (:request) do
29
- Rack::Request.new({
30
- 'HTTP_X_USERBIN_SIGNATURE' => 'abcd',
31
- 'CONTENT_TYPE' => 'application/json',
32
- 'rack.input' => StringIO.new()
33
- }.merge(args))
34
- end
35
-
36
- let (:response) do
37
- Rack::Response.new("", 200, {})
38
- end
39
-
40
- context 'when session is created' do
41
-
42
- it 'authenticates with class methods' do
43
- allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
44
- Userbin.authenticate!(request)
45
- Userbin.should be_authenticated
46
- Userbin.current_user.id.should == "abc"
47
- end
48
-
49
- it 'renews' do
50
- 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'})
51
- allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
52
- Userbin.authenticate!(request, Time.at(1478981882)) # expired 1s
53
- Userbin.current.id.should == 'Prars5v7xz2xwWvF5LEqfEUHCoNNsV7V'
54
- end
55
-
56
- it 'does not renew' do
57
- stub_request(:post, /.*userbin\.com.*/).to_return(:status => 404)
58
- allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
59
- Userbin.authenticate!(request, Time.at(1478981882)) # expired 1s
60
- end
61
-
62
- xit 'authenticate with correct signature' do
63
- expect {
64
- Userbin.authenticate!(request)
65
- }.to raise_error { Userbin::SecurityError }
66
- end
67
-
68
- it 'does not authenticate incorrect signature' do
69
- expect {
70
- Userbin.authenticate!(request)
71
- }.to raise_error { Userbin::SecurityError }
72
- end
73
- end
74
-
75
- context 'when session is deleted' do
76
- let (:args) do
77
- {
78
- "HTTP_COOKIE" => "userbin_signature=abcd;"
79
- }
80
- end
81
-
82
- it 'does not authenticate' do
83
- allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
84
- Userbin.authenticate!(request)
85
- Userbin.should_not be_authenticated
86
- Userbin.user.should be_nil
87
- end
88
- end
89
-
90
- context 'when params are present' do
91
- let (:args) do
92
- {
93
- "QUERY_STRING" => "userbin_signature=abcd&userbin_data=#{MultiJson.encode(session)}"
94
- }
95
- end
96
-
97
- xit 'authenticates with class methods' do
98
- allow(OpenSSL::HMAC).to receive(:hexdigest) { 'abcd' }
99
- Userbin.authenticate_events!(request)
100
- end
101
- end
102
- end