Module: JWT

Extended by:
Json
Defined in:
lib/jwt.rb,
lib/jwt/json.rb

Defined Under Namespace

Modules: Json Classes: DecodeError, ExpiredSignature, ImmatureSignature, IncorrectAlgorithm, InvalidAudError, InvalidIatError, InvalidIssuerError, InvalidJtiError, InvalidSubError, VerificationError

Constant Summary collapse

NAMED_CURVES =
{
  'prime256v1' => 'ES256',
  'secp384r1' => 'ES384',
  'secp521r1' => 'ES512',
}

Class Method Summary collapse

Methods included from Json

decode_json, encode_json

Class Method Details

.base64url_decode(str) ⇒ Object



76
77
78
79
# File 'lib/jwt.rb', line 76

def base64url_decode(str)
  str += '=' * (4 - str.length.modulo(4))
  Base64.decode64(str.tr('-_', '+/'))
end

.base64url_encode(str) ⇒ Object



81
82
83
# File 'lib/jwt.rb', line 81

def base64url_encode(str)
  Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
end

.decode(jwt, key = nil, verify = true, options = {}, &keyfinder) ⇒ Object

Raises:



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/jwt.rb', line 133

def decode(jwt, key=nil, verify=true, options={}, &keyfinder)
  raise JWT::DecodeError.new('Nil JSON web token') unless jwt

  header, payload, signature, signing_input = decoded_segments(jwt, verify)
  raise JWT::DecodeError.new('Not enough or too many segments') unless header && payload

  default_options = {
    :verify_expiration => true,
    :verify_not_before => true,
    :verify_iss => false,
    :verify_iat => false,
    :verify_jti => false,
    :verify_aud => false,
    :verify_sub => false,
    :leeway => 0
  }

  options = default_options.merge(options)

  if verify
    algo, key = signature_algorithm_and_key(header, key, &keyfinder)
    if options[:algorithm] && algo != options[:algorithm]
      raise JWT::IncorrectAlgorithm.new('Expected a different algorithm')
    end
    verify_signature(algo, key, signing_input, signature)
  end

  if options[:verify_expiration] && payload.include?('exp')
    raise JWT::ExpiredSignature.new('Signature has expired') unless payload['exp'].to_i > (Time.now.to_i - options[:leeway])
  end
  if options[:verify_not_before] && payload.include?('nbf')
    raise JWT::ImmatureSignature.new('Signature nbf has not been reached') unless payload['nbf'].to_i < (Time.now.to_i + options[:leeway])
  end
  if options[:verify_iss] && payload.include?('iss')
    raise JWT::InvalidIssuerError.new("Invalid issuer. Expected #{options['iss']}, received #{payload['iss']}") unless payload['iss'].to_s == options['iss'].to_s
  end
  if options[:verify_iat] && payload.include?('iat')
    raise JWT::InvalidIatError.new('Invalid iat') unless (payload['iat'].is_a?(Integer) and payload['iat'].to_i <= Time.now.to_i)
  end
  if options[:verify_aud] && payload.include?('aud')
    if payload['aud'].is_a?(Array)
      raise JWT::InvalidAudError.new('Invalid audience') unless payload['aud'].include?(options['aud'])
    else
      raise JWT::InvalidAudError.new("Invalid audience. Expected #{options['aud']}, received #{payload['aud']}") unless payload['aud'].to_s == options['aud'].to_s
    end
  end
  if options[:verify_sub] && payload.include?('sub')
    raise JWT::InvalidSubError.new("Invalid subject. Expected #{options['sub']}, received #{payload['sub']}") unless payload['sub'].to_s == options['sub'].to_s
  end
  if options[:verify_jti] && payload.include?('jti')
    raise JWT::InvalidJtiError.new('need iat for verify jwt id') unless payload.include?('iat')
    raise JWT::InvalidJtiError.new('Not a uniq jwt id') unless options['jti'].to_s == Digest::MD5.hexdigest("#{key}:#{payload['iat']}")
  end

  return payload,header
end

.decode_header_and_payload(header_segment, payload_segment) ⇒ Object



119
120
121
122
123
# File 'lib/jwt.rb', line 119

def decode_header_and_payload(header_segment, payload_segment)
  header = decode_json(base64url_decode(header_segment))
  payload = decode_json(base64url_decode(payload_segment))
  [header, payload]
end

.decoded_segments(jwt, verify = true) ⇒ Object



125
126
127
128
129
130
131
# File 'lib/jwt.rb', line 125

def decoded_segments(jwt, verify=true)
  header_segment, payload_segment, crypto_segment = raw_segments(jwt, verify)
  header, payload = decode_header_and_payload(header_segment, payload_segment)
  signature = base64url_decode(crypto_segment.to_s) if verify
  signing_input = [header_segment, payload_segment].join('.')
  [header, payload, signature, signing_input]
end

.encode(payload, key, algorithm = 'HS256', header_fields = {}) ⇒ Object



103
104
105
106
107
108
109
110
# File 'lib/jwt.rb', line 103

def encode(payload, key, algorithm='HS256', header_fields={})
  algorithm ||= 'none'
  segments = []
  segments << encoded_header(algorithm, header_fields)
  segments << encoded_payload(payload)
  segments << encoded_signature(segments.join('.'), key, algorithm)
  segments.join('.')
end

.encoded_header(algorithm = 'HS256', header_fields = {}) ⇒ Object



85
86
87
88
# File 'lib/jwt.rb', line 85

def encoded_header(algorithm='HS256', header_fields={})
  header = {'typ' => 'JWT', 'alg' => algorithm}.merge(header_fields)
  base64url_encode(encode_json(header))
end

.encoded_payload(payload) ⇒ Object



90
91
92
# File 'lib/jwt.rb', line 90

def encoded_payload(payload)
  base64url_encode(encode_json(payload))
end

.encoded_signature(signing_input, key, algorithm) ⇒ Object



94
95
96
97
98
99
100
101
# File 'lib/jwt.rb', line 94

def encoded_signature(signing_input, key, algorithm)
  if algorithm == 'none'
    ''
  else
    signature = sign(algorithm, signing_input, key)
    base64url_encode(signature)
  end
end

.raw_segments(jwt, verify = true) ⇒ Object

Raises:



112
113
114
115
116
117
# File 'lib/jwt.rb', line 112

def raw_segments(jwt, verify=true)
  segments = jwt.split('.')
  required_num_segments = verify ? [3] : [2,3]
  raise JWT::DecodeError.new('Not enough or too many segments') unless required_num_segments.include? segments.length
  segments
end

.secure_compare(a, b) ⇒ Object

From devise constant-time comparison algorithm to prevent timing attacks



217
218
219
220
221
222
223
224
# File 'lib/jwt.rb', line 217

def secure_compare(a, b)
  return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize
  l = a.unpack "C#{a.bytesize}"

  res = 0
  b.each_byte { |byte| res |= byte ^ l.shift }
  res == 0
end

.sign(algorithm, msg, key) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
# File 'lib/jwt.rb', line 32

def sign(algorithm, msg, key)
  if ['HS256', 'HS384', 'HS512'].include?(algorithm)
    sign_hmac(algorithm, msg, key)
  elsif ['RS256', 'RS384', 'RS512'].include?(algorithm)
    sign_rsa(algorithm, msg, key)
  elsif ['ES256', 'ES384', 'ES512'].include?(algorithm)
    sign_ecdsa(algorithm, msg, key)
  else
    raise NotImplementedError.new('Unsupported signing method')
  end
end

.sign_ecdsa(algorithm, msg, private_key) ⇒ Object



48
49
50
51
52
53
54
55
56
# File 'lib/jwt.rb', line 48

def sign_ecdsa(algorithm, msg, private_key)
  key_algorithm = NAMED_CURVES[private_key.group.curve_name]
  if algorithm != key_algorithm
    raise IncorrectAlgorithm.new("payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided")
  end

  digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
  private_key.dsa_sign_asn1(digest.digest(msg))
end

.sign_hmac(algorithm, msg, key) ⇒ Object



72
73
74
# File 'lib/jwt.rb', line 72

def sign_hmac(algorithm, msg, key)
  OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
end

.sign_rsa(algorithm, msg, private_key) ⇒ Object



44
45
46
# File 'lib/jwt.rb', line 44

def sign_rsa(algorithm, msg, private_key)
  private_key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
end

.signature_algorithm_and_key(header, key, &keyfinder) ⇒ Object



190
191
192
193
194
195
# File 'lib/jwt.rb', line 190

def signature_algorithm_and_key(header, key, &keyfinder)
  if keyfinder
    key = keyfinder.call(header)
  end
  [header['alg'], key]
end

.verify_ecdsa(algorithm, public_key, signing_input, signature) ⇒ Object



62
63
64
65
66
67
68
69
70
# File 'lib/jwt.rb', line 62

def verify_ecdsa(algorithm, public_key, signing_input, signature)
  key_algorithm = NAMED_CURVES[public_key.group.curve_name]
  if algorithm != key_algorithm
    raise IncorrectAlgorithm.new("payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided")
  end

  digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
  public_key.dsa_verify_asn1(digest.digest(signing_input), signature)
end

.verify_rsa(algorithm, public_key, signing_input, signature) ⇒ Object



58
59
60
# File 'lib/jwt.rb', line 58

def verify_rsa(algorithm, public_key, signing_input, signature)
  public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
end

.verify_signature(algo, key, signing_input, signature) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/jwt.rb', line 197

def verify_signature(algo, key, signing_input, signature)
  begin
    if ['HS256', 'HS384', 'HS512'].include?(algo)
      raise JWT::VerificationError.new('Signature verification failed') unless secure_compare(signature, sign_hmac(algo, signing_input, key))
    elsif ['RS256', 'RS384', 'RS512'].include?(algo)
      raise JWT::VerificationError.new('Signature verification failed') unless verify_rsa(algo, key, signing_input, signature)
    elsif ['ES256', 'ES384', 'ES512'].include?(algo)
      raise JWT::VerificationError.new('Signature verification failed') unless verify_ecdsa(algo, key, signing_input, signature)
    else
      raise JWT::VerificationError.new('Algorithm not supported')
    end
  rescue OpenSSL::PKey::PKeyError
    raise JWT::VerificationError.new('Signature verification failed')
  ensure
    OpenSSL.errors.clear
  end
end