From 438571c2f34fe08a3d325c4e4b5b5c55566c664c Mon Sep 17 00:00:00 2001 From: Vasyl Date: Fri, 11 Nov 2022 14:32:19 +0200 Subject: [PATCH] Add refresh token automatic reuse detection --- README.md | 11 +++++++- .../api_guard/tokens_controller.rb | 3 +- lib/api_guard/jwt_auth/json_web_token.rb | 8 +++--- lib/api_guard/jwt_auth/refresh_jwt_token.rb | 28 +++++++++++++++++-- .../templates/tokens_controller.rb | 3 +- 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1a2211a..feb1d09 100644 --- a/README.md +++ b/README.md @@ -424,7 +424,16 @@ To include token refreshing in your application you need to create a table to st Use below command to create a model `RefeshToken` with columns to store the token and the user reference ```bash -$ rails generate model refresh_token token:string:uniq user:references expire_at:datetime +$ rails generate model refresh_token token:string:uniq refresh_token:references user:references active:boolean expire_at:datetime +``` + +Then add `optional: true` for `refresh_token` association in `RefeshToken` model + +```ruby +class RefreshToken < ApplicationRecord + belongs_to :refresh_token, optional: true + belongs_to :user +end ``` Then, run migration to create the `refresh_tokens` table diff --git a/app/controllers/api_guard/tokens_controller.rb b/app/controllers/api_guard/tokens_controller.rb index 8c621fe..fd4e365 100644 --- a/app/controllers/api_guard/tokens_controller.rb +++ b/app/controllers/api_guard/tokens_controller.rb @@ -8,9 +8,8 @@ class TokensController < ApplicationController before_action :find_refresh_token, only: [:create] def create - create_token_and_set_header(current_resource, resource_name) + create_token_and_set_header(current_resource, resource_name, @refresh_token) - @refresh_token.destroy blacklist_token if ApiGuard.blacklist_token_after_refreshing render_success(message: I18n.t('api_guard.access_token.refreshed')) diff --git a/lib/api_guard/jwt_auth/json_web_token.rb b/lib/api_guard/jwt_auth/json_web_token.rb index 0af33f9..4a5dde0 100644 --- a/lib/api_guard/jwt_auth/json_web_token.rb +++ b/lib/api_guard/jwt_auth/json_web_token.rb @@ -35,7 +35,7 @@ def decode(token, verify = true) # # This creates expired JWT token if the argument 'expired_token' is true which can be used for testing. # This creates expired refresh token if the argument 'expired_refresh_token' is true which can be used for testing. - def jwt_and_refresh_token(resource, resource_name, expired_token = false, expired_refresh_token = false) + def jwt_and_refresh_token(resource, resource_name, expired_token = false, expired_refresh_token = false, previous_refresh_token = nil) payload = { "#{resource_name}_id": resource.id, exp: expired_token ? token_issued_at : token_expire_at, @@ -45,12 +45,12 @@ def jwt_and_refresh_token(resource, resource_name, expired_token = false, expire # Add custom data in the JWT token payload payload.merge!(resource.jwt_token_payload) if resource.respond_to?(:jwt_token_payload) - [encode(payload), new_refresh_token(resource, expired_refresh_token)] + [encode(payload), new_refresh_token(resource, expired_refresh_token, previous_refresh_token)] end # Create tokens and set response headers - def create_token_and_set_header(resource, resource_name) - access_token, refresh_token = jwt_and_refresh_token(resource, resource_name) + def create_token_and_set_header(resource, resource_name, previous_refresh_token = nil) + access_token, refresh_token = jwt_and_refresh_token(resource, resource_name, false, false, previous_refresh_token) set_token_headers(access_token, refresh_token) end diff --git a/lib/api_guard/jwt_auth/refresh_jwt_token.rb b/lib/api_guard/jwt_auth/refresh_jwt_token.rb index 9454ee7..68eec06 100644 --- a/lib/api_guard/jwt_auth/refresh_jwt_token.rb +++ b/lib/api_guard/jwt_auth/refresh_jwt_token.rb @@ -23,7 +23,22 @@ def refresh_tokens_for(resource) end def find_refresh_token_of(resource, refresh_token) - refresh_tokens_for(resource).where(token: refresh_token).where('expire_at IS NULL OR expire_at > ?', Time.now.utc).first + token = refresh_tokens_for(resource).where(token: refresh_token).where('expire_at IS NULL OR expire_at > ?', Time.now.utc).first + return nil unless check_token_reuse(resource, token) + + token + end + + def check_token_reuse(resource, refresh_token) + return true if refresh_token.active + destroy_refresh_token_family(resource, refresh_token) + + false + end + + def destroy_refresh_token_family(resource, invalid_refresh_token) + refresh_tokens_for(resource).where(refresh_token_id: invalid_refresh_token.id).destroy_all + invalid_refresh_token.destroy end # Generate and return unique refresh token for the resource @@ -36,10 +51,17 @@ def uniq_refresh_token(resource) # Create a new refresh_token for the current resource # This creates expired refresh_token if the argument 'expired_refresh_token' is true which can be used for testing. - def new_refresh_token(resource, expired_refresh_token = false) + def new_refresh_token(resource, expired_refresh_token = false, previous_refresh_token = nil) return unless refresh_token_enabled?(resource) - refresh_tokens_for(resource).create(token: uniq_refresh_token(resource), expire_at: expired_refresh_token ? Time.now.utc : refresh_token_expire_at).token + new_token_data = { token: uniq_refresh_token(resource), active: 1, expire_at: expired_refresh_token ? Time.now.utc : refresh_token_expire_at } + + if previous_refresh_token + previous_refresh_token.update(active: 0) + new_token_data[:refresh_token_id] = previous_refresh_token.id + end + + refresh_tokens_for(resource).create(new_token_data).token end def destroy_all_refresh_tokens(resource) diff --git a/lib/generators/api_guard/controllers/templates/tokens_controller.rb b/lib/generators/api_guard/controllers/templates/tokens_controller.rb index 0e861a0..2daf274 100644 --- a/lib/generators/api_guard/controllers/templates/tokens_controller.rb +++ b/lib/generators/api_guard/controllers/templates/tokens_controller.rb @@ -4,9 +4,8 @@ class TokensController < ApiGuard::TokensController # before_action :find_refresh_token, only: [:create] # def create - # create_token_and_set_header(current_resource, resource_name) + # create_token_and_set_header(current_resource, resource_name, @refresh_token) # - # @refresh_token.destroy # blacklist_token if ApiGuard.blacklist_token_after_refreshing # # render_success(message: I18n.t('api_guard.access_token.refreshed'))