local cjson   = require "cjson"
local helpers = require "spec.helpers"
local uuid    = require "kong.tools.uuid"
local admin_api = require "spec.fixtures.admin_api"
local sha256 = require "resty.sha256"
local jwt_encoder = require "kong.plugins.jwt.jwt_parser"


local math_random = math.random
local string_char = string.char
local string_gsub = string.gsub
local string_rep = string.rep


local ngx_encode_base64 = ngx.encode_base64


local PAYLOAD = {
  iss = nil,
  nbf = os.time(),
  iat = os.time(),
  exp = os.time() + 3600
}


local kong = {
  table = require("kong.pdk.table").new()
}


local function provision_code(host, extra_headers, client_id, code_challenge)
  local request_client = helpers.proxy_ssl_client()
  local body = {
      provision_key = "provision123",
      client_id = client_id or "clientid123",
      scope = "email",
      response_type = "code",
      state = "hello",
      authenticated_userid = "userid123",
  }
  if code_challenge then
    body["code_challenge"] = code_challenge
    body["code_method"] = "S256"
  end

  local res = assert(request_client:send {
    method = "POST",
    path = "/oauth2/authorize",
    body = body,
    headers = kong.table.merge({
      ["Host"] = host or "oauth2.test",
      ["Content-Type"] = "application/json"
    }, extra_headers)
  })
  assert.response(res).has.status(200)
  local body = assert.response(res).has.jsonbody()

  request_client:close()
  if body.redirect_uri then
    local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.test/kong\\?code=([\\w]{32,32})&state=hello$")
    assert.is_nil(err)
    local m, err = iterator()
    assert.is_nil(err)
    return m[1]
  end
end


local function provision_token(host, extra_headers, client_id, client_secret, code_challenge, code_verifier, require_secret)
  local code = provision_code(host, extra_headers, client_id, code_challenge)
  local request_client = helpers.proxy_ssl_client()
  require_secret = require_secret == nil or require_secret
  local body = { code = code,
                 client_id = client_id or "clientid123",
                 grant_type = "authorization_code" }
  if client_secret or require_secret then
    body["client_secret"] = client_secret or "secret123"
  end
  if code_verifier then
    body["code_verifier"] = code_verifier
  end

  local res = assert(request_client:send {
    method = "POST",
    path = "/oauth2/token",
    body = body,
    headers = kong.table.merge({
      ["Host"] = host or "oauth2.test",
      ["Content-Type"] = "application/json"
    }, extra_headers)
  })

  assert.response(res).has.status(200)
  local token = assert.response(res).has.jsonbody()
  assert.is_table(token)
  request_client:close()
  return token
end


local function refresh_token(host, refresh_token)
  local request_client = helpers.proxy_ssl_client()
  local res = assert(request_client:send {
    method  = "POST",
    path    = "/oauth2/token",
    body    = {
      refresh_token    = refresh_token,
      client_id        = "clientid123",
      client_secret    = "secret123",
      grant_type       = "refresh_token"
    },
    headers = {
      ["Host"]         = host or "oauth2.test",
      ["Content-Type"] = "application/json"
    }
  })
  assert.response(res).has.status(200)
  local token = assert.response(res).has.jsonbody()
  assert.is_table(token)
  request_client:close()
  return token
end


local function get_pkce_tokens(code_verifier)
  if not code_verifier then
    code_verifier = ''
    for i = 1, 50 do
      code_verifier = code_verifier .. string_char(math_random(65, 90))
    end
  end
  local digest = sha256:new()
  digest:update(code_verifier)
  local code_challenge = ngx_encode_base64(digest:final(), true)
  code_challenge = string_gsub(code_challenge, "+", "-")
  code_challenge = string_gsub(code_challenge, "/", "_")
  return code_challenge, code_verifier
end


for _, strategy in helpers.each_strategy() do

describe("Plugin: oauth2 [#" .. strategy .. "]", function()
  local db

  lazy_setup(function()
    local _
    _, db = helpers.get_db_utils(strategy, {
      "routes",
      "services",
      "consumers",
      "plugins",
      "keyauth_credentials",
      "jwt_secrets",
      "oauth2_credentials",
      "oauth2_authorization_codes",
      "oauth2_tokens",
    })

    assert(helpers.start_kong({
      database    = strategy,
      trusted_ips = "127.0.0.1",
      nginx_conf  = "spec/fixtures/custom_nginx.template",
    }))
  end)

  lazy_teardown(function()
    helpers.stop_kong()
  end)

  describe("access", function()
    local proxy_ssl_client
    local proxy_client
    local client1
    local nonexisting_anonymous = uuid.uuid() -- a non existing consumer id

    lazy_setup(function()

      local consumer = admin_api.consumers:insert {
        username = "bob"
      }

      local anonymous_user = admin_api.consumers:insert {
        username = "no-body"
      }

      client1 = admin_api.oauth2_credentials:insert {
        client_id      = "clientid123",
        client_secret  = "secret123",
        hash_secret    = true,
        redirect_uris  = { "http://google.test/kong" },
        name           = "testapp",
        consumer       = { id = consumer.id },
      }

      admin_api.oauth2_credentials:insert {
        client_id      = "clientid789",
        client_secret  = "secret789",
        redirect_uris  = { "http://google.test/kong?foo=bar&code=123" },
        name           = "testapp2",
        consumer       = { id = consumer.id },
      }

      admin_api.oauth2_credentials:insert {
        client_id     = "clientid333",
        client_secret = "secret333",
        hash_secret   = true,
        redirect_uris = { "http://google.test/kong" },
        name          = "testapp3",
        consumer      = { id = consumer.id },
      }

      admin_api.oauth2_credentials:insert {
        client_id     = "clientid456",
        client_secret = "secret456",
        redirect_uris = { "http://one.test/one/", "http://two.test/two" },
        name          = "testapp3",
        consumer      = { id = consumer.id },
      }

      admin_api.oauth2_credentials:insert {
        client_id     = "clientid1011",
        client_secret = "secret1011",
        hash_secret   = true,
        redirect_uris = { "http://google.test/kong", },
        name          = "testapp31",
        consumer      = { id = consumer.id },
      }

      admin_api.oauth2_credentials:insert {
        client_id     = "clientid10112",
        client_secret = "secret10112",
        redirect_uris = ngx.null,
        name          = "testapp311",
        consumer      = { id = consumer.id },
      }

      admin_api.oauth2_credentials:insert {
        client_id     = "clientid11211",
        client_secret = "secret11211",
        redirect_uris = { "http://google.test/kong", },
        name          = "testapp50",
        client_type   = "public",
        consumer      = { id = consumer.id },
      }

      local service1    = admin_api.services:insert()
      local service2    = admin_api.services:insert()
      local service2bis = admin_api.services:insert()
      local service3    = admin_api.services:insert()
      local service4    = admin_api.services:insert()
      local service5    = admin_api.services:insert()
      local service6    = admin_api.services:insert()
      local service7    = admin_api.services:insert()
      local service8    = admin_api.services:insert()
      local service9    = admin_api.services:insert()
      local service10   = admin_api.services:insert()
      local service11   = admin_api.services:insert()
      local service12   = admin_api.services:insert()
      local service13   = admin_api.services:insert()
      local service_c   = admin_api.services:insert()
      local service14   = admin_api.services:insert()
      local service15   = admin_api.services:insert()
      local service16   = admin_api.services:insert()
      local service17   = admin_api.services:insert()
      local service18   = admin_api.services:insert()
      local service19   = admin_api.services:insert()
      local service20   = admin_api.services:insert()
      local service21   = admin_api.services:insert()


      local route1 = assert(admin_api.routes:insert({
        hosts     = { "oauth2.test" },
        protocols = { "http", "https" },
        service   = service1,
      }))

      local route2 = assert(admin_api.routes:insert({
        hosts      = { "example-path.test" },
        protocols  = { "http", "https" },
        service    = service2,
      }))

      local route2bis = assert(admin_api.routes:insert({
        paths     = { "/somepath" },
        protocols = { "http", "https" },
        service   = service2bis,
      }))

      local route3 = assert(admin_api.routes:insert({
        hosts      = { "oauth2_3.test" },
        protocols  = { "http", "https" },
        service    = service3,
      }))

      local route4 = assert(admin_api.routes:insert({
        hosts      = { "oauth2_4.test" },
        protocols  = { "http", "https" },
        service    = service4,
      }))

      local route5 = assert(admin_api.routes:insert({
        hosts      = { "oauth2_5.test" },
        protocols  = { "http", "https" },
        service    = service5,
      }))

      local route6 = assert(admin_api.routes:insert({
        hosts      = { "oauth2_6.test" },
        protocols  = { "http", "https" },
        service    = service6,
      }))

      local route7 = assert(admin_api.routes:insert({
        hosts      = { "oauth2_7.test" },
        protocols  = { "http", "https" },
        service    = service7,
      }))

      local route8 = assert(admin_api.routes:insert({
        hosts      = { "oauth2_8.test" },
        protocols  = { "http", "https" },
        service    = service8,
      }))

      local route9 = assert(admin_api.routes:insert({
        hosts      = { "oauth2_9.test" },
        protocols  = { "http", "https" },
        service    = service9,
      }))

      local route10 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_10.test" },
        protocols   = { "http", "https" },
        service     = service10,
      }))

      local route11 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_11.test" },
        protocols   = { "http", "https" },
        service     = service11,
      }))

      local route12 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_12.test" },
        protocols   = { "http", "https" },
        service     = service12,
      }))

      local route13 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_13.test" },
        protocols   = { "http", "https" },
        service     = service13,
      }))

      local route_c = assert(admin_api.routes:insert({
        hosts       = { "oauth2__c.test" },
        protocols   = { "http", "https" },
        service     = service_c,
      }))

      local route14 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_14.test" },
        protocols   = { "http", "https" },
        service     = service14,
      }))

      local route15 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_15.test" },
        protocols   = { "http", "https" },
        service     = service15,
      }))

      local route16 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_16.test" },
        protocols   = { "http", "https" },
        service     = service16,
      }))

      local route17 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_17.test" },
        protocols   = { "http", "https" },
        service     = service17,
      }))

      local route18 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_18.test" },
        protocols   = { "http", "https" },
        service     = service18,
      }))

      local route19 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_19.test" },
        protocols   = { "http", "https" },
        service     = service19,
      }))

      local route20 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_20.test" },
        protocols   = { "http", "https" },
        service     = service20,
      }))

      local route21 = assert(admin_api.routes:insert({
        hosts       = { "oauth2_21.test" },
        protocols   = { "http", "https" },
        service     = service21,
      }))

      local service_grpc = assert(admin_api.services:insert {
          name = "grpc",
          url = helpers.grpcbin_url,
        })

      local route_grpc = assert(admin_api.routes:insert {
        protocols = { "grpc", "grpcs" },
        hosts     = { "oauth2_grpc.test" },
        paths = { "/hello.HelloService/SayHello" },
        service = service_grpc,
      })

      local route_provgrpc = assert(admin_api.routes:insert {
        hosts     = { "oauth2_grpc.test" },
        paths = { "/" },
        service = service_grpc,
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route_grpc.id },
        config   = {
          scopes = { "email", "profile", "user.email" },
        },
      })
      admin_api.oauth2_plugins:insert({
        route = { id = route_provgrpc.id },
        config   = {
          scopes = { "email", "profile", "user.email" },
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route1.id },
        config   = { scopes = { "email", "profile", "user.email" } },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route2.id }
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route2bis.id }
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route3.id },
        config   = { hide_credentials = true },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route4.id },
        config   = {
          enable_client_credentials = true,
          enable_authorization_code = false,
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route5.id },
        config   = {
          enable_password_grant     = true,
          enable_authorization_code = false,
          realm = "test-oauth2",
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route6.id },
        config   = {
          scopes                            = { "email", "profile", "user.email" },
          provision_key                     = "provision123",
          accept_http_if_already_terminated = true,
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route7.id },
        config   = {
          scopes    = { "email", "profile", "user.email" },
          anonymous = anonymous_user.id,
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route8.id },
        config   = {
          scopes             = { "email", "profile", "user.email" },
          global_credentials = true,
        },
      })


      admin_api.oauth2_plugins:insert({
        route = { id = route9.id },
        config   = {
          scopes             = { "email", "profile", "user.email" },
          global_credentials = true,
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route10.id },
        config   = {
          scopes             = { "email", "profile", "user.email" },
          global_credentials = true,
          anonymous          = nonexisting_anonymous, -- a non existing consumer id
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route11.id },
        config   = {
          scopes             = { "email", "profile", "user.email" },
          global_credentials = true,
          token_expiration   = 7,
          auth_header_name   = "custom_header_name",
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route12.id },
        config   = {
          scopes             = { "email", "profile", "user.email" },
          global_credentials = true,
          auth_header_name   = "custom_header_name",
          hide_credentials   = true,
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route13.id },
        config   = {
          scopes                   = { "email", "profile", "user.email" },
          global_credentials       = true,
          reuse_refresh_token = true,
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route_c.id },
        config   = {
          scopes = { "email", "profile", "user.email" },
          anonymous = anonymous_user.username,
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route14.id },
        config   = {
          scopes                   = { "email", "profile", "user.email" },
          global_credentials       = true,
          pkce = "none",
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route15.id },
        config   = {
          scopes                   = { "email", "profile", "user.email" },
          global_credentials       = true,
          pkce = "strict",
        }
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route16.id },
        config   = {
          scopes                   = { "email", "profile", "user.email" },
          global_credentials       = true,
          pkce = "lax",
        }
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route17.id },
        config   = {
          scopes                   = { "email", "profile", "user.email" },
          global_credentials       = true,
        }
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route18.id },
        config   = {
          scopes                   = { "scope18" },
          global_credentials       = true,
        }
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route19.id },
        config   = {
          scopes                   = { "scope19" },
          global_credentials       = true,
        }
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route20.id },
        config   = {
          scopes                   = { "scope20" },
          global_credentials       = true,
        }
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route21.id },
        config   = {
          scopes                   = { "scope20", "scope21" },
          global_credentials       = true,
        }
      })
    end)

    before_each(function ()
      proxy_client     = helpers.proxy_client()
      proxy_ssl_client = helpers.proxy_ssl_client()
    end)

    after_each(function()
      if proxy_client then proxy_client:close() end
      if proxy_ssl_client then proxy_ssl_client:close() end
    end)

    describe("OAuth2 Authorization", function()
      describe("Code Grant", function()
        it("returns an error when no provision_key is being sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            headers = {
              ["Host"] = "oauth2.test"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json)
          assert.are.equal("no-store", res.headers["cache-control"])
          assert.are.equal("no-cache", res.headers["pragma"])
        end)

        it("rejects gRPC call without credentials", function()
          local ok, err = helpers.proxy_client_grpcs(){
            service = "hello.HelloService.SayHello",
            opts = {
              ["-authority"] = "oauth2.test",
            },
          }
          assert.falsy(ok)
          assert.match("Code: Unauthenticated", err)
        end)

        it("returns an error when no parameter is being sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key    = "provision123"
            },
            headers = {
              ["Host"]         = "oauth2.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Missing authenticated_userid parameter", error = "invalid_authenticated_userid" }, json)
        end)
        it("returns an error when only provision_key and authenticated_userid are sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123"
            },
            headers                = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
          assert.are.equal("no-store", res.headers["cache-control"])
          assert.are.equal("no-cache", res.headers["pragma"])
        end)
        it("returns an error when only the client_id is being sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ redirect_uri = "http://google.test/kong?error=invalid_scope&error_description=You%20must%20specify%20a%20scope" }, json)
        end)
        it("returns an error when an invalid scope is being sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "wot"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ redirect_uri = "http://google.test/kong?error=invalid_scope&error_description=%22wot%22%20is%20an%20invalid%20scope" }, json)
        end)
        it("returns an error when no response_type is being sent", function()
          local res = assert(proxy_ssl_client:send {
            method = "POST",
            path = "/oauth2/authorize",
            body = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ redirect_uri = "http://google.test/kong?error=unsupported_response_type&error_description=Invalid%20response_type" }, json)
        end)
        it("returns an error with a state when no response_type is being sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              state                = "somestate"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ redirect_uri = "http://google.test/kong?error=unsupported_response_type&error_description=Invalid%20response_type&state=somestate" }, json)
        end)
        it("returns error when the redirect_uri does not match", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "code",
              redirect_uri         = "http://hello.test/"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ redirect_uri = "http://google.test/kong?error=invalid_request&error_description=Invalid%20redirect_uri%20that%20does%20not%20match%20with%20any%20redirect_uri%20created%20with%20the%20application" }, json)
        end)
        it("works even if redirect_uri contains a query string", function()
          local res = assert(proxy_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              authenticated_userid  = "id123",
              client_id             = "clientid789",
              scope                 = "email",
              response_type         = "code"
            },
            headers = {
              ["Host"]              = "oauth2_6.test",
              ["Content-Type"]      = "application/json",
              ["X-Forwarded-Proto"] = "https"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\?code=[\\w]{32,32}&foo=bar$"))
        end)
        it("works with multiple redirect_uris in the application", function()
          local res = assert(proxy_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              authenticated_userid  = "id123",
              client_id             = "clientid456",
              scope                 = "email",
              response_type         = "code"
            },
            headers = {
              ["Host"]              = "oauth2_6.test",
              ["Content-Type"]      = "application/json",
              ["X-Forwarded-Proto"] = "https"
            }
          })
          assert.response(res).has.status(200)
          local json = assert.response(res).has.jsonbody()
          assert.truthy(ngx.re.match(json.redirect_uri, "^http://one\\.test/one/\\?code=[\\w]{32,32}$"))
        end)
        it("fails when not under HTTPS", function()
          local res = assert(proxy_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "code"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          assert.response(res).has.status(400)
          local json = assert.response(res).has.jsonbody(res)

          assert.equal("You must use HTTPS", json.error_description)
          assert.equal("access_denied", json.error)
        end)
        it("works when not under HTTPS but accept_http_if_already_terminated is true", function()
          local res = assert(proxy_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              authenticated_userid  = "id123",
              client_id             = "clientid123",
              scope                 = "email",
              response_type         = "code"
            },
            headers = {
              ["Host"]              = "oauth2_6.test",
              ["Content-Type"]      = "application/json",
              ["X-Forwarded-Proto"] = "https"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\?code=[\\w]{32,32}$"))
        end)
        it("fails when not under HTTPS and accept_http_if_already_terminated is false", function()
          local res = assert(proxy_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              authenticated_userid  = "id123",
              client_id             = "clientid123",
              scope                 = "email",
              response_type         = "code"
            },
            headers = {
              ["Host"]              = "oauth2.test",
              ["Content-Type"]      = "application/json",
              ["X-Forwarded-Proto"] = "https"
            }
          })
          assert.response(res).has.status(400)
          local json = assert.response(res).has.jsonbody(res)

          assert.equal("You must use HTTPS", json.error_description)
          assert.equal("access_denied", json.error)
        end)
        it("returns success", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "code"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\?code=[\\w]{32,32}$"))
        end)
        it("fails with a path when using the DNS", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123a",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "code",
            },
            headers = {
              ["Host"]             = "example-path.test",
              ["Content-Type"]     = "application/json",
            },
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json)
        end)
        it("returns success with a path", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/somepath/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "code"
            },
            headers = {
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\?code=[\\w]{32,32}$"))
        end)
        it("returns success when requesting the url with final slash", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize/",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "code"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\?code=[\\w]{32,32}$"))
        end)
        it("returns success with a state", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "code",
              state                = "hello"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\?code=[\\w]{32,32}&state=hello$"))
          -- Checking headers
          assert.are.equal("no-store", res.headers["cache-control"])
          assert.are.equal("no-cache", res.headers["pragma"])
        end)
        it("returns success and store authenticated user properties", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "code",
              state                = "hello",
              authenticated_userid = "userid123"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\?code=[\\w]{32,32}&state=hello$"))

          local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.test/kong\\?code=([\\w]{32,32})&state=hello$")
          assert.is_nil(err)
          local m, err = iterator()
          assert.is_nil(err)
          local data = db.oauth2_authorization_codes:select_by_code(m[1])
          assert.are.equal(m[1], data.code)
          assert.are.equal("userid123", data.authenticated_userid)
          assert.are.equal("email", data.scope)
          assert.are.equal(client1.id, data.credential.id)
        end)
        it("returns success with a dotted scope and store authenticated user properties", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              client_id            = "clientid123",
              scope                = "user.email",
              response_type        = "code",
              state                = "hello",
              authenticated_userid = "userid123"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\?code=[\\w]{32,32}&state=hello$"))

          local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.test/kong\\?code=([\\w]{32,32})&state=hello$")
          assert.is_nil(err)
          local m, err = iterator()
          assert.is_nil(err)
          local data = db.oauth2_authorization_codes:select_by_code(m[1])
          assert.are.equal(m[1], data.code)
          assert.are.equal("userid123", data.authenticated_userid)
          assert.are.equal("user.email", data.scope)
        end)
        it("fails when code challenge method is not supported", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              client_id             = "clientid11211",
              scope                 = "user.email",
              response_type         = "code",
              state                 = "hello",
              authenticated_userid  = "userid123",
              code_challenge        = "1234",
              code_challenge_method = "foo",
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ redirect_uri = "http://google.test/kong?error=invalid_request&error_description=code_challenge_method%20is%20not%20supported%2c%20must%20be%20S256&state=hello" }, json)
        end)
        it("fails when code challenge method is provided without code challenge", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              client_id             = "clientid11211",
              scope                 = "user.email",
              response_type         = "code",
              state                 = "hello",
              authenticated_userid  = "userid123",
              code_challenge_method = "H256",
            },
            headers = {
              ["Host"]              = "oauth2.test",
              ["Content-Type"]      = "application/json",
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ redirect_uri = "http://google.test/kong?error=invalid_request&error_description=code_challenge%20is%20required%20when%20code_method%20is%20present&state=hello" }, json)
        end)
        it("fails when code challenge is not included for public client", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              client_id             = "clientid11211",
              scope                 = "user.email",
              response_type         = "code",
              state                 = "hello",
              authenticated_userid  = "userid123",
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ redirect_uri = "http://google.test/kong?error=invalid_request&error_description=code_challenge%20is%20required%20for%20public%20clients&state=hello" }, json)
        end)
        it("fails when code challenge is not included for confidential client when conf.pkce is strict", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              client_id             = "clientid123",
              scope                 = "user.email",
              response_type         = "code",
              state                 = "hello",
              authenticated_userid  = "userid123",
            },
            headers = {
              ["Host"]             = "oauth2_15.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ redirect_uri = "http://google.test/kong?error=invalid_request&error_description=code_challenge%20is%20required%20for%20confidential%20clients&state=hello" }, json)
        end)
        it("returns success when code challenge is not included for public client when conf.pkce is none", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              client_id             = "clientid11211",
              scope                 = "user.email",
              response_type         = "code",
              state                 = "hello",
              authenticated_userid  = "userid123",
            },
            headers = {
              ["Host"]             = "oauth2_14.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          local iterator, err = ngx.re.gmatch(json.redirect_uri, "^http://google\\.test/kong\\?code=([\\w]{32,32})&state=hello$")
          assert.is_nil(err)
          local m, err = iterator()
          assert.is_nil(err)
          db.oauth2_authorization_codes:select_by_code(m[1])
        end)
        it("returns success and defaults code method to S256 when not provided", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              client_id             = "clientid11211",
              scope                 = "user.email",
              response_type         = "code",
              state                 = "hello",
              authenticated_userid  = "userid123",
              code_challenge        = "1234",
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          local iterator, err = ngx.re.gmatch(json.redirect_uri, "^http://google\\.test/kong\\?code=([\\w]{32,32})&state=hello$")
          assert.is_nil(err)
          local m, err = iterator()
          assert.is_nil(err)
          local data = db.oauth2_authorization_codes:select_by_code(m[1])
          assert.are.equal("1234", data.challenge)
          assert.are.equal("S256", data.challenge_method)
        end)
        it("returns success and saves code challenge", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key         = "provision123",
              client_id             = "clientid11211",
              scope                 = "user.email",
              response_type         = "code",
              state                 = "hello",
              authenticated_userid  = "userid123",
              code_challenge        = "1234",
              code_challenge_method = "S256",
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          local iterator, err = ngx.re.gmatch(json.redirect_uri, "^http://google\\.test/kong\\?code=([\\w]{32,32})&state=hello$")
          assert.is_nil(err)
          local m, err = iterator()
          assert.is_nil(err)
          local data = db.oauth2_authorization_codes:select_by_code(m[1])
          assert.are.equal("1234", data.challenge)
          assert.are.equal("S256", data.challenge_method)
        end)
      end)

      describe("Implicit Grant", function()
        it("returns success", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "token"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\#access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$"))
          assert.are.equal("no-store", res.headers["cache-control"])
          assert.are.equal("no-cache", res.headers["pragma"])
        end)
        it("returns success and the state", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "token",
              state                = "wot"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\#access_token=[\\w]{32,32}&expires_in=[\\d]+&state=wot&token_type=bearer$"))
        end)
        it("returns success and the token should have the right expiration", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              scope                = "email",
              response_type        = "token"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\#access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$"))

          local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.test/kong\\#access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$")
          assert.is_nil(err)
          local m, err = iterator()
          assert.is_nil(err)
          local data = db.oauth2_tokens:select_by_access_token(m[1])
          assert.are.equal(m[1], data.access_token)
          assert.are.equal(5, data.expires_in)
          assert.falsy(data.refresh_token)
        end)
        it("returns success and store authenticated user properties", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              client_id            = "clientid123",
              scope                = "email  profile",
              response_type        = "token",
              authenticated_userid = "userid123"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\#access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$"))

          local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.test/kong\\#access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$")
          assert.is_nil(err)
          local m, err = iterator()
          assert.is_nil(err)
          local data = db.oauth2_tokens:select_by_access_token(m[1])
          assert.are.equal(m[1], data.access_token)
          assert.are.equal("userid123", data.authenticated_userid)
          assert.are.equal("email profile", data.scope)

          -- Checking that there is no refresh token since it's an implicit grant
          assert.are.equal(5, data.expires_in)
          assert.falsy(data.refresh_token)
        end)
        it("returns set the right upstream headers", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/authorize",
            body    = {
              provision_key        = "provision123",
              client_id            = "clientid123",
              scope                = "email  profile",
              response_type        = "token",
              authenticated_userid = "userid123"
            },
            headers = {
              ["Host"]             = "oauth2.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.test/kong\\#access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$")
          assert.is_nil(err)
          local m, err = iterator()
          assert.is_nil(err)
          local access_token = m[1]

          local res = assert(proxy_ssl_client:send {
            method  = "GET",
            path    = "/request?access_token=" .. access_token,
            headers = {
              ["Host"] = "oauth2.test"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.truthy(body.headers["x-consumer-id"])
          assert.are.equal("bob", body.headers["x-consumer-username"])
          assert.are.equal("email profile", body.headers["x-authenticated-scope"])
          assert.are.equal("userid123", body.headers["x-authenticated-userid"])
          assert.are.equal("clientid123", body.headers["x-credential-identifier"])
        end)
      end)

      describe("Client Credentials", function()
        it("returns an error when client_secret is not sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid123",
              scope            = "email",
              response_type    = "token"
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
        end)
        it("returns an error when empty client_id and empty client_secret is sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "",
              client_secret    = "",
              scope            = "email",
              response_type    = "token",
              grant_type       = "client_credentials",
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
        end)
        it("returns an error when missing client_id and missing client_secret is sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              scope            = "email",
              response_type    = "token",
              grant_type       = "client_credentials",
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
        end)
        it("returns an error when empty client_id and empty client_secret is sent regardless of method - without realm", function()
          local res = assert(proxy_ssl_client:send {
            method  = "GET",
            path    = "/oauth2/token?client_id&grant_type=client_credentials&client_secret",
            body    = {},
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(405, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "The HTTP method GET is invalid for the token endpoint",
                        error = "invalid_method" }, json)
          assert.are.equal('Bearer error="invalid_method", error_description="The HTTP method GET is invalid for the token endpoint"', res.headers["www-authenticate"])
        end)
        it("returns an error when empty client_id and empty client_secret is sent regardless of method - with realm", function()
          local res = assert(proxy_ssl_client:send {
            method  = "GET",
            path    = "/oauth2/token?client_id&grant_type=client_credentials&client_secret",
            body    = {},
            headers = {
              ["Host"]         = "oauth2_5.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(405, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "The HTTP method GET is invalid for the token endpoint",
                        error = "invalid_method" }, json)
          assert.are.equal('Bearer realm="test-oauth2", error="invalid_method", error_description="The HTTP method GET is invalid for the token endpoint"', res.headers["www-authenticate"])
        end)
        it("returns an error when grant_type is not sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid123",
              client_secret    = "secret123",
              scope            = "email",
              response_type    = "token"
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error = "unsupported_grant_type", error_description = "Invalid grant_type" }, json)
        end)
        it("fails when not under HTTPS", function()
          local res = assert(proxy_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid123",
              client_secret    = "secret123",
              scope            = "email",
              grant_type       = "client_credentials"
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json"
            }
          })
          assert.response(res).has.status(400)
          local json = assert.response(res).has.jsonbody(res)

          assert.equal("You must use HTTPS", json.error_description)
          assert.equal("access_denied", json.error)
        end)
        it("fails when setting authenticated_userid and no provision_key", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id            = "clientid123",
              client_secret        = "secret123",
              scope                = "email",
              grant_type           = "client_credentials",
              authenticated_userid = "user123"
            },
            headers = {
              ["Host"]             = "oauth2_4.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json)
        end)
        it("fails when setting authenticated_userid and invalid provision_key", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id            = "clientid123",
              client_secret        = "secret123",
              scope                = "email",
              grant_type           = "client_credentials",
              authenticated_userid = "user123",
              provision_key        = "hello"
            },
            headers = {
              ["Host"]             = "oauth2_4.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json)
        end)
        it("returns success", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid123",
              client_secret    = "secret123",
              scope            = "email",
              grant_type       = "client_credentials"
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("bearer", json.token_type)
          assert.equal(5, json.expires_in)
          assert.equal(32, #json.access_token)
          assert.matches("%w+", json.access_token)
        end)
        it("returns success with an application that has multiple redirect_uri", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid456",
              client_secret    = "secret456",
              scope            = "email",
              grant_type       = "client_credentials"
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("bearer", json.token_type)
          assert.equal(5, json.expires_in)
          assert.equal(32, #json.access_token)
          assert.matches("%w+", json.access_token)
        end)
        it("returns success with an application that has multiple redirect_uri, and by passing a valid redirect_uri", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid456",
              client_secret    = "secret456",
              scope            = "email",
              grant_type       = "client_credentials",
              redirect_uri     = "http://two.test/two"
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("bearer", json.token_type)
          assert.equal(5, json.expires_in)
          assert.equal(32, #json.access_token)
          assert.matches("%w+", json.access_token)
        end)
        it("returns success with an application that has not redirect_uri", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid10112",
              client_secret    = "secret10112",
              scope            = "email",
              grant_type       = "client_credentials",
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("bearer", json.token_type)
          assert.equal(5, json.expires_in)
          assert.equal(32, #json.access_token)
          assert.matches("%w+", json.access_token)
        end)
        it("returns success with authenticated_userid and valid provision_key", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id            = "clientid123",
              client_secret        = "secret123",
              scope                = "email",
              grant_type           = "client_credentials",
              authenticated_userid = "hello",
              provision_key        = "provision123"
            },
            headers = {
              ["Host"]             = "oauth2_4.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("bearer", json.token_type)
          assert.equal(5, json.expires_in)
          assert.equal(32, #json.access_token)
          assert.matches("%w+", json.access_token)
        end)
        it("returns success with authorization header", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              scope            = "email",
              grant_type       = "client_credentials"
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json",
              Authorization    = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("bearer", json.token_type)
          assert.equal(5, json.expires_in)
          assert.equal(32, #json.access_token)
          assert.matches("%w+", json.access_token)
        end)
        it("returns success with authorization header and client_id body param", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid123",
              scope            = "email",
              grant_type       = "client_credentials"
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json",
              Authorization    = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("bearer", json.token_type)
          assert.equal(5, json.expires_in)
          assert.equal(32, #json.access_token)
          assert.matches("%w+", json.access_token)
        end)
        it("returns an error with a wrong authorization header", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              scope            = "email",
              grant_type       = "client_credentials"
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "application/json",
              Authorization    = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTI0"
            }
          })
          local body = assert.res_status(401, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
          assert.are.equal("Basic realm=\"OAuth2.0\"", res.headers["www-authenticate"])
        end)
        it("sets the right upstream headers", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id            = "clientid123",
              client_secret        = "secret123",
              scope                = "email",
              grant_type           = "client_credentials",
              authenticated_userid = "hello",
              provision_key        = "provision123"
            },
            headers = {
              ["Host"]             = "oauth2_4.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))

          local res = assert(proxy_ssl_client:send {
            method  = "GET",
            path    = "/request?access_token=" .. body.access_token,
            headers = {
              ["Host"] = "oauth2_4.test"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.truthy(body.headers["x-consumer-id"])
          assert.are.equal("bob", body.headers["x-consumer-username"])
          assert.are.equal("email", body.headers["x-authenticated-scope"])
          assert.are.equal("hello", body.headers["x-authenticated-userid"])
          assert.are.equal("clientid123", body.headers["x-credential-identifier"])
        end)
        it("works in a multipart request", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id            = "clientid123",
              client_secret        = "secret123",
              scope                = "email",
              grant_type           = "client_credentials",
              authenticated_userid = "hello",
              provision_key        = "provision123"
            },
            headers = {
              ["Host"]             = "oauth2_4.test",
              ["Content-Type"]     = "multipart/form-data"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))

          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/request",
            body    = {
              access_token     = body.access_token
            },
            headers = {
              ["Host"]         = "oauth2_4.test",
              ["Content-Type"] = "multipart/form-data"
            }
          })
          assert.res_status(200, res)
        end)
      end)

      describe("Password Grant", function()
        it("blocks unauthorized requests - with no realm set", function()
          local res = assert(proxy_ssl_client:send {
            method  = "GET",
            path    = "/request",
            headers = {
              ["Host"] = "oauth2_4.test"
            }
          })
          local body = assert.res_status(401, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "The access token is missing", error = "invalid_request" }, json)
          assert.are.equal('Bearer', res.headers["www-authenticate"])
        end)
        it("blocks unauthorized requests - with realm set", function()
          local res = assert(proxy_ssl_client:send {
            method  = "GET",
            path    = "/request",
            headers = {
              ["Host"] = "oauth2_5.test"
            }
          })
          local body = assert.res_status(401, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "The access token is missing", error = "invalid_request" }, json)
          assert.are.equal('Bearer realm="test-oauth2"', res.headers["www-authenticate"])
        end)
        it("returns an error when client_secret is not sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid123",
              scope            = "email",
              response_type    = "token"
            },
            headers = {
              ["Host"]         = "oauth2_5.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
        end)
        it("returns an error when grant_type is not sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid123",
              client_secret    = "secret123",
              scope            = "email",
              response_type    = "token"
            },
            headers = {
              ["Host"]         = "oauth2_5.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error = "unsupported_grant_type", error_description = "Invalid grant_type" }, json)
        end)
        it("fails when no provision key is being sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid123",
              client_secret    = "secret123",
              scope            = "email",
              response_type    = "token",
              grant_type       = "password"
            },
            headers = {
              ["Host"]         = "oauth2_5.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json)
        end)
        it("fails when no provision key is being sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              client_id        = "clientid123",
              client_secret    = "secret123",
              scope            = "email",
              grant_type       = "password"
            },
            headers = {
              ["Host"]         = "oauth2_5.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid provision_key", error = "invalid_provision_key" }, json)
        end)
        it("fails when no authenticated user id is being sent", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              provision_key    = "provision123",
              client_id        = "clientid123",
              client_secret    = "secret123",
              scope            = "email",
              grant_type       = "password"
            },
            headers = {
              ["Host"]         = "oauth2_5.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Missing authenticated_userid parameter", error = "invalid_authenticated_userid" }, json)
        end)
        it("returns success", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              client_id            = "clientid123",
              client_secret        = "secret123",
              scope                = "email",
              grant_type           = "password"
            },
            headers = {
              ["Host"]             = "oauth2_5.test",
              ["Content-Type"]     = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("bearer", json.token_type)
          assert.equal(5, json.expires_in)
          assert.equal(32, #json.access_token)
          assert.matches("%w+", json.access_token)
          assert.equal(32, #json.refresh_token)
          assert.matches("%w+", json.refresh_token)
        end)
        it("returns success with authorization header", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              scope                = "email",
              grant_type           = "password"
            },
            headers = {
              ["Host"]             = "oauth2_5.test",
              ["Content-Type"]     = "application/json",
              Authorization        = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("bearer", json.token_type)
          assert.equal(5, json.expires_in)
          assert.equal(32, #json.access_token)
          assert.matches("%w+", json.access_token)
          assert.equal(32, #json.refresh_token)
          assert.matches("%w+", json.refresh_token)
        end)
        it("returns an error with a wrong authorization header", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              scope                = "email",
              grant_type           = "password"
            },
            headers = {
              ["Host"]             = "oauth2_5.test",
              ["Content-Type"]     = "application/json",
              Authorization        = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTI0"
            }
          })
          local body = assert.res_status(401, res)
          local json = cjson.decode(body)
          assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
          assert.are.equal("Basic realm=\"OAuth2.0\"", res.headers["www-authenticate"])
        end)
        it("sets the right upstream headers", function()
          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              provision_key        = "provision123",
              authenticated_userid = "id123",
              scope                = "email",
              grant_type           = "password"
            },
            headers = {
              ["Host"]             = "oauth2_5.test",
              ["Content-Type"]     = "application/json",
              Authorization        = "Basic Y2xpZW50aWQxMjM6c2VjcmV0MTIz"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))

          local res = assert(proxy_ssl_client:send {
            method  = "GET",
            path    = "/request?access_token=" .. body.access_token,
            headers = {
              ["Host"] = "oauth2_5.test"
            }
          })
          local body = cjson.decode(assert.res_status(200, res))
          assert.truthy(body.headers["x-consumer-id"])
          assert.are.equal("bob", body.headers["x-consumer-username"])
          assert.are.equal("email", body.headers["x-authenticated-scope"])
          assert.are.equal("id123", body.headers["x-authenticated-userid"])
          assert.are.equal("clientid123", body.headers["x-credential-identifier"])
        end)
      end)
    end)

    describe("OAuth2 Access Token", function()
      it("returns an error when nothing is being sent", function()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          headers = {
            ["Host"] = "oauth2.test"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
        -- Checking headers
        assert.are.equal("no-store", res.headers["cache-control"])
        assert.are.equal("no-cache", res.headers["pragma"])
      end)
      it("returns an error when only the code is being sent", function()
        local code = provision_code()

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
        -- Checking headers
        assert.are.equal("no-store", res.headers["cache-control"])
        assert.are.equal("no-cache", res.headers["pragma"])
      end)
      it("returns an error when only the code and client_secret are being sent", function()
        local code = provision_code()

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_secret    = "secret123"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
        -- Checking headers
        assert.are.equal("no-store", res.headers["cache-control"])
        assert.are.equal("no-cache", res.headers["pragma"])
      end)
      it("returns an error when grant_type is not being sent", function()
        local code = provision_code()

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error = "unsupported_grant_type", error_description = "Invalid grant_type" }, json)
      end)
      it("returns an error with a wrong code", function()
        local code = provision_code()

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code .. "hello",
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error = "invalid_request", error_description = "Invalid code" }, json)
      end)
      it("returns success without state", function()
        local code = provision_code()

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code            = code,
            client_id       = "clientid123",
            client_secret   = "secret123",
            grant_type      = "authorization_code"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("returns success with state", function()
        local code = provision_code()

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
            state            = "wot"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("fails when the client used for the code is not the same client used for the token", function()
        local code = provision_code(nil, nil, "clientid333")

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
            state            = "wot"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        assert.same({ error = "invalid_request", error_description = "Invalid code", state = "wot" }, cjson.decode(body))
      end)
      it("sets the right upstream headers", function()
        local code = provision_code()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))

        local res = assert(proxy_ssl_client:send {
          method  = "GET",
          path    = "/request?access_token=" .. body.access_token,
          headers = {
            ["Host"] = "oauth2.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.truthy(body.headers["x-consumer-id"])
        assert.are.equal("bob", body.headers["x-consumer-username"])
        assert.are.equal("email", body.headers["x-authenticated-scope"])
        assert.are.equal("userid123", body.headers["x-authenticated-userid"])
        assert.are.equal("clientid123", body.headers["x-credential-identifier"])
      end)
      it("fails when an authorization code is used more than once", function()
        local code = provision_code()

        local res = assert(proxy_ssl_client:send {
            method = "POST",
            path   = "/oauth2/token",
            body   = {
              code             = code,
              client_id        = "clientid123",
              client_secret    = "secret123",
              grant_type       = "authorization_code"
            },
            headers = {
              ["Host"]         = "oauth2.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(200, res)
          local json = cjson.decode(body)
          assert.equal("bearer", json.token_type)
          assert.equal(5, json.expires_in)
          assert.equal(32, #json.access_token)
          assert.matches("%w+", json.access_token)
          assert.equal(32, #json.refresh_token)
          assert.matches("%w+", json.refresh_token)

          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/oauth2/token",
            body    = {
              code             = code,
              client_id        = "clientid123",
              client_secret    = "secret123",
              grant_type       = "authorization_code"
            },
            headers = {
              ["Host"]         = "oauth2.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error = "invalid_request", error_description = "Invalid code" }, json)
      end)
      it("fails when an authorization code is used by another application", function()
        local code = provision_code()
        local res = assert(proxy_ssl_client:send {
            method = "POST",
            path   = "/oauth2/token",
            body    = {
              code             = code,
              client_id        = "clientid789",
              client_secret    = "secret789",
              grant_type       = "authorization_code"
            },
            headers = {
              ["Host"]         = "oauth2.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({ error = "invalid_request", error_description = "Invalid code" }, json)
      end)

      it("fails when an authorization code is used for another API", function()
        local code = provision_code()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code"
          },
          headers = {
            ["Host"]         = "oauth2_3.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error = "invalid_request", error_description = "Invalid code" }, json)
      end)
      it("succeeds when using code challenge", function()
        local challenge, verifier = get_pkce_tokens()
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code            = code,
            client_id       = "clientid11211",
            grant_type      = "authorization_code",
            code_verifier   = verifier
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("succeeds when authorization header used for public app", function()
        local challenge, verifier = get_pkce_tokens()
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            grant_type       = "authorization_code",
            code_verifier    = verifier
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json",
            Authorization    = "Basic Y2xpZW50aWQxMTIxMQ=="
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("succeeds when authorization header used for public app with colon", function()
        local challenge, verifier = get_pkce_tokens()
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            grant_type       = "authorization_code",
            code_verifier    = verifier
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json",
            Authorization    = "Basic Y2xpZW50aWQxMTIxMTo="
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("succeeds when authorization header used for public app with empty secret", function()
        local challenge, verifier = get_pkce_tokens()
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            grant_type       = "authorization_code",
            code_verifier    = verifier
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json",
            Authorization    = "Basic Y2xpZW50aWQxMTIxMTogICAg"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("fails when a secret provided for public app", function()
        local challenge, verifier = get_pkce_tokens()
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
            code_verifier    = verifier,
            client_secret    = "secret11211"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "client_secret is disallowed for public clients", error = "invalid_request" }, json)
      end)
      it("fails when a secret provided for public app in header", function()
        local challenge, verifier = get_pkce_tokens()
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            grant_type       = "authorization_code",
            code_verifier    = verifier,
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json",
            Authorization    = "Basic Y2xpZW50aWQxMTIxMTpzZWNyZXQxMTIxMQ=="
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "client_secret is disallowed for public clients", error = "invalid_request" }, json)
      end)
      it("fails when no code_verifier provided for public app", function()
        local challenge, _ = get_pkce_tokens()
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "code_verifier is required for PKCE authorization requests", error = "invalid_request" }, json)
      end)
      it("success when no code_verifier provided for public app without pkce when conf.pkce is none", function()
        local code = provision_code("oauth2_14.test")
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2_14.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("success when code challenge contains padding", function()
        local code_verifier = "abcdelfhigklmnopqrstuvwxyz0123456789abcdefg"
        local code_challenge = "2aC4cMSkAsMRtZbhHhiZkDW3sddRf_iTRGil1r9gi8w="
        local code = provision_code(nil, nil, "clientid11211", code_challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
            code_verifier    = code_verifier
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("succeeds when code challenge contains + or / characters", function()
        local code_verifier = "abcdelfhigklmnopqrstuvwxyz0123456789abcdefghijklmnop9"
        local code_challenge = "0LoS6Gtrw16r07+ZXsCf8MeAi21QHmKc3LJdUCA5w/o="
        local code = provision_code(nil, nil, "clientid11211", code_challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
            code_verifier    = code_verifier
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("fails when code verifier is greater than 128 characters", function()
        local code_verifier = string_rep("abc123", 30)
        local challenge, verifier = get_pkce_tokens(code_verifier)
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
            code_verifier    = verifier
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "code_verifier must be between 43 and 128 characters", error = "invalid_request" }, json)
      end)
      it("fails when code verifier is less than 43 characters", function()
        local challenge, verifier = get_pkce_tokens("abc123")
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body          = {
            code             = code,
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
            code_verifier    = verifier
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "code_verifier must be between 43 and 128 characters", error = "invalid_request" }, json)
      end)
      it("fails when code verifier is missing", function()
        local challenge, _ = get_pkce_tokens("abc123")
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body          = {
            code             = code,
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "code_verifier is required for PKCE authorization requests", error = "invalid_request" }, json)
      end)
      it("fails when secret does not match for non-authorization_code grant type", function()
        local challenge, _ = get_pkce_tokens()
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            provision_key        = "provision123",
            authenticated_userid = "id123",
            client_id            = "clientid123",
            scope                = "email",
            grant_type           = "password",
            client_secret        = "bogus",
            code                 = code
          },
          headers = {
            ["Host"]             = "oauth2_5.test",
            ["Content-Type"]     = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
      end)
      it("fails when code verifier is empty", function()
        local code = provision_code(nil, nil, "clientid11211", "abc123")
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
            code_verifier    = ""
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "code_verifier must be between 43 and 128 characters", error = "invalid_request" }, json)
      end)
      it("fails when code verifier is not a string", function()
        local code = provision_code(nil, nil, "clientid11211", "abc123")
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
            code_verifier    = 12
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "code_verifier is not a string", error = "invalid_request" }, json)
      end)
      it("fails when code verifier does not match challenge", function()
        local code = provision_code(nil, nil, "clientid11211", "abc123")
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
            code_verifier    = "abcdelfhigklmnopqrstuvwxyz0123456789abcdefg"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid code", error = "invalid_grant" }, json)
      end)
      it("fails when code verifier does not match challenge for confidential app when conf.pkce is strict", function()
        local challenge, _ = get_pkce_tokens()
        local code = provision_code("oauth2_15.test", nil, nil, challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
            code_verifier    = "abcdelfhigklmnopqrstuvwxyz0123456789abcdefg"
          },
          headers = {
            ["Host"]         = "oauth2_15.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid code", error = "invalid_grant" }, json)
      end)
      it("fails when wrong auth code provided for public app", function()
        local challenge, verifier = get_pkce_tokens()
        local code = provision_code(nil, nil, "clientid11211", challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code .. "hello",
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
            code_verifier    = verifier,
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid code", error = "invalid_request" }, json)
      end)
      it("fails when no auth code provided for public app", function()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            client_id        = "clientid11211",
            grant_type       = "authorization_code",
            code_verifier    = "verifier",
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid code", error = "invalid_request" }, json)
      end)
      it("fails when no secret provided for confidential app", function()
        local code = provision_code()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
      end)
      it("fails when no code verifier provided for confidential app when conf.pkce is strict", function()
        local challenge, _ = get_pkce_tokens()
        local code = provision_code("oauth2_15.test", nil, nil, challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2_15.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "code_verifier is required for PKCE authorization requests", error = "invalid_request" }, json)
      end)
      it("fails when no code verifier provided for confidential app with pkce when conf.pkce is lax", function()
        local challenge, _ = get_pkce_tokens()
        local code = provision_code("oauth2_16.test", nil, nil, challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2_16.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "code_verifier is required for PKCE authorization requests", error = "invalid_request" }, json)
      end)
      it("fails when no code verifier provided for confidential app with pkce when conf.pkce is none", function()
        local challenge, _ = get_pkce_tokens()
        local code = provision_code("oauth2_14.test", nil, nil, challenge)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2_14.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "code_verifier is required for PKCE authorization requests", error = "invalid_request" }, json)
      end)
      it("suceeds when no code verifier provided for confidential app without pkce when conf.pkce is none", function()
        local code = provision_code("oauth2_14.test")
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2_14.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("suceeds when no code verifier provided for confidential app without pkce when conf.pkce is lax", function()
        local code = provision_code("oauth2_16.test")
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2_16.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)

      it("fails when exchanging a code created by a different plugin instance when both plugin instances set global_credentials to true", function()
        local code = provision_code("oauth2_16.test") -- obtain a code from plugin oauth2_16.test
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2_17.test", -- exchange the code from plugin oauth2_17.test
            ["Content-Type"] = "application/json",
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({
          error = "invalid_request",
          error_description = "Invalid code",
        }, json)
      end)

      it("should not fail when plugin_id is not present which indicates it's an old code", function()
        local code = provision_code("oauth2_16.test")
        local db_code, err = db.oauth2_authorization_codes:select_by_code(code)
        assert.is_nil(err)
        db_code.plugin = ngx.null
        local _, _, err = db.oauth2_authorization_codes:update(db_code, db_code)
        assert.is_nil(err)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            code             = code,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "authorization_code",
          },
          headers = {
            ["Host"]         = "oauth2_16.test",
            ["Content-Type"] = "application/json",
          }
        })
        assert.res_status(200, res)
      end)
    end)

    describe("Making a request", function()
      it("fails when no access_token is being sent in an application/json body", function()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/request",
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "The access token is missing", error = "invalid_request" }, json)
        assert.are.equal("Bearer", res.headers["www-authenticate"])
      end)
      it("works when a correct access_token is being sent in the querystring", function()
        local token = provision_token()

        local res = assert(proxy_ssl_client:send {
          method  = "GET",
          path    = "/request?access_token=" .. token.access_token,
          headers = {
            ["Host"] = "oauth2.test"
          }
        })
        assert.res_status(200, res)
      end)
      it("works when a correct access_token is being sent in the custom header", function()
        local token = provision_token("oauth2_11.test",nil,"clientid1011","secret1011")

        local res = assert(proxy_ssl_client:send {
          method = "GET",
          path = "/request",
          headers = {
            ["Host"] = "oauth2_11.test",
            ["custom_header_name"] = "bearer " .. token.access_token,
          }
        })
        assert.res_status(200, res)
      end)
      it("works when a correct access_token is being sent in duplicate custom headers", function()
        local token = provision_token("oauth2_11.test",nil,"clientid1011","secret1011")

        local res = assert(proxy_ssl_client:send {
          method = "GET",
          path = "/request",
          headers = {
            ["Host"] = "oauth2_11.test",
            ["custom_header_name"] = { "bearer " .. token.access_token, "bearer " .. token.access_token },
          }
        })
        assert.res_status(200, res)
      end)
      it("fails when missing access_token is being sent in the custom header", function()
        local res = assert(proxy_ssl_client:send {
          method = "GET",
          path = "/request",
          headers = {
            ["Host"] = "oauth2_11.test",
            ["custom_header_name"] = "",
          }
        })
        assert.res_status(401, res)
        assert.are.equal("Bearer", res.headers["www-authenticate"])
      end)

      it("refreshing token fails when scope is mismatching", function ()
        -- provision code
        local code, body, res
        local request_client = helpers.proxy_ssl_client()
        body = {
            provision_key = "provision123",
            client_id = "clientid123",
            response_type = "code",
            scope = "scope18",
            state = "hello",
            authenticated_userid = "userid123",
        }
        res = assert(request_client:send {
          method = "POST",
          path = "/oauth2/authorize",
          body = body,
          headers = kong.table.merge({
            ["Host"] = "oauth2_18.test",
            ["Content-Type"] = "application/json"
          })
        })
        res = assert(cjson.decode(assert.res_status(200, res)))
        if res.redirect_uri then
          local iterator, err = ngx.re.gmatch(res.redirect_uri, "^http://google\\.test/kong\\?code=([\\w]{32,32})&state=hello$")
          assert.is_nil(err)
          local m, err = iterator()
          assert.is_nil(err)
          code = m[1]
        end

        -- provision token
        body = {
          code = code,
          client_id = "clientid123",
          client_secret = "secret123",
          grant_type = "authorization_code",
          redirect_uri = "http://google.test/kong",
        }
        res = assert(request_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = body,
          headers = {
            ["Host"]         = "oauth2_18.test",
            ["Content-Type"] = "application/json"
          }
        })
        local token = assert(cjson.decode(assert.res_status(200, res)))

        -- refresh token with mismatching scope
        res = assert(request_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            refresh_token    = token.refresh_token,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "refresh_token",
          },
          headers = {
            ["Host"]         = "oauth2_19.test",
            ["Content-Type"] = "application/json"
          }
        })
        res = assert(cjson.decode(assert.res_status(400, res)))
        assert.same({
          error = "invalid_scope",
          error_description = "Scope mismatch"
        }, res)
        request_client:close()
      end)

      it("refreshing token succeeds when scope is a subset", function()
        -- provision code
        local code, body, res
        local request_client = helpers.proxy_ssl_client()
        body = {
            provision_key = "provision123",
            client_id = "clientid123",
            response_type = "code",
            scope = "scope20",
            state = "hello",
            authenticated_userid = "userid123",
        }
        res = assert(request_client:send {
          method = "POST",
          path = "/oauth2/authorize",
          body = body,
          headers = kong.table.merge({
            ["Host"] = "oauth2_20.test",
            ["Content-Type"] = "application/json"
          })
        })
        res = assert(cjson.decode(assert.res_status(200, res)))
        if res.redirect_uri then
          local iterator, err = ngx.re.gmatch(res.redirect_uri, "^http://google\\.test/kong\\?code=([\\w]{32,32})&state=hello$")
          assert.is_nil(err)
          local m, err = iterator()
          assert.is_nil(err)
          code = m[1]
        end

        -- provision token
        body = {
          code = code,
          client_id = "clientid123",
          client_secret = "secret123",
          grant_type = "authorization_code",
          redirect_uri = "http://google.test/kong",
        }
        res = assert(request_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = body,
          headers = {
            ["Host"]         = "oauth2_20.test",
            ["Content-Type"] = "application/json"
          }
        })
        local token = assert(cjson.decode(assert.res_status(200, res)))

        -- refresh token with mismatching scope
        local res = assert(request_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            refresh_token    = token.refresh_token,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "refresh_token",
          },
          headers = {
            ["Host"]         = "oauth2_21.test",
            ["Content-Type"] = "application/json"
          }
        })
        assert(cjson.decode(assert.res_status(200, res)))
        request_client:close()
      end)

      it("fails when a correct access_token is being sent in the wrong header", function()
        local token = provision_token("oauth2_11.test",nil,"clientid1011","secret1011")

        local res = assert(proxy_ssl_client:send {
          method = "GET",
          path = "/request",
          headers = {
            ["Host"] = "oauth2_11.test",
            ["authorization"] = "bearer " .. token.access_token,
          }
        })
        assert.res_status(401, res)
        assert.are.equal("Bearer", res.headers["www-authenticate"])
      end)
      it("does not work when requesting a different API - with no realm set", function()
        local token = provision_token()

        local res = assert(proxy_ssl_client:send {
          method  = "GET",
          path    = "/request?access_token=" .. token.access_token,
          headers = {
            ["Host"] = "oauth2_3.test"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json)
        assert.are.equal('Bearer error="invalid_token", error_description="The access token is invalid or has expired"', res.headers["www-authenticate"])
      end)
      it("does not work when requesting a different API - with realm set", function()
        local token = provision_token()

        local res = assert(proxy_ssl_client:send {
          method  = "GET",
          path    = "/request?access_token=" .. token.access_token,
          headers = {
            ["Host"] = "oauth2_4.test"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json)
        assert.are.equal('Bearer error="invalid_token", error_description="The access token is invalid or has expired"', res.headers["www-authenticate"])
      end)
      it("works when a correct access_token is being sent in a form body", function()
        local token = provision_token()

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/request",
          body    = {
            access_token     = token.access_token
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        assert.res_status(200, res)
      end)
      it("does not throw an error when request has no body", function()
        -- Regression test for the following issue:
        -- https://github.com/Kong/kong/issues/3055
        --
        -- We want to make sure we do not attempt to parse a
        -- request body if the request isn't supposed to have
        -- one in the first place.

        helpers.clean_logfile()

        -- TEST: access with a GET request

        local token = provision_token()

        local res = assert(proxy_ssl_client:send {
          method = "GET",
          path = "/request?access_token=" .. token.access_token,
          headers = {
            ["Host"] = "oauth2.test"
          }
        })
        assert.res_status(200, res)

        -- Assertion: there should be no [error], including no error
        -- resulting from an invalid request body parsing that were
        -- previously thrown.
        assert.logfile().has.no.line("[error]", true)
      end)
      it("works when a correct access_token is being sent in an authorization header (bearer)", function()
        local token = provision_token()

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/request",
          headers = {
            ["Host"]      = "oauth2.test",
            Authorization = "bearer " .. token.access_token
          }
        })
        assert.res_status(200, res)
      end)
      it("works when a correct access_token is being sent in an authorization header (token)", function()
        local token = provision_token()

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/request",
          headers = {
            ["Host"]      = "oauth2.test",
            Authorization = "bearer " .. token.access_token
          }
        })
        local body = cjson.decode(assert.res_status(200, res))

        local consumer = db.consumers:select_by_username("bob")
        assert.are.equal(consumer.id, body.headers["x-consumer-id"])
        assert.are.equal(consumer.username, body.headers["x-consumer-username"])
        assert.are.equal("userid123", body.headers["x-authenticated-userid"])
        assert.are.equal("email", body.headers["x-authenticated-scope"])
        assert.are.equal("clientid123", body.headers["x-credential-identifier"])
        assert.is_nil(body.headers["x-anonymous-consumer"])
      end)

      it("accepts gRPC call with credentials", function()
        local token = provision_token("oauth2_grpc.test")

        local ok, res = helpers.proxy_client_grpcs(){
          service = "hello.HelloService.SayHello",
          opts = {
            ["-authority"] = "oauth2_grpc.test",
            ["-H"] = ("'authorization: bearer %s'"):format(token.access_token),
          },
        }
        assert.truthy(ok)
        assert.same({ reply = "hello noname" }, cjson.decode(res))
      end)


      it("returns HTTP 400 when scope is not a string", function()
        local invalid_values = {
          { "email" },
          123,
          false,
        }

        for _, val in ipairs(invalid_values) do
          local res = assert(proxy_ssl_client:send {
            method = "POST",
            path = "/oauth2/token",
            body = {
              provision_key = "provision123",
              authenticated_userid = "id123",
              client_id = "clientid123",
              client_secret="secret123",
              scope = val,
              grant_type = "password",
            },
            headers = {
              ["Host"] = "oauth2_5.test",
              ["Content-Type"] = "application/json"
            }
          })
          local body = assert.res_status(400, res)
          local json = cjson.decode(body)
          assert.same({
            error = "invalid_scope",
            error_description = "scope must be a string"
          }, json)
        end
      end)
      it("works with right credentials and anonymous", function()
        local token = provision_token("oauth2_7.test")
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/request",
          headers = {
            ["Host"]      = "oauth2_7.test",
            Authorization = "bearer " .. token.access_token
          }
        })
        local body = cjson.decode(assert.res_status(200, res))

        local consumer = db.consumers:select_by_username("bob")
        assert.are.equal(consumer.id, body.headers["x-consumer-id"])
        assert.are.equal(consumer.username, body.headers["x-consumer-username"])
        assert.are.equal("userid123", body.headers["x-authenticated-userid"])
        assert.are.equal("email", body.headers["x-authenticated-scope"])
        assert.are.equal("clientid123", body.headers["x-credential-identifier"])
        assert.is_nil(body.headers["x-anonymous-consumer"])
      end)
      it("works with wrong credentials and anonymous", function()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/request",
          headers = {
            ["Host"] = "oauth2_7.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.are.equal("true", body.headers["x-anonymous-consumer"])
        assert.equal('no-body', body.headers["x-consumer-username"])
        assert.are.equal(nil, body.headers["x-credential-identifier"])

      end)
      it("works with wrong credentials and username in anonymous", function()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/request",
          headers = {
            ["Host"] = "oauth2__c.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.are.equal("true", body.headers["x-anonymous-consumer"])
        assert.equal('no-body', body.headers["x-consumer-username"])
      end)
      it("errors when anonymous user doesn't exist", function()
        finally(function()
          if proxy_ssl_client then
            proxy_ssl_client:close()
          end

          proxy_ssl_client = helpers.proxy_ssl_client()
        end)

        local res = assert(proxy_ssl_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"] = "oauth2_10.test"
          }
        })
        local body = cjson.decode(assert.res_status(500, res))
        assert.same("anonymous consumer " .. nonexisting_anonymous .. " is configured but doesn't exist", body.message)
      end)
      it("returns success and the token should have the right expiration when a custom header is passed", function()
        local res = assert(proxy_ssl_client:send {
          method = "POST",
          path = "/oauth2/authorize",
          body = {
            provision_key = "provision123",
            authenticated_userid = "id123",
            client_id = "clientid1011",
            scope = "email",
            response_type = "token"
          },
          headers = {
            ["Host"] = "oauth2_11.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.is_table(ngx.re.match(body.redirect_uri, "^http://google\\.test/kong\\#access_token=[\\w]{32,32}&expires_in=[\\d]+&token_type=bearer$"))

        local iterator, err = ngx.re.gmatch(body.redirect_uri, "^http://google\\.test/kong\\#access_token=([\\w]{32,32})&expires_in=[\\d]+&token_type=bearer$")
        assert.is_nil(err)
        local m, err = iterator()
        assert.is_nil(err)
        local data = db.oauth2_tokens:select_by_access_token(m[1])
        assert.are.equal(m[1], data.access_token)
        assert.are.equal(7, data.expires_in)
        assert.falsy(data.refresh_token)
      end)
      it("returns success while accessing the correct service after accessing the wrong service first", function()
        local token = provision_token()

        -- hit the wrong service first, should return 401
        local res = assert(proxy_ssl_client:send {
          method  = "GET",
          path    = "/request?access_token=" .. token.access_token,
          headers = {
            ["Host"] = "oauth2_3.test"
          }
        })
        assert.res_status(401, res)

        -- hit the right service later, should return 200
        local res = assert(proxy_ssl_client:send {
          method  = "GET",
          path    = "/request?access_token=" .. token.access_token,
          headers = {
            ["Host"] = "oauth2.test"
          }
        })
        assert.res_status(200, res)
      end)

      describe("Global Credentials", function()
        it("does not access two different APIs that are not sharing global credentials", function()
          local token = provision_token("oauth2_8.test")

          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/request",
            headers = {
              ["Host"]      = "oauth2_8.test",
              Authorization = "bearer " .. token.access_token
            }
          })
          assert.res_status(200, res)

          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/request",
            headers = {
              ["Host"]      = "oauth2.test",
              Authorization = "bearer " .. token.access_token
            }
          })
          assert.res_status(401, res)
          assert.are.equal('Bearer error="invalid_token", error_description="The access token is invalid or has expired"', res.headers["WWW-Authenticate"])
        end)
        it("does not access two different APIs that are not sharing global credentials 2", function()
          local token = provision_token("oauth2.test")

          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/request",
            headers = {
              ["Host"]      = "oauth2_8.test",
              Authorization = "bearer " .. token.access_token
            }
          })
          assert.res_status(401, res)
          assert.are.equal('Bearer error="invalid_token", error_description="The access token is invalid or has expired"', res.headers['www-authenticate'])

          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/request",
            headers = {
              ["Host"]      = "oauth2.test",
              Authorization = "bearer " .. token.access_token
            }
          })
          assert.res_status(200, res)
        end)
        it("access two different APIs that are sharing global credentials", function()
          local token = provision_token("oauth2_8.test")

          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/request",
            headers = {
              ["Host"]      = "oauth2_8.test",
              Authorization = "bearer " .. token.access_token
            }
          })
          assert.res_status(200, res)

          local res = assert(proxy_ssl_client:send {
            method  = "POST",
            path    = "/request",
            headers = {
              ["Host"]      = "oauth2_9.test",
              Authorization = "bearer " .. token.access_token
            }
          })
          assert.res_status(200, res)
        end)
      end)
    end)

    describe("Authentication challenge", function()
      it("returns 401 Unauthorized without error if it lacks any authentication information", function()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/request",
          headers = {
            ["Host"] = "oauth2.test"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "The access token is missing", error = "invalid_request" }, json)
        assert.are.equal('Bearer', res.headers['www-authenticate'])
      end)
      it("returns 401 Unauthorized when an invalid access token is being sent via url parameter", function()
        local res = assert(proxy_ssl_client:send {
          method  = "GET",
          path    = "/request?access_token=invalid",
          headers = {
            ["Host"] = "oauth2.test"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json)
        assert.are.equal('Bearer error="invalid_token", error_description="The access token is invalid or has expired"', res.headers['www-authenticate'])
      end)
      it("returns 401 Unauthorized when an invalid access token is being sent via the Authorization header", function()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/request",
          headers = {
            ["Host"]      = "oauth2.test",
            Authorization = "bearer invalid"
          }
        })
        local body = assert.res_status(401, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json)
        assert.are.equal('Bearer error="invalid_token", error_description="The access token is invalid or has expired"', res.headers['www-authenticate'])
      end)
      it("returns 401 Unauthorized when token has expired", function()
        local token = provision_token()

        -- Token expires in 5 seconds
        local status, json, headers
        helpers.wait_until(function()
          local client = helpers.proxy_ssl_client()
          local res = assert(client:send {
            method  = "POST",
            path    = "/request",
            headers = {
              ["Host"]      = "oauth2.test",
              Authorization = "bearer " .. token.access_token
            }
          })
          local body = res:read_body()
          status = res.status
          headers = res.headers
          json = cjson.decode(body)
          client:close()
          return status == 401
        end, 7)
        assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json)
        assert.are.equal('Bearer error="invalid_token", error_description="The access token is invalid or has expired"', headers['www-authenticate'])
      end)
    end)

    describe("Refresh Token", function()
      it("does not refresh an invalid access token", function()
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            refresh_token    = "hello",
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "refresh_token"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error = "invalid_request", error_description = "Invalid refresh_token" }, json)
      end)
      it("refreshes an valid access token", function()
        local token = provision_token()

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            refresh_token    = token.refresh_token,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "refresh_token"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("refreshes public app without a secret", function()
        local challenge, verifier = get_pkce_tokens()
        local token = provision_token(nil, nil, "clientid11211", nil, challenge, verifier, false)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            refresh_token    = token.refresh_token,
            client_id        = "clientid11211",
            grant_type       = "refresh_token"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)
      end)
      it("fails to refresh when a secret provided for public app", function()
        local challenge, verifier = get_pkce_tokens()
        local token = provision_token(nil, nil, "clientid11211", nil, challenge, verifier, false)
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            refresh_token    = token.refresh_token,
            client_id        = "clientid11211",
            client_secret    = "secret11211",
            grant_type       = "refresh_token"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "client_secret is disallowed for public clients", error = "invalid_request" }, json)
      end)
      it("fails to refresh when no secret provided for confidential app", function()
        local token = provision_token(nil, nil, "clientid123")
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            refresh_token    = token.refresh_token,
            client_id        = "clientid123",
            grant_type       = "refresh_token"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
      end)
      it("refreshes an valid access token and checks that it belongs to the application", function()
        local token = provision_token(nil, nil, "clientid333", "secret333")

        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            refresh_token    = token.refresh_token,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "refresh_token"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json"
          }
        })
        local body = assert.res_status(400, res)
        local json = cjson.decode(body)
        assert.same({ error_description = "Invalid client authentication", error = "invalid_client" }, json)
        assert.are.equal("no-store", res.headers["cache-control"])
        assert.are.equal("no-cache", res.headers["pragma"])
      end)
      it("expires after 5 seconds", function()
        local token = provision_token()

        local res = assert(proxy_client:send {
          method  = "POST",
          path    = "/request",
          headers = {
            ["Host"]      = "oauth2.test",
            authorization = "bearer " .. token.access_token
          }
        })
        assert.res_status(200, res)

        local id = db.oauth2_tokens:select_by_access_token(token.access_token).id
        assert.truthy(db.oauth2_tokens:select({ id = id }))

        -- But waiting after the cache expiration (5 seconds) should block the request
        local status, json, res2
        helpers.wait_until(function()
          local client = helpers.proxy_client()
          res2 = assert(client:send {
            method  = "POST",
            path    = "/request",
            headers = {
              ["Host"]      = "oauth2.test",
              authorization = "bearer " .. token.access_token
            }
          })
          status = res2.status
          local body = res2:read_body()
          json = body and cjson.decode(body)
          return status == 401
        end, 7)
        assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json)
        assert.are.equal('Bearer error="invalid_token", error_description="The access token is invalid or has expired"', res2.headers["WWW-Authenticate"])

        -- Refreshing the token
        local res = assert(proxy_ssl_client:send {
          method  = "POST",
          path    = "/oauth2/token",
          body    = {
            refresh_token    = token.refresh_token,
            client_id        = "clientid123",
            client_secret    = "secret123",
            grant_type       = "refresh_token"
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/json",
            authorization    = "bearer " .. token.access_token
          }
        })
        local body = assert.res_status(200, res)
        local json = cjson.decode(body)
        assert.equal("bearer", json.token_type)
        assert.equal(5, json.expires_in)
        assert.equal(32, #json.access_token)
        assert.matches("%w+", json.access_token)
        assert.equal(32, #json.refresh_token)
        assert.matches("%w+", json.refresh_token)

        assert.falsy(token.access_token  == cjson.decode(body).access_token)
        assert.falsy(token.refresh_token == cjson.decode(body).refresh_token)

        assert.falsy(db.oauth2_tokens:select({ id = id }))
      end)
      it("does rewrite non-persistent refresh tokens", function ()
        local token = provision_token()
        local refreshed_token = refresh_token(nil, token.refresh_token)
        assert.is_table(refreshed_token)
        assert.falsy(token.refresh_token == refreshed_token.refresh_token)
      end)
      it("does not rewrite persistent refresh tokens", function()
        local token = provision_token("oauth2_13.test")
        local refreshed_token = refresh_token("oauth2_13.test", token.refresh_token)
        local new_access_token = db.oauth2_tokens:select_by_access_token(refreshed_token.access_token)
        local new_refresh_token = db.oauth2_tokens:select_by_refresh_token(token.refresh_token)
        assert.truthy(new_refresh_token)
        assert.same(new_access_token.id, new_refresh_token.id)


        -- check refreshing sets created_at so access token doesn't expire
        db.oauth2_tokens:update(new_refresh_token, {
          created_at = 123, -- set time as expired
        })

        local status, json, headers
        helpers.wait_until(function()
          local client = helpers.proxy_ssl_client()
          local first_res = assert(client:send {
            method  = "POST",
            path    = "/request",
            headers = {
              ["Host"]      = "oauth2_13.test",
              Authorization = "bearer " .. refreshed_token.access_token
            }
          })
          local nbody = first_res:read_body()
          status = first_res.status
          headers = first_res.headers
          json = cjson.decode(nbody)
          client:close()
          return status == 401
        end, 7)
        assert.same({ error_description = "The access token is invalid or has expired", error = "invalid_token" }, json)
        assert.are.equal('Bearer error="invalid_token", error_description="The access token is invalid or has expired"', headers['www-authenticate'])

        local final_refreshed_token = refresh_token("oauth2_13.test", refreshed_token.refresh_token)
        local last_res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]      = "oauth2_13.test",
            authorization = "bearer " .. final_refreshed_token.access_token
          }
        })
        local last_body = cjson.decode(assert.res_status(200, last_res))
        assert.equal("bearer " .. final_refreshed_token.access_token, last_body.headers.authorization)

      end)
    end)

    describe("Hide Credentials", function()
      it("does not hide credentials in the body", function()
        local token = provision_token()

        local res = assert(proxy_client:send {
          method  = "POST",
          path    = "/request",
          body    = {
            access_token     = token.access_token
          },
          headers = {
            ["Host"]         = "oauth2.test",
            ["Content-Type"] = "application/x-www-form-urlencoded"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.equal(token.access_token, body.post_data.params.access_token)
      end)
      it("hides credentials in the body", function()
        local token = provision_token("oauth2_3.test")

        local res = assert(proxy_client:send {
          method  = "POST",
          path    = "/request",
          body    = {
            access_token     = token.access_token
          },
          headers = {
            ["Host"]         = "oauth2_3.test",
            ["Content-Type"] = "application/x-www-form-urlencoded"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.is_nil(body.post_data.params.access_token)
      end)
      it("does not hide credentials in the querystring", function()
        local token = provision_token()

        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request?access_token=" .. token.access_token,
          headers = {
            ["Host"] = "oauth2.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.equal(token.access_token, body.uri_args.access_token)
      end)
      it("hides credentials in the querystring", function()
        local token = provision_token("oauth2_3.test")

        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request?access_token=" .. token.access_token,
          headers = {
            ["Host"] = "oauth2_3.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.is_nil(body.uri_args.access_token)
      end)
      it("hides credentials in the querystring for api with custom header", function()
        local token = provision_token("oauth2_12.test",nil,"clientid1011","secret1011")

        local res = assert(proxy_client:send {
          method = "GET",
          path = "/request?access_token=" .. token.access_token,
          headers = {
            ["Host"] = "oauth2_12.test"
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.is_nil(body.uri_args.access_token)
      end)
      it("does not hide credentials in the header", function()
        local token = provision_token()

        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]      = "oauth2.test",
            authorization = "bearer " .. token.access_token
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.equal("bearer " .. token.access_token, body.headers.authorization)
      end)
      it("hides credentials in the header", function()
        local token = provision_token("oauth2_3.test")

        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]      = "oauth2_3.test",
            authorization = "bearer " .. token.access_token
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.is_nil(body.headers.authorization)
      end)
      it("hides credentials in the custom header", function()
        local token = provision_token("oauth2_12.test",nil,"clientid1011","secret1011")

        local res = assert(proxy_client:send {
          method = "GET",
          path = "/request",
          headers = {
            ["Host"] = "oauth2_12.test",
            ["custom_header_name"] = "bearer " .. token.access_token
          }
        })
        local body = cjson.decode(assert.res_status(200, res))
        assert.is_nil(body.headers.authorization)
        assert.is_nil(body.headers.custom_header_name)
      end)
      it("does not abort when the request body is a multipart form upload", function()
        local token = provision_token("oauth2_3.test")

        local res = assert(proxy_client:send {
          method  = "POST",
          path    = "/request?access_token=" .. token.access_token,
          body    = {
            foo              = "bar"
          },
          headers = {
            ["Host"]         = "oauth2_3.test",
            ["Content-Type"] = "multipart/form-data"
          }
        })
        assert.res_status(200, res)
      end)
    end)
  end)


  describe("Plugin: oauth2 (access) [#" .. strategy .. "]", function()
    local proxy_client
    local user1
    local user2
    local anonymous
    local keyauth
    local jwt_secret

    lazy_setup(function()
      local service1 = admin_api.services:insert({
        path = "/request"
      })

      local route1 = assert(admin_api.routes:insert({
        hosts      = { "logical-and.test" },
        protocols  = { "http", "https" },
        service    = service1
      }))

      admin_api.oauth2_plugins:insert({
        route = { id = route1.id },
        config   = { scopes = { "email", "profile", "user.email" } },
      })

      admin_api.plugins:insert {
        name     = "key-auth",
        route = { id = route1.id },
      }

      anonymous = admin_api.consumers:insert {
        username = "Anonymous",
      }

      user1 = admin_api.consumers:insert {
        username = "Mickey",
      }

      user2 = admin_api.consumers:insert {
        username = "Aladdin",
      }

      local service2 = admin_api.services:insert({
        path = "/request"
      })

      local route2 = assert(admin_api.routes:insert({
        hosts      = { "logical-or.test" },
        protocols  = { "http", "https" },
        service    = service2
      }))

      local route3 = assert(admin_api.routes:insert({
        hosts      = { "logical-or-jwt.test" },
        protocols  = { "http", "https" },
        service    = service2
      }))

      admin_api.oauth2_plugins:insert({
        route = { id = route2.id },
        config   = {
          scopes    = { "email", "profile", "user.email" },
          anonymous = anonymous.id,
        },
      })

      admin_api.oauth2_plugins:insert({
        route = { id = route3.id },
        config   = {
          scopes    = { "email", "profile", "user.email" },
          anonymous = anonymous.id,
          realm     = "test-oauth2"
        },
      })

      admin_api.plugins:insert {
        name     = "key-auth",
        route = { id = route2.id },
        config   = {
          anonymous = anonymous.id,
        },
      }

      admin_api.plugins:insert {
        name     = "jwt",
        route = { id = route3.id },
        config   = {
          anonymous = anonymous.id,
        },
      }

      keyauth = admin_api.keyauth_credentials:insert({
        key      = "Mouse",
        consumer = { id = user1.id },
      })

      jwt_secret = admin_api.jwt_secrets:insert({
        consumer = { id = user1.id }
      })

      admin_api.oauth2_credentials:insert {
        client_id      = "clientid4567",
        client_secret  = "secret4567",
        redirect_uris  = { "http://google.test/kong" },
        name           = "testapp",
        consumer       = { id = user2.id },
      }

      proxy_client = helpers.proxy_client()
    end)


    lazy_teardown(function()
      if proxy_client then proxy_client:close() end
    end)

    describe("multiple auth without anonymous, logical AND", function()

      it("passes with all credentials provided", function()
        local token = provision_token("logical-and.test",
          { ["apikey"] = "Mouse"}, "clientid4567", "secret4567").access_token
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers  = {
            ["Host"]          = "logical-and.test",
            ["apikey"]        = "Mouse",
            -- we must provide the apikey again in the extra_headers, for the
            -- token endpoint, because that endpoint is also protected by the
            -- key-auth plugin. Otherwise getting the token simply fails.
            ["Authorization"] = "bearer " .. token,
          }
        })
        assert.response(res).has.status(200)
        assert.request(res).has.no.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.not_equal(id, anonymous.id)
        assert(id == user1.id or id == user2.id)

        local client_id = assert.request(res).has.header("x-credential-identifier")
        assert.equal(keyauth.id, client_id)
      end)

      it("fails 401, with only the first credential provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]   = "logical-and.test",
            ["apikey"] = "Mouse",
          }
        })
        assert.response(res).has.status(401)
        assert.are.equal("Bearer", res.headers["www-authenticate"])
      end)

      it("fails 401, with only the second credential provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]          = "logical-and.test",
            -- we must provide the apikey again in the extra_headers, for the
            -- token endpoint, because that endpoint is also protected by the
            -- key-auth plugin. Otherwise getting the token simply fails.
            ["Authorization"] = "bearer " .. provision_token("logical-and.test",
                  {["apikey"] = "Mouse"}).access_token,
          }
        })
        assert.response(res).has.status(401)
        assert.are.equal("Key", res.headers["www-authenticate"])
      end)

      it("fails 401, with no credential provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"] = "logical-and.test",
          }
        })
        assert.response(res).has.status(401)
        assert.are.equal("Bearer", res.headers["www-authenticate"])
      end)

    end)

    describe("multiple auth with anonymous, logical OR", function()

      it("passes with all credentials provided", function()
        local token = provision_token("logical-or.test", nil,
                                      "clientid4567", "secret4567").access_token
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]          = "logical-or.test",
            ["apikey"]        = "Mouse",
            ["Authorization"] = "bearer " .. token,
          }
        })
        assert.response(res).has.status(200)
        assert.request(res).has.no.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.not_equal(id, anonymous.id)
        assert(id == user1.id or id == user2.id)
        local client_id = assert.request(res).has.header("x-credential-identifier")
        assert.equal("clientid4567", client_id)
      end)

      it("passes with only the first credential provided (higher priority)", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path = "/request",
          headers = {
            ["Host"] = "logical-or.test",
            ["apikey"] = "Mouse",
            ["X-Authenticated-Scope"] = "all-access",
            ["X-Authenticated-UserId"] = "admin",
          }
        })

        assert.response(res).has.status(200)
        assert.request(res).has.no.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.not_equal(id, anonymous.id)
        assert.equal(user1.id, id)
        local client_id = assert.request(res).has.header("x-credential-identifier")
        assert.equal(keyauth.id, client_id)
        assert.request(res).has.no.header("x-authenticated-scope")
        assert.request(res).has.no.header("x-authenticated-userid")
      end)

      it("passes with only the first credential provided (lower priority)", function()
        PAYLOAD.iss = jwt_secret.key
        local jwt = jwt_encoder.encode(PAYLOAD, jwt_secret.secret)
        local authorization = "Bearer " .. jwt
        local res = assert(proxy_client:send {
          method  = "GET",
          path = "/request",
          headers = {
            ["Host"] = "logical-or-jwt.test",
            ["Authorization"] = authorization,
            ["X-Authenticated-Scope"] = "all-access",
            ["X-Authenticated-UserId"] = "admin",
          }
        })

        assert.response(res).has.status(200)
        assert.request(res).has.no.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.not_equal(id, anonymous.id)
        assert.equal(user1.id, id)
        local client_id = assert.request(res).has.header("x-credential-identifier")
        assert.equal(jwt_secret.key, client_id)
        assert.request(res).has.no.header("x-authenticated-scope")
        assert.request(res).has.no.header("x-authenticated-userid")
      end)

      it("passes with only the second credential provided", function()
        local token = provision_token("logical-or.test", nil,
                                      "clientid4567", "secret4567").access_token
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"]          = "logical-or.test",
            ["Authorization"] = "bearer " .. token,
          }
        })
        assert.response(res).has.status(200)
        assert.request(res).has.no.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.not_equal(id, anonymous.id)
        assert.equal(user2.id, id)
        local client_id = assert.request(res).has.header("x-credential-identifier")
        assert.equal("clientid4567", client_id)
      end)

      it("passes with no credential provided", function()
        local res = assert(proxy_client:send {
          method  = "GET",
          path    = "/request",
          headers = {
            ["Host"] = "logical-or.test",
          }
        })
        assert.response(res).has.status(200)
        assert.request(res).has.header("x-anonymous-consumer")
        local id = assert.request(res).has.header("x-consumer-id")
        assert.equal(id, anonymous.id)
        assert.request(res).has.no.header("x-credential-identifier")
      end)
    end)
  end)

  describe("Plugin: oauth2 (ttl) with #"..strategy, function()
    lazy_setup(function()
      local route11 = assert(admin_api.routes:insert({
        hosts     = { "oauth2_21.refresh.test" },
        protocols = { "http", "https" },
        service   = admin_api.services:insert(),
      }))

      admin_api.plugins:insert {
        name = "oauth2",
        route = { id = route11.id },
        config = {
          enable_authorization_code = true,
          mandatory_scope = false,
          provision_key = "provision123",
          global_credentials = false,
          refresh_token_ttl = 2
        }
      }

      local route12 = assert(admin_api.routes:insert({
        hosts     = { "oauth2_22.refresh.test" },
        protocols = { "http", "https" },
        service   = admin_api.services:insert(),
      }))

      admin_api.plugins:insert {
        name = "oauth2",
        route = { id = route12.id },
        config = {
          enable_authorization_code = true,
          mandatory_scope = false,
          provision_key = "provision123",
          global_credentials = false,
          refresh_token_ttl = 0
        }
      }

      local consumer = admin_api.consumers:insert {
        username = "bobo"
      }
      admin_api.oauth2_credentials:insert {
        client_id = "clientid7890",
        client_secret = "secret7890",
        redirect_uris = { "http://google.test/kong" },
        name = "testapp",
        consumer = { id = consumer.id },
      }
    end)

    describe("refresh token", function()
      it("is deleted after defined TTL", function()
        local token = provision_token("oauth2_21.refresh.test", nil, "clientid7890", "secret7890")
        local token_entity = db.oauth2_tokens:select_by_access_token(token.access_token)
        assert.is_table(token_entity)

        local err
        helpers.wait_until(function()
          token_entity, err = db.oauth2_tokens:select_by_access_token(token.access_token)
          return token_entity == nil and err == nil
        end, 3)
      end)

      it("is not deleted when when TTL is 0 == never", function()
        local token = provision_token("oauth2_22.refresh.test", nil, "clientid7890", "secret7890")
        local token_entity = db.oauth2_tokens:select_by_access_token(token.access_token)
        assert.is_table(token_entity)

        ngx.sleep(2.2)

        token_entity = db.oauth2_tokens:select_by_access_token(token.access_token)
        assert.is_table(token_entity)
      end)
    end)
  end)

  describe("Plugin: oauth2 regressions", function()
    it("responds 401 when using global token against non-global plugin", function()
      -- Regression test for issue:
      -- https://github.com/Kong/kong/issues/4232

      -- setup

      local route_token = assert(admin_api.routes:insert({
        hosts     = { "oauth2_regression_4232.test" },
        protocols = { "http", "https" },
        service   = admin_api.services:insert(),
      }))

      admin_api.plugins:insert {
        name = "oauth2",
        route = { id = route_token.id },
        config = {
          provision_key = "provision123",
          enable_authorization_code = true,
          global_credentials = true,
        }
      }

      local route_test = assert(admin_api.routes:insert({
        hosts     = { "oauth2_regression_4232_test.test" },
        protocols = { "http", "https" },
        service   = admin_api.services:insert(),
      }))

      admin_api.plugins:insert {
        name = "oauth2",
        route = { id = route_test.id },
        config = {
          enable_client_credentials = true,
          global_credentials = false,
        }
      }

      local consumer = admin_api.consumers:insert {
        username = "4232",
      }

      admin_api.oauth2_credentials:insert {
        client_id = "clientid_4232",
        client_secret = "secret_4232",
        redirect_uris = { "http://google.test/kong" },
        name = "4232_app",
        consumer = { id = consumer.id },
      }

      -- /setup

      local token = provision_token("oauth2_regression_4232.test", nil,
                                    "clientid_4232",
                                    "secret_4232")

      local proxy_ssl_client = helpers.proxy_ssl_client()

      local res = assert(proxy_ssl_client:send {
        method  = "POST",
        path    = "/anything",
        body    = {
          access_token = token.access_token
        },
        headers = {
          ["Host"]         = "oauth2_regression_4232_test.test",
          ["Content-Type"] = "application/json"
        }
      })
      local body = assert.res_status(401, res)
      local json = cjson.decode(body)
      assert.same({
        error_description = "The access token is global, but the current " ..
                            "plugin is configured without 'global_credentials'",
        error = "invalid_token",
      }, json)
      assert.are.equal('Bearer error="invalid_token", error_description="The access token is invalid or has expired"', res.headers["www-authenticate"])
    end)
  end)
end)

end
