From 916852f1de8f0371d03d08b3cfa08b07976b1cd4 Mon Sep 17 00:00:00 2001 From: Kevin Stewart Date: Mon, 7 Mar 2016 21:29:07 -0500 Subject: [PATCH] adding RS256 support adds `algorithm` specification on the credential, defaults to `HS256` --- kong/plugins/jwt/daos.lua | 3 +- kong/plugins/jwt/handler.lua | 5 ++ kong/plugins/jwt/jwt_parser.lua | 10 ++-- kong/plugins/jwt/migrations/cassandra.lua | 9 ++++ kong/plugins/jwt/migrations/postgres.lua | 9 ++++ spec/plugins/jwt/access_spec.lua | 10 ++++ spec/plugins/jwt/jwt_parser_spec.lua | 61 ++++++++++++++++++++++- 7 files changed, 100 insertions(+), 7 deletions(-) diff --git a/kong/plugins/jwt/daos.lua b/kong/plugins/jwt/daos.lua index e8061bef5af2..b52cc7ecd0a7 100644 --- a/kong/plugins/jwt/daos.lua +++ b/kong/plugins/jwt/daos.lua @@ -8,7 +8,8 @@ local SCHEMA = { created_at = {type = "timestamp", immutable = true, dao_insert_value = true}, consumer_id = {type = "id", required = true, foreign = "consumers:id"}, key = {type = "string", unique = true, default = utils.random_string}, - secret = {type = "string", unique = true, default = utils.random_string} + secret = {type = "string", unique = true, default = utils.random_string}, + algorithm = {type = "string", enum = {"HS256", "RS256"}, default = 'HS256'} }, marshall_event = function(self, t) return {id = t.id, consumer_id = t.consumer_id, key = t.key} diff --git a/kong/plugins/jwt/handler.lua b/kong/plugins/jwt/handler.lua index 6a3b1f223e54..ad82c17ebd42 100644 --- a/kong/plugins/jwt/handler.lua +++ b/kong/plugins/jwt/handler.lua @@ -87,6 +87,11 @@ function JwtHandler:access(conf) return responses.send_HTTP_FORBIDDEN("No credentials found for given '"..conf.key_claim_name.."'") end + -- Verify "alg" + if jwt.header.alg ~= jwt_secret.algorithm then + return responses.send_HTTP_FORBIDDEN("Invalid algorithm") + end + local jwt_secret_value = jwt_secret.secret if conf.secret_is_base64 then jwt_secret_value = jwt:b64_decode(jwt_secret_value) diff --git a/kong/plugins/jwt/jwt_parser.lua b/kong/plugins/jwt/jwt_parser.lua index f3bd99aed5ca..45c4896373d1 100644 --- a/kong/plugins/jwt/jwt_parser.lua +++ b/kong/plugins/jwt/jwt_parser.lua @@ -18,19 +18,21 @@ local string_rep = string.rep local setmetatable = setmetatable --- Supported algorithms for signing tokens. --- Only support HS256 for our use case. local alg_sign = { - ["HS256"] = function(data, key) return crypto.hmac.digest("sha256", data, key, true) end + ["HS256"] = function(data, key) return crypto.hmac.digest("sha256", data, key, true) end, --["HS384"] = function(data, key) return crypto.hmac.digest("sha384", data, key, true) end, --["HS512"] = function(data, key) return crypto.hmac.digest("sha512", data, key, true) end + ["RS256"] = function(data, key) return crypto.sign('sha256', data, crypto.pkey.from_pem(key, true)) end } --- Supported algorithms for verifying tokens. --- Only support HS256 for our use case. local alg_verify = { - ["HS256"] = function(data, signature, key) return signature == alg_sign["HS256"](data, key) end + ["HS256"] = function(data, signature, key) return signature == alg_sign["HS256"](data, key) end, --["HS384"] = function(data, signature, key) return signature == alg_sign["HS384"](data, key) end, --["HS512"] = function(data, signature, key) return signature == alg_sign["HS512"](data, key) end + ["RS256"] = function(data, signature, key) + return crypto.verify('sha256', data, signature, crypto.pkey.from_pem(key)) + end } --- base 64 encoding diff --git a/kong/plugins/jwt/migrations/cassandra.lua b/kong/plugins/jwt/migrations/cassandra.lua index de5a5c43dfe5..6dd12074b148 100644 --- a/kong/plugins/jwt/migrations/cassandra.lua +++ b/kong/plugins/jwt/migrations/cassandra.lua @@ -18,5 +18,14 @@ return { down = [[ DROP TABLE jwt_secrets; ]] + }, + { + name = "2016-03-07-jwt-auth", + up = [[ + ALTER TABLE jwt_secrets ADD algorithm text; + ]], + down = [[ + ALTER TABLE jwt_secrets DROP algorithm; + ]] } } diff --git a/kong/plugins/jwt/migrations/postgres.lua b/kong/plugins/jwt/migrations/postgres.lua index 2c81a1f1bc72..648ec34c3535 100644 --- a/kong/plugins/jwt/migrations/postgres.lua +++ b/kong/plugins/jwt/migrations/postgres.lua @@ -27,5 +27,14 @@ return { down = [[ DROP TABLE jwt_secrets; ]] + }, + { + name = "2016-03-07-jwt-auth", + up = [[ + ALTER TABLE jwt_secrets ADD COLUMN algorithm varchar(5); + ]], + down = [[ + ALTER TABLE jwt_secrets DROP COLUMN algorithm; + ]] } } diff --git a/spec/plugins/jwt/access_spec.lua b/spec/plugins/jwt/access_spec.lua index 6e3a48e71ecf..000657e15bdd 100644 --- a/spec/plugins/jwt/access_spec.lua +++ b/spec/plugins/jwt/access_spec.lua @@ -86,6 +86,16 @@ describe("JWT access", function() assert.equal("Invalid signature", body.message) end) + it("should return 403 Forbidden if the alg does not match the credential", function() + local header = {typ = "JWT", alg = 'RS256'} + local jwt = jwt_encoder.encode(PAYLOAD, jwt_secret.secret, 'HS256', header) + local authorization = "Bearer "..jwt + local response, status = http_client.get(STUB_GET_URL, nil, {host = "jwt.com", authorization = authorization}) + assert.equal(403, status) + local body = json.decode(response) + assert.equal("Invalid algorithm", body.message) + end) + it("should proxy the request with token and consumer headers if it was verified", function() PAYLOAD.iss = jwt_secret.key local jwt = jwt_encoder.encode(PAYLOAD, jwt_secret.secret) diff --git a/spec/plugins/jwt/jwt_parser_spec.lua b/spec/plugins/jwt/jwt_parser_spec.lua index 59e6b9f4dca8..9abf04efb2a4 100644 --- a/spec/plugins/jwt/jwt_parser_spec.lua +++ b/spec/plugins/jwt/jwt_parser_spec.lua @@ -2,9 +2,51 @@ require "kong.tools.ngx_stub" local jwt_parser = require "kong.plugins.jwt.jwt_parser" +local rs256_private_key = [[ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAw5mp3MS3hVLkHwB9lMrEx34MjYCmKeH/XeMLexNpTd1FzuNv +6rArovTY763CDo1Tp0xHz0LPlDJJtpqAgsnfDwCcgn6ddZTo1u7XYzgEDfS8J4SY +dcKxZiSdVTpb9k7pByXfnwK/fwq5oeBAJXISv5ZLB1IEVZHhUvGCH0udlJ2vadqu +R03phBHcvlNmMbJGWAetkdcKyi+7TaW7OUSjlge4WYERgYzBB6eJH+UfPjmw3aSP +ZcNXt2RckPXEbNrL8TVXYdEvwLJoJv9/I8JPFLiGOm5uTMEk8S4txs2efueg1Xyy +milCKzzuXlJvrvPA4u6HI7qNvuvkvUjQmwBHgwIDAQABAoIBAQCP3ZblTT8abdRh +xQ+Y/+bqQBjlfwk4ZwRXvuYz2Rwr7CMrP3eSq4785ZAmAaxo3aP4ug9bL23UN4Sm +LU92YxqQQ0faZ1xTHnp/k96SGKJKzYYSnuEwREoMscOS60C2kmWtHzsyDmhg/bd5 +i6JCqHuHtPhsYvPTKGANjJrDf+9gXazArmwYrdTnyBeFC88SeRG8uH2lP2VyqHiw +ZvEQ3PkRRY0yJRqEtrIRIlgVDuuu2PhPg+MR4iqR1RONjDUFaSJjR7UYWY/m/dmg +HlalqpKjOzW6RcMmymLKaW6wF3y8lbs0qCjCYzrD3bZnlXN1kIw6cxhplfrSNyGZ +BY/qWytJAoGBAO8UsagT8tehCu/5smHpG5jgMY96XKPxFw7VYcZwuC5aiMAbhKDO +OmHxYrXBT/8EQMIk9kd4r2JUrIx+VKO01wMAn6fF4VMrrXlEuOKDX6ZE1ay0OJ0v +gCmFtKB/EFXXDQLV24pgYgQLxnj+FKFV2dQLmv5ZsAVcmBHSkM9PBdUlAoGBANFx +QPuVaSgRLFlXw9QxLXEJbBFuljt6qgfL1YDj/ANgafO8HMepY6jUUPW5LkFye188 +J9wS+EPmzSJGxdga80DUnf18yl7wme0odDI/7D8gcTfu3nYcCkQzeykZNGAwEe+0 +SvhXB9fjWgs8kFIjJIxKGmlMJRMHWN1qaECEkg2HAoGBAIb93EHW4as21wIgrsPx +5w8up00n/d7jZe2ONiLhyl0B6WzvHLffOb/Ll7ygZhbLw/TbAePhFMYkoTjCq++z +UCP12i/U3yEi7FQopWvgWcV74FofeEfoZikLwa1NkV+miUYskkVTnoRCUdJHREbE +PrYnx2AOLAEbAxItHm6vY8+xAoGAL85JBePpt8KLu+zjfximhamf6C60zejGzLbD +CgN/74lfRcoHS6+nVs73l87n9vpZnLhPZNVTo7QX2J4M5LHqGj8tvMFyM895Yv+b +3ihnFVWjYh/82Tq3QS/7Cbt+EAKI5Yzim+LJoIZ9dBkj3Au3eOolMym1QK2ppAh4 +uVlJORsCgYBv/zpNukkXrSxVHjeZj582nkdAGafYvT0tEQ1u3LERgifUNwhmHH+m +1OcqJKpbgQhGzidXK6lPiVFpsRXv9ICP7o96FjmQrMw2lAfC7stYnFLKzv+cj8L9 +h4hhNWM6i/DHXjPsHgwdzlX4ulq8M7dR8Oqm9DrbdAyWz8h8/kzsnA== +-----END RSA PRIVATE KEY----- +]] + +local rs256_public_key = [[ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw5mp3MS3hVLkHwB9lMrE +x34MjYCmKeH/XeMLexNpTd1FzuNv6rArovTY763CDo1Tp0xHz0LPlDJJtpqAgsnf +DwCcgn6ddZTo1u7XYzgEDfS8J4SYdcKxZiSdVTpb9k7pByXfnwK/fwq5oeBAJXIS +v5ZLB1IEVZHhUvGCH0udlJ2vadquR03phBHcvlNmMbJGWAetkdcKyi+7TaW7OUSj +lge4WYERgYzBB6eJH+UfPjmw3aSPZcNXt2RckPXEbNrL8TVXYdEvwLJoJv9/I8JP +FLiGOm5uTMEk8S4txs2efueg1XyymilCKzzuXlJvrvPA4u6HI7qNvuvkvUjQmwBH +gwIDAQAB +-----END PUBLIC KEY----- +]] + describe("JWT parser", function() describe("Encoding", function() - it("should properly encode", function() + it("should properly encode using HS256", function() local token = jwt_parser.encode({ sub = "1234567890", name = "John Doe", @@ -12,6 +54,14 @@ describe("JWT parser", function() }, "secret") assert.equal("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwibmFtZSI6IkpvaG4gRG9lIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.eNK_fimsCW3Q-meOXyc_dnZHubl2D4eZkIcn6llniCk", token) end) + it("should properly encode using RS256", function() + local token = jwt_parser.encode({ + sub = "1234567890", + name = "John Doe", + admin = true + }, rs256_private_key, 'RS256') + assert.equal("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwibmFtZSI6IkpvaG4gRG9lIiwic3ViIjoiMTIzNDU2Nzg5MCJ9.EiOLxyMimY8vbLR8EcGOlXAiEe-eEVn7Aewgu0gYIBPyiEhVTq0CzB_XtHoQ_0y4gBBBZVRnz1pgruOtNmOzcaoXnyplFm1IbrCCBKYQeA4lanmu_-Wzk6Dw4p-TimRHpf8EEHBUJSEbVEyet3cpozUo2Ep0dEfA_Nf3T-g8RjfOYXkFTr3M6FuIDq95cFZloH-DRGodUVQX508wgggtcFKN-Pi7_rWzBtQwP2u4CrFD4ZJbn2sxobzSlFb9fn4nRh_-rPPjDSeHVKwrpsYpFSLBJxwX-KhbeGUfalg2eu9tHLDPHC4gTCpoQKxxRIwfMjW5zlHOZhohKZV2ZtpcgA", token) + end) end) describe("Decoding", function() it("should throw an error if not given a string", function() @@ -31,13 +81,20 @@ describe("JWT parser", function() end) end) describe("Verify signature", function() - it("should verify a signature", function() + it("should verify a signature using HS256", function() local token = jwt_parser.encode({sub = "foo"}, "secret") local jwt, err = jwt_parser:new(token) assert.falsy(err) assert.True(jwt:verify_signature("secret")) assert.False(jwt:verify_signature("invalid")) end) + it("should verify a signature using RS256", function() + local token = jwt_parser.encode({sub = "foo"}, rs256_private_key, 'RS256') + local jwt, err = jwt_parser:new(token) + assert.falsy(err) + assert.True(jwt:verify_signature(rs256_public_key)) + assert.False(jwt:verify_signature(rs256_public_key:gsub('QAB', 'zzz'))) + end) end) describe("Verify registered claims", function() it("should require claims passed as arguments", function()