OmniAuth Clover OAuth2 Strategy
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 installOr install it yourself:
$ gem install omniauth-clover-oauth2Clover Developer Setup
- Go to the Clover Developer Dashboard
- Create a new app or select an existing one
- Navigate to App Settings > REST Configuration
- Note your App ID (Client ID) and App Secret (Client Secret)
- 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
endAdd 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
endFor 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 environmentAuth 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:
-
JSON Token Exchange: Clover requires
Content-Type: application/jsonfor the token exchange endpoint (not the standardapplication/x-www-form-urlencoded). -
Split Endpoints: Clover uses different domains for authorization (
www.clover.com) and token/API endpoints (api.clover.com). -
State Parameter: Clover doesn't properly return the OAuth state parameter, so this gem sets
provider_ignores_statetotrueby default. -
Merchant/Employee Context: The OAuth callback includes
merchant_idandemployee_idquery parameters, which are used to fetch additional user information. -
Token Refresh Endpoint: Clover uses
/oauth/v2/refreshfor token refresh (not the standard/oauth/token), with a JSON body. -
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
endToken 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}"
endCheck 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
endTokenResult 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.
- Fork it
- Create your feature branch (
git checkout -b feature/my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin feature/my-new-feature) - 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 rubocopLicense
The gem is available as open source under the terms of the MIT License.