Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add refresh token automatic reuse detection #64

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions app/controllers/api_guard/tokens_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down
8 changes: 4 additions & 4 deletions lib/api_guard/jwt_auth/json_web_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand Down
28 changes: 25 additions & 3 deletions lib/api_guard/jwt_auth/refresh_jwt_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand Down