userbin 0.4.5 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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