Module: Philiprehberger::JwtKit::Decoder
- Defined in:
- lib/philiprehberger/jwt_kit/decoder.rb
Overview
Decodes and validates JWT tokens.
Class Method Summary collapse
-
.base64url_decode(data) ⇒ String
Base64url-decodes a string.
-
.decode(token, config) ⇒ Hash
Decodes a JWT token and validates its claims.
-
.peek(token) ⇒ Hash
Decodes a JWT token without verifying the signature.
-
.resolve_secret(header_segment, config) ⇒ String?
Resolves the signing secret from the secrets array by kid, or falls back to config.secret.
-
.secure_compare(a, b) ⇒ Boolean
Constant-time string comparison to prevent timing attacks.
-
.validate_audience!(payload, config) ⇒ Object
Validates the audience claim.
-
.validate_expiration!(payload) ⇒ Object
Validates the expiration claim.
-
.validate_issuer!(payload, config) ⇒ Object
Validates the issuer claim.
-
.validate_not_before!(payload) ⇒ Object
Validates the not-before claim.
-
.verify_signature!(signing_input, signature, config, secret: nil) ⇒ Object
Verifies the token signature.
Class Method Details
.base64url_decode(data) ⇒ String
Base64url-decodes a 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.
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.
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.
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.
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.
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.
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.
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.
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.
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 |