userbin 1.0.4 → 1.1.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 +30 -82
- data/lib/userbin.rb +17 -11
- data/lib/userbin/client.rb +116 -0
- data/lib/userbin/errors.rb +11 -2
- data/lib/userbin/jwt.rb +5 -9
- data/lib/userbin/models/challenge.rb +3 -1
- data/lib/userbin/models/channel.rb +5 -0
- data/lib/userbin/models/{base.rb → model.rb} +7 -1
- data/lib/userbin/models/monitoring.rb +6 -0
- data/lib/userbin/models/session.rb +1 -7
- data/lib/userbin/models/token.rb +4 -0
- data/lib/userbin/models/user.rb +1 -1
- data/lib/userbin/request.rb +38 -3
- data/lib/userbin/session_store.rb +35 -0
- data/lib/userbin/session_token.rb +31 -0
- data/lib/userbin/utils.rb +1 -0
- data/lib/userbin/version.rb +1 -1
- data/spec/fixtures/vcr_cassettes/challenge_create.yml +48 -0
- data/spec/fixtures/vcr_cassettes/challenge_verify.yml +42 -0
- data/spec/helpers_spec.rb +3 -3
- data/spec/models/challenge_spec.rb +18 -0
- data/spec/models/session_spec.rb +0 -19
- data/spec/utils_spec.rb +24 -5
- metadata +17 -8
- data/lib/userbin/helpers.rb +0 -83
- data/spec/configuration_spec.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eda8c2ec2217b83cb4943e8302c0844f7d72c158
|
4
|
+
data.tar.gz: ad7583bd3627cd2898cd2f95a687393e8e48d2dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 888dea6e532ce801149b4f3ae95cf875835f5c5a61fbc81fe636983b266e7310097052c64d2b4c3dcccee0c5440ff79b2abaca0fd21c813feafd6328258da2a4
|
7
|
+
data.tar.gz: b5abb4dc7fc8f4f5f8b9a3d4a89a6d409c31322ce7748996d093d2eb72b8260972c998f02cd7621824334c788d875d97a323d55051892f386f2330cf30e9c8ff
|
data/README.md
CHANGED
@@ -1,15 +1,13 @@
|
|
1
|
+
# Ruby SDK for Userbin
|
1
2
|
|
2
3
|
[![Build Status](https://travis-ci.org/userbin/userbin-ruby.png)](https://travis-ci.org/userbin/userbin-ruby)
|
3
4
|
[![Gem Version](https://badge.fury.io/rb/userbin.png)](http://badge.fury.io/rb/userbin)
|
4
5
|
[![Dependency Status](https://gemnasium.com/userbin/userbin-ruby.png)](https://gemnasium.com/userbin/userbin-ruby)
|
5
6
|
|
6
|
-
# Ruby SDK for Userbin
|
7
|
-
|
8
|
-
> Using Ruby on Rails? Install [Userbin for Devise](https://github.com/userbin/devise_userbin) for super-quick integration.
|
9
7
|
|
10
|
-
|
8
|
+
[Userbin](https://userbin.com) provides an additional security layer to your application by adding user activity monitoring, real-time threat protection and two-factor authentication in a white-label package. Your users **do not** need to be signed up or registered for Userbin before using the service and there's no need for them to download any proprietary apps. Also, Userbin requires **no modification of your current database schema** as it uses your local user IDs.
|
11
9
|
|
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.
|
10
|
+
<!-- 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. -->
|
13
11
|
|
14
12
|
## Getting started
|
15
13
|
|
@@ -32,109 +30,59 @@ require 'userbin'
|
|
32
30
|
Userbin.api_secret = "YOUR_API_SECRET"
|
33
31
|
```
|
34
32
|
|
35
|
-
|
36
|
-
|
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.
|
38
|
-
|
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.
|
40
|
-
|
41
|
-
#### Example
|
33
|
+
Initialize a Userbin client for every incoming HTTP request and add it to the environment so that it's accessible during the request lifetime.
|
42
34
|
|
43
35
|
```ruby
|
44
|
-
|
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
|
-
}
|
55
|
-
|
56
|
-
session_token =
|
57
|
-
Userbin.authenticate(session[:userbin], current_user.id, options)
|
58
|
-
|
59
|
-
session[:userbin] = session_token
|
36
|
+
env['userbin'] = Userbin::Client.new(request)
|
60
37
|
```
|
61
38
|
|
62
|
-
#### Arguments
|
63
|
-
|
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.
|
65
|
-
|
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.
|
67
|
-
|
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.
|
70
|
-
|
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.
|
72
|
-
|
73
|
-
## Two-factor authentication
|
74
39
|
|
75
|
-
Two-factor authentication is available to your users out-of-the-box. By browsing to their security settings page, they're able to configure Google Authenticator and SMS settings, set up a backup phone number, and download their recovery codes.
|
76
40
|
|
77
|
-
|
41
|
+
## Monitor a user
|
78
42
|
|
79
|
-
|
43
|
+
To monitor a logged in user, simply call `authorize!` on the Userbin object. You need to pass the user id, and optionally a hash of [user properties](.), preferrable including at least `email`. This call only result in an HTTP request once every 5 minutes.
|
80
44
|
|
81
|
-
|
45
|
+
```ruby
|
46
|
+
# do this for *every* request, right after current_user is assigned
|
47
|
+
env['userbin'].authorize!(current_user.id, { email: current_user.email })
|
48
|
+
```
|
82
49
|
|
83
|
-
|
50
|
+
Clear the session when the user logs out.
|
84
51
|
|
85
52
|
```ruby
|
86
|
-
|
87
|
-
|
88
|
-
case factor
|
89
|
-
when :authenticator
|
90
|
-
render 'two_factor_authenticator_form'
|
91
|
-
when :sms
|
92
|
-
render 'two_factor_sms_form'
|
93
|
-
end
|
53
|
+
env['userbin'].logout
|
94
54
|
```
|
95
55
|
|
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.
|
97
56
|
|
98
|
-
|
57
|
+
Done! Now log in to your application and watch the user appear in your Userbin dashboard.
|
99
58
|
|
100
|
-
|
59
|
+
## Add a link to the user's security settings
|
101
60
|
|
102
|
-
|
61
|
+
Create a new route where you redirect the user to its [security settings page](.), where they can configure two-factor authentication, revoke suspicious sessions and set up notifications.
|
103
62
|
|
104
63
|
```ruby
|
105
|
-
|
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
|
64
|
+
redirect_to env['userbin'].security_settings_url
|
117
65
|
```
|
118
66
|
|
119
|
-
##
|
67
|
+
## Activate two-factor authentication
|
120
68
|
|
121
|
-
|
122
|
-
|
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 settings page.
|
69
|
+
If the user has enabled two-factor authentication, `two_factor_authenticate!` 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.
|
124
70
|
|
125
71
|
```ruby
|
126
|
-
|
127
|
-
|
72
|
+
factor = env['userbin'].two_factor_authenticate!
|
73
|
+
|
74
|
+
case factor
|
75
|
+
when :authenticator then render 'authenticator_form'
|
76
|
+
when :sms then render 'sms_form'
|
128
77
|
end
|
129
78
|
```
|
130
79
|
|
131
|
-
|
132
|
-
|
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.
|
80
|
+
The user enters the authentication code in the form and posts it to your handler.
|
134
81
|
|
135
82
|
```ruby
|
136
|
-
|
137
|
-
token = session.delete(:userbin) # remove the local reference
|
138
|
-
Userbin.deauthenticate(token)
|
139
|
-
rescue Userbin::Error; end
|
83
|
+
env['userbin'].two_factor_verify(params[:code])
|
140
84
|
```
|
85
|
+
|
86
|
+
## Handling errors
|
87
|
+
|
88
|
+
If any request runs into an subclass of `Userbin::Error` will be raised with more details on what went wrong.
|
data/lib/userbin.rb
CHANGED
@@ -6,20 +6,26 @@ require 'net/http'
|
|
6
6
|
require 'request_store'
|
7
7
|
require 'active_support/core_ext/hash/indifferent_access'
|
8
8
|
|
9
|
-
require
|
10
|
-
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
15
|
-
require
|
9
|
+
require 'userbin/version'
|
10
|
+
|
11
|
+
require 'userbin/configuration'
|
12
|
+
require 'userbin/client'
|
13
|
+
require 'userbin/errors'
|
14
|
+
require 'userbin/session_store'
|
15
|
+
require 'userbin/jwt'
|
16
|
+
require 'userbin/utils'
|
17
|
+
require 'userbin/request'
|
18
|
+
require 'userbin/session_token'
|
16
19
|
|
17
20
|
module Userbin
|
18
21
|
API = Userbin.setup_api
|
19
22
|
end
|
20
23
|
|
21
24
|
# These need to be required after setting up Her
|
22
|
-
require
|
23
|
-
require
|
24
|
-
require
|
25
|
-
require
|
25
|
+
require 'userbin/models/model'
|
26
|
+
require 'userbin/models/challenge'
|
27
|
+
require 'userbin/models/channel'
|
28
|
+
require 'userbin/models/token'
|
29
|
+
require 'userbin/models/session'
|
30
|
+
require 'userbin/models/user'
|
31
|
+
require 'userbin/models/monitoring'
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Userbin
|
2
|
+
class Client
|
3
|
+
|
4
|
+
attr_accessor :request_context
|
5
|
+
|
6
|
+
def initialize(request, opts = {})
|
7
|
+
# Save a reference in the per-request store so that the request
|
8
|
+
# middleware in request.rb can access it
|
9
|
+
RequestStore.store[:userbin] = self
|
10
|
+
|
11
|
+
# By default the session token is persisted in the Rack store, which may
|
12
|
+
# in turn point to any source. But this option gives you an option to
|
13
|
+
# use any store, such as Redis or Memcached to store your Userbin tokens.
|
14
|
+
if opts[:session_store]
|
15
|
+
@session_store = opts[:session_store]
|
16
|
+
else
|
17
|
+
@session_store = Userbin::SessionStore::Rack.new(request.session)
|
18
|
+
end
|
19
|
+
|
20
|
+
@request_context = {
|
21
|
+
ip: request.ip,
|
22
|
+
user_agent: request.user_agent
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def session_token=(value)
|
27
|
+
if value && value != @session_store.read
|
28
|
+
@session_store.write(value)
|
29
|
+
elsif !value
|
30
|
+
@session_store.destroy
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def session_token
|
35
|
+
token = @session_store.read
|
36
|
+
Userbin::SessionToken.new(token) if token
|
37
|
+
end
|
38
|
+
|
39
|
+
def authorize!(user_id, user_attrs = {})
|
40
|
+
# The user identifier is used in API paths so it needs to be cleaned
|
41
|
+
user_id = URI.encode(user_id.to_s)
|
42
|
+
|
43
|
+
@session_store.user_id = user_id
|
44
|
+
|
45
|
+
if !session_token
|
46
|
+
# Create a session, and implicitly a user with user_attrs
|
47
|
+
session = Userbin::Session.post(
|
48
|
+
"users/#{user_id}/sessions", user: user_attrs)
|
49
|
+
|
50
|
+
# Set the session token for use in all subsequent requests
|
51
|
+
self.session_token = session.token
|
52
|
+
else
|
53
|
+
if session_token.expired?
|
54
|
+
Userbin::Monitoring.heartbeat
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# This method ends the current monitoring session. It should be called
|
60
|
+
# whenever the user logs out from your system.
|
61
|
+
#
|
62
|
+
def logout
|
63
|
+
return unless session_token
|
64
|
+
|
65
|
+
# Destroy the current session specified in the session token
|
66
|
+
begin
|
67
|
+
Userbin::Session.destroy_existing('current')
|
68
|
+
rescue Userbin::Error; end
|
69
|
+
|
70
|
+
# Clear the session token
|
71
|
+
self.session_token = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
# This method creates a two-factor challenge for the current user, if the
|
75
|
+
# user has enabled a device for authentication.
|
76
|
+
#
|
77
|
+
# If there already exists a challenge on the current session, it will be
|
78
|
+
# returned. Otherwise a new will be created.
|
79
|
+
#
|
80
|
+
def two_factor_authenticate!
|
81
|
+
return unless session_token
|
82
|
+
|
83
|
+
if session_token.needs_challenge?
|
84
|
+
Userbin::Challenge.post("users/current/challenges")
|
85
|
+
return two_factor_method
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Once a two factor challenge has been created using
|
90
|
+
# two_factor_authenticate!, the response code from the user is verified
|
91
|
+
# using this method.
|
92
|
+
#
|
93
|
+
def two_factor_verify(response)
|
94
|
+
# Need to have an active challenge to verify it
|
95
|
+
return unless session_token && session_token.has_challenge?
|
96
|
+
|
97
|
+
challenge = Userbin::Challenge.new('current')
|
98
|
+
challenge.verify(response: response)
|
99
|
+
end
|
100
|
+
|
101
|
+
def security_settings_url
|
102
|
+
raise Userbin::Error unless session_token
|
103
|
+
return "https://security.userbin.com/?session_token=#{session_token}"
|
104
|
+
end
|
105
|
+
|
106
|
+
# If a two-factor authentication process has been started, this method will
|
107
|
+
# return the method which is used to perform the authentication. Eg.
|
108
|
+
# :authenticator or :sms
|
109
|
+
#
|
110
|
+
def two_factor_method
|
111
|
+
return unless session_token
|
112
|
+
return session_token.challenge_type
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
data/lib/userbin/errors.rb
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
class Userbin::Error < Exception; end
|
2
|
-
|
3
|
-
class Userbin::
|
2
|
+
|
3
|
+
class Userbin::RequestError < Userbin::Error; end
|
4
4
|
class Userbin::SecurityError < Userbin::Error; end
|
5
5
|
class Userbin::ConfigurationError < Userbin::Error; end
|
6
|
+
|
7
|
+
class Userbin::ApiError < Userbin::Error; end
|
8
|
+
|
9
|
+
class Userbin::BadRequest < Userbin::ApiError; end
|
10
|
+
class Userbin::UnauthorizedError < Userbin::ApiError; end
|
11
|
+
class Userbin::ForbiddenError < Userbin::ApiError; end
|
12
|
+
class Userbin::NotFoundError < Userbin::ApiError; end
|
13
|
+
class Userbin::UserUnauthorizedError < Userbin::ApiError; end
|
14
|
+
class Userbin::InvalidParametersError < Userbin::ApiError; end
|
data/lib/userbin/jwt.rb
CHANGED
@@ -2,8 +2,7 @@ require 'jwt'
|
|
2
2
|
|
3
3
|
module Userbin
|
4
4
|
class JWT
|
5
|
-
|
6
|
-
attr_reader :payload
|
5
|
+
attr_accessor :header, :payload
|
7
6
|
|
8
7
|
def initialize(jwt)
|
9
8
|
begin
|
@@ -21,6 +20,10 @@ module Userbin
|
|
21
20
|
Time.now.utc > Time.at(@header['exp']).utc
|
22
21
|
end
|
23
22
|
|
23
|
+
def merge!(payload = {})
|
24
|
+
@payload.merge!(payload)
|
25
|
+
end
|
26
|
+
|
24
27
|
def to_json
|
25
28
|
@payload
|
26
29
|
end
|
@@ -29,12 +32,5 @@ module Userbin
|
|
29
32
|
::JWT.encode(@payload, Userbin.config.api_secret, "HS256", @header)
|
30
33
|
end
|
31
34
|
|
32
|
-
def app_id
|
33
|
-
@header['aud']
|
34
|
-
end
|
35
|
-
|
36
|
-
def merge!(payload = {})
|
37
|
-
@payload.merge!(payload)
|
38
|
-
end
|
39
35
|
end
|
40
36
|
end
|
@@ -1,10 +1,16 @@
|
|
1
1
|
require 'her'
|
2
2
|
|
3
3
|
module Userbin
|
4
|
-
class
|
4
|
+
class Model
|
5
5
|
include Her::Model
|
6
6
|
use_api Userbin::API
|
7
7
|
|
8
|
+
def initialize(args = {})
|
9
|
+
# allow initializing with id as a string
|
10
|
+
args = { id: args } if args.is_a? String
|
11
|
+
super(args)
|
12
|
+
end
|
13
|
+
|
8
14
|
METHODS.each do |method|
|
9
15
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
10
16
|
def self.instance_#{method}(action)
|
data/lib/userbin/models/user.rb
CHANGED
data/lib/userbin/request.rb
CHANGED
@@ -57,12 +57,39 @@ module Userbin
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
+
# Sends the active session token in a header, and extracts the returned
|
61
|
+
# session token and sets it locally.
|
62
|
+
#
|
63
|
+
class SessionToken < Faraday::Middleware
|
64
|
+
def call(env)
|
65
|
+
userbin = RequestStore.store[:userbin]
|
66
|
+
return @app.call(env) unless userbin
|
67
|
+
|
68
|
+
# get the session token from our local store
|
69
|
+
if userbin.session_token
|
70
|
+
env[:request_headers]['X-Userbin-Session-Token'] =
|
71
|
+
userbin.session_token.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
# call the API
|
75
|
+
response = @app.call(env)
|
76
|
+
|
77
|
+
# update the local store with the updated session token
|
78
|
+
token = response.env.response_headers['x-userbin-session-token']
|
79
|
+
userbin.session_token = token if token
|
80
|
+
|
81
|
+
response
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
60
85
|
# Adds request context like IP address and user agent to any request.
|
61
86
|
#
|
62
87
|
class ContextHeaders < Faraday::Middleware
|
63
88
|
def call(env)
|
64
|
-
|
65
|
-
|
89
|
+
userbin = RequestStore.store[:userbin]
|
90
|
+
return @app.call(env) unless userbin
|
91
|
+
|
92
|
+
userbin.request_context.each do |key, value|
|
66
93
|
header =
|
67
94
|
"X-Userbin-#{key.to_s.gsub('_', '-').gsub(/\w+/) {|m| m.capitalize}}"
|
68
95
|
env[:request_headers][header] = value
|
@@ -83,11 +110,19 @@ module Userbin
|
|
83
110
|
when 403
|
84
111
|
raise Userbin::ForbiddenError.new(
|
85
112
|
MultiJson.decode(env[:body])['message'])
|
113
|
+
when 404
|
114
|
+
raise Userbin::NotFoundError.new(
|
115
|
+
MultiJson.decode(env[:body])['message'])
|
86
116
|
when 419
|
87
117
|
raise Userbin::UserUnauthorizedError.new(
|
88
118
|
MultiJson.decode(env[:body])['message'])
|
89
119
|
when 400..599
|
90
|
-
|
120
|
+
begin
|
121
|
+
message = MultiJson.decode(env[:body])['message']
|
122
|
+
raise Userbin::Error.new(message)
|
123
|
+
rescue MultiJson::ParseError
|
124
|
+
raise Userbin::ApiError.new
|
125
|
+
end
|
91
126
|
else
|
92
127
|
parse(env[:body])
|
93
128
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Userbin
|
2
|
+
class SessionStore
|
3
|
+
class Rack < SessionStore
|
4
|
+
def initialize(session)
|
5
|
+
@session = session
|
6
|
+
end
|
7
|
+
|
8
|
+
def user_id
|
9
|
+
@session['userbin.user_id']
|
10
|
+
end
|
11
|
+
|
12
|
+
def user_id=(value)
|
13
|
+
@session['userbin.user_id'] = value
|
14
|
+
end
|
15
|
+
|
16
|
+
def read
|
17
|
+
@session[key]
|
18
|
+
end
|
19
|
+
|
20
|
+
def write(value)
|
21
|
+
@session[key] = value
|
22
|
+
end
|
23
|
+
|
24
|
+
def destroy
|
25
|
+
@session.delete(key)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def key
|
31
|
+
"userbin.user.#{user_id}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'jwt'
|
2
|
+
|
3
|
+
module Userbin
|
4
|
+
class SessionToken
|
5
|
+
def initialize(token)
|
6
|
+
if token
|
7
|
+
@jwt = Userbin::JWT.new(token)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
@jwt.to_token
|
13
|
+
end
|
14
|
+
|
15
|
+
def expired?
|
16
|
+
@jwt.expired?
|
17
|
+
end
|
18
|
+
|
19
|
+
def needs_challenge?
|
20
|
+
@jwt.payload['vfy'] > 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_challenge?
|
24
|
+
!!@jwt.payload['chg']
|
25
|
+
end
|
26
|
+
|
27
|
+
def challenge_type
|
28
|
+
@jwt.payload['chg']['typ'] if has_challenge?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/userbin/utils.rb
CHANGED
@@ -11,6 +11,7 @@ module Userbin
|
|
11
11
|
c.use Userbin::Request::Middleware::BasicAuth, api_secret
|
12
12
|
c.use Userbin::Request::Middleware::EnvironmentHeaders
|
13
13
|
c.use Userbin::Request::Middleware::ContextHeaders
|
14
|
+
c.use Userbin::Request::Middleware::SessionToken
|
14
15
|
c.use FaradayMiddleware::EncodeJson
|
15
16
|
c.use Userbin::Request::Middleware::JSONParser
|
16
17
|
c.use Faraday::Adapter::NetHttp
|
data/lib/userbin/version.rb
CHANGED
@@ -0,0 +1,48 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: post
|
5
|
+
uri: https://:secretkey@secure.userbin.com/v1/users/dTxR68nzuRXT4wrB2HJ4hanYtcaGSz2y/challenges
|
6
|
+
body:
|
7
|
+
encoding: UTF-8
|
8
|
+
string: "{}"
|
9
|
+
headers:
|
10
|
+
User-Agent:
|
11
|
+
- Userbin/v1 RubyBindings/1.0.4
|
12
|
+
X-Userbin-Client-User-Agent:
|
13
|
+
- '{"bindings_version":"1.0.4","lang":"ruby","lang_version":"2.1.1 p76 (2014-02-24)","platform":"x86_64-darwin13.0","publisher":"userbin","uname":"Darwin
|
14
|
+
Johans-MacBook-Pro-2.local 13.2.0 Darwin Kernel Version 13.2.0: Thu Apr 17
|
15
|
+
23:03:13 PDT 2014; root:xnu-2422.100.13~1/RELEASE_X86_64 x86_64"}'
|
16
|
+
Content-Type:
|
17
|
+
- application/json
|
18
|
+
Accept-Encoding:
|
19
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
20
|
+
Accept:
|
21
|
+
- "*/*"
|
22
|
+
response:
|
23
|
+
status:
|
24
|
+
code: 201
|
25
|
+
message: 'Created '
|
26
|
+
headers:
|
27
|
+
Content-Type:
|
28
|
+
- application/json
|
29
|
+
Content-Length:
|
30
|
+
- '476'
|
31
|
+
X-Ua-Compatible:
|
32
|
+
- IE=Edge
|
33
|
+
Etag:
|
34
|
+
- '"ee1750920e2acc320adbd6826d5299be"'
|
35
|
+
Cache-Control:
|
36
|
+
- max-age=0, private, must-revalidate
|
37
|
+
Server:
|
38
|
+
- WEBrick/1.3.1 (Ruby/2.1.1/2014-02-24)
|
39
|
+
Date:
|
40
|
+
- Sat, 07 Jun 2014 20:42:33 GMT
|
41
|
+
Connection:
|
42
|
+
- Keep-Alive
|
43
|
+
body:
|
44
|
+
encoding: UTF-8
|
45
|
+
string: '{"id":"UWwy5FrWf9DTeoTpJz1LpBp4dPkWZ2Ne","created_at":"2014-06-07T20:42:33Z","channel":{"id":"Ff892rfGx3TwNF33sQUz3S51NsV24w7H","created_at":"2014-06-07T20:25:27Z","primary":true,"type":"token","token":{"id":"VVG3qirUxy8mUSkmzy3QpPcuhLN1JY4r","created_at":"2014-06-07T20:24:39Z","verified":true,"qr_url":"https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth%3A%2F%2Ftotp%2FUserbin%3Abrissmyr%40gmail.com%3Fsecret%3Dxulnnls324pajcfn%26issuer%3DUserbin"}}}'
|
46
|
+
http_version:
|
47
|
+
recorded_at: Sat, 07 Jun 2014 20:42:33 GMT
|
48
|
+
recorded_with: VCR 2.9.0
|
@@ -0,0 +1,42 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: post
|
5
|
+
uri: https://:secretkey@secure.userbin.com/v1/challenges/UWwy5FrWf9DTeoTpJz1LpBp4dPkWZ2Ne/verify
|
6
|
+
body:
|
7
|
+
encoding: UTF-8
|
8
|
+
string: '{"response":"000000"}'
|
9
|
+
headers:
|
10
|
+
User-Agent:
|
11
|
+
- Userbin/v1 RubyBindings/1.0.4
|
12
|
+
X-Userbin-Client-User-Agent:
|
13
|
+
- '{"bindings_version":"1.0.4","lang":"ruby","lang_version":"2.1.1 p76 (2014-02-24)","platform":"x86_64-darwin13.0","publisher":"userbin","uname":"Darwin
|
14
|
+
Johans-MacBook-Pro-2.local 13.2.0 Darwin Kernel Version 13.2.0: Thu Apr 17
|
15
|
+
23:03:13 PDT 2014; root:xnu-2422.100.13~1/RELEASE_X86_64 x86_64"}'
|
16
|
+
Content-Type:
|
17
|
+
- application/json
|
18
|
+
Accept-Encoding:
|
19
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
20
|
+
Accept:
|
21
|
+
- "*/*"
|
22
|
+
response:
|
23
|
+
status:
|
24
|
+
code: 204
|
25
|
+
message: 'No Content '
|
26
|
+
headers:
|
27
|
+
X-Ua-Compatible:
|
28
|
+
- IE=Edge
|
29
|
+
Cache-Control:
|
30
|
+
- no-cache
|
31
|
+
Server:
|
32
|
+
- WEBrick/1.3.1 (Ruby/2.1.1/2014-02-24)
|
33
|
+
Date:
|
34
|
+
- Sat, 07 Jun 2014 20:52:43 GMT
|
35
|
+
Connection:
|
36
|
+
- Keep-Alive
|
37
|
+
body:
|
38
|
+
encoding: UTF-8
|
39
|
+
string: ''
|
40
|
+
http_version:
|
41
|
+
recorded_at: Sat, 07 Jun 2014 20:52:43 GMT
|
42
|
+
recorded_with: VCR 2.9.0
|
data/spec/helpers_spec.rb
CHANGED
@@ -3,14 +3,14 @@ require 'spec_helper'
|
|
3
3
|
describe 'Userbin helpers' do
|
4
4
|
let(:token) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlhdCI6MTM5ODIzOTIwMywiZXhwIjoxMzk4MjQyODAzfQ.eyJ1c2VyX2lkIjoiZUF3djVIdGRiU2s4Yk1OWVpvanNZdW13UXlLcFhxS3IifQ.Apa7EmT5T1sOYz4Af0ERTDzcnUvSalailNJbejZ2ddQ' }
|
5
5
|
|
6
|
-
|
6
|
+
xit 'creates a session' do
|
7
7
|
Userbin::Session.should_receive(:post).
|
8
8
|
with("users/user%201234/sessions", user: {email: 'valid@example.com'}).
|
9
9
|
and_return(Userbin::Session.new(token: token))
|
10
10
|
Userbin.authenticate(nil, 'user 1234', properties: {email: 'valid@example.com'})
|
11
11
|
end
|
12
12
|
|
13
|
-
|
13
|
+
xit 'refreshes, and does not create a session' do
|
14
14
|
Userbin::Session.should_not_receive(:create)
|
15
15
|
Userbin::Session.any_instance.should_receive(:refresh).
|
16
16
|
and_return(Userbin::Session.new(token: token))
|
@@ -27,7 +27,7 @@ describe 'Userbin helpers' do
|
|
27
27
|
Userbin.authenticate(token, opts)
|
28
28
|
end
|
29
29
|
|
30
|
-
|
30
|
+
xit 'deauthenticates with context' do
|
31
31
|
Userbin::Session.should_receive(:destroy_existing)
|
32
32
|
|
33
33
|
jwt = Userbin::JWT.new(token)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Userbin::Challenge' do
|
4
|
+
it 'creates a challenge' do
|
5
|
+
VCR.use_cassette('challenge_create') do
|
6
|
+
challenge = Userbin::Challenge.post(
|
7
|
+
"users/dTxR68nzuRXT4wrB2HJ4hanYtcaGSz2y/challenges")
|
8
|
+
challenge.channel.token.id.should == 'VVG3qirUxy8mUSkmzy3QpPcuhLN1JY4r'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'verifies a challenge' do
|
13
|
+
VCR.use_cassette('challenge_verify') do
|
14
|
+
challenge = Userbin::Challenge.new(id: 'UWwy5FrWf9DTeoTpJz1LpBp4dPkWZ2Ne')
|
15
|
+
challenge.verify(response: '000000')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/spec/models/session_spec.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe 'Userbin::Session' do
|
4
|
-
|
5
4
|
let(:session_token) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImlzcyI6InVzZXItMjQxMiIsInN1YiI6IlMyb2R4UmVabkdxaHF4UGFRN1Y3a05rTG9Ya0daUEZ6IiwiYXVkIjoiODAwMDAwMDAwMDAwMDAwIiwiZXhwIjoxMzk5NDc5Njc1LCJpYXQiOjEzOTk0Nzk2NjUsImp0aSI6MH0.eyJjaGFsbGVuZ2UiOnsiaWQiOiJUVENqd3VyM3lwbTRUR1ZwWU43cENzTXFxOW9mWEVBSCIsInR5cGUiOiJvdHBfYXV0aGVudGljYXRvciJ9fQ.LT9mUzJEbsizbFxcpMo3zbms0aCDBzfgMbveMGSi1-s' }
|
6
5
|
|
7
6
|
it 'creates a session' do
|
@@ -12,22 +11,4 @@ describe 'Userbin::Session' do
|
|
12
11
|
Userbin::JWT.new(session.token).header['iss'].should == user_id
|
13
12
|
end
|
14
13
|
end
|
15
|
-
|
16
|
-
it 'refreshes a session' do
|
17
|
-
VCR.use_cassette('session_refresh') do
|
18
|
-
session = Userbin::Session.new(token: session_token)
|
19
|
-
session = session.refresh(user: {name: 'New Name'})
|
20
|
-
|
21
|
-
session.token.should_not == session_token
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
it 'verifies a session' do
|
26
|
-
VCR.use_cassette('session_verify') do
|
27
|
-
Userbin::JWT.new(session_token).payload['challenge'].should_not be_nil
|
28
|
-
session = Userbin::Session.new(token: session_token)
|
29
|
-
session = session.verify(response: '017010')
|
30
|
-
Userbin::JWT.new(session.token).payload['challenge'].should be_nil
|
31
|
-
end
|
32
|
-
end
|
33
14
|
end
|
data/spec/utils_spec.rb
CHANGED
@@ -1,5 +1,23 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
+
class MemoryStore < Userbin::SessionStore
|
4
|
+
def initialize
|
5
|
+
@value = nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def read
|
9
|
+
@value
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(value)
|
13
|
+
@value = value
|
14
|
+
end
|
15
|
+
|
16
|
+
def destroy
|
17
|
+
@value = nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
3
21
|
describe 'Userbin utils' do
|
4
22
|
describe 'ContextHeaders middleware' do
|
5
23
|
before do
|
@@ -26,11 +44,12 @@ describe 'Userbin utils' do
|
|
26
44
|
end
|
27
45
|
|
28
46
|
it 'sets context headers from env' do
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
47
|
+
request = Rack::Request.new(Rack::MockRequest.env_for('/',
|
48
|
+
"HTTP_USER_AGENT" => "Mozilla", "REMOTE_ADDR" => "8.8.8.8"))
|
49
|
+
Userbin::Client.new(request, session_store: MemoryStore.new)
|
50
|
+
Userbin::User.create()
|
51
|
+
@env['request_headers']['X-Userbin-Ip'].should == '8.8.8.8'
|
52
|
+
@env['request_headers']['X-Userbin-User-Agent'].should == 'Mozilla'
|
34
53
|
end
|
35
54
|
end
|
36
55
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: userbin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Johan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-07-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: her
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.7.2
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.7.2
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: faraday_middleware
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -173,18 +173,24 @@ extra_rdoc_files: []
|
|
173
173
|
files:
|
174
174
|
- README.md
|
175
175
|
- lib/userbin.rb
|
176
|
+
- lib/userbin/client.rb
|
176
177
|
- lib/userbin/configuration.rb
|
177
178
|
- lib/userbin/errors.rb
|
178
|
-
- lib/userbin/helpers.rb
|
179
179
|
- lib/userbin/jwt.rb
|
180
|
-
- lib/userbin/models/base.rb
|
181
180
|
- lib/userbin/models/challenge.rb
|
181
|
+
- lib/userbin/models/channel.rb
|
182
|
+
- lib/userbin/models/model.rb
|
183
|
+
- lib/userbin/models/monitoring.rb
|
182
184
|
- lib/userbin/models/session.rb
|
185
|
+
- lib/userbin/models/token.rb
|
183
186
|
- lib/userbin/models/user.rb
|
184
187
|
- lib/userbin/request.rb
|
188
|
+
- lib/userbin/session_store.rb
|
189
|
+
- lib/userbin/session_token.rb
|
185
190
|
- lib/userbin/utils.rb
|
186
191
|
- lib/userbin/version.rb
|
187
|
-
- spec/
|
192
|
+
- spec/fixtures/vcr_cassettes/challenge_create.yml
|
193
|
+
- spec/fixtures/vcr_cassettes/challenge_verify.yml
|
188
194
|
- spec/fixtures/vcr_cassettes/session_create.yml
|
189
195
|
- spec/fixtures/vcr_cassettes/session_refresh.yml
|
190
196
|
- spec/fixtures/vcr_cassettes/session_verify.yml
|
@@ -194,6 +200,7 @@ files:
|
|
194
200
|
- spec/fixtures/vcr_cassettes/user_update.yml
|
195
201
|
- spec/helpers_spec.rb
|
196
202
|
- spec/jwt_spec.rb
|
203
|
+
- spec/models/challenge_spec.rb
|
197
204
|
- spec/models/session_spec.rb
|
198
205
|
- spec/models/user_spec.rb
|
199
206
|
- spec/spec_helper.rb
|
@@ -223,7 +230,8 @@ signing_key:
|
|
223
230
|
specification_version: 4
|
224
231
|
summary: Userbin
|
225
232
|
test_files:
|
226
|
-
- spec/
|
233
|
+
- spec/fixtures/vcr_cassettes/challenge_create.yml
|
234
|
+
- spec/fixtures/vcr_cassettes/challenge_verify.yml
|
227
235
|
- spec/fixtures/vcr_cassettes/session_create.yml
|
228
236
|
- spec/fixtures/vcr_cassettes/session_refresh.yml
|
229
237
|
- spec/fixtures/vcr_cassettes/session_verify.yml
|
@@ -233,6 +241,7 @@ test_files:
|
|
233
241
|
- spec/fixtures/vcr_cassettes/user_update.yml
|
234
242
|
- spec/helpers_spec.rb
|
235
243
|
- spec/jwt_spec.rb
|
244
|
+
- spec/models/challenge_spec.rb
|
236
245
|
- spec/models/session_spec.rb
|
237
246
|
- spec/models/user_spec.rb
|
238
247
|
- spec/spec_helper.rb
|
data/lib/userbin/helpers.rb
DELETED
@@ -1,83 +0,0 @@
|
|
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_settings_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
|