Module: JWT
- Extended by:
- Json
- Defined in:
- lib/jwt.rb,
lib/jwt/json.rb
Overview
JSON Web Token implementation
Should be up to date with the latest spec: self-issued.info/docs/draft-jones-json-web-token-06.html
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
- .asn1_to_raw(signature, public_key) ⇒ Object
- .base64url_decode(str) ⇒ Object
- .base64url_encode(str) ⇒ Object
- .decode(jwt, key = nil, verify = true, options = {}, &keyfinder) ⇒ Object
- .decode_header_and_payload(header_segment, payload_segment) ⇒ Object
- .decoded_segments(jwt, verify = true) ⇒ Object
- .encode(payload, key, algorithm = 'HS256', header_fields = {}) ⇒ Object
- .encoded_header(algorithm = 'HS256', header_fields = {}) ⇒ Object
- .encoded_payload(payload) ⇒ Object
- .encoded_signature(signing_input, key, algorithm) ⇒ Object
- .raw_segments(jwt, verify = true) ⇒ Object
- .raw_to_asn1(signature, private_key) ⇒ Object
-
.secure_compare(a, b) ⇒ Object
From devise constant-time comparison algorithm to prevent timing attacks.
- .sign(algorithm, msg, key) ⇒ Object
- .sign_ecdsa(algorithm, msg, private_key) ⇒ Object
- .sign_hmac(algorithm, msg, key) ⇒ Object
- .sign_rsa(algorithm, msg, private_key) ⇒ Object
- .signature_algorithm_and_key(header, key, &keyfinder) ⇒ Object
- .verify_ecdsa(algorithm, public_key, signing_input, signature) ⇒ Object
- .verify_rsa(algorithm, public_key, signing_input, signature) ⇒ Object
- .verify_signature(algo, key, signing_input, signature) ⇒ Object
Methods included from Json
Class Method Details
.asn1_to_raw(signature, public_key) ⇒ Object
229 230 231 232 |
# File 'lib/jwt.rb', line 229 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
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
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, ={}, &keyfinder) fail JWT::DecodeError.new('Nil JSON web token') unless jwt header, payload, signature, signing_input = decoded_segments(jwt, verify) fail JWT::DecodeError.new('Not enough or too many segments') unless header && payload = { :verify_expiration => true, :verify_not_before => true, :verify_iss => false, :verify_iat => false, :verify_jti => false, :verify_aud => false, :verify_sub => false, :leeway => 0 } = .merge() if verify algo, key = signature_algorithm_and_key(header, key, &keyfinder) if [:algorithm] && algo != [:algorithm] fail JWT::IncorrectAlgorithm.new('Expected a different algorithm') end verify_signature(algo, key, signing_input, signature) end if [:verify_expiration] && payload.include?('exp') fail JWT::ExpiredSignature.new('Signature has expired') unless payload['exp'].to_i > (Time.now.to_i - [:leeway]) end if [:verify_not_before] && payload.include?('nbf') fail JWT::ImmatureSignature.new('Signature nbf has not been reached') unless payload['nbf'].to_i < (Time.now.to_i + [:leeway]) end if [:verify_iss] && ['iss'] fail JWT::InvalidIssuerError.new("Invalid issuer. Expected #{['iss']}, received #{payload['iss'] || '<none>'}") unless payload['iss'].to_s == ['iss'].to_s end if [:verify_iat] && payload.include?('iat') fail JWT::InvalidIatError.new('Invalid iat') unless payload['iat'].is_a?(Integer) && payload['iat'].to_i <= Time.now.to_i end if [:verify_aud] && ['aud'] if payload['aud'].is_a?(Array) fail JWT::InvalidAudError.new('Invalid audience') unless payload['aud'].include?(['aud'].to_s) else fail JWT::InvalidAudError.new("Invalid audience. Expected #{['aud']}, received #{payload['aud'] || '<none>'}") unless payload['aud'].to_s == ['aud'].to_s end end if [:verify_sub] && payload.include?('sub') fail JWT::InvalidSubError.new("Invalid subject. Expected #{['sub']}, received #{payload['sub']}") unless payload['sub'].to_s == ['sub'].to_s end if [:verify_jti] && payload.include?('jti') fail JWT::InvalidJtiError.new('need iat for verify jwt id') unless payload.include?('iat') fail JWT::InvalidJtiError.new('Not a uniq jwt id') unless ['jti'].to_s == Digest::MD5.hexdigest("#{key}:#{payload['iat']}") end [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
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] fail JWT::DecodeError.new('Not enough or too many segments') unless required_num_segments.include? segments.length segments end |
.raw_to_asn1(signature, private_key) ⇒ Object
222 223 224 225 226 227 |
# File 'lib/jwt.rb', line 222 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
213 214 215 216 217 218 219 220 |
# File 'lib/jwt.rb', line 213 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 fail 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 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
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 |
# File 'lib/jwt.rb', line 190 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
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 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
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
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/jwt.rb', line 195 def verify_signature(algo, key, signing_input, signature) if ['HS256', 'HS384', 'HS512'].include?(algo) fail JWT::VerificationError.new('Signature verification failed') unless secure_compare(signature, sign_hmac(algo, signing_input, key)) elsif ['RS256', 'RS384', 'RS512'].include?(algo) fail JWT::VerificationError.new('Signature verification failed') unless verify_rsa(algo, key, signing_input, signature) elsif ['ES256', 'ES384', 'ES512'].include?(algo) fail JWT::VerificationError.new('Signature verification failed') unless verify_ecdsa(algo, key, signing_input, signature) else fail JWT::VerificationError.new('Algorithm not supported') end rescue OpenSSL::PKey::PKeyError raise JWT::VerificationError.new('Signature verification failed') ensure OpenSSL.errors.clear end |