Module: EasyTypeJWT

Extended by:
Json
Defined in:
lib/entitlement/easy_type_jwt.rb,
lib/entitlement/easy_type_jwt.rb

Overview

encoding: utf-8

Defined Under Namespace

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

Class Method Summary collapse

Methods included from Json

decode_json, encode_json

Class Method Details

.asn1_to_raw(signature, public_key) ⇒ Object



245
246
247
248
# File 'lib/entitlement/easy_type_jwt.rb', line 245

def asn1_to_raw(signature, public_key)
  byte_size = (public_key.group.degree + 7) / 8
  OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
end

.base64url_decode(str) ⇒ Object



92
93
94
95
# File 'lib/entitlement/easy_type_jwt.rb', line 92

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

.base64url_encode(str) ⇒ Object



97
98
99
# File 'lib/entitlement/easy_type_jwt.rb', line 97

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

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



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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/entitlement/easy_type_jwt.rb', line 149

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

  header, payload, signature, signing_input = decoded_segments(jwt, verify)
  fail EasyTypeJWT::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]
      fail EasyTypeJWT::IncorrectAlgorithm.new('Expected a different algorithm')
    end
    verify_signature(algo, key, signing_input, signature)
  end

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

  [payload, header]
end

.decode_header_and_payload(header_segment, payload_segment) ⇒ Object



135
136
137
138
139
# File 'lib/entitlement/easy_type_jwt.rb', line 135

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



141
142
143
144
145
146
147
# File 'lib/entitlement/easy_type_jwt.rb', line 141

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



119
120
121
122
123
124
125
126
# File 'lib/entitlement/easy_type_jwt.rb', line 119

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



101
102
103
104
# File 'lib/entitlement/easy_type_jwt.rb', line 101

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



106
107
108
# File 'lib/entitlement/easy_type_jwt.rb', line 106

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

.encoded_signature(signing_input, key, algorithm) ⇒ Object



110
111
112
113
114
115
116
117
# File 'lib/entitlement/easy_type_jwt.rb', line 110

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



128
129
130
131
132
133
# File 'lib/entitlement/easy_type_jwt.rb', line 128

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

.raw_to_asn1(signature, private_key) ⇒ Object



238
239
240
241
242
243
# File 'lib/entitlement/easy_type_jwt.rb', line 238

def raw_to_asn1(signature, private_key)
  byte_size = (private_key.group.degree + 7) / 8
  r = signature[0..(byte_size - 1)]
  s = signature[byte_size..-1]
  OpenSSL::ASN1::Sequence.new([r, s].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
end

.secure_compare(a, b) ⇒ Object

From devise constant-time comparison algorithm to prevent timing attacks



229
230
231
232
233
234
235
236
# File 'lib/entitlement/easy_type_jwt.rb', line 229

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



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/entitlement/easy_type_jwt.rb', line 48

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
    fail NotImplementedError.new('Unsupported signing method')
  end
end

.sign_ecdsa(algorithm, msg, private_key) ⇒ Object



64
65
66
67
68
69
70
71
72
# File 'lib/entitlement/easy_type_jwt.rb', line 64

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

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

.sign_hmac(algorithm, msg, key) ⇒ Object



88
89
90
# File 'lib/entitlement/easy_type_jwt.rb', line 88

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



60
61
62
# File 'lib/entitlement/easy_type_jwt.rb', line 60

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



206
207
208
209
# File 'lib/entitlement/easy_type_jwt.rb', line 206

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

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



78
79
80
81
82
83
84
85
86
# File 'lib/entitlement/easy_type_jwt.rb', line 78

def verify_ecdsa(algorithm, public_key, signing_input, signature)
  key_algorithm = NAMED_CURVES[public_key.group.curve_name]
  if algorithm != key_algorithm
    fail 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), raw_to_asn1(signature, public_key))
end

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



74
75
76
# File 'lib/entitlement/easy_type_jwt.rb', line 74

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



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/entitlement/easy_type_jwt.rb', line 211

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