Ruby SDK for Userbin

Build Status Gem Version Dependency Status

Userbin 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.

Table of Contents

Getting Started

Add the userbin gem to your Gemfile

gem "userbin"

Install the gem

bundle install

Load and configure the library with your Userbin API secret in an initializer or similar.

require 'userbin'
Userbin.api_secret = "YOUR_API_SECRET"

Setup User Monitoring

You should call login as soon as the user has logged in to your application. Pass a unique user identifier, and an optional hash of user properties which are used when searching for users in your dashboard. This will create a Session resource and return a corresponding session token which is stored in the Userbin client.

def 
  userbin.(current_user.id, email: current_user.email)
end

Once logged in to Userbin, all requests made through the Userbin instance are on behalf of the currently logged in user.

When a user logs out from within your application, call logout to remove the session from the user's active sessions.

def your_after_logout_hook
  userbin.logout
end

The real magic happens when you use authorize! to control access to only those logged in to Userbin, which is probably everywhere you allow authenticated users. This makes sure that the session token created by login is valid and up to date, and raises UserUnauthorizedError if it's not. Reasons for this include being automatically locked down due to suspicious behavior or the session being remotely revoked.

Note: The session token will be refreshed every 5 minutes. This means that even though a session becomes invalid, no exceptions will be generated until the next refresh. E.g. revoking a session from the dashboard might take up to 5 minutes to happen.

class AccountController < ApplicationController
  before_filter :authenticate_user! # from e.g. Devise
  before_filter { userbin.authorize! }
  # ...
end

You should catch these errors in one place and log out the authenticated user.

class ApplicationController < ActionController::Base
  rescue_from Userbin::UserUnauthorizedError do |e|
    sign_out # log out your user locally
    redirect_to root_url
  end
end

That's it! Now log in to your application and watch your user appear in the Userbin dashboard.

Active Sessions

Show a list of sessions currently signed to a user's account.

The context is from the last recorded security event on a session.

userbin.sessions.each do |session|
  puts session.id          # => 'yt9BkoHzcQoou4jqbQbJUqqMdxyxvCBr'
  puts session.context.ip  # => '88.12.129.1'
end

Destroy a session to revoke access and trigger a UserUnauthorizedError the next time authorize! refreshes the session token, which is within 5 minutes.

userbin.sessions.destroy('yt9BkoHzcQoou4jqbQbJUqqMdxyxvCBr')

Security Events

List a user's recent account activity, which include security events such as user logins and failed two-factor attempts. See the Event API for a list of all the available events.

userbin.events.each do |event|
  puts event.name                        # => 'session.created'
  puts event.context.ip                  # => '88.12.129.1'
  puts event.context.location.country    # => 'Sweden'
  puts event.context.user_agent.browser  # => 'Chrome'
end

Two-factor Authentication

Using two-factor authentication involves two steps: pairing and authenticating.

Pairing

Before your users can protect their account with two-factor authentication, they will need to pair their their preferred way of authenticating. The Pairing API lets users add, verify, and remove authentication channels. Only verified pairings are valid for authentication.

Pairing with Google Authenticator

The user visits a page to add Google Authenticator to their account. First create a new Authenticator pairing to generate a QR code image.

@authenticator = userbin.pairings.create(type: 'authenticator')

Render a page containing the QR code, which the user scans with Google Authenticator.

<img src="<%= @authenticator[:qr_url] %>">

After scanning the QR code, the user will enter the 6 digit token that Google Authenticator displays, and submit the form. Capture the response and verify the pairing.

begin
  userbin.pairings.verify(params[:pairing_id], response: params[:code])
rescue Userbin::InvalidParametersError
  flash.notice = 'Wrong code, try again'
end

Pairing with Phone Number (SMS)

Create a new phone number pairing which will send out a verification SMS.

@phone_number = userbin.pairings.create(
  type: 'phone_number', number: '+1739855455')

Catch the code from the user to pair the phone number.

begin
  userbin.pairings.verify(params[:pairing_id], response: params[:code])
rescue Userbin::InvalidParametersError
  flash.notice = 'Wrong code, try again'
end

Pairing with YubiKey

YubiKeys are immediately verified for two-factor authentication.

begin
  userbin.pairings.create(type: 'yubikey', otp: params[:code])
rescue Userbin::InvalidParametersError
  flash.notice = 'Wrong code, try again'
end

Enabling and Disabling

For the sake of flexibility, two-factor authentication isn't enabled automatically when you add your first pairing.

userbin.enable_mfa!
userbin.disable_mfa!

Authenticating

If the user has enabled two-factor authentication, authorize! might raise ChallengeRequiredError, which means they'll have to verify a challenge to proceed.

Capture this error just as with UserUnauthorizedError and redirect the user to a path not protected by authorize!.

If the user tries to reach a path protected by authorize! after a challenge has been created but still not verified, the session will be destroyed and UserUnauthorizedError raised.

class ApplicationController < ActionController::Base
  rescue_from Userbin::ChallengeRequiredError do |exception|
    redirect_to show_challenge_path
  end
  # ...
end

Create a challenge, which will send the user and SMS if this is the default pairing. After the challenge has been verified, authorize! will not throw any further exceptions until any suspicious behavior is detected.

When you call trust_device, the user will not be challenged for secondary authentication when they log in to your application from that device for a set period of time. You could add this to your form as a checkbox option.

class ChallengeController < ApplicationController
  def show
    @challenge = userbin.challenges.create
  end

  def verify
    challenge_id = params.require(:challenge_id)
    code = params.require(:code)

    userbin.challenges.verify(challenge_id, response: code)

    # Avoid verification on next login for better experience
    userbin.trust_device if params[:trust_device]

    # Yay, the challenge was verified!
    redirect_to root_url

  rescue Userbin::ForbiddenError => e
    sign_out # log out your user locally

    flash.notice = 'Wrong code, bye!'
    redirect_to root_path
  end
end

Backup Codes

List or generate new backup codes used for when the user didn't bring their authentication device.

userbin.backup_codes
userbin.generate_backup_codes(count: 8)

List Pairings

List all pairings.

# List all pairings
userbin.pairings.each do |pairing|
  puts pairing.id      # => 'yt9BkoHzcQoou4jqbQbJUqqMdxyxvCBr'
  puts pairing.type    # => 'authenticator'
  puts pairing.default # => true
end

Set a pairing as the default one.

userbin.pairings.set_default!('yt9BkoHzcQoou4jqbQbJUqqMdxyxvCBr')

Remove a pairing. If you remove the default pairing, two-factor authentication will be disabled.

userbin.pairings.destroy('yt9BkoHzcQoou4jqbQbJUqqMdxyxvCBr')

Configuration

Userbin.configure do |config|
  # Same as setting it through Userbin.api_secret
  config.api_secret = 'secret'

  # Userbin::RequestError is raised when timing out (default: 2.0)
  config.request_timeout = 2.0
end