Module: GRApiManager::JWT

Defined in:
lib/gr_api_manager.rb

Overview


JWT — Zero-dependency JSON Web Token encoder and decoder (HS256).

Defined Under Namespace

Classes: DecodeError, ExpiredSignature

Class Method Summary collapse

Class Method Details

.base64url_decode(str) ⇒ Object



49
50
51
52
53
54
# File 'lib/gr_api_manager.rb', line 49

def self.base64url_decode(str)
  padded = str + ('=' * ((4 - (str.length % 4)) % 4))
  Base64.urlsafe_decode64(padded)
rescue ArgumentError => e
  raise DecodeError, "Invalid Base64URL string: #{e.message}"
end

.base64url_encode(str) ⇒ Object



45
46
47
# File 'lib/gr_api_manager.rb', line 45

def self.base64url_encode(str)
  Base64.urlsafe_encode64(str, padding: false)
end

.decode(token, secret) ⇒ Object

Decodes and verifies a JWT token. Returns the payload hash with symbolized keys.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/gr_api_manager.rb', line 79

def self.decode(token, secret)
  raise ArgumentError, "JWT secret cannot be blank" if secret.to_s.strip.empty?
  raise DecodeError, "Token cannot be blank" if token.nil? || token.to_s.strip.empty?

  parts = token.to_s.split('.')
  raise DecodeError, "Invalid JWT format. Expected 3 segments separated by dots." unless parts.size == 3

  header_b64, payload_b64, signature_b64 = parts
  signing_input = "#{header_b64}.#{payload_b64}"

  digest       = OpenSSL::Digest.new('sha256')
  expected_sig = OpenSSL::HMAC.digest(digest, secret.to_s, signing_input)
  actual_sig   = base64url_decode(signature_b64)

  is_valid = if OpenSSL.respond_to?(:secure_compare)
               OpenSSL.secure_compare(expected_sig, actual_sig)
             else
               expected_sig == actual_sig
             end

  raise DecodeError, "Invalid JWT signature" unless is_valid

  payload_json = base64url_decode(payload_b64)
  payload      = JSON.parse(payload_json, symbolize_names: true)

  if payload[:exp]
    exp_time = payload[:exp].to_i
    raise ExpiredSignature, "JWT signature has expired" if Time.now.to_i > exp_time
  end

  payload
rescue JSON::ParserError => e
  raise DecodeError, "Invalid JSON payload in token: #{e.message}"
end

.encode(payload, secret, exp: nil, algorithm: 'HS256') ⇒ Object

Encodes a payload hash into a JWT token signed with HMAC-SHA256. Options:

exp: Integer – expiration timestamp (epoch in seconds).

Raises:

  • (ArgumentError)


59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/gr_api_manager.rb', line 59

def self.encode(payload, secret, exp: nil, algorithm: 'HS256')
  raise ArgumentError, "JWT secret cannot be blank" if secret.to_s.strip.empty?

  data = payload.dup
  data = data.transform_keys(&:to_sym) if data.is_a?(Hash)
  data[:exp] = exp.to_i if exp

  header = { typ: 'JWT', alg: algorithm }
  header_b64    = base64url_encode(header.to_json)
  payload_b64   = base64url_encode(data.to_json)
  signing_input = "#{header_b64}.#{payload_b64}"

  digest        = OpenSSL::Digest.new('sha256')
  signature     = OpenSSL::HMAC.digest(digest, secret.to_s, signing_input)
  signature_b64 = base64url_encode(signature)

  "#{signing_input}.#{signature_b64}"
end