---
title: "Add JWT Authentication to Your Lapis API"
description: "Secure your Lapis API using JWT authentication with JWKS."
canonicalUrl: "https://zuplo.com/use-cases/api-key-auth/lua/lapis/jwt-backend"
framework: "Lapis"
language: "Lua"
authStrategy: "JWT with JWKS"
pageType: use-case
---

# Add JWT Authentication to Your Lapis API

Secure your Lapis API using JWT authentication with JWKS.

## How Zuplo Handles It

Let Zuplo issue short-lived JWTs signed with a JWKS your Lapis backend can verify — no long-lived API keys touch your origin.

## Lapis Backend Code

```lua
local lapis = require("lapis")
local jwt = require("luajwtjitsi")
local httpc = require("http.client")
local cjson = require("cjson")

local app = lapis.Application()

-- JWKS and Issuer
local ISSUER = "https://my-api-a32f34.zuplo.api/__zuplo/issuer"
local JWKS_URI = ISSUER .. "/.well-known/jwks.json"

-- Cache for JWKS
local jwks_cache = nil
local jwks_cache_time = nil

-- Retrieve JWKS data
local function get_jwks()
  local response, err = httpc.get(JWKS_URI)
  if not response then
    return nil, "Failed to fetch JWKS: " .. tostring(err)
  end

  local jwks, decode_err = cjson.decode(response.body)
  if not jwks then
    return nil, "Failed to decode JWKS: " .. tostring(decode_err)
  end

  jwks_cache = jwks
  jwks_cache_time = os.time()
  return jwks
end

-- Get signing key from JWKS
local function get_key(kid)
  if not jwks_cache or os.time() - jwks_cache_time > 600 then
    -- Refresh JWKS cache every 10 minutes
    local _, err = get_jwks()
    if err then return nil, err end
  end

  for _, key in ipairs(jwks_cache.keys) do
    if key.kid == kid then
      return key
    end
  end
  return nil, "Key not found"
end

-- Middleware to validate JWT
local function validate_jwt(req, res)
  local auth_header = req.headers["Authorization"]
  if not auth_header then
    return res:status(401):json({ error = "No token provided" })
  end

  local token = string.match(auth_header, "Bearer (.+)")
  if not token then
    return res:status(401):json({ error = "Invalid authorization header format" })
  end

  local decoded, err = jwt.decode(token, nil, true)
  if not decoded then
    return res:status(401):json({ error = "Invalid token", details = err })
  end

  local key, key_err = get_key(decoded.header.kid)
  if key_err then
    return res:status(401):json({ error = "Invalid token", details = key_err })
  end

  local verified, verify_err = jwt.verify(token, jwt.pkey.from_string(key.x5c[1], "rs256"), { iss = ISSUER })
  if not verified then
    return res:status(401):json({ error = "Invalid token", details = verify_err })
  end

  req.user = decoded.payload
end

-- Example protected route
app:match("/protected", validate_jwt, function(self)
  return {
    json = {
      message = "Access granted",
      user = self.user
    }
  }
end)

return app
```

## Example Request

```bash
curl -X GET \
  'https://your-api.zuplo.dev/your-route' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

## Learn More

- [API Key Authentication on Zuplo](https://zuplo.com/docs/policies/api-key-auth-inbound)
- [JWT Authentication on Zuplo](https://zuplo.com/docs/policies/open-id-jwt-auth-inbound)
- [All use cases](https://zuplo.com/use-cases)
