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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7a6270eb20148c6bebb1da458c2a6430ace408da
4
- data.tar.gz: 2d901a0419f4604cb4a65e85c6a6b7c74a7e266b
3
+ metadata.gz: 0cdbc3c605fb145fe06f5586752132f09420cd82
4
+ data.tar.gz: cc87e2c5709a7070d2102f51ea609cb489c7f00e
5
5
  SHA512:
6
- metadata.gz: d1f7c44451117fed832fc641fcf0116656c43d6ec9639065da58d8173974d48020a89e4c12f7b34dd304325b73e1952fce8b044dead9cd8aabdb17a8330be333
7
- data.tar.gz: 02de0cea84a2f0bc8e3830e91754623f44b7e2d4fb0926b0ba90037b8e8b649397e67ea2b8cf5619eb548edb6f99770168243149f8c634cf827ef04763c7bd30
6
+ metadata.gz: 5939b5a66eb93ac00db2bcfe573edd1c31c4f961f7d11cc510a83a0c6daf7a43d1646474d65b3772ec1237fa0119b0178c99aa4986ee24502b250a03b7fe7577
7
+ data.tar.gz: d1cd8a693a8ae66770a497c34418452e31074ebbe1bb317d529155175c5ca02166885f404571c772bc66e270b863d99db5a33eca7f342b545af134312842e5a2
data/README.md CHANGED
@@ -1,164 +1,140 @@
1
+
1
2
  [![Build Status](https://travis-ci.org/userbin/userbin-ruby.png)](https://travis-ci.org/userbin/userbin-ruby)
2
3
  [![Gem Version](https://badge.fury.io/rb/userbin.png)](http://badge.fury.io/rb/userbin)
3
4
  [![Dependency Status](https://gemnasium.com/userbin/userbin-ruby.png)](https://gemnasium.com/userbin/userbin-ruby)
4
5
 
5
- Userbin for Ruby
6
- ================
7
-
8
- Userbin for Ruby adds user authentication, login flows and user management to your **Rails**, **Sinatra** or **Rack** app.
9
-
10
- [Userbin](https://userbin.com) provides a set of login, signup, and password reset forms that drop right into your application without any need of styling or writing markup. Connect your users via traditional logins or third party social networks. We take care of linking accounts across networks, resetting passwords, and keeping everything safe and secure.
11
-
12
- [Create a free account](https://userbin.com) at Userbin to start accepting users in your application.
6
+ # Ruby SDK for Userbin
13
7
 
14
- Installation
15
- ------------
8
+ > Using Ruby on Rails? Install [Userbin for Devise](https://github.com/userbin/devise_userbin) for super-quick integration.
16
9
 
17
- 1. Add the `userbin` gem to your `Gemfile`
10
+ This library's purpose is to provide an additional security layer to your application by adding multi-factor authentication, user activity monitoring, and real-time threat protection in a white-label package. Your users **do not** need to be signed up or registered for Userbin before using the service.
18
11
 
19
- ```ruby
20
- gem "userbin"
21
- ```
12
+ Your users can now easily activate two-factor authentication, configure the level of security in terms of monitoring and notifications and take action on suspicious behaviour. These settings are available as a per-user security settings page which is easily customized to fit your current layout.
22
13
 
23
- 1. Install the gem
14
+ ## Getting started
24
15
 
25
- ```shell
26
- bundle install
27
- ```
16
+ Add the `userbin` gem to your `Gemfile`
28
17
 
29
- 2. Configure the Userbin module with the credentials you got from signing up.
30
-
31
- In a Rails app, put the following code into a new file at `config/initializers/userbin.rb`, and in Sinatra put it in your main application file and add `require "userbin"`.
32
-
33
- ```ruby
34
- Userbin.configure do |config|
35
- config.app_id = "YOUR_APP_ID"
36
- config.api_secret = "YOUR_API_SECRET"
37
- end
38
- ```
39
-
40
- If you don't configure the `app_id` and `api_secret`, the Userbin module will read the `USERBIN_APP_ID` and `USERBIN_API_SECRET` environment variables. This may come in handy on Heroku.
18
+ ```ruby
19
+ gem "userbin"
20
+ ```
41
21
 
42
- 3. **Rack/Sinatra apps only**: Activate the Userbin Rack middleware
22
+ Install the gem
43
23
 
44
- ```ruby
45
- use Userbin::Authentication
46
- ```
24
+ ```bash
25
+ bundle install
26
+ ```
47
27
 
28
+ Load and configure the library with your Userbin API secret.
48
29
 
49
- Usage
50
- -----
30
+ ```ruby
31
+ require 'userbin'
32
+ Userbin.api_secret = "YOUR_API_SECRET"
33
+ ```
51
34
 
52
- ### Forms
35
+ ## Authenticate
53
36
 
54
- An easy way to integrate Userbin is via the [Widget](https://userbin.com/docs/javascript#widget), which will take care of building forms, validating input and provides a drop-in design that adapts nicely to all devices.
37
+ `authenticate` is the key component of the Userbin API. It lets you tie a user to their actions and record properties about them. Whenever any suspious behaviour is detected or a user gets locked out, a call to `authenticate` may throw an exception which needs to be handled by your application.
55
38
 
56
- The Widget is fairly high level, so remember that you can still use Userbin with your [own forms](https://userbin.com) if it doesn't fit your use-case.
39
+ You’ll want to `authenticate` a user with any relevant information as soon as the current user object is assigned in your application. The method returns a *session token* that you store in a session or cookie for future reference. Either you use the Userbin session token as the ground truth for the user being logged in, or you can store it separately in tandem with your current session.
57
40
 
58
- The following links will open up the Widget with the login or the signup form respectively.
41
+ #### Example
59
42
 
60
- ```html
61
- <a class="ub-login">Log in</a>
62
- ```
43
+ ```ruby
63
44
 
64
- ```html
65
- <a class="ub-signup">Sign up</a>
66
- ```
45
+ options = {
46
+ properties: {
47
+ email: current_user.email,
48
+ name: current_user.full_name
49
+ },
50
+ context: {
51
+ ip: request.ip,
52
+ user_agent: request.user_agent
53
+ }
54
+ }
67
55
 
68
- The logout link will clear the session and redirect the user back to your root path:
56
+ session_token =
57
+ Userbin.authenticate(session[:userbin], current_user.id, options)
69
58
 
70
- ```html
71
- <a class="ub-logout">Log out</a>
59
+ session[:userbin] = session_token
72
60
  ```
73
61
 
74
- ### The current user
62
+ #### Arguments
75
63
 
76
- Userbin keeps track of the currently logged in user which can be accessed through the `current_user` property. This automatically taps into libraries such as the authorization solution [CanCan](https://github.com/ryanb/cancan).
64
+ The first argument is a session token from a previous call to `authenticate`. This variable is obviously nil on the very first call, where a HTTP request will be made and a new session created.
77
65
 
78
- ```erb
79
- Welcome to your account, <%= current_user.email %>
80
- ```
66
+ The second argument is a locally unique identifier for the logged in user, commonly the `id` field. This is the identifier you'll use further on when querying the user.
81
67
 
82
- To check if a user is logged in, use `user_logged_in?` (or its alias `user_signed_in?` if you prefer Devise conventions)
68
+ - `properties` (Hash, optional) - A Hash of properties you know about the user. See the User reference documentation for available fields and their meaning.
69
+ - `context` (Hash, optional) - A Hash specifying the user_agent and ip for the current request.
83
70
 
84
- ```erb
85
- <% if user_logged_in? %>
86
- You are logged in!
87
- <% end %>
88
- ```
71
+ > Note that every call to `authenticate` **does not** result in an HTTP request. Only the very first call, as well as expired session tokens result in a request. Session tokens expire every 5 minutes.
89
72
 
90
- **Rack/Sinatra apps only**: Since above helpers aren't available outside Rails, instead use `Userbin.current_user` and `Userbin.user_logged_in?`.
73
+ ## Two-factor authentication
91
74
 
92
- Configuration
93
- -------------
75
+ Two-factor authentication is available to your users out-of-the-box. By browsing to their Security Page, they're able to configure Google Authenticator and SMS settings, set up a backup phone number, and download their recovery codes.
94
76
 
95
- The `Userbin.configure` block supports a range of options additional to the Userbin credentials. None of the following options are mandatory.
77
+ The session token returned from `authenticate` indicates if two-factor authentication is required from the user once your application asks for it. You can do this immediately after you've called `authenticate`, or you can wait until later. You have complete control over what actions you when you want to require two-factor authentication, e.g. when logging in, changing account information, making a purchase etc.
96
78
 
97
- ### protected_path
79
+ ### Step 1: Prompt the user
98
80
 
99
- By default, Userbin reloads the current page on a successful login. If you set the `protected_path` option, users will be redirected to this path instead.
81
+ `two_factor_authenticate!` acts as a gateway in your application. If the user has enabled two-factor authentication, this method will return the second factor that is used to authenticate. If SMS is used, this call will also send out an SMS to the user's registered phone number.
100
82
 
101
- Once set, this path and any sub-path of it will be protected from unauthenticated users by instead rendering a login form.
83
+ When `two_factor_authenticate!` returns non-falsy value, you should display the appropriate form to the user, requesting their authentication code.
102
84
 
103
85
  ```ruby
104
- config.protected_path = '/dashboard'
86
+ factor = Userbin.two_factor_authenticate!(session[:userbin])
87
+
88
+ case factor
89
+ when :authenticator
90
+ render 'two_factor_authenticator_form'
91
+ when :sms
92
+ render 'two_factor_sms_form'
93
+ end
105
94
  ```
106
95
 
107
- ### root_path
96
+ > Note that this call may return a factor more than once per session since Userbin continously scans for behaviour that would require another round of two-factor authentication, such as the user switching to another IP address or web browser.
108
97
 
109
- By default, Userbin reloads the current page on a successful logout. If you set the `root_path` option, users will be redirected to this path instead.
110
-
111
- ```ruby
112
- config.root_path = '/login'
113
- ```
98
+ ### Step 2: Verify the code
114
99
 
115
- ### create_user and find_user
100
+ The user enters the authentication code in the form and posts it to your handler. The last step is for your application to verify the code with Userbin by calling `verify_code`. The session token will get updated on a successful verification, so you'll need to update it in your local session or cookie.
116
101
 
117
- By default, `current_user` will reference a *limited* Userbin profile, enabling you to work without a database. If you override the functions `create_user` and `find_user`, the current user will instead reference one of your models. The `profile` object is an *extended* Userbin profile. For more information about the available attributes in the profile see the [Userbin profile](https://userbin.com/docs/concepts) documentation.
102
+ `code` can be either a code from the Google Authenticator app, an SMS, or one of the user's recovery codes.
118
103
 
119
104
  ```ruby
120
- config.create_user = Proc.new { |profile|
121
- User.create! do |user|
122
- user.userbin_id = profile.id
123
- user.email = profile.email
124
- user.photo = profile.image
125
- end
126
- }
127
-
128
- config.find_user = Proc.new { |userbin_id|
129
- User.find_by_userbin_id(userbin_id)
130
- }
105
+ begin
106
+ session[:userbin] =
107
+ Userbin.verify_code(session[:userbin], params[:code])
108
+
109
+ redirect_to logged_in_path
110
+ rescue Userbin::UserUnauthorizedError => error
111
+ # invalid code, show the form again
112
+ rescue Userbin::Forbidden => error
113
+ # no tries remaining, log out
114
+ rescue Userbin::Error => error
115
+ # other error, log out
116
+ end
131
117
  ```
132
118
 
133
- You'll need to migrate your users and add a reference to the Userbin profile:
119
+ ## Security page
134
120
 
135
- ```ruby
136
- rails g migration AddUserbinIdToUsers userbin_id:integer:index
137
- ```
121
+ Every user has access to their security settings, which is a hosted page on Userbin. Here users can configure two-factor authentication, revoke suspicious sessions and set up notifications. The security page can be customized to fit your current layout by going to the appearance settings in your Userbin dashboard.
138
122
 
139
- ### skip_script_injection
140
-
141
- By default, the Userbin middleware will automatically insert a `<script>` tag before the closing `</body>` in your HTML files in order to handle forms, sessions and user tracking. This script loads everything asynchronously, so it won't affect your page load speed. However if you want to have control of this procedure, set `skip_script_injection` to true and initialize the library yourself. To do that, checkout the [Userbin.js configuration guide](https://userbin.com/docs/javascript#configuration).
123
+ **Important:** Since the generated URL contains a Userbin session token that needs to be up-to-date, it's crucial that you don't use this helper directly in your HTML, but instead create a new route where you redirect to the security page.
142
124
 
143
125
  ```ruby
144
- config.skip_script_injection = true
126
+ get '/security'
127
+ redirect Userbin.security_page_url
128
+ end
145
129
  ```
146
130
 
131
+ ## De-authenticate
147
132
 
148
- Further configuration and customization
149
- ---------------------------------------
150
-
151
- Your Userbin dashboard gives you access to a range of functionality:
133
+ Whenever a user is logged out from your application, you should inform Userbin about this so that the active session is properly terminated. This prevents the session from being used further on.
152
134
 
153
- - Configure the appearance of the login widget to feel more integrated with your service
154
- - Connect 10+ OAuth providers like Facebook, Github and Google.
155
- - Use Markdown to generate mobile-ready transactional emails
156
- - Invite users to your application
157
- - See who is logging in and when
158
- - User management: block, remove and impersonate users
159
- - Export all your user data from Userbin
160
-
161
-
162
- Documentation
163
- -------------
164
- For complete documentation go to [userbin.com/docs](https://userbin.com/docs)
135
+ ```ruby
136
+ begin
137
+ token = session.delete(:userbin) # remove the local reference
138
+ Userbin.deauthenticate(token)
139
+ rescue Userbin::Error; end
140
+ ```
data/lib/userbin.rb CHANGED
@@ -1,48 +1,27 @@
1
+ require 'pry'
2
+
1
3
  require 'her'
4
+ require 'faraday_middleware'
2
5
  require 'multi_json'
3
6
  require 'openssl'
4
7
  require 'net/http'
8
+ require 'request_store'
9
+ require 'active_support/core_ext/hash/indifferent_access'
5
10
 
6
- require "userbin/userbin"
7
- require "userbin/basic_auth"
8
-
9
- require "userbin/railtie" if defined?(Rails::Railtie)
10
-
11
- api_endpoint = ENV.fetch('USERBIN_API_ENDPOINT') {
12
- "https://api.userbin.com"
13
- }
14
-
15
- @api = Her::API.setup url: api_endpoint do |c|
16
- c.use Userbin::BasicAuth
17
- c.use Faraday::Request::UrlEncoded
18
- c.use Her::Middleware::DefaultParseJSON
19
- c.use Faraday::Adapter::NetHttp
20
- end
21
-
11
+ require "userbin/version"
22
12
  require "userbin/configuration"
23
- require "userbin/events"
24
- require "userbin/current"
25
- require "userbin/session"
26
- require "userbin/authentication"
27
-
28
- class Userbin::Error < Exception; end
29
- class Userbin::SecurityError < Userbin::Error; end
30
- class Userbin::ConfigurationError < Userbin::Error; end
13
+ require "userbin/request"
14
+ require "userbin/jwt"
15
+ require "userbin/utils"
16
+ require "userbin/helpers"
17
+ require "userbin/errors"
31
18
 
32
19
  module Userbin
33
- class << self
34
- def configure(config_hash=nil)
35
- if config_hash
36
- config_hash.each do |k,v|
37
- config.send("#{k}=", v)
38
- end
39
- end
40
-
41
- yield(config) if block_given?
42
- end
43
-
44
- def config
45
- @configuration ||= Userbin::Configuration.new
46
- end
47
- end
20
+ API = Userbin.setup_api
48
21
  end
22
+
23
+ # These need to be required after setting up Her
24
+ require "userbin/models/base"
25
+ require "userbin/models/challenge"
26
+ require "userbin/models/session"
27
+ require "userbin/models/user"
@@ -1,27 +1,25 @@
1
1
  module Userbin
2
- class Configuration
3
- attr_accessor :create_user
4
- attr_accessor :find_user
5
- attr_accessor :protected_path
6
- attr_accessor :root_path
7
- attr_accessor :skip_script_injection
8
-
9
- # restricted_path is obsolete
10
- alias :restricted_path :protected_path
11
- alias :restricted_path= :protected_path=
2
+ class << self
3
+ def configure(config_hash=nil)
4
+ if config_hash
5
+ config_hash.each do |k,v|
6
+ config.send("#{k}=", v)
7
+ end
8
+ end
12
9
 
13
- def initialize
14
- self.skip_script_injection = false
10
+ yield(config) if block_given?
15
11
  end
16
12
 
17
- def app_id
18
- ENV['USERBIN_APP_ID'] || @_app_id
13
+ def config
14
+ @configuration ||= Userbin::Configuration.new
19
15
  end
20
16
 
21
- def app_id=(value)
22
- @_app_id = value
17
+ def api_secret=(api_secret)
18
+ config.api_secret = api_secret
23
19
  end
20
+ end
24
21
 
22
+ class Configuration
25
23
  def api_secret
26
24
  ENV['USERBIN_API_SECRET'] || @_api_secret
27
25
  end
@@ -0,0 +1,5 @@
1
+ class Userbin::Error < Exception; end
2
+ class Userbin::Forbidden < Userbin::Error; end
3
+ class Userbin::UserUnauthorizedError < Userbin::Error; end
4
+ class Userbin::SecurityError < Userbin::Error; end
5
+ class Userbin::ConfigurationError < Userbin::Error; end
@@ -0,0 +1,83 @@
1
+ module Userbin
2
+ class << self
3
+
4
+ def authenticate(session_token, user_id, opts = {})
5
+ session = Userbin::Session.new(token: session_token)
6
+
7
+ user_data = opts.fetch(:properties, {})
8
+
9
+ if session.token
10
+ if session.expired?
11
+ session = Userbin.with_context(opts[:context]) do
12
+ session.refresh(user: user_data)
13
+ end
14
+ end
15
+ else
16
+ session = Userbin.with_context(opts[:context]) do
17
+ Userbin::Session.post(
18
+ "users/#{URI.encode(user_id.to_s)}/sessions", user: user_data)
19
+ end
20
+ end
21
+
22
+ session_token = session.token
23
+
24
+ # By encoding the context to the JWT payload, we avoid having to
25
+ # fetch the context for subsequent Userbin calls during the
26
+ # current request
27
+ jwt = Userbin::JWT.new(session_token)
28
+ jwt.merge!(context: opts[:context])
29
+ jwt.to_token
30
+ end
31
+
32
+ def deauthenticate(session_token)
33
+ return unless session_token
34
+
35
+ # Extract context from authenticated session token
36
+ jwt = Userbin::JWT.new(session_token)
37
+ context = jwt.payload['context']
38
+
39
+ Userbin.with_context(context) do
40
+ Userbin::Session.destroy_existing(session_token)
41
+ end
42
+ end
43
+
44
+ def two_factor_authenticate!(session_token)
45
+ return unless session_token
46
+
47
+ challenge = Userbin::JWT.new(session_token).payload['challenge']
48
+
49
+ if challenge
50
+ case challenge['type']
51
+ when 'otp_authenticator' then :authenticator
52
+ when 'otp_sms' then :sms
53
+ end
54
+ end
55
+ end
56
+
57
+ # TODO: almost the same as deauthenticate. Refactor?
58
+ def verify_code(session_token, response)
59
+ return unless session_token
60
+
61
+ # Extract context from authenticated session token
62
+ jwt = Userbin::JWT.new(session_token)
63
+ context = jwt.payload['context']
64
+
65
+ session = Userbin.with_context(context) do
66
+ Userbin::Session.new(token: session_token).verify(response: response)
67
+ end
68
+
69
+ session.token
70
+ end
71
+
72
+ def security_page_url(session_token)
73
+ return '' unless session_token
74
+ begin
75
+ app_id = Userbin::JWT.new(session_token).app_id
76
+ "https://security.userbin.com/?session_token=#{session_token}"
77
+ rescue Userbin::Error
78
+ ''
79
+ end
80
+ end
81
+
82
+ end
83
+ end