No release in over 3 years
An OmniAuth strategy for authenticating with Clover POS using OAuth 2.0. Supports both sandbox and production environments with automatic endpoint switching.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 2.0
~> 2.1
~> 13.0
~> 3.12
~> 1.50
~> 0.22
~> 3.18

Runtime

>= 1.0, < 3.0
~> 0.6
 Project Readme

OmniAuth Clover OAuth2 Strategy

Gem Version CI

An OmniAuth strategy for authenticating with Clover POS using OAuth 2.0.

Installation

Add this line to your application's Gemfile:

gem 'omniauth-clover-oauth2'

Then execute:

$ bundle install

Or install it yourself:

$ gem install omniauth-clover-oauth2

Clover Developer Setup

  1. Go to the Clover Developer Dashboard
  2. Create a new app or select an existing one
  3. Navigate to App Settings > REST Configuration
  4. Note your App ID (Client ID) and App Secret (Client Secret)
  5. Configure your Site URL and Callback URL (e.g., https://yourapp.com/auth/clover_oauth2/callback)

For more details, read the Clover OAuth 2.0 documentation.

Usage

Standalone OmniAuth

Add the middleware to your application in config/initializers/omniauth.rb:

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :clover_oauth2,
           ENV['CLOVER_CLIENT_ID'],
           ENV['CLOVER_CLIENT_SECRET'],
           sandbox: Rails.env.development?
end

# Required for OmniAuth 2.0+
OmniAuth.config.allowed_request_methods = %i[get post]

You can now access the OmniAuth Clover OAuth2 URL at /auth/clover_oauth2.

With Devise

Add the provider to your Devise configuration in config/initializers/devise.rb:

config.omniauth :clover_oauth2,
                ENV['CLOVER_CLIENT_ID'],
                ENV['CLOVER_CLIENT_SECRET'],
                sandbox: !Rails.env.production?

Do not create a separate config/initializers/omniauth.rb file when using Devise, as it will conflict.

Add to your routes in config/routes.rb:

devise_for :users, controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }

Make your User model omniauthable in app/models/user.rb:

devise :omniauthable, omniauth_providers: [:clover_oauth2]

Create the callbacks controller at app/controllers/users/omniauth_callbacks_controller.rb:

class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def clover_oauth2
    @user = User.from_omniauth(request.env['omniauth.auth'])

    if @user.persisted?
      flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Clover')
      sign_in_and_redirect @user, event: :authentication
    else
      session['devise.clover_data'] = request.env['omniauth.auth'].except('extra')
      redirect_to new_user_registration_url, alert: @user.errors.full_messages.join("\n")
    end
  end

  def failure
    redirect_to root_path, alert: "Authentication failed: #{failure_message}"
  end
end

Add the from_omniauth method to your User model:

def self.from_omniauth(auth)
  where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
    user.email = auth.info.email
    user.password = Devise.friendly_token[0, 20]
    user.name = auth.info.name
    # Add any other fields you need
  end
end

For your views, create a login link:

<%= link_to "Sign in with Clover", user_clover_oauth2_omniauth_authorize_path, method: :post %>

Configuration Options

Option Default Description
sandbox true Use sandbox environment. Set to false for production.
provider_ignores_state true Clover doesn't properly return the OAuth state parameter.

Example with all options:

provider :clover_oauth2,
         ENV['CLOVER_CLIENT_ID'],
         ENV['CLOVER_CLIENT_SECRET'],
         sandbox: false  # Use production environment

Auth Hash

Here's an example of the authentication hash available in the callback by accessing request.env['omniauth.auth']:

{
  "provider" => "clover_oauth2",
  "uid" => "ABCD1234567890",
  "info" => {
    "email" => "employee@example.com",
    "first_name" => "John",
    "last_name" => "Doe",
    "name" => "John Doe",
    "merchant_id" => "ABCD1234567890",
    "employee_id" => "EFGH0987654321"
  },
  "credentials" => {
    "token" => "ACCESS_TOKEN",
    "refresh_token" => "REFRESH_TOKEN",
    "expires_at" => 1704067200,
    "expires" => true
  },
  "extra" => {
    "merchant_id" => "ABCD1234567890",
    "employee_id" => "EFGH0987654321",
    "business_name" => "Acme Restaurant",
    "raw_info" => {
      "merchant" => {
        "id" => "ABCD1234567890",
        "name" => "Acme Restaurant",
        "address" => { ... }
      },
      "employee" => {
        "id" => "EFGH0987654321",
        "name" => "John Doe",
        "email" => "employee@example.com",
        "role" => "ADMIN"
      }
    }
  }
}

Sandbox vs Production

Clover uses different URLs for sandbox and production environments:

Environment Authorize URL API URL
Sandbox https://sandbox.dev.clover.com https://apisandbox.dev.clover.com
Production https://www.clover.com https://api.clover.com

The gem automatically handles this based on the sandbox option.

Important: You need separate Clover apps for sandbox and production. Make sure to use the correct credentials for each environment.

Clover OAuth2 Specifics

This gem handles several Clover-specific OAuth2 behaviors:

  1. JSON Token Exchange: Clover requires Content-Type: application/json for the token exchange endpoint (not the standard application/x-www-form-urlencoded).

  2. Split Endpoints: Clover uses different domains for authorization (www.clover.com) and token/API endpoints (api.clover.com).

  3. State Parameter: Clover doesn't properly return the OAuth state parameter, so this gem sets provider_ignores_state to true by default.

  4. Merchant/Employee Context: The OAuth callback includes merchant_id and employee_id query parameters, which are used to fetch additional user information.

  5. Token Refresh Endpoint: Clover uses /oauth/v2/refresh for token refresh (not the standard /oauth/token), with a JSON body.

  6. Single-Use Refresh Tokens: Refresh tokens are invalidated after use. Always save the new refresh token returned from each refresh call.

Storing Tokens

You'll likely want to store the access token and refresh token to make API calls later:

def self.from_omniauth(auth)
  user = where(provider: auth.provider, uid: auth.uid).first_or_create do |u|
    u.email = auth.info.email
    u.password = Devise.friendly_token[0, 20]
  end

  # Update tokens on each login
  user.update(
    clover_access_token: auth.credentials.token,
    clover_refresh_token: auth.credentials.refresh_token,
    clover_token_expires_at: Time.at(auth.credentials.expires_at),
    clover_merchant_id: auth.info.merchant_id,
    clover_employee_id: auth.info.employee_id
  )

  user
end

Token Refresh

Clover access tokens expire (typically after 30 minutes, configurable in your app settings). This gem includes a TokenClient class to easily refresh tokens in your Rails app.

Important: Single-Use Refresh Tokens

Clover refresh tokens are single-use. Each time you refresh, you get a new refresh token and the old one is invalidated. Always save the new refresh token returned from the refresh call!

How It Works

The TokenClient uses Clover's OAuth v2 refresh endpoint (/oauth/v2/refresh) with JSON body containing client_id and refresh_token.

Basic Usage

# Create a client instance
client = OmniAuth::CloverOauth2::TokenClient.new(
  client_id: ENV['CLOVER_CLIENT_ID'],
  client_secret: ENV['CLOVER_CLIENT_SECRET'],
  sandbox: Rails.env.development?
)

# Refresh an expired token
result = client.refresh_token(user.clover_refresh_token)

if result.success?
  user.update!(
    clover_access_token: result.access_token,
    clover_refresh_token: result.refresh_token,
    clover_token_expires_at: Time.at(result.expires_at)
  )
else
  Rails.logger.error "Token refresh failed: #{result.error}"
end

Check Token Expiration

# Check if token is expired (with 5-minute buffer by default)
client.token_expired?(user.clover_token_expires_at)

# Custom buffer (e.g., refresh 1 hour before expiry)
client.token_expired?(user.clover_token_expires_at, buffer_seconds: 3600)

Rails Service Example

# app/services/clover_api_service.rb
class CloverApiService
  def initialize(user)
    @user = user
    @client = OmniAuth::CloverOauth2::TokenClient.new(
      client_id: ENV['CLOVER_CLIENT_ID'],
      client_secret: ENV['CLOVER_CLIENT_SECRET'],
      sandbox: !Rails.env.production?
    )
  end

  def with_valid_token
    refresh_if_expired!
    yield @user.clover_access_token
  end

  private

  def refresh_if_expired!
    return unless @client.token_expired?(@user.clover_token_expires_at)

    result = @client.refresh_token(@user.clover_refresh_token)
    raise "Token refresh failed: #{result.error}" unless result.success?

    @user.update!(
      clover_access_token: result.access_token,
      clover_refresh_token: result.refresh_token,
      clover_token_expires_at: Time.at(result.expires_at)
    )
  end
end

# Usage
CloverApiService.new(current_user).with_valid_token do |token|
  # Make API calls with valid token
  response = Faraday.get("https://api.clover.com/v3/merchants/#{merchant_id}") do |req|
    req.headers['Authorization'] = "Bearer #{token}"
  end
end

TokenResult Object

The refresh_token method returns a TokenResult object with:

Method Description
success? Returns true if refresh succeeded
failure? Returns true if refresh failed
access_token The new access token
refresh_token The new refresh token
expires_at Unix timestamp when token expires
expires_in Seconds until token expires
error Error message if failed
raw_response Full response hash from Clover

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/dan1d/omniauth-clover-oauth2.

  1. Fork it
  2. Create your feature branch (git checkout -b feature/my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin feature/my-new-feature)
  5. Create a new Pull Request

Development

After checking out the repo, run bundle install to install dependencies. Then, run bundle exec rspec to run the tests.

bundle install
bundle exec rspec
bundle exec rubocop

License

The gem is available as open source under the terms of the MIT License.