userbin 0.4.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +87 -111
- data/lib/userbin.rb +18 -39
- data/lib/userbin/configuration.rb +14 -16
- data/lib/userbin/errors.rb +5 -0
- data/lib/userbin/helpers.rb +83 -0
- data/lib/userbin/jwt.rb +40 -0
- data/lib/userbin/models/base.rb +24 -0
- data/lib/userbin/models/challenge.rb +4 -0
- data/lib/userbin/models/session.rb +11 -0
- data/lib/userbin/models/user.rb +9 -0
- data/lib/userbin/request.rb +99 -0
- data/lib/userbin/utils.rb +28 -0
- data/lib/userbin/version.rb +1 -1
- data/spec/configuration_spec.rb +8 -0
- data/spec/fixtures/vcr_cassettes/session_create.yml +47 -0
- data/spec/fixtures/vcr_cassettes/session_refresh.yml +47 -0
- data/spec/fixtures/vcr_cassettes/session_verify.yml +47 -0
- data/spec/fixtures/vcr_cassettes/user_find.yml +44 -0
- data/spec/fixtures/vcr_cassettes/user_find_non_existing.yml +42 -0
- data/spec/fixtures/vcr_cassettes/user_import.yml +46 -0
- data/spec/fixtures/vcr_cassettes/user_update.yml +47 -0
- data/spec/helpers_spec.rb +38 -0
- data/spec/jwt_spec.rb +67 -0
- data/spec/models/session_spec.rb +33 -0
- data/spec/models/user_spec.rb +43 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/utils_spec.rb +36 -0
- metadata +128 -36
- data/lib/userbin/authentication.rb +0 -132
- data/lib/userbin/basic_auth.rb +0 -31
- data/lib/userbin/current.rb +0 -17
- data/lib/userbin/events.rb +0 -40
- data/lib/userbin/rails/auth_helpers.rb +0 -22
- data/lib/userbin/railtie.rb +0 -14
- data/lib/userbin/session.rb +0 -26
- data/lib/userbin/userbin.rb +0 -104
- data/spec/session_spec.rb +0 -40
- data/spec/userbin_spec.rb +0 -102
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cdbc3c605fb145fe06f5586752132f09420cd82
|
4
|
+
data.tar.gz: cc87e2c5709a7070d2102f51ea609cb489c7f00e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
15
|
-
------------
|
8
|
+
> Using Ruby on Rails? Install [Userbin for Devise](https://github.com/userbin/devise_userbin) for super-quick integration.
|
16
9
|
|
17
|
-
|
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
|
-
|
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
|
-
|
14
|
+
## Getting started
|
24
15
|
|
25
|
-
|
26
|
-
bundle install
|
27
|
-
```
|
16
|
+
Add the `userbin` gem to your `Gemfile`
|
28
17
|
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
22
|
+
Install the gem
|
43
23
|
|
44
|
-
|
45
|
-
|
46
|
-
|
24
|
+
```bash
|
25
|
+
bundle install
|
26
|
+
```
|
47
27
|
|
28
|
+
Load and configure the library with your Userbin API secret.
|
48
29
|
|
49
|
-
|
50
|
-
|
30
|
+
```ruby
|
31
|
+
require 'userbin'
|
32
|
+
Userbin.api_secret = "YOUR_API_SECRET"
|
33
|
+
```
|
51
34
|
|
52
|
-
|
35
|
+
## Authenticate
|
53
36
|
|
54
|
-
|
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
|
-
|
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
|
-
|
41
|
+
#### Example
|
59
42
|
|
60
|
-
```
|
61
|
-
<a class="ub-login">Log in</a>
|
62
|
-
```
|
43
|
+
```ruby
|
63
44
|
|
64
|
-
|
65
|
-
|
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
|
-
|
56
|
+
session_token =
|
57
|
+
Userbin.authenticate(session[:userbin], current_user.id, options)
|
69
58
|
|
70
|
-
|
71
|
-
<a class="ub-logout">Log out</a>
|
59
|
+
session[:userbin] = session_token
|
72
60
|
```
|
73
61
|
|
74
|
-
|
62
|
+
#### Arguments
|
75
63
|
|
76
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
73
|
+
## Two-factor authentication
|
91
74
|
|
92
|
-
|
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 `
|
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
|
-
###
|
79
|
+
### Step 1: Prompt the user
|
98
80
|
|
99
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
110
|
-
|
111
|
-
```ruby
|
112
|
-
config.root_path = '/login'
|
113
|
-
```
|
98
|
+
### Step 2: Verify the code
|
114
99
|
|
115
|
-
|
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
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
119
|
+
## Security page
|
134
120
|
|
135
|
-
|
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
|
-
|
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
|
-
|
126
|
+
get '/security'
|
127
|
+
redirect Userbin.security_page_url
|
128
|
+
end
|
145
129
|
```
|
146
130
|
|
131
|
+
## De-authenticate
|
147
132
|
|
148
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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/
|
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/
|
24
|
-
require "userbin/
|
25
|
-
require "userbin/
|
26
|
-
require "userbin/
|
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
|
-
|
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
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
14
|
-
self.skip_script_injection = false
|
10
|
+
yield(config) if block_given?
|
15
11
|
end
|
16
12
|
|
17
|
-
def
|
18
|
-
|
13
|
+
def config
|
14
|
+
@configuration ||= Userbin::Configuration.new
|
19
15
|
end
|
20
16
|
|
21
|
-
def
|
22
|
-
|
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,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
|