Module: Philiprehberger::JwtKit::Decoder

Defined in:
lib/philiprehberger/jwt_kit/decoder.rb

Overview

Decodes and validates JWT tokens.

Class Method Summary collapse

Class Method Details

.base64url_decode(data) ⇒ String

Base64url-decodes a string.

Parameters:

  • data (String)

    base64url-encoded string

Returns:

  • (String)

    decoded string



64
65
66
67
68
# File 'lib/philiprehberger/jwt_kit/decoder.rb', line 64

def base64url_decode(data)
  Base64.urlsafe_decode64(data)
rescue ArgumentError
  raise DecodeError, 'Invalid token: malformed base64'
end

.decode(token, config) ⇒ Hash

Decodes a JWT token and validates its claims.

Parameters:

  • token (String)

    JWT token string

  • config (Configuration)

    JWT configuration

Returns:

  • (Hash)

    decoded payload with string keys

Raises:



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/philiprehberger/jwt_kit/decoder.rb', line 18

def decode(token, config)
  raise DecodeError, 'Token must be a string' unless token.is_a?(String)

  parts = token.split('.', -1)
  raise DecodeError, 'Invalid token format: expected 3 segments' unless parts.length == 3

  header_segment, payload_segment, signature_segment = parts

  signing_secret = resolve_secret(header_segment, config)
  verify_signature!("#{header_segment}.#{payload_segment}", signature_segment, config, secret: signing_secret)

  payload = JSON.parse(base64url_decode(payload_segment))

  validate_expiration!(payload)
  validate_not_before!(payload)
  validate_issuer!(payload, config)
  validate_audience!(payload, config)

  payload
rescue JSON::ParserError
  raise DecodeError, 'Invalid token: malformed JSON'
end

.peek(token) ⇒ Hash

Decodes a JWT token without verifying the signature.

Parameters:

  • token (String)

    JWT token

Returns:

  • (Hash)

    with :header and :payload keys

Raises:



46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/philiprehberger/jwt_kit/decoder.rb', line 46

def peek(token)
  raise DecodeError, 'Token must be a string' unless token.is_a?(String)

  parts = token.split('.', -1)
  raise DecodeError, 'Invalid token format: expected 3 segments' unless parts.length == 3

  header = JSON.parse(base64url_decode(parts[0]))
  payload = JSON.parse(base64url_decode(parts[1]))

  { header: header, payload: payload }
rescue JSON::ParserError
  raise DecodeError, 'Invalid token: malformed JSON'
end

.resolve_secret(header_segment, config) ⇒ String?

Resolves the signing secret from the secrets array by kid, or falls back to config.secret.

Parameters:

  • header_segment (String)

    base64url-encoded header

  • config (Configuration)

    JWT configuration

Returns:

  • (String, nil)

    the resolved secret



75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/philiprehberger/jwt_kit/decoder.rb', line 75

def resolve_secret(header_segment, config)
  if config.secrets.is_a?(Array) && !config.secrets.empty?
    header = JSON.parse(base64url_decode(header_segment))
    kid = header['kid']
    if kid
      entry = config.secrets.find { |s| (s[:kid] || s['kid']) == kid }
      raise InvalidSignature, "Unknown kid: #{kid}" unless entry

      return entry[:secret] || entry['secret']
    end
  end
  config.secret
end

.secure_compare(a, b) ⇒ Boolean

Constant-time string comparison to prevent timing attacks.

Parameters:

  • a (String)

    first string

  • b (String)

    second string

Returns:

  • (Boolean)

    true if the strings are equal



154
155
156
157
158
159
160
161
162
# File 'lib/philiprehberger/jwt_kit/decoder.rb', line 154

def secure_compare(a, b)
  return false unless a.bytesize == b.bytesize

  left = a.unpack('C*')
  right = b.unpack('C*')
  result = 0
  left.each_with_index { |byte, i| result |= byte ^ right[i] }
  result.zero?
end

.validate_audience!(payload, config) ⇒ Object

Validates the audience claim.

Parameters:

  • payload (Hash)

    decoded payload

  • config (Configuration)

    JWT configuration

Raises:



139
140
141
142
143
144
145
146
147
# File 'lib/philiprehberger/jwt_kit/decoder.rb', line 139

def validate_audience!(payload, config)
  return unless config.audience

  token_aud = Array(payload['aud'])
  expected_aud = Array(config.audience)
  return if expected_aud.intersect?(token_aud)

  raise InvalidAudience, "Invalid audience: expected #{config.audience}"
end

.validate_expiration!(payload) ⇒ Object

Validates the expiration claim.

Parameters:

  • payload (Hash)

    decoded payload

Raises:



105
106
107
108
109
110
# File 'lib/philiprehberger/jwt_kit/decoder.rb', line 105

def validate_expiration!(payload)
  exp = payload['exp']
  return unless exp

  raise TokenExpired, 'Token has expired' if exp.to_i <= Time.now.to_i
end

.validate_issuer!(payload, config) ⇒ Object

Validates the issuer claim.

Parameters:

  • payload (Hash)

    decoded payload

  • config (Configuration)

    JWT configuration

Raises:



128
129
130
131
132
# File 'lib/philiprehberger/jwt_kit/decoder.rb', line 128

def validate_issuer!(payload, config)
  return unless config.issuer

  raise InvalidIssuer, "Invalid issuer: expected #{config.issuer}" unless payload['iss'] == config.issuer
end

.validate_not_before!(payload) ⇒ Object

Validates the not-before claim.

Parameters:

  • payload (Hash)

    decoded payload

Raises:



116
117
118
119
120
121
# File 'lib/philiprehberger/jwt_kit/decoder.rb', line 116

def validate_not_before!(payload)
  nbf = payload['nbf']
  return unless nbf

  raise TokenNotYetValid, 'Token is not yet valid' if nbf.to_i > Time.now.to_i
end

.verify_signature!(signing_input, signature, config, secret: nil) ⇒ Object

Verifies the token signature.

Parameters:

  • signing_input (String)

    header.payload string

  • signature (String)

    base64url-encoded signature

  • config (Configuration)

    JWT configuration

  • secret (String, nil) (defaults to: nil)

    optional secret override

Raises:



96
97
98
99
# File 'lib/philiprehberger/jwt_kit/decoder.rb', line 96

def verify_signature!(signing_input, signature, config, secret: nil)
  expected = Encoder.sign(signing_input, config, secret: secret)
  raise InvalidSignature, 'Token signature is invalid' unless secure_compare(expected, signature)
end