├── .rspec ├── .rubocop.yml ├── Gemfile ├── Manifest ├── .gitignore ├── spec ├── fixtures │ └── certs │ │ ├── ec256-public.pem │ │ ├── ec256-wrong-public.pem │ │ ├── ec384-public.pem │ │ ├── ec384-wrong-public.pem │ │ ├── ec512-public.pem │ │ ├── ec512-wrong-public.pem │ │ ├── rsa-1024-public.pem │ │ ├── ec256-private.pem │ │ ├── ec256-wrong-private.pem │ │ ├── ec384-private.pem │ │ ├── ec384-wrong-private.pem │ │ ├── ec512-private.pem │ │ ├── ec512-wrong-private.pem │ │ ├── rsa-2048-public.pem │ │ ├── rsa-2048-wrong-public.pem │ │ ├── rsa-4096-public.pem │ │ ├── rsa-1024-private.pem │ │ ├── rsa-2048-private.pem │ │ ├── rsa-2048-wrong-private.pem │ │ └── rsa-4096-private.pem ├── spec_helper.rb ├── integration │ └── readme_examples_spec.rb ├── jwt │ └── verify_spec.rb └── jwt_spec.rb ├── .travis.yml ├── Rakefile ├── .codeclimate.yml ├── lib ├── jwt │ ├── json.rb │ ├── version.rb │ ├── error.rb │ ├── decode.rb │ └── verify.rb └── jwt.rb ├── LICENSE ├── ruby-jwt.gemspec ├── README.md └── CHANGELOG.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Metrics/LineLength: 2 | Enabled: false 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | source 'https://rubygems.org' 3 | 4 | gemspec 5 | -------------------------------------------------------------------------------- /Manifest: -------------------------------------------------------------------------------- 1 | Rakefile 2 | README.md 3 | LICENSE 4 | lib/jwt.rb 5 | lib/jwt/json.rb 6 | spec/spec_helper.rb 7 | spec/jwt_spec.rb 8 | Manifest 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | jwt.gemspec 3 | pkg 4 | Gemfile.lock 5 | coverage/ 6 | .DS_Store 7 | .rbenv-gemsets 8 | .ruby-version 9 | .vscode/ 10 | .bundle 11 | bin/ -------------------------------------------------------------------------------- /spec/fixtures/certs/ec256-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAr+WbDE5VtIDGhtYMxvEc6cMsDBc 3 | /DX1wuhIMu8dQzOLSt0tpqK9MVfXbVfrKdayVFgoWzs8MilcYq0QIhKx/w== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec256-wrong-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEPmuXZT3jpJnEMVPOW6RMsmxeGLOCE1PN 3 | 6fwvUwOsxv7YnyoQ5/bpo64n+Jp4slSl1aUNoCBF2oz9bS0iyBo3jg== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec384-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEkMGE4kb17tYTB3HkqLDLP/Ln+dLCSN47 3 | ypHUpL7kQD8Us4ba1LwkA51bE4RauGjnWWWNaodhKyRrc5q8fqRLCbyZ73zkxtzR 4 | lCkWpXpPi0H1agFErwzCpcXZM9nuu7bK 5 | -----END PUBLIC KEY----- 6 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec384-wrong-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEMF5MeARQRi1285ecla38Su3q/IUot/Ig 3 | xCtvu3Ha0dd+1p+SVJy+Fk8F085MlIHwTi97BzaM8H4FNNxg/xDlBCqs7CCYs9nG 4 | z2xQC3eIUvvE+LMwy1QvSZdVupNAgLZj 5 | -----END PUBLIC KEY----- 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | cache: bundler 3 | language: ruby 4 | rvm: 5 | - 1.9.3 6 | - 2.0.0 7 | - 2.1.0 8 | - 2.2.0 9 | - 2.3.0 10 | script: "bundle exec rspec" 11 | addons: 12 | code_climate: 13 | repo_token: e87b175db123ab42ca2ca4420abaa13c0dc2085608402b9a25f08a83ca3ba202 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:test) 7 | 8 | task default: :test 9 | rescue LoadError 10 | puts 'RSpec rake tasks not available. Please run "bundle install" to install missing dependencies.' 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec512-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBMO+GMGk2z2Ok7VpsceiyQ8wl9pyl 3 | gj5ynWXBB/TXLvIOUY0QUatsmnI2tzqEupwNmHg54K3gScWa9++j+jbOl3EBVgwO 4 | ivzAk6xAQrD8V2TcXfCkfbzBloYDbVka3Cs8CX0JK7LsnoWZG22MhZwFsgESh2bl 5 | e1VF5nhJkUv3/dzD6lQ= 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec512-wrong-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAbRMVZ7lxnkNkjJ+7KysKhDJJM3F3 3 | 5pPyw8jGzHuZbpgFOy47qsSDHJ6B3YC7LjG2KTGnMq+ygxdHTb330Bq6zaMAC3/D 4 | GuGGvjUEM/gxXRvpmeBvvaGdrSlwaqK3k7JTwBs4lRn6t3KjBEaw5j5o9s7JWF01 5 | ywR3FgLwZ0jIJAsELUc= 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /spec/fixtures/certs/rsa-1024-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDO/ahgFDvniFoQ1dm+MdnkBi+T 3 | s5W9AtQNgw4ZHIdPnqEzSgW70opKEu8hnlLqsIyU2BC2op/xOanipdbXObuFlA6b 4 | th1cYRI+YJlR3BbPGOIL6YbJud9m0gIsBlCDLm4e/E45ZS+emudISP7/SF7zxvxZ 5 | lnr1z7HTm7nIIVBvuQIDAQAB 6 | -----END PUBLIC KEY----- 7 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | rubocop: 3 | enabled: true 4 | golint: 5 | enabled: false 6 | gofmt: 7 | enabled: false 8 | eslint: 9 | enabled: false 10 | csslint: 11 | enabled: false 12 | 13 | ratings: 14 | paths: 15 | - lib/** 16 | - "**.rb" 17 | 18 | exclude_paths: 19 | - spec/**/* 20 | - vendor/**/* 21 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec256-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BggqhkjOPQMBBw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHcCAQEEIJmVse5uPfj6B4TcXrUAvf9/8pJh+KrKKYLNcmOnp/vPoAoGCCqGSM49 6 | AwEHoUQDQgAEAr+WbDE5VtIDGhtYMxvEc6cMsDBc/DX1wuhIMu8dQzOLSt0tpqK9 7 | MVfXbVfrKdayVFgoWzs8MilcYq0QIhKx/w== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec256-wrong-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BgUrgQQACg== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHQCAQEEICfA4AaomONdmPTzeyrx5U/jugYXTERyb5U3ETTv7Hx7oAcGBSuBBAAK 6 | oUQDQgAEPmuXZT3jpJnEMVPOW6RMsmxeGLOCE1PN6fwvUwOsxv7YnyoQ5/bpo64n 7 | +Jp4slSl1aUNoCBF2oz9bS0iyBo3jg== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /lib/jwt/json.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'json' 3 | 4 | module JWT 5 | # JSON fallback implementation or ruby 1.8.x 6 | module Json 7 | def decode_json(encoded) 8 | JSON.parse(encoded) 9 | rescue JSON::ParserError 10 | raise JWT::DecodeError, 'Invalid segment encoding' 11 | end 12 | 13 | def encode_json(raw) 14 | JSON.generate(raw) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec384-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BgUrgQQAIg== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MIGkAgEBBDDxOljqUKw9YNhkluSJIBAYO1YXcNtS+vckd5hpTZ5toxsOlwbmyrnU 6 | Tn+D5Xma1m2gBwYFK4EEACKhZANiAASQwYTiRvXu1hMHceSosMs/8uf50sJI3jvK 7 | kdSkvuRAPxSzhtrUvCQDnVsThFq4aOdZZY1qh2ErJGtzmrx+pEsJvJnvfOTG3NGU 8 | KRalek+LQfVqAUSvDMKlxdkz2e67tso= 9 | -----END EC PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec384-wrong-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BgUrgQQAIg== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MIGkAgEBBDAfZW47dSKnC5JkSVOk1ERxCIi/IJ1p1WBnVGx4hnrNHy+dxtaZJaF+ 6 | YLInFQ/QbYegBwYFK4EEACKhZANiAAQwXkx4BFBGLXbzl5yVrfxK7er8hSi38iDE 7 | K2+7cdrR137Wn5JUnL4WTwXTzkyUgfBOL3sHNozwfgU03GD/EOUEKqzsIJiz2cbP 8 | bFALd4hS+8T4szDLVC9Jl1W6k0CAtmM= 9 | -----END EC PRIVATE KEY----- 10 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec512-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BgUrgQQAIw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MIHcAgEBBEIB0/+ffxEj7j62xvGaB5pvzk888e412ESO/EK/K0QlS9dSF8+Rj1rG 6 | zqpRB8fvDnoe8xdmkW/W5GKzojMyv7YQYumgBwYFK4EEACOhgYkDgYYABAEw74Yw 7 | aTbPY6TtWmxx6LJDzCX2nKWCPnKdZcEH9Ncu8g5RjRBRq2yacja3OoS6nA2YeDng 8 | reBJxZr376P6Ns6XcQFWDA6K/MCTrEBCsPxXZNxd8KR9vMGWhgNtWRrcKzwJfQkr 9 | suyehZkbbYyFnAWyARKHZuV7VUXmeEmRS/f93MPqVA== 10 | -----END EC PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /spec/fixtures/certs/ec512-wrong-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BgUrgQQAIw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MIHbAgEBBEG/KbA2oCbiCT6L3V8XSz2WKBy0XhGvIFbl/ZkXIXnkYt+1B7wViSVo 6 | KCHuMFsi6xU/5nE1EuDG2UsQJmKeAMkIOKAHBgUrgQQAI6GBiQOBhgAEAG0TFWe5 7 | cZ5DZIyfuysrCoQySTNxd+aT8sPIxsx7mW6YBTsuO6rEgxyegd2Auy4xtikxpzKv 8 | soMXR02999Aaus2jAAt/wxrhhr41BDP4MV0b6Zngb72hna0pcGqit5OyU8AbOJUZ 9 | +rdyowRGsOY+aPbOyVhdNcsEdxYC8GdIyCQLBC1H 10 | -----END EC PRIVATE KEY----- 11 | -------------------------------------------------------------------------------- /spec/fixtures/certs/rsa-2048-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4GzZTLU48c4WbyvHi+QK 3 | rB71x+T0eq5hqDbQqnlYjhD1Ika7io1iplsdJWJuyxfYbUkb2Ol0fj4koZ/GS6lg 4 | CZr4+8UHbr1qf0Eu5HZSpszs2YxY8U5RHnrpw67co7hlgAR9HbyNf5XIYgLV9ldH 5 | H/eazwnc3F/hgNsV0xjScVilejgocJ4zcsyymvW8t42lteM7bI867ZuJhGop/V+Y 6 | 0HFyrMsPoQyLuCUpr6ulOfrkr7ZOdhAIG8r1HcjOp/AUjM15vfXcbUZjkM/Vloif 7 | X1YitU3upMGJ8/DpFGffMOImrn5r6BT494V8rRyN2qvQoAkLJpqZ0avLxwiR2lgV 8 | QQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /spec/fixtures/certs/rsa-2048-wrong-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzHAVGaW9j4l3/b4ngcjj 3 | oIoIcnsQEWOMqErb5VhLZMGIq1gEO5qxPDAwooKsNotzcAOB3ZyLn7p5D+dmOrNU 4 | YkYWgYITNGeSifrnVqQugd5Fh1L8K7zOGltUo2UtjbN4uJ56tzxBMZp2wejs2/Qu 5 | 0eu0xZK3To+YkDcWOk92rmNgmUSQC/kNyIOj+yBvOo3wTk6HvbhoIarCgJ6Lay1v 6 | /hMLyQLzwRY/Qfty1FTIDyTv2dch47FsfkZ1KAL+MbUnHuCBPzGxRjXa8Iy9Z7YG 7 | xrYasUt1b0um64bscxoIiCu8yLL8jlg01Rwrjr/MTwKRhwXlMp8B7HTonwtaG6ar 8 | JwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /lib/jwt/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # frozen_string_literal: true 3 | 4 | # Moments version builder module 5 | module JWT 6 | def self.gem_version 7 | Gem::Version.new VERSION::STRING 8 | end 9 | 10 | # Moments version builder module 11 | module VERSION 12 | # major version 13 | MAJOR = 1 14 | # minor version 15 | MINOR = 6 16 | # tiny version 17 | TINY = 0 18 | # alpha, beta, etc. tag 19 | PRE = 'dev' 20 | 21 | # Build version string 22 | STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/jwt/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module JWT 3 | class DecodeError < StandardError; end 4 | class VerificationError < DecodeError; end 5 | class ExpiredSignature < DecodeError; end 6 | class IncorrectAlgorithm < DecodeError; end 7 | class ImmatureSignature < DecodeError; end 8 | class InvalidIssuerError < DecodeError; end 9 | class InvalidIatError < DecodeError; end 10 | class InvalidAudError < DecodeError; end 11 | class InvalidSubError < DecodeError; end 12 | class InvalidJtiError < DecodeError; end 13 | class InvalidPayload < DecodeError; end 14 | end 15 | -------------------------------------------------------------------------------- /spec/fixtures/certs/rsa-4096-public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqETmgWBi5rCmb7euJplA 3 | /9xs65+bncc9Yvs5zjyycXSW82JfRuyguGm0OvA2wog24dR4N2kT/87DcGtp5JqJ 4 | WADVFNr+2V2r6i57/OMLruRpn3p2r95dmo0COE+BxPFl7XEBT8JbH57ZtpgcB3/x 5 | kS14nLOWFf96hrXPlXJC+VMVKVZmA8k2LRh42vT5wUf4U0Doy/p7yFNSFFa6Q8ww 6 | e4TBy/z/f+rhFD1w8rxlYjallee/ocm7bjZCwbJGMm7orLViqWfsFX3O35PeoJ5h 7 | /7uJ7iRwvTFERkTdwWP/0BeKBeItBR3YFc2mut+V9W+WKRkMSL6Crc+oVSx3p8aB 8 | 7j9SZFzQiRtes4BYETpX1xl2mgIq5hvsFbLw7ESrlIodiwUMTrSIid2DQ6q80kv1 9 | zXPr4+Id6L0sJLxPCaXnTmNtasSwyedJJYxLjwhHJwtzFAeaq18H3O791YKhjAJ6 10 | YxK3zJ59jTE6Pkvqjq183f2PGHVRvgSN7aCmI6MBUUB5wDP2K8zX2sh40/uPDVSd 11 | 6ei1vl3DpPk+h8iExx6AzbohfqZ+5RUUNx127L3MaQvOVC5TxV+R99gwKW++wzcV 12 | uO3m2KqVUj+K1uYBy3KBCUMBbckpEWGbN++jcdV5oJX6fsC66nOmKlntYwCL/pRw 13 | w+oLsbzF8J3dxeDbKNF9JDsCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'simplecov' 3 | require 'simplecov-json' 4 | require 'codeclimate-test-reporter' 5 | 6 | SimpleCov.configure do 7 | root File.join(File.dirname(__FILE__), '..') 8 | project_name 'Ruby JWT - Ruby JSON Web Token implementation' 9 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 10 | SimpleCov::Formatter::HTMLFormatter, 11 | SimpleCov::Formatter::JSONFormatter 12 | ]) 13 | 14 | add_filter 'spec' 15 | end 16 | 17 | SimpleCov.start if ENV['COVERAGE'] 18 | CodeClimate::TestReporter.start if ENV['CODECLIMATE_REPO_TOKEN'] 19 | 20 | CERT_PATH = File.join(File.dirname(__FILE__), 'fixtures', 'certs') 21 | 22 | RSpec.configure do |config| 23 | config.expect_with :rspec do |c| 24 | c.syntax = [:should, :expect] 25 | end 26 | 27 | config.run_all_when_everything_filtered = true 28 | config.filter_run :focus 29 | 30 | config.order = 'random' 31 | end 32 | -------------------------------------------------------------------------------- /spec/fixtures/certs/rsa-1024-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXgIBAAKBgQDO/ahgFDvniFoQ1dm+MdnkBi+Ts5W9AtQNgw4ZHIdPnqEzSgW7 3 | 0opKEu8hnlLqsIyU2BC2op/xOanipdbXObuFlA6bth1cYRI+YJlR3BbPGOIL6YbJ 4 | ud9m0gIsBlCDLm4e/E45ZS+emudISP7/SF7zxvxZlnr1z7HTm7nIIVBvuQIDAQAB 5 | AoGBAMzFQAccvU6GI6O4C5sOsiHUxMh3xtCftaxQVGgfQvVPVuXoeteep1Q0ewFl 6 | IV4vnkO5pH8pTtVTWG9x5KIy6QCql4qvr2jkOm4mo9uogrpNklvBl2lN4Lxubj0N 7 | mGRXaM3hckZl8+JT6uzfBfjy+pd8AOigJGPQCOZn4gmANW7pAkEA82Nh4wpj6ZRU 8 | NBiBq3ONZuH4xJm59MI2FWRJsGUFUYdSaFwyKKim52/13d8iUb7v9utWQFRatCXz 9 | Lqw9fQyVrwJBANm3dBOVxpUPrYEQsG0q2rdP+u6U3woylxwtQgJxImZKZmmJlPr8 10 | 9v23rhydvCe1ERPYe7EjF4RGWVPN3KLdExcCQDdzNfL3BApMS97OkoRQQC/nXbjU 11 | 2SPlN1MqVQuGCG8pqGG0V40h11y1CkvxMS10ldEojq77SOrwFnZUsXGS82sCQQC6 12 | XdO7QCaxSq5XIRYlHN4EtS40NLOIYy3/LK6osHel4GIyTVd+UjSLk0QzssJxqwln 13 | V5TqWQO0cxPcLQiFUYEZAkEA2G84ilb9QXOgbNyoE1VifNk49hhodbSskLb86uwY 14 | Vgtzq1ZsqoPBCasr4WRiXt270n+mo5dNYRlZwiUn9lH78Q== 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Jeff Lindsay 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /ruby-jwt.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib/', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'jwt/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'jwt' 7 | spec.version = JWT.gem_version 8 | spec.authors = [ 9 | 'Jeff Lindsay', 10 | 'Tim Rudat' 11 | ] 12 | spec.email = 'timrudat@gmail.com' 13 | spec.summary = 'JSON Web Token implementation in Ruby' 14 | spec.description = 'A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.' 15 | spec.homepage = 'http://github.com/jwt/ruby-jwt' 16 | spec.license = 'MIT' 17 | 18 | spec.files = `git ls-files -z`.split("\x0") 19 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 21 | spec.require_paths = %w(lib) 22 | 23 | spec.add_development_dependency 'bundler' 24 | spec.add_development_dependency 'rake' 25 | spec.add_development_dependency 'json', '< 2.0' 26 | spec.add_development_dependency 'rspec' 27 | spec.add_development_dependency 'simplecov' 28 | spec.add_development_dependency 'simplecov-json' 29 | spec.add_development_dependency 'codeclimate-test-reporter' 30 | end 31 | -------------------------------------------------------------------------------- /spec/fixtures/certs/rsa-2048-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEA4GzZTLU48c4WbyvHi+QKrB71x+T0eq5hqDbQqnlYjhD1Ika7 3 | io1iplsdJWJuyxfYbUkb2Ol0fj4koZ/GS6lgCZr4+8UHbr1qf0Eu5HZSpszs2YxY 4 | 8U5RHnrpw67co7hlgAR9HbyNf5XIYgLV9ldHH/eazwnc3F/hgNsV0xjScVilejgo 5 | cJ4zcsyymvW8t42lteM7bI867ZuJhGop/V+Y0HFyrMsPoQyLuCUpr6ulOfrkr7ZO 6 | dhAIG8r1HcjOp/AUjM15vfXcbUZjkM/VloifX1YitU3upMGJ8/DpFGffMOImrn5r 7 | 6BT494V8rRyN2qvQoAkLJpqZ0avLxwiR2lgVQQIDAQABAoIBAEH0Ozgr2fxWEInD 8 | V/VooypKPvjr9F1JejGxSkmPN9MocKIOH3dsbZ1uEXa3ItBUxan4XlK06SNgp+tH 9 | xULfF/Y6sQlsse59hBq50Uoa69dRShn1AP6JgZVvkduMPBNxUYL5zrs6emsQXb9Q 10 | DglDRQfEAJ7vyxSIqQDxYcyT8uSUF70dqFe+E9B2VE3D6ccHc98k41pJrAFAUFH1 11 | wwvDhfyYr7/Ultut9wzpZvU1meF3Vna3GOUHfxrG6wu1G+WIWHGjouzThsc1qiVI 12 | BtMCJxuCt5fOXRbU4STbMqhB6sZHiOh6J/dZU6JwRYt+IS8FB6kCNFSEWZWQledJ 13 | XqtYSQECgYEA9nmnFTRj3fTBq9zMXfCRujkSy6X2bOb39ftNXzHFuc+I6xmv/3Bs 14 | P9tDdjueP/SnCb7i/9hXkpEIcxjrjiqgcvD2ym1hE4q+odMzRAXYMdnmzI34SVZE 15 | U5hYJcYsXNKrTTleba7QgqdORmyJ9FwqLO40udvmrZMY223XDwgRkOkCgYEA6RkO 16 | 5wjjrWWp/G1YN3KXZTS1m2/eGrUThohXKAfAjbWWiouNLW2msXrxEWsPRL6xKiHu 17 | X9cwZwzi3MstAgk+bphUGUVUkGKNDjWHJA25tDYjbPtkd6xbL4eCHsKpNL3HNYr9 18 | N0CIvgn7qjaHRBem0iK7T6keY4axaSVddEwYapkCgYEA13K5qaB1F4Smcpt8DTWH 19 | vPe8xUUaZlFzOJLmLCsuwmB2N8Ppg2j7RspcaxJsH021YaB5ftjWm+ipMSr8ZPY/ 20 | 8JlPsNzxuYpTXtNmAbT2KYVm6THEch61dTk6/DIBf1YrpUJbl5by7vJeStL/uBmE 21 | SGgksL5XIyzs0opuLdaIvFkCgYAyBLWE8AxjFfCvAQuwAj/ocLITo6KmWnrRIIqL 22 | RXaVMgUWv7FQsTnW1cnK8g05tC2yG8vZ9wQk6Mf5lwOWb0NdWgSZ0528ydj41pWk 23 | L+nMeN2LMjqxz2NVxJ8wWJcUgTCxFZ0WcRumo9/D+6V1ABpE9zz4cBLcSnfhVypB 24 | nV6T6QKBgQCSZNCQ9HPxjAgYcsqc5sjNwuN1GHQZSav3Tye3k6zHENe1lsteT9K8 25 | xciGIuhybKZBvB4yImIIHCtnH+AS+mHAGqHarjNDMfvjOq0dMibPx4+bkIiHdBIH 26 | Xz+j5kmntvFiUnzr0Z/Tcqo+r8FvyCo1YWgwqGP8XoFrswD7gy7cZw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/fixtures/certs/rsa-2048-wrong-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAzHAVGaW9j4l3/b4ngcjjoIoIcnsQEWOMqErb5VhLZMGIq1gE 3 | O5qxPDAwooKsNotzcAOB3ZyLn7p5D+dmOrNUYkYWgYITNGeSifrnVqQugd5Fh1L8 4 | K7zOGltUo2UtjbN4uJ56tzxBMZp2wejs2/Qu0eu0xZK3To+YkDcWOk92rmNgmUSQ 5 | C/kNyIOj+yBvOo3wTk6HvbhoIarCgJ6Lay1v/hMLyQLzwRY/Qfty1FTIDyTv2dch 6 | 47FsfkZ1KAL+MbUnHuCBPzGxRjXa8Iy9Z7YGxrYasUt1b0um64bscxoIiCu8yLL8 7 | jlg01Rwrjr/MTwKRhwXlMp8B7HTonwtaG6arJwIDAQABAoIBAGFR4dmJusl/qW1T 8 | fj8cQLAFxaupxaZhe24J5NAyzgEy2Dqo9ariIwkB78UM66ozjEqAgOvcP+NTw5m8 9 | kD/VapA1yTTxlO7XdzzUAhiOo80S4IphCMZRZNPLMmluGtdf3lIUr1pXBrn0TCBX 10 | H5o9jaREzpNXGof9d6T/dEdh2J9+uE/p1xE5GSxQfaPheZzCG7636La/DcArg/UR 11 | +TusPqp62BEmk96pE/KKJRmEeH+WnPfSh6sMpLxi3hkEU7AynpliGT6Z6xV4csBI 12 | S/rdpkcj5DWpbnQzkwdrnL2Q+POEq/vlx5/NlezvtQPNLvQWDyY4yBCoMKGb3EbX 13 | xrxP7MECgYEA/kwe4P0Mqk+087IyhjDBGPfcMt8gfYc9nzNfIYSWdSwuSag/hqHq 14 | I4GwHQzUV9ix3iM6w5hin10yAzWxCYZg9hquV+lSvNNpGB76FX6oOqwuAhyQMRwv 15 | eW+VUyfFXeJugwL5JuIaNTvwPpQVDHYtELLifie+uzJ5HC6dhg/XchcCgYEAzc5/ 16 | +IXjOlExd/mBgFk/5Y87ifA0ZOgbaJXifYgU0aNSgz1piHxU3n2p4jJ9lSdwwCl2 17 | Fb5EN7666t20PL5QcXJ5ZdaTRLzRlYiqTWzfYHBgttbB1Jl3Ed9GsKuzRgaRqGFC 18 | ANJSqZlKG0NZ3keRtuKdFwq+IVOnsQr9g0TZiXECgYEAqUgtCiMKCloTIGMQpSnR 19 | cXiWWjsUmturls4Q1vQ3YHrvuVLKLyqb/dT4Uu5WcMAs765OESThCit0/pQAbVHK 20 | PCpYwubskAzAGjGM00BEZwJ1gixXhIm5xMIWCowgI7Z3ULlq+IptXeCvtkjHlksZ 21 | BtO+WLLGkkEwRCV38WWcSzMCgYA/Xxqgl/mD94RYAQgTUWgPc69Nph08BQyLg7ue 22 | E8z1UGkT6FEaqc4oRGGPOSTaTK63PQ0TXOb8k0pTD7l0CtYSWMFwzkXCoLGYbeCi 23 | vqd5tqDRLAe7QxYa9rl5pSUqptMrGeeNATZa6sya4H5Hp5oCyny8n54z/OJh7ZRq 24 | W0TwwQKBgQDDP7ksm2pcqadaVAmODdOlaDHbaEcxp8wN7YVz0lM3UpJth96ukbj7 25 | S39eJhXYWOn6oJQb/lN9fGOYqjg3y6IchGZDp67ATvWYvn/NY0R7mt4K4oHx5TuN 26 | rSQlP3WmOGv8Kemw892uRfW/jZyBEHhsfS213WDttVPn9F635GdNWw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /lib/jwt/decode.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'jwt/json' 3 | require 'jwt/verify' 4 | 5 | # JWT::Decode module 6 | module JWT 7 | extend JWT::Json 8 | 9 | # Decoding logic for JWT 10 | class Decode 11 | attr_reader :header, :payload, :signature 12 | 13 | def initialize(jwt, key, verify, options, &keyfinder) 14 | @jwt = jwt 15 | @key = key 16 | @verify = verify 17 | @options = options 18 | @keyfinder = keyfinder 19 | end 20 | 21 | def decode_segments 22 | header_segment, payload_segment, crypto_segment = raw_segments(@jwt, @verify) 23 | @header, @payload = decode_header_and_payload(header_segment, payload_segment) 24 | @signature = Decode.base64url_decode(crypto_segment.to_s) if @verify 25 | signing_input = [header_segment, payload_segment].join('.') 26 | [@header, @payload, @signature, signing_input] 27 | end 28 | 29 | def raw_segments(jwt, verify) 30 | segments = jwt.split('.') 31 | required_num_segments = verify ? [3] : [2, 3] 32 | raise(JWT::DecodeError, 'Not enough or too many segments') unless required_num_segments.include? segments.length 33 | segments 34 | end 35 | private :raw_segments 36 | 37 | def decode_header_and_payload(header_segment, payload_segment) 38 | header = JWT.decode_json(Decode.base64url_decode(header_segment)) 39 | payload = JWT.decode_json(Decode.base64url_decode(payload_segment)) 40 | [header, payload] 41 | end 42 | private :decode_header_and_payload 43 | 44 | def self.base64url_decode(str) 45 | str += '=' * (4 - str.length.modulo(4)) 46 | Base64.decode64(str.tr('-_', '+/')) 47 | end 48 | 49 | def verify 50 | @options.each do |key, val| 51 | next unless key.to_s =~ /verify/ 52 | 53 | Verify.send(key, payload, @options) if val 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/fixtures/certs/rsa-4096-private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJJwIBAAKCAgEAqETmgWBi5rCmb7euJplA/9xs65+bncc9Yvs5zjyycXSW82Jf 3 | RuyguGm0OvA2wog24dR4N2kT/87DcGtp5JqJWADVFNr+2V2r6i57/OMLruRpn3p2 4 | r95dmo0COE+BxPFl7XEBT8JbH57ZtpgcB3/xkS14nLOWFf96hrXPlXJC+VMVKVZm 5 | A8k2LRh42vT5wUf4U0Doy/p7yFNSFFa6Q8wwe4TBy/z/f+rhFD1w8rxlYjallee/ 6 | ocm7bjZCwbJGMm7orLViqWfsFX3O35PeoJ5h/7uJ7iRwvTFERkTdwWP/0BeKBeIt 7 | BR3YFc2mut+V9W+WKRkMSL6Crc+oVSx3p8aB7j9SZFzQiRtes4BYETpX1xl2mgIq 8 | 5hvsFbLw7ESrlIodiwUMTrSIid2DQ6q80kv1zXPr4+Id6L0sJLxPCaXnTmNtasSw 9 | yedJJYxLjwhHJwtzFAeaq18H3O791YKhjAJ6YxK3zJ59jTE6Pkvqjq183f2PGHVR 10 | vgSN7aCmI6MBUUB5wDP2K8zX2sh40/uPDVSd6ei1vl3DpPk+h8iExx6AzbohfqZ+ 11 | 5RUUNx127L3MaQvOVC5TxV+R99gwKW++wzcVuO3m2KqVUj+K1uYBy3KBCUMBbckp 12 | EWGbN++jcdV5oJX6fsC66nOmKlntYwCL/pRww+oLsbzF8J3dxeDbKNF9JDsCAwEA 13 | AQKCAgBJF8TZJjlP5CQoGy227pNhkSpvH6HFY6qyuFZf09XfmrmHd4/Tiy41bRUx 14 | FO90iR7t8hFWYHqjf/k9eCtDdi164MGukYJqgVoQG6kYLLgCfI21DMlJk9otLFtu 15 | gnroRcP05EWhk9dpYONJgcGLMHSKj6n4x7nGTHe41HkbfcrB6ukiT7l4o4q5BAxb 16 | cFadMtoXr/ZvxJrIZgkddJ7snGHjBcP5DCkgM7MZy6aoilWv1/UNrOF9MdgNA9zz 17 | rrD3b136x7/XvqC6pS+bxuvJ8YK4R4qeu42NYT07GOcK/pk8lz0JWTodIt2eevqV 18 | 6lGFj7c2mv7PCpJRVgbVGL/RTVVap/+jbcRVLdnYKsII/dANG7iXnfwRgkLWet5D 19 | OOsPuvIuyiSaJIwcdRE3SSO+tZhKLt+gh/oLxBPw5Ex0FwsVTtYn3Q/X3EAx+Wph 20 | eFcRr3TVkDg0MfdWWkgk16DvYB5cWc29coTaH1g+2juadNHbtVAigwJorKc6sxH3 21 | QGsW0WQJ8ZRZgJkSUuu3nr7QD3ZrgHptONQAh1RWGnIWi6OlMfaPdMo+SDnnL5SG 22 | mpOPjWadDc1XvMFnKQYMYB5GWU/ZNmnZmDLyg1Pc0Y+qRUc0s83nZFHN60KnUrSz 23 | 0MZDspSFtr0fMx0b2/EB4EbuXd3QjQURF6P6HtWBu6oFnzu1AQKCAQEA2R9BKJgJ 24 | vNP+DUu8NBzwmi0cKlAiaxt+w90i5DWq1XWPKgi+RVLkaQSJqHoYQVNgEwL/cWxp 25 | s2r3GCMNIdOrGdcm8dX/6UYFpRaFcViTycVEA7cwZOMppgqr2Q+ZYX42K7HObUVL 26 | JGvdEWWWfSsynUGsrG87DC1gl94ANxCdqkZdbW5d3X0w5v7M/1tlrmAeskZSZpeT 27 | 8BwwM6REb0U/B4/i8TLtLi/PGmMTOIxW41uKS/S6kq/gwyv+jNNO0ljhPt25iSbV 28 | K5ZHS4YuPKLl0tZMaOkPco9s6t4ES/Y317zQoTzUkAAkkFO4QPzRZL0ESqVBNR0h 29 | Ao7FLmFZzFHpoQKCAQEAxmZBn0UrJLXkD7bw66y4RwzjQYmxLlkEl3uvjrvVSuL1 30 | mAHDW58aGIpFFZ8QSTtNewIBQYNifp/cdFHAagqtl/iMGEegaOpJAKu/ykrkfZUp 31 | 7mYDNng4ZWpypeKaGMAQoNzZiUpF+BDnqbeb/kgYu6sNlh9gRHR79rgAuZQxZ/1B 32 | tE8WcUFi4CnTq2QLqX4LwMuZHWXAJQoMoW3K5av+J544lIM6GdMJuIONtBBkKVQD 33 | ErrJ0bqYeykrFS6pKl/NBCZLGo5xFFRiYEdZ1GlA3uW3EGKppz6PS7194+x5UVts 34 | xZPUfkgdFjWCczkl4JDoWfaNn5sgXtiVbGh1n3gYWwKCAQB7vHEg1kyuXU4qe5/d 35 | PyTraIvlnVeQHNJIgy0QS3l5Pw8A0IzG6y+anehpqHNMP1zAWPQEytkOVAZPriIc 36 | xgl7p37dUa0PX0V2SPhxmR5YXeCeEXc197PTmb9H67jos8nhauqOoW/qaMJK2M9D 37 | tCubLUNf3eAT14R16CHNP93qnUE/TSeXQ3JsIofne0neb47u4F6zcuzvaNEbjSEn 38 | HJqID7sw5GoA6WQo0I+yqWAXICMXmHf/gtYfxGHEFeSUwexULH5BKG1R8sncw7J0 39 | Ag3h8xkGrNON4SkcTLy8Iay/eS6YxRcKndo4mk2mU65tr77TX4xi3Z/jWkQLY5WO 40 | eJwhAoIBABO17wkSxyGDjJ/fDfpsE3bDmgRV2KuBHoqqOBvXH26sM7ghXLZKjT4o 41 | 5ooqXmTYJm91GIjYs71exnkr8hDW9L4nbEuxOgeSVyRg69H+NMshOaQ8sE8GDJxO 42 | wgsnAyY4Vq6UomwYW/E0RL/AxRezM/nZGaVzgo3qgLJXP4MwbOQm7hMq1FD2LQuW 43 | PDhH3Ty+kA5ca97W0Asd/3k+Pi0pNDvdZUOj8e7E369cKoTcKAdPGGsQ8aILhsCd 44 | q3EUTKwwDl8+KrH9utBJPejQzeTjfBVo/xH6q145QeVFcy9ku/zQN3M9p5vQMEuX 45 | j1lBMTkpTFw7uYBE2idyHw5BJoZsWQcCggEADfZTChqnOncItSflzGoaAACrr4/x 46 | KyT/4A+cPMCs11JN9J+EWsCezya2o1l/NF7YPcBR4qjCmFMEiq5GxH5fGLQp0aa7 47 | V13mHA8XBQ25OW2K7BGJhMHdbuvTnl6jsOfC4+t7P2bUAYxoP6/ncxTzZ5OlBN5k 48 | aMv9firWl1kSKK75ww9DWn6j0rQ4dBetwX45EMcs+iKIdydg0fmJxR2EJ+uQsCFy 49 | xcWBEDqV7qLUi6UrAPL3v/DXUv9wKcKOTbKw/aNE8+YTWMUO330GCJ5cVU1eTL5t 50 | UrcNKOJkFIj7jJUCzv6vcy++hMJEbNXnnTVRnky6e9C2vwzMl33njntapg== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /lib/jwt/verify.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'jwt/error' 3 | 4 | module JWT 5 | # JWT verify methods 6 | class Verify 7 | class << self 8 | %w(verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub).each do |method_name| 9 | define_method method_name do |payload, options| 10 | new(payload, options).send(method_name) 11 | end 12 | end 13 | end 14 | 15 | def initialize(payload, options) 16 | @payload = payload 17 | @options = options 18 | end 19 | 20 | def verify_aud 21 | return unless (options_aud = extract_option(:aud)) 22 | 23 | if @payload['aud'].is_a?(Array) 24 | verify_aud_array(@payload['aud'], options_aud) 25 | else 26 | raise( 27 | JWT::InvalidAudError, 28 | "Invalid audience. Expected #{options_aud}, received #{@payload['aud'] || ''}" 29 | ) unless @payload['aud'].to_s == options_aud.to_s 30 | end 31 | end 32 | 33 | def verify_aud_array(audience, options_aud) 34 | if options_aud.is_a?(Array) 35 | options_aud.each do |aud| 36 | raise(JWT::InvalidAudError, 'Invalid audience') unless audience.include?(aud.to_s) 37 | end 38 | else 39 | raise(JWT::InvalidAudError, 'Invalid audience') unless audience.include?(options_aud.to_s) 40 | end 41 | end 42 | 43 | def verify_expiration 44 | return unless @payload.include?('exp') 45 | 46 | if @payload['exp'].to_i <= (Time.now.to_i - leeway) 47 | raise(JWT::ExpiredSignature, 'Signature has expired') 48 | end 49 | end 50 | 51 | def verify_iat 52 | return unless @payload.include?('iat') 53 | 54 | if !@payload['iat'].is_a?(Numeric) || @payload['iat'].to_f > (Time.now.to_f + leeway) 55 | raise(JWT::InvalidIatError, 'Invalid iat') 56 | end 57 | end 58 | 59 | def verify_iss 60 | return unless (options_iss = extract_option(:iss)) 61 | 62 | if @payload['iss'].to_s != options_iss.to_s 63 | raise( 64 | JWT::InvalidIssuerError, 65 | "Invalid issuer. Expected #{options_iss}, received #{@payload['iss'] || ''}" 66 | ) 67 | end 68 | end 69 | 70 | def verify_jti 71 | options_verify_jti = extract_option(:verify_jti) 72 | if options_verify_jti.respond_to?(:call) 73 | raise(JWT::InvalidJtiError, 'Invalid jti') unless options_verify_jti.call(@payload['jti']) 74 | else 75 | raise(JWT::InvalidJtiError, 'Missing jti') if @payload['jti'].to_s.strip.empty? 76 | end 77 | end 78 | 79 | def verify_not_before 80 | return unless @payload.include?('nbf') 81 | 82 | if @payload['nbf'].to_i > (Time.now.to_i + leeway) 83 | raise(JWT::ImmatureSignature, 'Signature nbf has not been reached') 84 | end 85 | end 86 | 87 | def verify_sub 88 | return unless (options_sub = extract_option(:sub)) 89 | 90 | raise( 91 | JWT::InvalidSubError, 92 | "Invalid subject. Expected #{options_sub}, received #{@payload['sub'] || ''}" 93 | ) unless @payload['sub'].to_s == options_sub.to_s 94 | end 95 | 96 | private 97 | 98 | def extract_option(key) 99 | @options.values_at(key.to_sym, key.to_s).compact.first 100 | end 101 | 102 | def leeway 103 | extract_option :leeway 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/integration/readme_examples_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative '../spec_helper' 3 | require 'jwt' 4 | 5 | describe 'README.md code test' do 6 | context 'algorithm usage' do 7 | let(:payload) { { data: 'test' } } 8 | 9 | it 'NONE' do 10 | token = JWT.encode payload, nil, 'none' 11 | decoded_token = JWT.decode token, nil, false 12 | 13 | expect(token).to eq 'eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.' 14 | expect(decoded_token).to eq [ 15 | { 'data' => 'test' }, 16 | { 'typ' => 'JWT', 'alg' => 'none' } 17 | ] 18 | end 19 | 20 | it 'HMAC' do 21 | token = JWT.encode payload, 'my$ecretK3y', 'HS256' 22 | decoded_token = JWT.decode token, 'my$ecretK3y', false 23 | 24 | expect(token).to eq 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.ZxW8go9hz3ETCSfxFxpwSkYg_602gOPKearsf6DsxgY' 25 | expect(decoded_token).to eq [ 26 | { 'data' => 'test' }, 27 | { 'typ' => 'JWT', 'alg' => 'HS256' } 28 | ] 29 | end 30 | 31 | it 'RSA' do 32 | rsa_private = OpenSSL::PKey::RSA.generate 2048 33 | rsa_public = rsa_private.public_key 34 | 35 | token = JWT.encode payload, rsa_private, 'RS256' 36 | decoded_token = JWT.decode token, rsa_public, true, algorithm: 'RS256' 37 | 38 | expect(decoded_token).to eq [ 39 | { 'data' => 'test' }, 40 | { 'typ' => 'JWT', 'alg' => 'RS256' } 41 | ] 42 | end 43 | 44 | it 'ECDSA' do 45 | ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1' 46 | ecdsa_key.generate_key 47 | ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key 48 | ecdsa_public.private_key = nil 49 | 50 | token = JWT.encode payload, ecdsa_key, 'ES256' 51 | decoded_token = JWT.decode token, ecdsa_public, true, algorithm: 'ES256' 52 | 53 | expect(decoded_token).to eq [ 54 | { 'data' => 'test' }, 55 | { 'typ' => 'JWT', 'alg' => 'ES256' } 56 | ] 57 | end 58 | end 59 | 60 | context 'claims' do 61 | let(:hmac_secret) { 'MyP4ssW0rD' } 62 | 63 | context 'exp' do 64 | it 'without leeway' do 65 | exp = Time.now.to_i + 4 * 3600 66 | exp_payload = { data: 'data', exp: exp } 67 | 68 | token = JWT.encode exp_payload, hmac_secret, 'HS256' 69 | 70 | expect do 71 | JWT.decode token, hmac_secret, true, algorithm: 'HS256' 72 | end.not_to raise_error 73 | end 74 | 75 | it 'with leeway' do 76 | exp = Time.now.to_i - 10 77 | leeway = 30 # seconds 78 | 79 | exp_payload = { data: 'data', exp: exp } 80 | 81 | token = JWT.encode exp_payload, hmac_secret, 'HS256' 82 | 83 | expect do 84 | JWT.decode token, hmac_secret, true, leeway: leeway, algorithm: 'HS256' 85 | end.not_to raise_error 86 | end 87 | end 88 | 89 | context 'nbf' do 90 | it 'without leeway' do 91 | nbf = Time.now.to_i - 3600 92 | nbf_payload = { data: 'data', nbf: nbf } 93 | token = JWT.encode nbf_payload, hmac_secret, 'HS256' 94 | 95 | expect do 96 | JWT.decode token, hmac_secret, true, algorithm: 'HS256' 97 | end.not_to raise_error 98 | end 99 | 100 | it 'with leeway' do 101 | nbf = Time.now.to_i + 10 102 | leeway = 30 103 | nbf_payload = { data: 'data', nbf: nbf } 104 | token = JWT.encode nbf_payload, hmac_secret, 'HS256' 105 | 106 | expect do 107 | JWT.decode token, hmac_secret, true, leeway: leeway, algorithm: 'HS256' 108 | end.not_to raise_error 109 | end 110 | end 111 | 112 | it 'iss' do 113 | iss = 'My Awesome Company Inc. or https://my.awesome.website/' 114 | iss_payload = { data: 'data', iss: iss } 115 | 116 | token = JWT.encode iss_payload, hmac_secret, 'HS256' 117 | 118 | expect do 119 | JWT.decode token, hmac_secret, true, iss: iss, algorithm: 'HS256' 120 | end.not_to raise_error 121 | end 122 | 123 | context 'aud' do 124 | it 'array' do 125 | aud = %w(Young Old) 126 | aud_payload = { data: 'data', aud: aud } 127 | 128 | token = JWT.encode aud_payload, hmac_secret, 'HS256' 129 | 130 | expect do 131 | JWT.decode token, hmac_secret, true, aud: %w(Old Young), verify_aud: true, algorithm: 'HS256' 132 | end.not_to raise_error 133 | end 134 | 135 | it 'string' do 136 | expect do 137 | end.not_to raise_error 138 | end 139 | end 140 | 141 | it 'jti' do 142 | iat = Time.now.to_i 143 | hmac_secret = 'test' 144 | jti_raw = [hmac_secret, iat].join(':').to_s 145 | jti = Digest::MD5.hexdigest(jti_raw) 146 | jti_payload = { data: 'data', iat: iat, jti: jti } 147 | 148 | token = JWT.encode jti_payload, hmac_secret, 'HS256' 149 | 150 | expect do 151 | JWT.decode token, hmac_secret, true, verify_jti: true, algorithm: 'HS256' 152 | end.not_to raise_error 153 | end 154 | 155 | context 'iat' do 156 | it 'without leeway' do 157 | iat = Time.now.to_i 158 | iat_payload = { data: 'data', iat: iat } 159 | 160 | token = JWT.encode iat_payload, hmac_secret, 'HS256' 161 | 162 | expect do 163 | JWT.decode token, hmac_secret, true, verify_iat: true, algorithm: 'HS256' 164 | end.not_to raise_error 165 | end 166 | 167 | it 'with leeway' do 168 | iat = Time.now.to_i - 7 169 | iat_payload = { data: 'data', iat: iat, leeway: 10 } 170 | 171 | token = JWT.encode iat_payload, hmac_secret, 'HS256' 172 | 173 | expect do 174 | JWT.decode token, hmac_secret, true, verify_iat: true, algorithm: 'HS256' 175 | end.not_to raise_error 176 | end 177 | end 178 | 179 | it 'sub' do 180 | sub = 'Subject' 181 | sub_payload = { data: 'data', sub: sub } 182 | 183 | token = JWT.encode sub_payload, hmac_secret, 'HS256' 184 | 185 | expect do 186 | JWT.decode token, hmac_secret, true, 'sub' => sub, :verify_sub => true, :algorithm => 'HS256' 187 | end.not_to raise_error 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /lib/jwt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'base64' 3 | require 'openssl' 4 | require 'jwt/decode' 5 | require 'jwt/error' 6 | require 'jwt/json' 7 | 8 | # JSON Web Token implementation 9 | # 10 | # Should be up to date with the latest spec: 11 | # https://tools.ietf.org/html/rfc7519#section-4.1.5 12 | module JWT 13 | extend JWT::Json 14 | 15 | NAMED_CURVES = { 16 | 'prime256v1' => 'ES256', 17 | 'secp384r1' => 'ES384', 18 | 'secp521r1' => 'ES512' 19 | }.freeze 20 | 21 | DEFAULT_OPTIONS = { 22 | verify_expiration: true, 23 | verify_not_before: true, 24 | verify_iss: false, 25 | verify_iat: false, 26 | verify_jti: false, 27 | verify_aud: false, 28 | verify_sub: false, 29 | leeway: 0 30 | }.freeze 31 | 32 | module_function 33 | 34 | def sign(algorithm, msg, key) 35 | if %w(HS256 HS384 HS512).include?(algorithm) 36 | sign_hmac(algorithm, msg, key) 37 | elsif %w(RS256 RS384 RS512).include?(algorithm) 38 | sign_rsa(algorithm, msg, key) 39 | elsif %w(ES256 ES384 ES512).include?(algorithm) 40 | sign_ecdsa(algorithm, msg, key) 41 | else 42 | raise NotImplementedError, 'Unsupported signing method' 43 | end 44 | end 45 | 46 | def sign_rsa(algorithm, msg, private_key) 47 | private_key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg) 48 | end 49 | 50 | def sign_ecdsa(algorithm, msg, private_key) 51 | key_algorithm = NAMED_CURVES[private_key.group.curve_name] 52 | if algorithm != key_algorithm 53 | raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided" 54 | end 55 | 56 | digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha')) 57 | asn1_to_raw(private_key.dsa_sign_asn1(digest.digest(msg)), private_key) 58 | end 59 | 60 | def verify_rsa(algorithm, public_key, signing_input, signature) 61 | public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input) 62 | end 63 | 64 | def verify_ecdsa(algorithm, public_key, signing_input, signature) 65 | key_algorithm = NAMED_CURVES[public_key.group.curve_name] 66 | if algorithm != key_algorithm 67 | raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided" 68 | end 69 | 70 | digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha')) 71 | public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key)) 72 | end 73 | 74 | def sign_hmac(algorithm, msg, key) 75 | OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg) 76 | end 77 | 78 | def base64url_encode(str) 79 | Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '') 80 | end 81 | 82 | def encoded_header(algorithm = 'HS256', header_fields = {}) 83 | header = { 'typ' => 'JWT', 'alg' => algorithm }.merge(header_fields) 84 | base64url_encode(encode_json(header)) 85 | end 86 | 87 | def encoded_payload(payload) 88 | raise InvalidPayload, 'exp claim must be an integer' if payload['exp'] && payload['exp'].is_a?(Time) 89 | base64url_encode(encode_json(payload)) 90 | end 91 | 92 | def encoded_signature(signing_input, key, algorithm) 93 | if algorithm == 'none' 94 | '' 95 | else 96 | signature = sign(algorithm, signing_input, key) 97 | base64url_encode(signature) 98 | end 99 | end 100 | 101 | def encode(payload, key, algorithm = 'HS256', header_fields = {}) 102 | algorithm ||= 'none' 103 | segments = [] 104 | segments << encoded_header(algorithm, header_fields) 105 | segments << encoded_payload(payload) 106 | segments << encoded_signature(segments.join('.'), key, algorithm) 107 | segments.join('.') 108 | end 109 | 110 | def decoded_segments(jwt, key = nil, verify = true, custom_options = {}, &keyfinder) 111 | raise(JWT::DecodeError, 'Nil JSON web token') unless jwt 112 | 113 | merged_options = DEFAULT_OPTIONS.merge(custom_options) 114 | 115 | decoder = Decode.new jwt, key, verify, merged_options, &keyfinder 116 | decoder.decode_segments 117 | end 118 | 119 | def decode(jwt, key = nil, verify = true, custom_options = {}, &keyfinder) 120 | raise(JWT::DecodeError, 'Nil JSON web token') unless jwt 121 | 122 | merged_options = DEFAULT_OPTIONS.merge(custom_options) 123 | decoder = Decode.new jwt, key, verify, merged_options, &keyfinder 124 | header, payload, signature, signing_input = decoder.decode_segments 125 | decode_verify_signature(key, header, signature, signing_input, merged_options, &keyfinder) if verify 126 | decoder.verify 127 | 128 | raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload 129 | 130 | [payload, header] 131 | end 132 | 133 | def decode_verify_signature(key, header, signature, signing_input, options, &keyfinder) 134 | algo, key = signature_algorithm_and_key(header, key, &keyfinder) 135 | if options[:algorithm] && algo != options[:algorithm] 136 | raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' 137 | end 138 | verify_signature(algo, key, signing_input, signature) 139 | end 140 | 141 | def signature_algorithm_and_key(header, key, &keyfinder) 142 | key = yield(header) if keyfinder 143 | [header['alg'], key] 144 | end 145 | 146 | def verify_signature(algo, key, signing_input, signature) 147 | verify_signature_algo(algo, key, signing_input, signature) 148 | rescue OpenSSL::PKey::PKeyError 149 | raise JWT::VerificationError, 'Signature verification raised' 150 | ensure 151 | OpenSSL.errors.clear 152 | end 153 | 154 | def verify_signature_algo(algo, key, signing_input, signature) 155 | if %w(HS256 HS384 HS512).include?(algo) 156 | raise(JWT::VerificationError, 'Signature verification raised') unless secure_compare(signature, sign_hmac(algo, signing_input, key)) 157 | elsif %w(RS256 RS384 RS512).include?(algo) 158 | raise(JWT::VerificationError, 'Signature verification raised') unless verify_rsa(algo, key, signing_input, signature) 159 | elsif %w(ES256 ES384 ES512).include?(algo) 160 | raise(JWT::VerificationError, 'Signature verification raised') unless verify_ecdsa(algo, key, signing_input, signature) 161 | else 162 | raise JWT::VerificationError, 'Algorithm not supported' 163 | end 164 | end 165 | 166 | # From devise 167 | # constant-time comparison algorithm to prevent timing attacks 168 | def secure_compare(a, b) 169 | return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize 170 | l = a.unpack "C#{a.bytesize}" 171 | 172 | res = 0 173 | b.each_byte { |byte| res |= byte ^ l.shift } 174 | res.zero? 175 | end 176 | 177 | def raw_to_asn1(signature, private_key) 178 | byte_size = (private_key.group.degree + 7) / 8 179 | r = signature[0..(byte_size - 1)] 180 | s = signature[byte_size..-1] 181 | OpenSSL::ASN1::Sequence.new([r, s].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der 182 | end 183 | 184 | def asn1_to_raw(signature, public_key) 185 | byte_size = (public_key.group.degree + 7) / 8 186 | OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join 187 | end 188 | 189 | def base64url_decode(str) 190 | Decode.base64url_decode(str) 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /spec/jwt/verify_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | require 'jwt/verify' 4 | 5 | module JWT 6 | RSpec.describe Verify do 7 | let(:base_payload) { { 'user_id' => 'some@user.tld' } } 8 | let(:options) { { leeway: 0 } } 9 | 10 | context '.verify_aud(payload, options)' do 11 | let(:scalar_aud) { 'ruby-jwt-audience' } 12 | let(:array_aud) { %w(ruby-jwt-aud test-aud ruby-ruby-ruby) } 13 | let(:scalar_payload) { base_payload.merge('aud' => scalar_aud) } 14 | let(:array_payload) { base_payload.merge('aud' => array_aud) } 15 | 16 | it 'must raise JWT::InvalidAudError when the singular audience does not match' do 17 | expect do 18 | Verify.verify_aud(scalar_payload, options.merge(aud: 'no-match')) 19 | end.to raise_error JWT::InvalidAudError 20 | end 21 | 22 | it 'must raise JWT::InvalidAudError when the payload has an array and none match the supplied value' do 23 | expect do 24 | Verify.verify_aud(array_payload, options.merge(aud: 'no-match')) 25 | end.to raise_error JWT::InvalidAudError 26 | end 27 | 28 | it 'must raise JWT::InvalidAudError when the singular audience does not match and the options aud key is a string' do 29 | expect do 30 | Verify.verify_aud(scalar_payload, options.merge('aud' => 'no-match')) 31 | end.to raise_error JWT::InvalidAudError 32 | end 33 | 34 | it 'must allow a matching singular audience to pass' do 35 | Verify.verify_aud(scalar_payload, options.merge(aud: scalar_aud)) 36 | end 37 | 38 | it 'must allow a matching audence to pass when the options key is a string' do 39 | Verify.verify_aud(scalar_payload, options.merge('aud' => scalar_aud)) 40 | end 41 | 42 | it 'must allow an array with any value matching the one in the options' do 43 | Verify.verify_aud(array_payload, options.merge(aud: array_aud.first)) 44 | end 45 | 46 | it 'must allow an array with any value matching the one in the options with a string options key' do 47 | Verify.verify_aud(array_payload, options.merge('aud' => array_aud.first)) 48 | end 49 | 50 | it 'should allow strings or symbols in options array' do 51 | options['aud'] = [ 52 | 'ruby-jwt-aud', 53 | 'test-aud', 54 | 'ruby-ruby-ruby', 55 | :test 56 | ] 57 | 58 | array_payload['aud'].push('test') 59 | 60 | Verify.verify_aud(array_payload, options) 61 | end 62 | end 63 | 64 | context '.verify_expiration(payload, options)' do 65 | let(:leeway) { 10 } 66 | let(:payload) { base_payload.merge('exp' => (Time.now.to_i - 5)) } 67 | 68 | it 'must raise JWT::ExpiredSignature when the token has expired' do 69 | expect do 70 | Verify.verify_expiration(payload, options) 71 | end.to raise_error JWT::ExpiredSignature 72 | end 73 | 74 | it 'must allow some leeway in the expiration when configured' do 75 | Verify.verify_expiration(payload, options.merge(leeway: 10)) 76 | end 77 | 78 | it 'must be expired if the exp claim equals the current time' do 79 | payload['exp'] = Time.now.to_i 80 | 81 | expect do 82 | Verify.verify_expiration(payload, options) 83 | end.to raise_error JWT::ExpiredSignature 84 | end 85 | end 86 | 87 | context '.verify_iat(payload, options)' do 88 | let(:iat) { Time.now.to_f } 89 | let(:payload) { base_payload.merge('iat' => iat) } 90 | 91 | it 'must allow a valid iat' do 92 | Verify.verify_iat(payload, options) 93 | end 94 | 95 | it 'must allow configured leeway' do 96 | Verify.verify_iat(payload.merge('iat' => (iat + 60)), options.merge(leeway: 70)) 97 | end 98 | 99 | it 'must properly handle integer times' do 100 | Verify.verify_iat(payload.merge('iat' => Time.now.to_i), options) 101 | end 102 | 103 | it 'must raise JWT::InvalidIatError when the iat value is not Numeric' do 104 | expect do 105 | Verify.verify_iat(payload.merge('iat' => 'not a number'), options) 106 | end.to raise_error JWT::InvalidIatError 107 | end 108 | 109 | it 'must raise JWT::InvalidIatError when the iat value is in the future' do 110 | expect do 111 | Verify.verify_iat(payload.merge('iat' => (iat + 120)), options) 112 | end.to raise_error JWT::InvalidIatError 113 | end 114 | end 115 | 116 | context '.verify_iss(payload, options)' do 117 | let(:iss) { 'ruby-jwt-gem' } 118 | let(:payload) { base_payload.merge('iss' => iss) } 119 | 120 | let(:invalid_token) { JWT.encode base_payload, payload[:secret] } 121 | 122 | it 'must raise JWT::InvalidIssuerError when the configured issuer does not match the payload issuer' do 123 | expect do 124 | Verify.verify_iss(payload, options.merge(iss: 'mismatched-issuer')) 125 | end.to raise_error JWT::InvalidIssuerError 126 | end 127 | 128 | it 'must raise JWT::InvalidIssuerError when the payload does not include an issuer' do 129 | expect do 130 | Verify.verify_iss(base_payload, options.merge(iss: iss)) 131 | end.to raise_error(JWT::InvalidIssuerError, /received /) 132 | end 133 | 134 | it 'must allow a matching issuer to pass' do 135 | Verify.verify_iss(payload, options.merge(iss: iss)) 136 | end 137 | end 138 | 139 | context '.verify_jti(payload, options)' do 140 | let(:payload) { base_payload.merge('jti' => 'some-random-uuid-or-whatever') } 141 | 142 | it 'must allow any jti when the verfy_jti key in the options is truthy but not a proc' do 143 | Verify.verify_jti(payload, options.merge(verify_jti: true)) 144 | end 145 | 146 | it 'must raise JWT::InvalidJtiError when the jti is missing' do 147 | expect do 148 | Verify.verify_jti(base_payload, options) 149 | end.to raise_error JWT::InvalidJtiError, /missing/i 150 | end 151 | 152 | it 'must raise JWT::InvalidJtiError when the jti is an empty string' do 153 | expect do 154 | Verify.verify_jti(base_payload.merge('jti' => ' '), options) 155 | end.to raise_error JWT::InvalidJtiError, /missing/i 156 | end 157 | 158 | it 'must raise JWT::InvalidJtiError when verify_jti proc returns false' do 159 | expect do 160 | Verify.verify_jti(payload, options.merge(verify_jti: ->(_jti) { false })) 161 | end.to raise_error JWT::InvalidJtiError, /invalid/i 162 | end 163 | 164 | it 'true proc should not raise JWT::InvalidJtiError' do 165 | Verify.verify_jti(payload, options.merge(verify_jti: ->(_jti) { true })) 166 | end 167 | end 168 | 169 | context '.verify_not_before(payload, options)' do 170 | let(:payload) { base_payload.merge('nbf' => (Time.now.to_i + 5)) } 171 | 172 | it 'must raise JWT::ImmatureSignature when the nbf in the payload is in the future' do 173 | expect do 174 | Verify.verify_not_before(payload, options) 175 | end.to raise_error JWT::ImmatureSignature 176 | end 177 | 178 | it 'must allow some leeway in the token age when configured' do 179 | Verify.verify_not_before(payload, options.merge(leeway: 10)) 180 | end 181 | end 182 | 183 | context '.verify_sub(payload, options)' do 184 | let(:sub) { 'ruby jwt subject' } 185 | 186 | it 'must raise JWT::InvalidSubError when the subjects do not match' do 187 | expect do 188 | Verify.verify_sub(base_payload.merge('sub' => 'not-a-match'), options.merge(sub: sub)) 189 | end.to raise_error JWT::InvalidSubError 190 | end 191 | 192 | it 'must allow a matching sub' do 193 | Verify.verify_sub(base_payload.merge('sub' => sub), options.merge(sub: sub)) 194 | end 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /spec/jwt_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'jwt' 3 | require 'jwt/decode' 4 | 5 | describe JWT do 6 | let(:payload) { { 'user_id' => 'some@user.tld' } } 7 | 8 | let :data do 9 | { 10 | :secret => 'My$ecretK3y', 11 | :rsa_private => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'rsa-2048-private.pem'))), 12 | :rsa_public => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'rsa-2048-public.pem'))), 13 | :wrong_rsa_private => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'rsa-2048-wrong-public.pem'))), 14 | :wrong_rsa_public => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'rsa-2048-wrong-public.pem'))), 15 | 'ES256_private' => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec256-private.pem'))), 16 | 'ES256_public' => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec256-public.pem'))), 17 | 'ES384_private' => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec384-private.pem'))), 18 | 'ES384_public' => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec384-public.pem'))), 19 | 'ES512_private' => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec512-private.pem'))), 20 | 'ES512_public' => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec512-public.pem'))), 21 | 'NONE' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.', 22 | 'HS256' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.tCGvlClld0lbQ3NZaH8y53n5RSBr3zlS4Oy5bXqvzZQ', 23 | 'HS384' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.sj1gc01SawlJSrPZgmveifJ8CzZRYAWjejWm4FRaGaAISESJ9Ncf12fCz2vHrITm', 24 | 'HS512' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.isjhsWMZpRQOWw6LKtlY4L6tMDNkLr0qZ3bQe_xRFXWhzVvJlkclTbLVa1J6Dlj2WyZ_I1jEobTaFMDoXPzwWg', 25 | 'RS256' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.u82QrhjZTtwve5akvfWS_4LPywbkb1Yp0nUwZJWtTW0ID7dY9rRiQF5KGj2UDLZotqRlUjyNQgE_hB5BBzICDQdCjQHQoYWE5n_D2wV4PMu7Qg3FVKoBFbf8ee6irodu10fgYxpUIZtvbWw52_6k6A9IoSLSzx_lCcxoVGdW90dUuKhBcZkDtY5WNuQg7MiDthupSL1-V4Y1jmT_7o8tLNGFiocyZfGNw4yGpEOGNvD5WePNit0xsnbj6dEquovUvSFKsMaQXp2PVDEkLOiLMcyk0RrHqrHw2eNSCquWTH8PhX5Up-CVmjQM5zF9ibkaiq8NyPtsy-7rgtbyVMqXBQ', 26 | 'RS384' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.2_jPwOsUWJ-3r6lXMdJGPdhLNJQSSEmY2mrDXCwNJk-2YhMIqKAzJJCbyso_A1hS7BVkXmHt54RCcNJXroZBOgmGavCcYTPMaT6sCvVVvJJ_wn7jzKHNAJfL5nWeynTQIBWmL-m_v9QpZAgPALdeqjPRv4JHePZm23kvrUgQOxef2ldXv1l6IB3zfF72uEbk9T5pKBvgeeeQ46xm_HtkpXqMdqcTHawUXeXhuiWxuWfy9pAvhm8ivxwJhiQ15-sQNBlS9lG1_gQz1xaZ_Ou_n1nhNfGwpK5HeS0AgmqsqyCOvaGHeAuAOPZ_dSC3cFKu2AP7kc6_AKBgwJzh4agkXg', 27 | 'RS512' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.abwof7BqTvuLkN69OhEuFTP7vjGzfvAvooQdwIRne_a88MsjCq31n4UPvyIlY9_8u69rpU79RbMsrq_UZ6L85zP83EcyYI-HOfFZgYDAL3DJ7biBD99JTzyOsH_2i_E6yCkevjEX6uL_Am_C7jpWyePJQkYzTFni6mW4W1T9UobiVGA1tIZ-XOJDPHHxZkGu6W8lKW0UCsr9Ge2SCSlTs_LDSOa34gqMC5GP89unhLqSMqEMJ_Nm6Rj0rnmk87wBZM-b04LLteWuEU59QDNa4nMTjfXW74U4hX9n5EECDPQdQMecgxlUbFunAfZaoNzP4m7H4vux2FzYkjkXhdqnnw', 28 | 'ES256' => '', 29 | 'ES384' => '', 30 | 'ES512' => '' 31 | } 32 | end 33 | 34 | after(:each) do 35 | expect(OpenSSL.errors).to be_empty 36 | end 37 | 38 | context 'alg: NONE' do 39 | let(:alg) { 'none' } 40 | 41 | it 'should generate a valid token' do 42 | token = JWT.encode payload, nil, alg 43 | 44 | expect(token).to eq data['NONE'] 45 | end 46 | 47 | it 'should decode a valid token' do 48 | jwt_payload, header = JWT.decode data['NONE'], nil, false 49 | 50 | expect(header['alg']).to eq alg 51 | expect(jwt_payload).to eq payload 52 | end 53 | 54 | it 'should display a better error message if payload exp is_a?(Time)' do 55 | payload['exp'] = Time.now 56 | 57 | expect do 58 | JWT.encode payload, nil, alg 59 | end.to raise_error JWT::InvalidPayload 60 | end 61 | end 62 | 63 | %w(HS256 HS384 HS512).each do |alg| 64 | context "alg: #{alg}" do 65 | it 'should generate a valid token' do 66 | token = JWT.encode payload, data[:secret], alg 67 | 68 | expect(token).to eq data[alg] 69 | end 70 | 71 | it 'should decode a valid token' do 72 | jwt_payload, header = JWT.decode data[alg], data[:secret] 73 | 74 | expect(header['alg']).to eq alg 75 | expect(jwt_payload).to eq payload 76 | end 77 | 78 | it 'wrong secret should raise JWT::DecodeError' do 79 | expect do 80 | JWT.decode data[alg], 'wrong_secret' 81 | end.to raise_error JWT::DecodeError 82 | end 83 | 84 | it 'wrong secret and verify = false should not raise JWT::DecodeError' do 85 | expect do 86 | JWT.decode data[alg], 'wrong_secret', false 87 | end.not_to raise_error 88 | end 89 | end 90 | end 91 | 92 | %w(RS256 RS384 RS512).each do |alg| 93 | context "alg: #{alg}" do 94 | it 'should generate a valid token' do 95 | token = JWT.encode payload, data[:rsa_private], alg 96 | 97 | expect(token).to eq data[alg] 98 | end 99 | 100 | it 'should decode a valid token' do 101 | jwt_payload, header = JWT.decode data[alg], data[:rsa_public] 102 | 103 | expect(header['alg']).to eq alg 104 | expect(jwt_payload).to eq payload 105 | end 106 | 107 | it 'wrong key should raise JWT::DecodeError' do 108 | key = OpenSSL::PKey.read File.read(File.join(CERT_PATH, 'rsa-2048-wrong-public.pem')) 109 | 110 | expect do 111 | JWT.decode data[alg], key 112 | end.to raise_error JWT::DecodeError 113 | end 114 | 115 | it 'wrong key and verify = false should not raise JWT::DecodeError' do 116 | key = OpenSSL::PKey.read File.read(File.join(CERT_PATH, 'rsa-2048-wrong-public.pem')) 117 | 118 | expect do 119 | JWT.decode data[alg], key, false 120 | end.not_to raise_error 121 | end 122 | end 123 | end 124 | 125 | %w(ES256 ES384 ES512).each do |alg| 126 | context "alg: #{alg}" do 127 | before(:each) do 128 | data[alg] = JWT.encode payload, data["#{alg}_private"], alg 129 | end 130 | 131 | let(:wrong_key) { OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec256-wrong-public.pem'))) } 132 | 133 | it 'should generate a valid token' do 134 | jwt_payload, header = JWT.decode data[alg], data["#{alg}_public"] 135 | 136 | expect(header['alg']).to eq alg 137 | expect(jwt_payload).to eq payload 138 | end 139 | 140 | it 'should decode a valid token' do 141 | jwt_payload, header = JWT.decode data[alg], data["#{alg}_public"] 142 | 143 | expect(header['alg']).to eq alg 144 | expect(jwt_payload).to eq payload 145 | end 146 | 147 | it 'wrong key should raise JWT::DecodeError' do 148 | expect do 149 | JWT.decode data[alg], wrong_key 150 | end.to raise_error JWT::DecodeError 151 | end 152 | 153 | it 'wrong key and verify = false should not raise JWT::DecodeError' do 154 | expect do 155 | JWT.decode data[alg], wrong_key, false 156 | end.not_to raise_error 157 | end 158 | end 159 | end 160 | 161 | context 'Invalid' do 162 | it 'algorithm should raise NotImplementedError' do 163 | expect do 164 | JWT.encode payload, 'secret', 'HS255' 165 | end.to raise_error NotImplementedError 166 | end 167 | 168 | it 'ECDSA curve_name should raise JWT::IncorrectAlgorithm' do 169 | key = OpenSSL::PKey::EC.new 'secp256k1' 170 | key.generate_key 171 | 172 | expect do 173 | JWT.encode payload, key, 'ES256' 174 | end.to raise_error JWT::IncorrectAlgorithm 175 | 176 | token = JWT.encode payload, data['ES256_private'], 'ES256' 177 | key.private_key = nil 178 | 179 | expect do 180 | JWT.decode token, key 181 | end.to raise_error JWT::IncorrectAlgorithm 182 | end 183 | end 184 | 185 | context 'Verify' do 186 | context 'algorithm' do 187 | it 'should raise JWT::IncorrectAlgorithm on missmatch' do 188 | token = JWT.encode payload, data[:secret], 'HS512' 189 | 190 | expect do 191 | JWT.decode token, data[:secret], true, algorithm: 'HS384' 192 | end.to raise_error JWT::IncorrectAlgorithm 193 | 194 | expect do 195 | JWT.decode token, data[:secret], true, algorithm: 'HS512' 196 | end.not_to raise_error 197 | end 198 | end 199 | 200 | context 'issuer claim' do 201 | let(:iss) { 'ruby-jwt-gem' } 202 | let(:invalid_token) { JWT.encode payload, data[:secret] } 203 | 204 | let :token do 205 | iss_payload = payload.merge(iss: iss) 206 | JWT.encode iss_payload, data[:secret] 207 | end 208 | 209 | it 'if verify_iss is set to false (default option) should not raise JWT::InvalidIssuerError' do 210 | expect do 211 | JWT.decode token, data[:secret], true, iss: iss 212 | end.not_to raise_error 213 | end 214 | end 215 | end 216 | 217 | context 'Base64' do 218 | it 'urlsafe replace + / with - _' do 219 | allow(Base64).to receive(:encode64) { 'string+with/non+url-safe/characters_' } 220 | expect(JWT.base64url_encode('foo')).to eq('string-with_non-url-safe_characters_') 221 | end 222 | end 223 | 224 | describe 'secure comparison' do 225 | it 'returns true if strings are equal' do 226 | expect(JWT.secure_compare('Foo', 'Foo')).to eq true 227 | end 228 | 229 | it 'returns false if either input is nil or empty' do 230 | [nil, ''].each do |bad| 231 | expect(JWT.secure_compare(bad, 'Foo')).to eq false 232 | expect(JWT.secure_compare('Foo', bad)).to eq false 233 | end 234 | end 235 | 236 | it 'retuns false if the strings are different' do 237 | expect(JWT.secure_compare('Foo', 'Bar')).to eq false 238 | end 239 | end 240 | end 241 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JWT 2 | 3 | [![Build Status](https://travis-ci.org/jwt/ruby-jwt.svg)](https://travis-ci.org/jwt/ruby-jwt) 4 | [![Code Climate](https://codeclimate.com/github/jwt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/jwt/ruby-jwt) 5 | [![Test Coverage](https://codeclimate.com/github/jwt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/jwt/ruby-jwt/coverage) 6 | [![Issue Count](https://codeclimate.com/github/jwt/ruby-jwt/badges/issue_count.svg)](https://codeclimate.com/github/jwt/ruby-jwt) 7 | 8 | A pure ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard. 9 | 10 | If you have further questions releated to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt). 11 | 12 | ## Announcements 13 | 14 | * Ruby 1.9.3 support will be dropped by December 31st, 2016. 15 | * Version 1.5.3 yanked. See: [#132](https://github.com/jwt/ruby-jwt/issues/132) and [#133](https://github.com/jwt/ruby-jwt/issues/133) 16 | 17 | ## Installing 18 | 19 | ### Using Rubygems: 20 | ```bash 21 | sudo gem install jwt 22 | ``` 23 | 24 | ### Using Bundler: 25 | Add the following to your Gemfile 26 | ``` 27 | gem 'jwt' 28 | ``` 29 | And run `bundle install` 30 | 31 | ## Algorithms and Usage 32 | 33 | The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using cryptographic signing, you need to specify the algorithm in the options hash whenever you call JWT.decode to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). 34 | 35 | See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1) 36 | 37 | **NONE** 38 | 39 | * none - unsigned token 40 | 41 | ```ruby 42 | require 'jwt' 43 | 44 | payload = {:data => 'test'} 45 | 46 | # IMPORTANT: set nil as password parameter 47 | token = JWT.encode payload, nil, 'none' 48 | 49 | # eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9. 50 | puts token 51 | 52 | # Set password to nil and validation to false otherwise this won't work 53 | decoded_token = JWT.decode token, nil, false 54 | 55 | # Array 56 | # [ 57 | # {"data"=>"test"}, # payload 58 | # {"typ"=>"JWT", "alg"=>"none"} # header 59 | # ] 60 | puts decoded_token 61 | ``` 62 | 63 | **HMAC** (default: HS256) 64 | 65 | * HS256 - HMAC using SHA-256 hash algorithm (default) 66 | * HS384 - HMAC using SHA-384 hash algorithm 67 | * HS512 - HMAC using SHA-512 hash algorithm 68 | 69 | ```ruby 70 | hmac_secret = 'my$ecretK3y' 71 | 72 | token = JWT.encode payload, hmac_secret, 'HS256' 73 | 74 | # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.ZxW8go9hz3ETCSfxFxpwSkYg_602gOPKearsf6DsxgY 75 | puts token 76 | 77 | decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' } 78 | 79 | # Array 80 | # [ 81 | # {"data"=>"test"}, # payload 82 | # {"typ"=>"JWT", "alg"=>"HS256"} # header 83 | # ] 84 | puts decoded_token 85 | ``` 86 | 87 | **RSA** 88 | 89 | * RS256 - RSA using SHA-256 hash algorithm 90 | * RS384 - RSA using SHA-384 hash algorithm 91 | * RS512 - RSA using SHA-512 hash algorithm 92 | 93 | ```ruby 94 | rsa_private = OpenSSL::PKey::RSA.generate 2048 95 | rsa_public = rsa_private.public_key 96 | 97 | token = JWT.encode payload, rsa_private, 'RS256' 98 | 99 | # eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ0ZXN0IjoiZGF0YSJ9.c2FynXNyi6_PeKxrDGxfS3OLwQ8lTDbWBWdq7oMviCy2ZfFpzvW2E_odCWJrbLof-eplHCsKzW7MGAntHMALXgclm_Cs9i2Exi6BZHzpr9suYkrhIjwqV1tCgMBCQpdeMwIq6SyKVjgH3L51ivIt0-GDDPDH1Rcut3jRQzp3Q35bg3tcI2iVg7t3Msvl9QrxXAdYNFiS5KXH22aJZ8X_O2HgqVYBXfSB1ygTYUmKTIIyLbntPQ7R22rFko1knGWOgQCoYXwbtpuKRZVFrxX958L2gUWgb4jEQNf3fhOtkBm1mJpj-7BGst00o8g_3P2zHy-3aKgpPo1XlKQGjRrrxA 100 | puts token 101 | 102 | decoded_token = JWT.decode token, rsa_public, true, { :algorithm => 'RS256' } 103 | 104 | # Array 105 | # [ 106 | # {"data"=>"test"}, # payload 107 | # {"typ"=>"JWT", "alg"=>"RS256"} # header 108 | # ] 109 | puts decoded_token 110 | ``` 111 | 112 | **ECDSA** 113 | 114 | * ES256 - ECDSA using P-256 and SHA-256 115 | * ES384 - ECDSA using P-384 and SHA-384 116 | * ES512 - ECDSA using P-521 and SHA-512 117 | 118 | ```ruby 119 | ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1' 120 | ecdsa_key.generate_key 121 | ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key 122 | ecdsa_public.private_key = nil 123 | 124 | token = JWT.encode payload, ecdsa_key, 'ES256' 125 | 126 | # eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJ0ZXN0IjoiZGF0YSJ9.MEQCIAtShrxRwP1L9SapqaT4f7hajDJH4t_rfm-YlZcNDsBNAiB64M4-JRfyS8nRMlywtQ9lHbvvec9U54KznzOe1YxTyA 127 | puts token 128 | 129 | decoded_token = JWT.decode token, ecdsa_public, true, { :algorithm => 'ES256' } 130 | 131 | # Array 132 | # [ 133 | # {"test"=>"data"}, # payload 134 | # {"typ"=>"JWT", "alg"=>"ES256"} # header 135 | # ] 136 | puts decoded_token 137 | ``` 138 | 139 | **RSASSA-PSS** 140 | 141 | Not implemented. 142 | 143 | ## Support for reserved claim names 144 | JSON Web Token defines some reserved claim names and defines how they should be 145 | used. JWT supports these reserved claim names: 146 | 147 | - 'exp' (Expiration Time) Claim 148 | - 'nbf' (Not Before Time) Claim 149 | - 'iss' (Issuer) Claim 150 | - 'aud' (Audience) Claim 151 | - 'jti' (JWT ID) Claim 152 | - 'iat' (Issued At) Claim 153 | - 'sub' (Subject) Claim 154 | 155 | ### Expiration Time Claim 156 | 157 | From [Oauth JSON Web Token 4.1.4. "exp" (Expiration Time) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.4): 158 | 159 | > The `exp` (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the `exp` claim requires that the current date/time MUST be before the expiration date/time listed in the `exp` claim. Implementers MAY provide for some small `leeway`, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a ***NumericDate*** value. Use of this claim is OPTIONAL. 160 | 161 | **Handle Expiration Claim** 162 | 163 | ```ruby 164 | exp = Time.now.to_i + 4 * 3600 165 | exp_payload = { :data => 'data', :exp => exp } 166 | 167 | token = JWT.encode exp_payload, hmac_secret, 'HS256' 168 | 169 | begin 170 | decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' } 171 | rescue JWT::ExpiredSignature 172 | # Handle expired token, e.g. logout user or deny access 173 | end 174 | ``` 175 | 176 | **Adding Leeway** 177 | 178 | ```ruby 179 | exp = Time.now.to_i - 10 180 | leeway = 30 # seconds 181 | 182 | exp_payload = { :data => 'data', :exp => exp } 183 | 184 | # build expired token 185 | token = JWT.encode exp_payload, hmac_secret, 'HS256' 186 | 187 | begin 188 | # add leeway to ensure the token is still accepted 189 | decoded_token = JWT.decode token, hmac_secret, true, { :leeway => leeway, :algorithm => 'HS256' } 190 | rescue JWT::ExpiredSignature 191 | # Handle expired token, e.g. logout user or deny access 192 | end 193 | ``` 194 | 195 | ### Not Before Time Claim 196 | 197 | From [Oauth JSON Web Token 4.1.5. "nbf" (Not Before) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.5): 198 | 199 | > The `nbf` (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. The processing of the `nbf` claim requires that the current date/time MUST be after or equal to the not-before date/time listed in the `nbf` claim. Implementers MAY provide for some small `leeway`, usually no more than a few minutes, to account for clock skew. Its value MUST be a number containing a ***NumericDate*** value. Use of this claim is OPTIONAL. 200 | 201 | **Handle Not Before Claim** 202 | 203 | ```ruby 204 | nbf = Time.now.to_i - 3600 205 | nbf_payload = { :data => 'data', :nbf => nbf } 206 | 207 | token = JWT.encode nbf_payload, hmac_secret, 'HS256' 208 | 209 | begin 210 | decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' } 211 | rescue JWT::ImmatureSignature 212 | # Handle invalid token, e.g. logout user or deny access 213 | end 214 | ``` 215 | 216 | **Adding Leeway** 217 | 218 | ```ruby 219 | nbf = Time.now.to_i + 10 220 | leeway = 30 221 | 222 | nbf_payload = { :data => 'data', :nbf => nbf } 223 | 224 | # build expired token 225 | token = JWT.encode nbf_payload, hmac_secret, 'HS256' 226 | 227 | begin 228 | # add leeway to ensure the token is valid 229 | decoded_token = JWT.decode token, hmac_secret, true, { :leeway => leeway, :algorithm => 'HS256' } 230 | rescue JWT::ImmatureSignature 231 | # Handle invalid token, e.g. logout user or deny access 232 | end 233 | ``` 234 | 235 | ### Issuer Claim 236 | 237 | From [Oauth JSON Web Token 4.1.1. "iss" (Issuer) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.1): 238 | 239 | > The `iss` (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The `iss` value is a case-sensitive string containing a ***StringOrURI*** value. Use of this claim is OPTIONAL. 240 | 241 | ```ruby 242 | iss = 'My Awesome Company Inc. or https://my.awesome.website/' 243 | iss_payload = { :data => 'data', :iss => iss } 244 | 245 | token = JWT.encode iss_payload, hmac_secret, 'HS256' 246 | 247 | begin 248 | # Add iss to the validation to check if the token has been manipulated 249 | decoded_token = JWT.decode token, hmac_secret, true, { :iss => iss, :verify_iss => true, :algorithm => 'HS256' } 250 | rescue JWT::InvalidIssuerError 251 | # Handle invalid token, e.g. logout user or deny access 252 | end 253 | ``` 254 | 255 | ### Audience Claim 256 | 257 | From [Oauth JSON Web Token 4.1.3. "aud" (Audience) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.3): 258 | 259 | > The `aud` (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the `aud` claim when this claim is present, then the JWT MUST be rejected. In the general case, the `aud` value is an array of case-sensitive strings, each containing a ***StringOrURI*** value. In the special case when the JWT has one audience, the `aud` value MAY be a single case-sensitive string containing a ***StringOrURI*** value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL. 260 | 261 | ```ruby 262 | aud = ['Young', 'Old'] 263 | aud_payload = { :data => 'data', :aud => aud } 264 | 265 | token = JWT.encode aud_payload, hmac_secret, 'HS256' 266 | 267 | begin 268 | # Add aud to the validation to check if the token has been manipulated 269 | decoded_token = JWT.decode token, hmac_secret, true, { :aud => aud, :verify_aud => true, :algorithm => 'HS256' } 270 | rescue JWT::InvalidAudError 271 | # Handle invalid token, e.g. logout user or deny access 272 | puts 'Audience Error' 273 | end 274 | ``` 275 | 276 | ### JWT ID Claim 277 | 278 | From [Oauth JSON Web Token 4.1.7. "jti" (JWT ID) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.7): 279 | 280 | > The `jti` (JWT ID) claim provides a unique identifier for the JWT. The identifier value MUST be assigned in a manner that ensures that there is a negligible probability that the same value will be accidentally assigned to a different data object; if the application uses multiple issuers, collisions MUST be prevented among values produced by different issuers as well. The `jti` claim can be used to prevent the JWT from being replayed. The `jti` value is a case-sensitive string. Use of this claim is OPTIONAL. 281 | 282 | ```ruby 283 | # Use the secret and iat to create a unique key per request to prevent replay attacks 284 | jti_raw = [hmac_secret, iat].join(':').to_s 285 | jti = Digest::MD5.hexdigest(jti_raw) 286 | jti_payload = { :data => 'data', :iat => iat, :jti => jti } 287 | 288 | token = JWT.encode jti_payload, hmac_secret, 'HS256' 289 | 290 | begin 291 | # If :verify_jti is true, validation will pass if a JTI is present 292 | #decoded_token = JWT.decode token, hmac_secret, true, { :verify_jti => true, :algorithm => 'HS256' } 293 | # Alternatively, pass a proc with your own code to check if the JTI has already been used 294 | decoded_token = JWT.decode token, hmac_secret, true, { :verify_jti => proc { |jti| my_validation_method(jti) }, :algorithm => 'HS256' } 295 | rescue JWT::InvalidJtiError 296 | # Handle invalid token, e.g. logout user or deny access 297 | puts 'Error' 298 | end 299 | 300 | ``` 301 | 302 | ### Issued At Claim 303 | 304 | From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.6): 305 | 306 | > The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a ***NumericDate*** value. Use of this claim is OPTIONAL. 307 | 308 | ```ruby 309 | iat = Time.now.to_i 310 | iat_payload = { :data => 'data', :iat => iat } 311 | 312 | token = JWT.encode iat_payload, hmac_secret, 'HS256' 313 | 314 | begin 315 | # Add iat to the validation to check if the token has been manipulated 316 | decoded_token = JWT.decode token, hmac_secret, true, { :verify_iat => true, :algorithm => 'HS256' } 317 | rescue JWT::InvalidIatError 318 | # Handle invalid token, e.g. logout user or deny access 319 | end 320 | ``` 321 | 322 | ### Subject Claim 323 | 324 | From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.2): 325 | 326 | > The `sub` (subject) claim identifies the principal that is the subject of the JWT. The Claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The sub value is a case-sensitive string containing a ***StringOrURI*** value. Use of this claim is OPTIONAL. 327 | 328 | ```ruby 329 | sub = 'Subject' 330 | sub_payload = { :data => 'data', :sub => sub } 331 | 332 | token = JWT.encode sub_payload, hmac_secret, 'HS256' 333 | 334 | begin 335 | # Add sub to the validation to check if the token has been manipulated 336 | decoded_token = JWT.decode token, hmac_secret, true, { 'sub' => sub, :verify_sub => true, :algorithm => 'HS256' } 337 | rescue JWT::InvalidSubError 338 | # Handle invalid token, e.g. logout user or deny access 339 | end 340 | ``` 341 | 342 | # Development and Tests 343 | 344 | We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with 345 | 346 | ```bash 347 | rake release 348 | ``` 349 | 350 | The tests are written with rspec. Given you have installed the dependencies via bundler, you can run tests with 351 | 352 | ```bash 353 | bundle exec rspec 354 | ``` 355 | 356 | **If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)** 357 | 358 | ## Contributors 359 | 360 | * Jordan Brough 361 | * Ilya Zhitomirskiy 362 | * Daniel Grippi 363 | * Jeff Lindsay 364 | * Bob Aman 365 | * Micah Gates 366 | * Rob Wygand 367 | * Ariel Salomon (Oscil8) 368 | * Paul Battley 369 | * Zane Shannon [@zshannon](https://github.com/zshannon) 370 | * Brian Fletcher [@punkle](https://github.com/punkle) 371 | * Alex [@ZhangHanDong](https://github.com/ZhangHanDong) 372 | * John Downey [@jtdowney](https://github.com/jtdowney) 373 | * Adam Greene [@skippy](https://github.com/skippy) 374 | * Tim Rudat [@excpt](https://github.com/excpt) - Maintainer 375 | 376 | ## License 377 | 378 | MIT 379 | 380 | Copyright (c) 2011 Jeff Lindsay 381 | 382 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 383 | 384 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 385 | 386 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 387 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v1.5.6](https://github.com/jwt/ruby-jwt/tree/v1.5.6) (2016-09-19) 4 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v1.5.5...v1.5.6) 5 | 6 | **Fixed bugs:** 7 | 8 | - Fix missing symbol handling in aud verify code [\#166](https://github.com/jwt/ruby-jwt/pull/166) ([excpt](https://github.com/excpt)) 9 | 10 | **Merged pull requests:** 11 | 12 | - Fix rubocop code smells [\#167](https://github.com/jwt/ruby-jwt/pull/167) ([excpt](https://github.com/excpt)) 13 | 14 | ## [v1.5.5](https://github.com/jwt/ruby-jwt/tree/v1.5.5) (2016-09-16) 15 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v1.5.4...v1.5.5) 16 | 17 | **Implemented enhancements:** 18 | 19 | - JWT.decode always raises JWT::ExpiredSignature for tokens created with Time objects passed as the `exp` parameter [\#148](https://github.com/jwt/ruby-jwt/issues/148) 20 | 21 | **Fixed bugs:** 22 | 23 | - expiration check does not give "Signature has expired" error for the exact time of expiration [\#157](https://github.com/jwt/ruby-jwt/issues/157) 24 | - JTI claim broken? [\#152](https://github.com/jwt/ruby-jwt/issues/152) 25 | - Audience Claim broken? [\#151](https://github.com/jwt/ruby-jwt/issues/151) 26 | - 1.5.3 breaks compatibility with 1.5.2 [\#133](https://github.com/jwt/ruby-jwt/issues/133) 27 | - Version 1.5.3 breaks 1.9.3 compatibility, but not documented as such [\#132](https://github.com/jwt/ruby-jwt/issues/132) 28 | - Fix: exp claim check [\#161](https://github.com/jwt/ruby-jwt/pull/161) ([excpt](https://github.com/excpt)) 29 | 30 | **Closed issues:** 31 | 32 | - Rendering Json Results in JWT::DecodeError [\#162](https://github.com/jwt/ruby-jwt/issues/162) 33 | - PHP Libraries [\#154](https://github.com/jwt/ruby-jwt/issues/154) 34 | - \[security\] Signature verified after expiration/sub/iss checks [\#153](https://github.com/jwt/ruby-jwt/issues/153) 35 | - Is ruby-jwt thread-safe? [\#150](https://github.com/jwt/ruby-jwt/issues/150) 36 | - JWT 1.5.3 [\#143](https://github.com/jwt/ruby-jwt/issues/143) 37 | - gem install v 1.5.3 returns error [\#141](https://github.com/jwt/ruby-jwt/issues/141) 38 | - Adding a CHANGELOG [\#140](https://github.com/jwt/ruby-jwt/issues/140) 39 | 40 | **Merged pull requests:** 41 | 42 | - Bump version [\#165](https://github.com/jwt/ruby-jwt/pull/165) ([excpt](https://github.com/excpt)) 43 | - Improve error message for exp claim in payload [\#164](https://github.com/jwt/ruby-jwt/pull/164) ([excpt](https://github.com/excpt)) 44 | - Fix \#151 and code refactoring [\#163](https://github.com/jwt/ruby-jwt/pull/163) ([excpt](https://github.com/excpt)) 45 | - Signature validation before claim verification [\#160](https://github.com/jwt/ruby-jwt/pull/160) ([excpt](https://github.com/excpt)) 46 | - Create specs for README.md examples [\#159](https://github.com/jwt/ruby-jwt/pull/159) ([excpt](https://github.com/excpt)) 47 | - Tiny Readme Improvement [\#156](https://github.com/jwt/ruby-jwt/pull/156) ([b264](https://github.com/b264)) 48 | - Added test execution to Rakefile [\#147](https://github.com/jwt/ruby-jwt/pull/147) ([jabbrwcky](https://github.com/jabbrwcky)) 49 | - Add more bling bling to the site [\#146](https://github.com/jwt/ruby-jwt/pull/146) ([excpt](https://github.com/excpt)) 50 | - Bump version [\#145](https://github.com/jwt/ruby-jwt/pull/145) ([excpt](https://github.com/excpt)) 51 | - Add first content and basic layout [\#144](https://github.com/jwt/ruby-jwt/pull/144) ([excpt](https://github.com/excpt)) 52 | - Add a changelog file [\#142](https://github.com/jwt/ruby-jwt/pull/142) ([excpt](https://github.com/excpt)) 53 | - Return decoded\_segments [\#139](https://github.com/jwt/ruby-jwt/pull/139) ([akostrikov](https://github.com/akostrikov)) 54 | 55 | ## [v1.5.4](https://github.com/jwt/ruby-jwt/tree/v1.5.4) (2016-03-24) 56 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v1.5.3...v1.5.4) 57 | 58 | **Closed issues:** 59 | 60 | - 404 at https://rubygems.global.ssl.fastly.net/gems/jwt-1.5.3.gem [\#137](https://github.com/jwt/ruby-jwt/issues/137) 61 | 62 | **Merged pull requests:** 63 | 64 | - Update README.md [\#138](https://github.com/jwt/ruby-jwt/pull/138) ([excpt](https://github.com/excpt)) 65 | - Fix base64url\_decode [\#136](https://github.com/jwt/ruby-jwt/pull/136) ([excpt](https://github.com/excpt)) 66 | - Fix ruby 1.9.3 compatibility [\#135](https://github.com/jwt/ruby-jwt/pull/135) ([excpt](https://github.com/excpt)) 67 | - iat can be a float value [\#134](https://github.com/jwt/ruby-jwt/pull/134) ([llimllib](https://github.com/llimllib)) 68 | 69 | ## [v1.5.3](https://github.com/jwt/ruby-jwt/tree/v1.5.3) (2016-02-24) 70 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.5.2...v1.5.3) 71 | 72 | **Implemented enhancements:** 73 | 74 | - Refactor obsolete code for ruby 1.8 support [\#120](https://github.com/jwt/ruby-jwt/issues/120) 75 | - Fix "Rubocop/Metrics/CyclomaticComplexity" issue in lib/jwt.rb [\#106](https://github.com/jwt/ruby-jwt/issues/106) 76 | - Fix "Rubocop/Metrics/CyclomaticComplexity" issue in lib/jwt.rb [\#105](https://github.com/jwt/ruby-jwt/issues/105) 77 | - Allow a proc to be passed for JTI verification [\#126](https://github.com/jwt/ruby-jwt/pull/126) ([yahooguntu](https://github.com/yahooguntu)) 78 | - Relax restrictions on "jti" claim verification [\#113](https://github.com/jwt/ruby-jwt/pull/113) ([lwe](https://github.com/lwe)) 79 | 80 | **Closed issues:** 81 | 82 | - Verifications not functioning in latest release [\#128](https://github.com/jwt/ruby-jwt/issues/128) 83 | - Base64 is generating invalid length base64 strings - cross language interop [\#127](https://github.com/jwt/ruby-jwt/issues/127) 84 | - Digest::Digest is deprecated; use Digest [\#119](https://github.com/jwt/ruby-jwt/issues/119) 85 | - verify\_rsa no method 'verify' for class String [\#115](https://github.com/jwt/ruby-jwt/issues/115) 86 | - Add a changelog [\#111](https://github.com/jwt/ruby-jwt/issues/111) 87 | 88 | **Merged pull requests:** 89 | 90 | - Drop ruby 1.9.3 support [\#131](https://github.com/jwt/ruby-jwt/pull/131) ([excpt](https://github.com/excpt)) 91 | - Allow string hash keys in validation configurations [\#130](https://github.com/jwt/ruby-jwt/pull/130) ([tpickett66](https://github.com/tpickett66)) 92 | - Add ruby 2.3.0 for travis ci testing [\#123](https://github.com/jwt/ruby-jwt/pull/123) ([excpt](https://github.com/excpt)) 93 | - Remove obsolete json code [\#122](https://github.com/jwt/ruby-jwt/pull/122) ([excpt](https://github.com/excpt)) 94 | - Add fancy badges to README.md [\#118](https://github.com/jwt/ruby-jwt/pull/118) ([excpt](https://github.com/excpt)) 95 | - Refactor decode and verify functionality [\#117](https://github.com/jwt/ruby-jwt/pull/117) ([excpt](https://github.com/excpt)) 96 | - Drop echoe dependency for gem releases [\#116](https://github.com/jwt/ruby-jwt/pull/116) ([excpt](https://github.com/excpt)) 97 | - Updated readme for iss/aud options [\#114](https://github.com/jwt/ruby-jwt/pull/114) ([ryanmcilmoyl](https://github.com/ryanmcilmoyl)) 98 | - Fix error misspelling [\#112](https://github.com/jwt/ruby-jwt/pull/112) ([kat3kasper](https://github.com/kat3kasper)) 99 | 100 | ## [jwt-1.5.2](https://github.com/jwt/ruby-jwt/tree/jwt-1.5.2) (2015-10-27) 101 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.5.1...jwt-1.5.2) 102 | 103 | **Implemented enhancements:** 104 | 105 | - Must we specify algorithm when calling decode to avoid vulnerabilities? [\#107](https://github.com/jwt/ruby-jwt/issues/107) 106 | - Code review: Rspec test refactoring [\#85](https://github.com/jwt/ruby-jwt/pull/85) ([excpt](https://github.com/excpt)) 107 | 108 | **Fixed bugs:** 109 | 110 | - aud verifies if aud is passed in, :sub does not [\#102](https://github.com/jwt/ruby-jwt/issues/102) 111 | - iat check does not use leeway so nbf could pass, but iat fail [\#83](https://github.com/jwt/ruby-jwt/issues/83) 112 | 113 | **Closed issues:** 114 | 115 | - Test ticket from Code Climate [\#104](https://github.com/jwt/ruby-jwt/issues/104) 116 | - Test ticket from Code Climate [\#100](https://github.com/jwt/ruby-jwt/issues/100) 117 | - Is it possible to decode the payload without validating the signature? [\#97](https://github.com/jwt/ruby-jwt/issues/97) 118 | - What is audience? [\#96](https://github.com/jwt/ruby-jwt/issues/96) 119 | - Options hash uses both symbols and strings as keys. [\#95](https://github.com/jwt/ruby-jwt/issues/95) 120 | 121 | **Merged pull requests:** 122 | 123 | - Fix incorrect `iat` examples [\#109](https://github.com/jwt/ruby-jwt/pull/109) ([kjwierenga](https://github.com/kjwierenga)) 124 | - Update docs to include instructions for the algorithm parameter. [\#108](https://github.com/jwt/ruby-jwt/pull/108) ([aarongray](https://github.com/aarongray)) 125 | - make sure :sub check behaves like :aud check [\#103](https://github.com/jwt/ruby-jwt/pull/103) ([skippy](https://github.com/skippy)) 126 | - Change hash syntax [\#101](https://github.com/jwt/ruby-jwt/pull/101) ([excpt](https://github.com/excpt)) 127 | - Include LICENSE and README.md in gem [\#99](https://github.com/jwt/ruby-jwt/pull/99) ([bkeepers](https://github.com/bkeepers)) 128 | - Remove unused variable in the sample code. [\#98](https://github.com/jwt/ruby-jwt/pull/98) ([hypermkt](https://github.com/hypermkt)) 129 | - Fix iat claim example [\#94](https://github.com/jwt/ruby-jwt/pull/94) ([larrylv](https://github.com/larrylv)) 130 | - Fix wrong description in README.md [\#93](https://github.com/jwt/ruby-jwt/pull/93) ([larrylv](https://github.com/larrylv)) 131 | - JWT and JWA are now RFC. [\#92](https://github.com/jwt/ruby-jwt/pull/92) ([aj-michael](https://github.com/aj-michael)) 132 | - Update README.md [\#91](https://github.com/jwt/ruby-jwt/pull/91) ([nsarno](https://github.com/nsarno)) 133 | - Fix missing verify parameter in docs [\#90](https://github.com/jwt/ruby-jwt/pull/90) ([ernie](https://github.com/ernie)) 134 | - Iat check uses leeway. [\#89](https://github.com/jwt/ruby-jwt/pull/89) ([aj-michael](https://github.com/aj-michael)) 135 | - nbf check allows exact time matches. [\#88](https://github.com/jwt/ruby-jwt/pull/88) ([aj-michael](https://github.com/aj-michael)) 136 | 137 | ## [jwt-1.5.1](https://github.com/jwt/ruby-jwt/tree/jwt-1.5.1) (2015-06-22) 138 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.5.0...jwt-1.5.1) 139 | 140 | **Implemented enhancements:** 141 | 142 | - Fix either README or source code [\#78](https://github.com/jwt/ruby-jwt/issues/78) 143 | - Validate against draft 20 [\#38](https://github.com/jwt/ruby-jwt/issues/38) 144 | 145 | **Fixed bugs:** 146 | 147 | - ECDSA signature verification fails for valid tokens [\#84](https://github.com/jwt/ruby-jwt/issues/84) 148 | - Shouldn't verification of additional claims, like iss, aud etc. be enforced when in options? [\#81](https://github.com/jwt/ruby-jwt/issues/81) 149 | - Fix either README or source code [\#78](https://github.com/jwt/ruby-jwt/issues/78) 150 | - decode fails with 'none' algorithm and verify [\#75](https://github.com/jwt/ruby-jwt/issues/75) 151 | 152 | **Closed issues:** 153 | 154 | - Doc mismatch: uninitialized constant JWT::ExpiredSignature [\#79](https://github.com/jwt/ruby-jwt/issues/79) 155 | - TypeError when specifying a wrong algorithm [\#77](https://github.com/jwt/ruby-jwt/issues/77) 156 | - jti verification doesn't prevent replays [\#73](https://github.com/jwt/ruby-jwt/issues/73) 157 | 158 | **Merged pull requests:** 159 | 160 | - Correctly sign ECDSA JWTs [\#87](https://github.com/jwt/ruby-jwt/pull/87) ([jurriaan](https://github.com/jurriaan)) 161 | - fixed results of decoded tokens in readme [\#86](https://github.com/jwt/ruby-jwt/pull/86) ([piscolomo](https://github.com/piscolomo)) 162 | - Force verification of "iss" and "aud" claims [\#82](https://github.com/jwt/ruby-jwt/pull/82) ([lwe](https://github.com/lwe)) 163 | 164 | ## [jwt-1.5.0](https://github.com/jwt/ruby-jwt/tree/jwt-1.5.0) (2015-05-09) 165 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.4.1...jwt-1.5.0) 166 | 167 | **Implemented enhancements:** 168 | 169 | - Needs to support asymmetric key signatures over shared secrets [\#46](https://github.com/jwt/ruby-jwt/issues/46) 170 | - Implement Elliptic Curve Crypto Signatures [\#74](https://github.com/jwt/ruby-jwt/pull/74) ([jtdowney](https://github.com/jtdowney)) 171 | - Add an option to verify the signature on decode [\#71](https://github.com/jwt/ruby-jwt/pull/71) ([javawizard](https://github.com/javawizard)) 172 | 173 | **Closed issues:** 174 | 175 | - Check JWT vulnerability [\#76](https://github.com/jwt/ruby-jwt/issues/76) 176 | 177 | **Merged pull requests:** 178 | 179 | - Fixed some examples to make them copy-pastable [\#72](https://github.com/jwt/ruby-jwt/pull/72) ([jer](https://github.com/jer)) 180 | 181 | ## [jwt-1.4.1](https://github.com/jwt/ruby-jwt/tree/jwt-1.4.1) (2015-03-12) 182 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.4.0...jwt-1.4.1) 183 | 184 | **Fixed bugs:** 185 | 186 | - jti verification not working per the spec [\#68](https://github.com/jwt/ruby-jwt/issues/68) 187 | - Verify ISS should be off by default [\#66](https://github.com/jwt/ruby-jwt/issues/66) 188 | 189 | **Merged pull requests:** 190 | 191 | - Fix \#66 \#68 [\#69](https://github.com/jwt/ruby-jwt/pull/69) ([excpt](https://github.com/excpt)) 192 | - When throwing errors, mention expected/received values [\#65](https://github.com/jwt/ruby-jwt/pull/65) ([rolodato](https://github.com/rolodato)) 193 | 194 | ## [jwt-1.4.0](https://github.com/jwt/ruby-jwt/tree/jwt-1.4.0) (2015-03-10) 195 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.3.0...jwt-1.4.0) 196 | 197 | **Closed issues:** 198 | 199 | - The behavior using 'json' differs from 'multi\_json' [\#41](https://github.com/jwt/ruby-jwt/issues/41) 200 | 201 | **Merged pull requests:** 202 | 203 | - Release 1.4.0 [\#64](https://github.com/jwt/ruby-jwt/pull/64) ([excpt](https://github.com/excpt)) 204 | - Update README.md and remove dead code [\#63](https://github.com/jwt/ruby-jwt/pull/63) ([excpt](https://github.com/excpt)) 205 | - Add 'iat/ aud/ sub/ jti' support for ruby-jwt [\#62](https://github.com/jwt/ruby-jwt/pull/62) ([ZhangHanDong](https://github.com/ZhangHanDong)) 206 | - Add 'iss' support for ruby-jwt [\#61](https://github.com/jwt/ruby-jwt/pull/61) ([ZhangHanDong](https://github.com/ZhangHanDong)) 207 | - Clarify .encode API in README [\#60](https://github.com/jwt/ruby-jwt/pull/60) ([jbodah](https://github.com/jbodah)) 208 | 209 | ## [jwt-1.3.0](https://github.com/jwt/ruby-jwt/tree/jwt-1.3.0) (2015-02-24) 210 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.2.1...jwt-1.3.0) 211 | 212 | **Closed issues:** 213 | 214 | - Signature Verification to Return Verification Error rather than decode error [\#57](https://github.com/jwt/ruby-jwt/issues/57) 215 | - Incorrect readme for leeway [\#55](https://github.com/jwt/ruby-jwt/issues/55) 216 | - What is the reason behind stripping the = in base64 encoding? [\#54](https://github.com/jwt/ruby-jwt/issues/54) 217 | - Preperations for version 2.x [\#50](https://github.com/jwt/ruby-jwt/issues/50) 218 | - Release a new version [\#47](https://github.com/jwt/ruby-jwt/issues/47) 219 | - Catch up for ActiveWhatever 4.1.1 series [\#40](https://github.com/jwt/ruby-jwt/issues/40) 220 | 221 | **Merged pull requests:** 222 | 223 | - raise verification error for signiture verification [\#58](https://github.com/jwt/ruby-jwt/pull/58) ([punkle](https://github.com/punkle)) 224 | - Added support for not before claim verification [\#56](https://github.com/jwt/ruby-jwt/pull/56) ([punkle](https://github.com/punkle)) 225 | - Preperations for version 2.x [\#49](https://github.com/jwt/ruby-jwt/pull/49) ([excpt](https://github.com/excpt)) 226 | 227 | ## [jwt-1.2.1](https://github.com/jwt/ruby-jwt/tree/jwt-1.2.1) (2015-01-22) 228 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.2.0...jwt-1.2.1) 229 | 230 | **Closed issues:** 231 | 232 | - JWT.encode\({"exp": 10}, "secret"\) [\#52](https://github.com/jwt/ruby-jwt/issues/52) 233 | - JWT.encode\({"exp": 10}, "secret"\) [\#51](https://github.com/jwt/ruby-jwt/issues/51) 234 | 235 | **Merged pull requests:** 236 | 237 | - Accept expiration claims as string [\#53](https://github.com/jwt/ruby-jwt/pull/53) ([yarmand](https://github.com/yarmand)) 238 | 239 | ## [jwt-1.2.0](https://github.com/jwt/ruby-jwt/tree/jwt-1.2.0) (2014-11-24) 240 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.13...jwt-1.2.0) 241 | 242 | **Closed issues:** 243 | 244 | - set token to expire [\#42](https://github.com/jwt/ruby-jwt/issues/42) 245 | 246 | **Merged pull requests:** 247 | 248 | - Added support for `exp` claim [\#45](https://github.com/jwt/ruby-jwt/pull/45) ([zshannon](https://github.com/zshannon)) 249 | - rspec 3 breaks passing tests [\#44](https://github.com/jwt/ruby-jwt/pull/44) ([zshannon](https://github.com/zshannon)) 250 | 251 | ## [jwt-0.1.13](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.13) (2014-05-08) 252 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-1.0.0...jwt-0.1.13) 253 | 254 | **Closed issues:** 255 | 256 | - yanking of version 0.1.12 causes issues [\#39](https://github.com/jwt/ruby-jwt/issues/39) 257 | - Semantic versioning [\#37](https://github.com/jwt/ruby-jwt/issues/37) 258 | - Update gem to get latest changes [\#36](https://github.com/jwt/ruby-jwt/issues/36) 259 | 260 | ## [jwt-1.0.0](https://github.com/jwt/ruby-jwt/tree/jwt-1.0.0) (2014-05-07) 261 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.11...jwt-1.0.0) 262 | 263 | **Closed issues:** 264 | 265 | - API request - JWT::decoded\_header\(\) [\#26](https://github.com/jwt/ruby-jwt/issues/26) 266 | 267 | **Merged pull requests:** 268 | 269 | - return header along with playload after decoding [\#35](https://github.com/jwt/ruby-jwt/pull/35) ([sawyerzhang](https://github.com/sawyerzhang)) 270 | - Raise JWT::DecodeError on nil token [\#34](https://github.com/jwt/ruby-jwt/pull/34) ([tjmw](https://github.com/tjmw)) 271 | - Make MultiJson optional for Ruby 1.9+ [\#33](https://github.com/jwt/ruby-jwt/pull/33) ([petergoldstein](https://github.com/petergoldstein)) 272 | - Allow access to header and payload without signature verification [\#32](https://github.com/jwt/ruby-jwt/pull/32) ([petergoldstein](https://github.com/petergoldstein)) 273 | - Update specs to use RSpec 3.0.x syntax [\#31](https://github.com/jwt/ruby-jwt/pull/31) ([petergoldstein](https://github.com/petergoldstein)) 274 | - Travis - Add Ruby 2.0.0, 2.1.0, Rubinius [\#30](https://github.com/jwt/ruby-jwt/pull/30) ([petergoldstein](https://github.com/petergoldstein)) 275 | 276 | ## [jwt-0.1.11](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.11) (2014-01-17) 277 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.10...jwt-0.1.11) 278 | 279 | **Closed issues:** 280 | 281 | - url safe encode and decode [\#28](https://github.com/jwt/ruby-jwt/issues/28) 282 | - Release [\#27](https://github.com/jwt/ruby-jwt/issues/27) 283 | 284 | **Merged pull requests:** 285 | 286 | - fixed urlsafe base64 encoding [\#29](https://github.com/jwt/ruby-jwt/pull/29) ([tobscher](https://github.com/tobscher)) 287 | 288 | ## [jwt-0.1.10](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.10) (2014-01-10) 289 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.8...jwt-0.1.10) 290 | 291 | **Closed issues:** 292 | 293 | - change to signature of JWT.decode method [\#14](https://github.com/jwt/ruby-jwt/issues/14) 294 | 295 | **Merged pull requests:** 296 | 297 | - Fix warning: assigned but unused variable - e [\#25](https://github.com/jwt/ruby-jwt/pull/25) ([sferik](https://github.com/sferik)) 298 | - Echoe doesn't define a license= method [\#24](https://github.com/jwt/ruby-jwt/pull/24) ([sferik](https://github.com/sferik)) 299 | - Use OpenSSL::Digest instead of deprecated OpenSSL::Digest::Digest [\#23](https://github.com/jwt/ruby-jwt/pull/23) ([JuanitoFatas](https://github.com/JuanitoFatas)) 300 | - Handle some invalid JWTs [\#22](https://github.com/jwt/ruby-jwt/pull/22) ([steved](https://github.com/steved)) 301 | - Add MIT license to gemspec [\#21](https://github.com/jwt/ruby-jwt/pull/21) ([nycvotes-dev](https://github.com/nycvotes-dev)) 302 | - Tweaks and improvements [\#20](https://github.com/jwt/ruby-jwt/pull/20) ([threedaymonk](https://github.com/threedaymonk)) 303 | - Don't leave errors in OpenSSL.errors when there is a decoding error. [\#19](https://github.com/jwt/ruby-jwt/pull/19) ([lowellk](https://github.com/lowellk)) 304 | 305 | ## [jwt-0.1.8](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.8) (2013-03-14) 306 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.7...jwt-0.1.8) 307 | 308 | **Merged pull requests:** 309 | 310 | - Contrib and update [\#18](https://github.com/jwt/ruby-jwt/pull/18) ([threedaymonk](https://github.com/threedaymonk)) 311 | - Verify if verify is truthy \(not just true\) [\#17](https://github.com/jwt/ruby-jwt/pull/17) ([threedaymonk](https://github.com/threedaymonk)) 312 | 313 | ## [jwt-0.1.7](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.7) (2013-03-07) 314 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.6...jwt-0.1.7) 315 | 316 | **Merged pull requests:** 317 | 318 | - Catch MultiJson::LoadError and reraise as JWT::DecodeError [\#16](https://github.com/jwt/ruby-jwt/pull/16) ([rwygand](https://github.com/rwygand)) 319 | 320 | ## [jwt-0.1.6](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.6) (2013-03-05) 321 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.5...jwt-0.1.6) 322 | 323 | **Merged pull requests:** 324 | 325 | - Fixes a theoretical timing attack [\#15](https://github.com/jwt/ruby-jwt/pull/15) ([mgates](https://github.com/mgates)) 326 | - Use StandardError as parent for DecodeError [\#13](https://github.com/jwt/ruby-jwt/pull/13) ([Oscil8](https://github.com/Oscil8)) 327 | 328 | ## [jwt-0.1.5](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.5) (2012-07-20) 329 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.4...jwt-0.1.5) 330 | 331 | **Closed issues:** 332 | 333 | - Unable to specify signature header fields [\#7](https://github.com/jwt/ruby-jwt/issues/7) 334 | 335 | **Merged pull requests:** 336 | 337 | - MultiJson dependency uses ~\> but should be \>= [\#12](https://github.com/jwt/ruby-jwt/pull/12) ([sporkmonger](https://github.com/sporkmonger)) 338 | - Oops. :-\) [\#11](https://github.com/jwt/ruby-jwt/pull/11) ([sporkmonger](https://github.com/sporkmonger)) 339 | - Fix issue with signature verification in JRuby [\#10](https://github.com/jwt/ruby-jwt/pull/10) ([sporkmonger](https://github.com/sporkmonger)) 340 | - Depend on MultiJson [\#9](https://github.com/jwt/ruby-jwt/pull/9) ([lautis](https://github.com/lautis)) 341 | - Allow for custom headers on encode and decode [\#8](https://github.com/jwt/ruby-jwt/pull/8) ([dgrijalva](https://github.com/dgrijalva)) 342 | - Missing development dependency for echoe gem. [\#6](https://github.com/jwt/ruby-jwt/pull/6) ([sporkmonger](https://github.com/sporkmonger)) 343 | 344 | ## [jwt-0.1.4](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.4) (2011-11-11) 345 | [Full Changelog](https://github.com/jwt/ruby-jwt/compare/jwt-0.1.3...jwt-0.1.4) 346 | 347 | **Merged pull requests:** 348 | 349 | - Fix for RSA verification [\#5](https://github.com/jwt/ruby-jwt/pull/5) ([jordan-brough](https://github.com/jordan-brough)) 350 | 351 | ## [jwt-0.1.3](https://github.com/jwt/ruby-jwt/tree/jwt-0.1.3) (2011-06-30) 352 | **Closed issues:** 353 | 354 | - signatures calculated incorrectly \(hexdigest instead of digest\) [\#1](https://github.com/jwt/ruby-jwt/issues/1) 355 | 356 | **Merged pull requests:** 357 | 358 | - Bumped a version and added a .gemspec using rake build\_gemspec [\#3](https://github.com/jwt/ruby-jwt/pull/3) ([zhitomirskiyi](https://github.com/zhitomirskiyi)) 359 | - Added RSA support [\#2](https://github.com/jwt/ruby-jwt/pull/2) ([zhitomirskiyi](https://github.com/zhitomirskiyi)) 360 | 361 | 362 | 363 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* --------------------------------------------------------------------------------