Module: ApiGuard::JwtAuth::JsonWebToken

Included in:
Test::ControllerHelper
Defined in:
lib/api_guard/jwt_auth/json_web_token.rb

Overview

Common module for JWT operations

Class Method Summary collapse

Class Method Details

.authenticate_and_set_resource(resource_name) ⇒ Object

Authenticate the JWT token and set resource



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 156

def self.authenticate_and_set_resource(resource_name)
  @resource_name = resource_name

  @token = request.headers['Authorization']&.split('Bearer ')&.last
  return render_error(401, message: I18n.t('api_guard.access_token.missing')) unless @token

  authenticate_token

  # Render error response only if no resource found and no previous render happened
  render_error(401, message: I18n.t('api_guard.access_token.invalid')) if !current_resource && !performed?
rescue JWT::DecodeError => e
  if e.message == 'Signature has expired'
    render_error(401, message: I18n.t('api_guard.access_token.expired'))
  else
    render_error(401, message: I18n.t('api_guard.access_token.invalid'))
  end
end

.authenticate_tokenObject

Authenticate the resource with the ‘{resource_name}_id’ in the decoded JWT token and also, check for valid issued at time and not blacklisted

Also, set “current_{resource_name}” method and “@current_{resource_name}” instance variable for accessing the authenticated resource



204
205
206
207
208
209
210
211
212
213
214
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 204

def self.authenticate_token
  return unless decode_token

  resource = find_resource_from_token(@resource_name.classify.constantize)

  if resource && valid_issued_at?(resource) && !blacklisted?(resource)
    define_current_resource_accessors(resource)
  else
    render_error(401, message: I18n.t('api_guard.access_token.invalid'))
  end
end

.blacklist_tokenObject

Blacklist the current JWT token from future access



133
134
135
136
137
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 133

def self.blacklist_token

  return unless token_blacklisting_enabled?(current_resource)
  blacklisted_tokens_for(current_resource).create(token: @token, expire_at: Time.at(@decoded_token[:exp]).utc)
end

.blacklisted?(resource) ⇒ Boolean

Returns whether the JWT token is blacklisted or not

Returns:

  • (Boolean)


126
127
128
129
130
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 126

def self.blacklisted?(resource)
  return false unless token_blacklisting_enabled?(resource)

  blacklisted_tokens_for(resource).exists?(token: @token)
end

.blacklisted_token_association(resource) ⇒ Object

blacklisted ======================================================



112
113
114
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 112

def self.blacklisted_token_association(resource)
  resource.class.blacklisted_token_association
end

.blacklisted_tokens_for(resource) ⇒ Object



120
121
122
123
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 120

def self.blacklisted_tokens_for(resource)
  blacklisted_token_association = blacklisted_token_association(resource)
  resource.send(blacklisted_token_association)
end

.create_token_and_set_header(resource, resource_name) ⇒ Object

Create tokens and set response headers



51
52
53
54
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 51

def self.create_token_and_set_header(resource, resource_name)
  access_token, refresh_token = jwt_and_refresh_token(resource, resource_name)
  set_token_headers(access_token, refresh_token)
end

.current_resourceObject



223
224
225
226
227
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 223

def self.current_resource
  return unless respond_to?("current_#{@resource_name}")

  public_send("current_#{@resource_name}")
end

.current_timeObject



10
11
12
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 10

def self.current_time
  @current_time ||= Time.now.utc
end

.decode(token, verify = true) ⇒ Object

Decode the JWT token and return the payload



28
29
30
31
32
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 28

def self.decode(token, verify = true)
  HashWithIndifferentAccess.new(
    JWT.decode(token, ApiGuard.token_signing_secret, verify, verify_iat: true)[0]
  )
end

.decode_tokenObject

Decode the JWT token and don’t verify token expiry for refresh token API request



176
177
178
179
180
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 176

def self.decode_token
  # TODO: Set token refresh controller dynamic
  verify_token = (controller_name != 'tokens' || action_name != 'create')
  @decoded_token = decode(@token, verify_token)
end

.define_current_resource_accessors(resource) ⇒ Object

Defines “current_{resource_name}” method and “@current_{resource_name}” instance variable that returns “resource” value



192
193
194
195
196
197
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 192

def self.define_current_resource_accessors(resource)
  define_singleton_method("current_#{@resource_name}") do
    instance_variable_get("@current_#{@resource_name}") ||
      instance_variable_set("@current_#{@resource_name}", resource)
  end
end

.destroy_all_refresh_tokens(resource) ⇒ Object



105
106
107
108
109
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 105

def self.destroy_all_refresh_tokens(resource)
  return unless refresh_token_enabled?(resource)

  refresh_tokens_for(resource).destroy_all
end

.encode(payload) ⇒ Object

Encode the payload with the secret key and return the JWT token



23
24
25
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 23

def self.encode(payload)
  JWT.encode(payload, ApiGuard.token_signing_secret)
end

.find_refresh_token_of(resource, refresh_token) ⇒ Object



86
87
88
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 86

def self.find_refresh_token_of(resource, refresh_token)
  refresh_tokens_for(resource).find_by_token(refresh_token)
end

.find_resource_from_token(resource_class) ⇒ Object



216
217
218
219
220
221
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 216

def self.find_resource_from_token(resource_class)
  resource_id = @decoded_token[:"#{@resource_name}_id"]
  return if resource_id.blank?

  resource_class.find_by(id: resource_id)
end

.invalidate_old_jwt_tokens(resource) ⇒ Object

Set token issued at to current timestamp to restrict access to old access(JWT) tokens



65
66
67
68
69
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 65

def self.invalidate_old_jwt_tokens(resource)
  return unless ApiGuard.invalidate_old_tokens_on_password_change

  resource.token_issued_at = Time.at(token_issued_at).utc
end

.jwt_and_refresh_token(resource, resource_name, expired_token = false) ⇒ Object

Create a JWT token with resource detail in payload. Also, create refresh token if enabled for the resource.

This creates expired JWT token if the argument ‘expired_token’ is true which can be used for testing.



38
39
40
41
42
43
44
45
46
47
48
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 38

def self.jwt_and_refresh_token(resource, resource_name, expired_token = false)
  payload = {
    "#{resource_name}_id": resource.id,
    exp: expired_token ? token_issued_at : token_expire_at,
    iat: token_issued_at
  }

  # Add custom data in the JWT token payload
  payload.merge!(resource.jwt_token_payload) if resource.respond_to?(:jwt_token_payload)
  [self.encode(payload), self.new_refresh_token(resource)]
end

.method_missing(name, *args) ⇒ Object



140
141
142
143
144
145
146
147
148
149
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 140

def self.method_missing(name, *args)
  method_name = name.to_s

  if method_name.start_with?('authenticate_and_set_')
    resource_name = method_name.split('authenticate_and_set_')[1]
    authenticate_and_set_resource(resource_name)
  else
    super
  end
end

.new_refresh_token(resource) ⇒ Object

Create a new refresh_token for the current resource



99
100
101
102
103
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 99

def self.new_refresh_token(resource)
  return unless refresh_token_enabled?(resource)

  refresh_tokens_for(resource).create(token: uniq_refresh_token(resource)).token
end

.refresh_token_association(resource) ⇒ Object

refresh token code=======================================



73
74
75
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 73

def self.refresh_token_association(resource)
  resource.class.refresh_token_association
end

.refresh_token_enabled?(resource) ⇒ Boolean

Returns:

  • (Boolean)


77
78
79
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 77

def self.refresh_token_enabled?(resource)
  refresh_token_association(resource).present?
end

.refresh_tokens_for(resource) ⇒ Object



81
82
83
84
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 81

def self.refresh_tokens_for(resource)
  refresh_token_association = refresh_token_association(resource)
  resource.send(refresh_token_association)
end

.respond_to_missing?(method_name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


151
152
153
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 151

def self.respond_to_missing?(method_name, include_private = false)
  method_name.to_s.start_with?('authenticate_and_set_') || super
end

.set_token_headers(token, refresh_token = nil) ⇒ Object

Set token details in response headers



57
58
59
60
61
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 57

def self.set_token_headers(token, refresh_token = nil)
  response.headers['Access-Token'] = token
  response.headers['Refresh-Token'] = refresh_token if refresh_token
  response.headers['Expire-At'] = token_expire_at.to_s
end

.token_blacklisting_enabled?(resource) ⇒ Boolean

Returns:

  • (Boolean)


116
117
118
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 116

def self.token_blacklisting_enabled?(resource)
  blacklisted_token_association(resource).present?
end

.token_expire_atObject



14
15
16
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 14

def self.token_expire_at
  @token_expire_at ||= (current_time + ApiGuard.token_validity).to_i
end

.token_issued_atObject



18
19
20
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 18

def self.token_issued_at
  @token_issued_at ||= current_time.to_i
end

.uniq_refresh_token(resource) ⇒ Object

Generate and return unique refresh token for the resource



91
92
93
94
95
96
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 91

def self.uniq_refresh_token(resource)
  loop do
    random_token = SecureRandom.urlsafe_base64
    return random_token unless refresh_tokens_for(resource).exists?(token: random_token)
  end
end

.valid_issued_at?(resource) ⇒ Boolean

Returns whether the JWT token is issued after the last password change Returns true if password hasn’t changed by the user

Returns:

  • (Boolean)


184
185
186
187
188
# File 'lib/api_guard/jwt_auth/json_web_token.rb', line 184

def self.valid_issued_at?(resource)
  return true unless ApiGuard.invalidate_old_tokens_on_password_change

  !resource.token_issued_at || @decoded_token[:iat] >= resource.token_issued_at.to_i
end