JSON Web Token (JWT) based authentication solution with token refreshing & blacklisting for APIs built on Rails.
This is built using Ruby JWT gem. Currently API Guard supports only HS256 algorithm for cryptographic signing.
- API Guard
Add this line to your application's Gemfile:
gem 'api_guard'
And then execute in your terminal:
$ bundle install
Or install it yourself as:
$ gem install api_guard
Below steps are provided assuming the model in User
.
If you already using Devise authentication in your app refer this Wiki for configuring API Guard Authentication to work with Devise.
Create a model for User with below command
$ rails generate model user name:string email:string:uniq password_digest:string
Then, run migration to create the users
table
$ rails db:migrate
Add has_secure_password
in User
model for password authentication
class User < ApplicationRecord
has_secure_password
end
Then, add bcrypt
gem in your Gemfile which is used by
has_secure_password
for encrypting password and authentication
gem 'bcrypt', '~> 3.1.7'
And then execute in your terminal:
$ bundle install
Add this line to the application routes (config/routes.rb
) file:
api_guard_routes for: 'users'
This will generate default routes such as sign up, sign in, sign out, token refresh, password change for User.
If you already using Devise authentication in your app refer this Wiki for configuring API Guard routes to work with Devise.
This will create an user and responds with access token, refresh token and access token expiry in the response header.
Example request:
# URL
POST "/users/sign_up"
# Request body
{
"email": "user@apiguard.com",
"password": "api_password",
"password_confirmation": "api_password"
}
Example response body:
{
"status": "success",
"message": "Signed up successfully"
}
Example response headers:
Access-Token: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1NDY3MDgwMjAsImlhdCI6MTU0NjcwNjIyMH0.F_JM7fUcKEAq9ZxXMxNb3Os-WeY-tuRYQnKXr_bWo5E
Refresh-Token: Iy9s0S4Lf7Xh9MbFFBdxkw
Expire-At: 1546708020
The access token will only be valid till the expiry time. After the expiry you need to refresh the token and get new access token and refresh token.
You can customize the parameters of this API by overriding the controller code if needed.
This will authenticate the user with email and password and respond with access token, refresh token and access token expiry in the response header.
To make this work, the resource model (User) should have an
authenticate
method as available in has_secure_password. You can use has_secure_password or your own logic to authenticate the user inauthenticate
method.
Example request:
# URL
POST "/users/sign_in"
# Request body
{
"email": "user@apiguard.com",
"password": "api_password"
}
Example response body:
{
"status": "success",
"message": "Signed in successfully"
}
Example response headers:
The response headers for this request will be same as registration API.
You can customize the parameters of this API by overriding the controller code if needed.
To authenticate the API request just add this before_action in the controller:
before_action :authenticate_and_set_user
Send the access token got in sign in API in the Authorization header in the API request as below. Also, make sure you add "Bearer" before the access token in the header value.
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1NDY3MDgwMjAsImlhdCI6MTU0NjcwNjIyMH0.F_JM7fUcKEAq9ZxXMxNb3Os-WeY-tuRYQnKXr_bWo5E
Then, you can get the current authenticated user using below method:
current_user
and also, using below instance variable:
@current_user
Note: Replace
_user
with your model name if your model is not User.
This will work only if token refreshing configured for the resource. Please see token refreshing for details about configuring token refreshing.
Once the access token expires it won't work and the authenticate_and_set_user
method used in before_action in
controller will respond with 401 (Unauthenticated).
To refresh the expired access token and get new access and refresh token you can use this request with both access token and request token (which you got in sign in API) in the request header.
Example request:
# URL
POST "/users/tokens"
# Request header
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1NDY3MDgwMjAsImlhdCI6MTU0NjcwNjIyMH0.F_JM7fUcKEAq9ZxXMxNb3Os-WeY-tuRYQnKXr_bWo5E
Refresh-Token: Iy9s0S4Lf7Xh9MbFFBdxkw
Example response body:
{
"status": "success",
"message": "Token refreshed successfully"
}
Example response headers:
The response headers for this request will be same as registration API.
To change password of an user you can use this request with the access token in the header and new password in the body.
By default, changing password will invalidate all old access tokens and refresh tokens generated for this user and responds with new access token and refresh token.
Example request:
# URL
PATCH "/users/passwords"
# Request header
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1NDY3MDgwMjAsImlhdCI6MTU0NjcwNjIyMH0.F_JM7fUcKEAq9ZxXMxNb3Os-WeY-tuRYQnKXr_bWo5E
# Request body
{
"password": "api_password_new",
"password_confirmation": "api_password_new"
}
Example response body:
{
"status": "success",
"message": "Password changed successfully"
}
Example response headers:
The response headers for this request will be same as registration API.
You can use this request to sign out an user. This will blacklist the current access token from future use if token blacklisting configured.
Example request:
# URL
DELETE "/users/sign_out"
# Request header
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1NDY3MDgwMjAsImlhdCI6MTU0NjcwNjIyMH0.F_JM7fUcKEAq9ZxXMxNb3Os-WeY-tuRYQnKXr_bWo5E
Example response:
{
"status": "success",
"message": "Signed out successfully"
}
You can use this request to delete an user. This will delete the user and its associated refresh tokens.
Example request:
# URL
DELETE "/users/delete"
# Request header
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1NDY3MDgwMjAsImlhdCI6MTU0NjcwNjIyMH0.F_JM7fUcKEAq9ZxXMxNb3Os-WeY-tuRYQnKXr_bWo5E
Example response:
{
"status": "success",
"message": "Account deleted successfully"
}
To configure the API Guard you need to first create an initializer using
$ rails generate api_guard:initializer
This will generate an initializer named api_guard.rb in your app config/initializers directory with default configurations.
config/initializers/api_guard.rb
ApiGuard.setup do |config|
# Validity of the JWT access token
# Default: 1 day
# config.token_validity = 1.day
# Secret key for signing (encoding & decoding) the JWT access token
# Default: 'secret_key_base' from Rails secrets
# config.token_signing_secret = 'my_signing_secret'
# Invalidate old tokens on changing the password
# Default: false
# config.invalidate_old_tokens_on_password_change = false
# Blacklist JWT access token after refreshing
# Default: false
# config.blacklist_token_after_refreshing = false
end
By default, the validity of the JWT access token is 1 day from the creation. Override this by configuring token_validity
config.token_validity = 1.hour # Set one hour validity for access tokens
On accessing the authenticated API with expired access token, API Guard will respond 401 (Unauthenticated) with message "Access token expired".
By default, the secret_key_base
from the Rails secrets will be used for signing (encoding & decoding) the JWT access token.
Override this by configuring token_signing_secret
config.token_signing_secret = 'my_signing_secret'
Note: Avoid committing this token signing secret in your version control (GIT) and always keep this secure. As, exposing this allow anyone to generate JWT access token and give full access to APIs. Better way is storing this value in environment variable or in encrypted secrets (Rails 5.2+)
By default, API Guard will not invalidate old JWT access tokens on changing password. If you need, you can enable it by
configuring invalidate_old_tokens_on_password_change
to true
.
Note: To make this work, a column named
token_issued_at
with datatypedatetime
is needed in the resource table.
config.invalidate_old_tokens_on_password_change = true
If your app allows multiple logins then, you must set this value to true
so that, this prevent access for all logins
(access tokens) on changing the password.
To include token refreshing in your application you need to create a table to store the refresh tokens.
Use below command to create a model RefeshToken
with columns to store the token and the user reference
$ rails generate model refresh_token token:string:uniq user:references
Then, run migration to create the refresh_tokens
table
$ rails db:migrate
Note: Replace
user
in the above command with your model name if your model is not User.
After creating model and table for refresh token configure the association in the resource model using
api_guard_associations
method
class User < ApplicationRecord
api_guard_associations refresh_token: 'refresh_tokens'
has_many :refresh_tokens, dependent: :delete_all
end
If you also have token blacklisting or whitelisting enabled you need to specify both associations as below
api_guard_associations refresh_token: 'refresh_tokens', blacklisted_token: 'blacklisted_tokens', whitelisted_token: 'whitelisted_tokens'
This gem offers two ways to revoke JWT tokens:
- Blacklisting
- Whitelisting
Note: If you are coming from an version 0.3.0 or below, you will need to add the jti column to your blacklisting table like so:
rails g migration AddJtiToBlacklistedTokens jti:string:uniq:index
To include token blacklisting in your application you need to create a table to store the refresh tokens. This will be used to blacklist a JWT access token from future use. The access token will be blacklisted on successful sign out of the resource.
Use below command to create a model RefeshToken
with columns to store the token and the user reference
$ rails generate model blacklisted_token jti:string:uniq:index user:references expire_at:datetime
Then, run migration to create the blacklisted_tokens
table
$ rails db:migrate
Note: Replace
user
in the above command with your model name if your model is not User.
After creating model and table for blacklisted token configure the association in the resource model using
api_guard_associations
method
class User < ApplicationRecord
api_guard_associations blacklisted_token: 'blacklisted_tokens'
has_many :blacklisted_tokens, dependent: :delete_all
end
If you also have token refreshing enabled you need to specify both associations as below
api_guard_associations refresh_token: 'refresh_tokens', blacklisted_token: 'blacklisted_tokens'
And, as this creates rows in blacklisted_tokens
table you need to have a mechanism to delete the expired blacklisted
tokens to prevent this table from growing. One option is to have a CRON job to run a task daily that deletes the
blacklisted tokens that are expired i.e. expire_at < DateTime.now
.
Blacklisting after refreshing token
By default, the JWT access token will not be blacklisted on refreshing the JWT access token. To enable this, you can configure it in API Guard initializer as below,
config.blacklist_token_after_refreshing = true
To include token whitelisting, you must create a table that will store the jti of all the current active & valid tokens which will be used in authorization.
Use the command below to generate a WhitelistedToken
model that will store the jti, expiry and a reference to the user.
$ rails g model WhitelistedToken jti:string:uniq:index user:references exp:datetime
and
$ rake db:migrate
Note: Replace
user
in the above command with your model name if your model is not User.
After creating the model and table for WhitelistedToken
you need to configure the association in the resource model using api_guard_associations
method, like so:
class User < ApplicationRecord
api_guard_associations whitelisted_tokens: :whitelisted_tokens
has_many :whitelisted_tokens, dependent: :delete_all
end
If you want to include refreshing strategies or a blacklist alongside, you would need to use the following line instead:
api_guard_associations refresh_token: :refresh_tokens, blacklisted_token: :whitelisted_tokens
In order to keep the whitelist clean, you can create a CRON job to run a task daily that deletes the whitelisted tokens that are expired i.e. exp < DateTime.now
.
Remove Whitelist after refreshing token
By default, any whitelisted tokens will not be removed after refreshing the access granting access to any clients with the whitelisted tokens.
This setting can be changed like so:
config.remove_whitelist_after_refreshing = true # default is false
You can override the default API Guard controllers and customize the code as your need by generating the controllers in your app
$ rails generate api_guard:controllers users
In above command users
is the scope of the controllers. If needed, you can replace users
with your own scope.
This will generate all default controllers for users
in the directory app/controllers/users.
Then, configure this controller in the routes
api_guard_routes for: 'users', controller: {
registration: 'users/registration',
authentication: 'users/authentication',
passwords: 'users/passwords',
tokens: 'users/tokens'
}
You can also specify the controllers that you need to generate using -c
or --controllers
option.
$ rails generate api_guard:controllers users -c registration authentication
Available controllers: registration, authentication, tokens, passwords
You can skip specific controller routes generated by API Guard
api_guard_routes for: 'users', except: [:registration]
Above config will skip registration related API Guard controller routes for the resource user.
You can also specify only the controller routes you need,
api_guard_routes for: 'users', only: [:authentication]
Available controllers: registration, authentication, tokens, passwords
Customizing the routes
To customize the default routes generated by API Guard you can use api_guard_scope
in the routes.
api_guard_routes for: 'users', except: [:registration]
api_guard_scope 'users' do
post 'account/create' => 'users/registration#create'
delete 'account/delete' => 'users/registration#destroy'
end
Above configuration will replace default registration routes users/sign_up
& users/delete
with account/create
&
account/delete
API Guard uses I18n for success and error messages. You can create your own locale file and customize the messages for any language.
en:
api_guard:
authentication:
signed_in: 'Signed in successfully'
signed_out: 'Signed out successfully'
You can find the complete list of available keys in this file: https://github.com/Gokul595/api_guard/blob/master/config/locales/en.yml
This gem should work with the graphql-ruby without much extra effort. You can see below for an example on how to integrate it:
#### graphql_controller.rb
class GraphqlController < ApplicationController
include ApiGuard::JwtAuth::Authentication
before_action :authenticate
def execute
context = {
controller: self,
current_user: @current_user,
}
#...
end
private
def authenticate
# This method checks for the JWT token in the authentication headers
# and sets the current resource if the token is valid.
# It does not render any feedback so it's perfect for use in Graphql
valid_token?
end
end
class SignIn < GraphQL::Schema::RelayClassicMutation
include ApiGuard::JwtAuth::RefreshJwtToken
include ApiGuard::JwtAuth::JsonWebToken
argument :email, String, required: true
argument :password, String, required: true
field :user, UserType, null: true
field :access_token, String, null: true
field :refresh_token, String, null: true
field :error, String, null: true
def resolve(email:, password:)
user = User.authenticate(email, password)
if user.present?
access_token, refresh_token = jwt_and_refresh_token(user, 'user')
{ user: user, access_token: access_token, refresh_token: refresh_token }
else
# Assumes you are using Devise
error_message = I18n.t(
"devise.failure.invalid",
authentication_keys: User.authentication_keys.join("/")
)
{ error: error_message }
end
end
end
class SignOut < GraphQL::Schema::RelayClassicMutation
include ApiGuard::JwtAuth::Logout
field :signed_out, Boolean, null: false
def resolve
current_user = context[:current_user]
return { signed_out: false } unless current_user.present?
logout! current_user
{ signed_out: true }
end
end
class RefreshAccessToken < GraphQL::Schema::RelayClassicMutation
include ApiGuard::JwtAuth::JsonWebToken
field :refresh_token, String, null: true
field :access_token, String, null: true
def resolve
current_user = context[:current_user]
return { refresh_token: nil, access_token: nil } unless current_user.present?
access_token, refresh_token = jwt_and_refresh_token(user, 'user')
{ access_token: access_token, refresh_token: refresh_token }
end
end
API Guard comes with helper for creating JWT access token and refresh token for the resource which you can use it for testing the controllers of your application.
For using it, just include the helper in your test framework.
RSpec
If you're using RSpec as your test framework then include the helper in spec/rails_helper.rb file
RSpec.configure do |config|
config.include ApiGuard::Test::ControllerHelper
end
Minitest
If you're using Minitest as your test framework then include the helper in your test file
include ApiGuard::Test::ControllerHelper
After including the helper, you can use this method to create the JWT access token and refresh token for the resource
jwt_and_refresh_token(user, 'user')
Where the first argument is the resource(User) object and the second argument is the resource name which is user
.
This method will return two values which is access token and refresh token.
If you need expired JWT access token for testing you can pass the third optional argument value as true
jwt_and_refresh_token(user, 'user', true)
Then, you can set the access token and refresh token in appropriate request header on each test request.
Bug reports and pull requests are welcome on GitHub at https://github.com/Gokul595/api_guard. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
The gem is available as open source under the terms of the MIT License.