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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d9b8ce69ab60a585a76e9a70407998c381af3ff5
4
- data.tar.gz: f28be477edf5edd04eb8a0d115b2f30e7925fa54
3
+ metadata.gz: eda8c2ec2217b83cb4943e8302c0844f7d72c158
4
+ data.tar.gz: ad7583bd3627cd2898cd2f95a687393e8e48d2dd
5
5
  SHA512:
6
- metadata.gz: 6dff1ce4527789fd98d09dad1ca96c735839ef1c40af6bd6d2cb0f551d7e326ac9ebd36dee7cef2de96f5a3291acfe2776ed583a57bed251b8e2bda419e659e2
7
- data.tar.gz: 812f6a193e50662368adc8d193301005455034ae3e65b2ffc2f36b152893ff81da19d12d9890331fb538b09545eab3923af75288b75787a7ac50670bb64eea0a
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
- 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.
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
- ## Authenticate
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
- 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.
41
+ ## Monitor a user
78
42
 
79
- ### Step 1: Prompt the user
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
- `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.
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
- When `two_factor_authenticate!` returns non-falsy value, you should display the appropriate form to the user, requesting their authentication code.
50
+ Clear the session when the user logs out.
84
51
 
85
52
  ```ruby
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
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
- ### Step 2: Verify the code
57
+ Done! Now log in to your application and watch the user appear in your Userbin dashboard.
99
58
 
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.
59
+ ## Add a link to the user's security settings
101
60
 
102
- `code` can be either a code from the Google Authenticator app, an SMS, or one of the user's recovery codes.
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
- 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
64
+ redirect_to env['userbin'].security_settings_url
117
65
  ```
118
66
 
119
- ## User security settings
67
+ ## Activate two-factor authentication
120
68
 
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 settings page can be customized to fit your current layout by going to the appearance settings in your Userbin dashboard.
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
- get '/security'
127
- redirect Userbin.security_settings_url
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
- ## De-authenticate
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
- begin
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 "userbin/version"
10
- require "userbin/configuration"
11
- require "userbin/request"
12
- require "userbin/jwt"
13
- require "userbin/utils"
14
- require "userbin/helpers"
15
- require "userbin/errors"
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 "userbin/models/base"
23
- require "userbin/models/challenge"
24
- require "userbin/models/session"
25
- require "userbin/models/user"
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
@@ -1,5 +1,14 @@
1
1
  class Userbin::Error < Exception; end
2
- class Userbin::Forbidden < Userbin::Error; end
3
- class Userbin::UserUnauthorizedError < Userbin::Error; end
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
- attr_reader :header
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,4 +1,6 @@
1
1
  module Userbin
2
- class Challenge < Base
2
+ class Challenge < Model
3
+ has_one :channel
4
+ instance_post :verify
3
5
  end
4
6
  end
@@ -0,0 +1,5 @@
1
+ module Userbin
2
+ class Channel < Model
3
+ has_one :token
4
+ end
5
+ end
@@ -1,10 +1,16 @@
1
1
  require 'her'
2
2
 
3
3
  module Userbin
4
- class Base
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)
@@ -0,0 +1,6 @@
1
+ module Userbin
2
+ class Monitoring < Model
3
+ collection_path '/v1' # Her doesn't accept the empty string
4
+ custom_post :heartbeat
5
+ end
6
+ end
@@ -1,11 +1,5 @@
1
1
  module Userbin
2
- class Session < Base
3
- primary_key :token
2
+ class Session < Model
4
3
  instance_post :refresh
5
- instance_post :verify
6
-
7
- def expired?
8
- Userbin::JWT.new(token).expired?
9
- end
10
4
  end
11
5
  end
@@ -0,0 +1,4 @@
1
+ module Userbin
2
+ class Token < Model
3
+ end
4
+ end
@@ -1,5 +1,5 @@
1
1
  module Userbin
2
- class User < Base
2
+ class User < Model
3
3
  custom_post :import
4
4
  instance_post :lock
5
5
  instance_post :unlock
@@ -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
- userbin_headers = RequestStore.store.fetch(:userbin_headers, [])
65
- userbin_headers.each do |key, value|
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
- raise Userbin::Error.new(MultiJson.decode(env[:body])['message'])
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
@@ -1,3 +1,3 @@
1
1
  module Userbin
2
- VERSION = "1.0.4"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -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
- it 'creates a session' do
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
- it 'refreshes, and does not create a session' do
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
- it 'deauthenticates with context' do
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
@@ -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
- Userbin.with_context(ip: '8.8.8.8', user_agent: 'Mozilla') do
30
- Userbin::User.create()
31
- @env['request_headers']['X-Userbin-Ip'].should == '8.8.8.8'
32
- @env['request_headers']['X-Userbin-User-Agent'].should == 'Mozilla'
33
- end
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
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-05-14 00:00:00.000000000 Z
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.6.8
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.6.8
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/configuration_spec.rb
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/configuration_spec.rb
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
@@ -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
@@ -1,8 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe 'Userbin configuration' do
4
- it 'sets API secret without using block' do
5
- #Userbin.api_secret = 'secret'
6
- #Userbin.config.api_secret.should == 'secret'
7
- end
8
- end