nginx
nginx/nginx-jwt.lua
First basic pass at JWT auth. Mostly just a fork of https://github.com/ficusio/openresty, with a few twists: * We've narrowed down some of the configuration options, and we're passing more headers (essentially exposing all the data in the JWT as headers). * We no longer automatically return a 401 unauthorized if the JWT verification fails; we just don't assign it the headers. The consuming service can decide whether or not they want to accept the request. * We automatically fail the verification of a JWT if the token has expired in the last minute (or shouldn't be used for the next minute). If the token has expired, we return a 401 that our clients can catch and use a refresh token automatically from. If the token can't be used for another minute, we quietly just refuse to add auth headers to the request.
| paddy@0 | 1 local jwt = require "resty.jwt" |
| paddy@0 | 2 local cjson = require "cjson" |
| paddy@0 | 3 local basexx = require "basexx" |
| paddy@0 | 4 local secret = os.getenv("JWT_SECRET") |
| paddy@0 | 5 |
| paddy@0 | 6 assert(secret ~= nil, "Environment variable JWT_SECRET not set") |
| paddy@0 | 7 |
| paddy@0 | 8 if os.getenv("JWT_SECRET_IS_BASE64_ENCODED") == 'true' then |
| paddy@0 | 9 -- convert from URL-safe Base64 to Base64 |
| paddy@0 | 10 local r = #secret % 4 |
| paddy@0 | 11 if r == 2 then |
| paddy@0 | 12 secret = secret .. "==" |
| paddy@0 | 13 elseif r == 3 then |
| paddy@0 | 14 secret = secret .. "=" |
| paddy@0 | 15 end |
| paddy@0 | 16 secret = string.gsub(secret, "-", "+") |
| paddy@0 | 17 secret = string.gsub(secret, "_", "/") |
| paddy@0 | 18 |
| paddy@0 | 19 -- convert from Base64 to UTF-8 string |
| paddy@0 | 20 secret = basexx.from_base64(secret) |
| paddy@0 | 21 end |
| paddy@0 | 22 |
| paddy@0 | 23 local M = {} |
| paddy@0 | 24 |
| paddy@0 | 25 function M.auth(claim_specs) |
| paddy@0 | 26 -- strip our headers to avoid spoofing |
| paddy@0 | 27 ngx.req.clear_header("User-Id") |
| paddy@0 | 28 ngx.req.clear_header("Scopes") |
| paddy@0 | 29 ngx.req.clear_header("JWT-Scope") |
| paddy@0 | 30 ngx.req.clear_header("JWT-Issuer") |
| paddy@0 | 31 ngx.req.clear_header("JWT-Subject") |
| paddy@0 | 32 ngx.req.clear_header("JWT-Expiration-Time") |
| paddy@0 | 33 ngx.req.clear_header("JWT-Not-Before") |
| paddy@0 | 34 ngx.req.clear_header("JWT-Issued-At") |
| paddy@0 | 35 |
| paddy@0 | 36 -- require Authorization request header |
| paddy@0 | 37 local auth_header = ngx.var.http_Authorization |
| paddy@0 | 38 |
| paddy@0 | 39 if auth_header == nil then |
| paddy@0 | 40 ngx.log(ngx.INFO, "No Authorization header") |
| paddy@0 | 41 -- ngx.exit(ngx.HTTP_UNAUTHORIZED) |
| paddy@0 | 42 return |
| paddy@0 | 43 else |
| paddy@0 | 44 ngx.log(ngx.INFO, "Authorization: " .. auth_header) |
| paddy@0 | 45 |
| paddy@0 | 46 -- require Bearer token |
| paddy@0 | 47 local _, _, token = string.find(auth_header, "Bearer%s+(.+)") |
| paddy@0 | 48 |
| paddy@0 | 49 if token == nil then |
| paddy@0 | 50 ngx.log(ngx.INFO, "Missing token") |
| paddy@0 | 51 -- ngx.exit(ngx.HTTP_UNAUTHORIZED) |
| paddy@0 | 52 return |
| paddy@0 | 53 else |
| paddy@0 | 54 ngx.log(ngx.INFO, "Token: " .. token) |
| paddy@0 | 55 |
| paddy@0 | 56 -- require valid JWT |
| paddy@0 | 57 local jwt_obj = jwt:verify(secret, token, 60) |
| paddy@0 | 58 if jwt_obj.verified == false then |
| paddy@0 | 59 if string.find(jwt_obj.reason, "expired at") ~= nil then |
| paddy@0 | 60 ngx.status = ngx.HTTP_UNAUTHORIZED |
| paddy@0 | 61 ngx.say('{"error": "access_denied", "header": "authorization"}') |
| paddy@0 | 62 return ngx.exit(ngx.HTTP_UNAUTHORIZED) |
| paddy@0 | 63 else |
| paddy@0 | 64 ngx.log(ngx.WARN, "Invalid token: ".. jwt_obj.reason) |
| paddy@0 | 65 -- ngx.exit(ngx.HTTP_UNAUTHORIZED) |
| paddy@0 | 66 end |
| paddy@0 | 67 return |
| paddy@0 | 68 else |
| paddy@0 | 69 ngx.log(ngx.INFO, "JWT: " .. cjson.encode(jwt_obj)) |
| paddy@0 | 70 |
| paddy@0 | 71 -- write the headers |
| paddy@0 | 72 ngx.req.set_header("User-Id", jwt_obj.payload.sub) |
| paddy@0 | 73 ngx.req.set_header("Scopes", jwt_obj.payload.scope) |
| paddy@0 | 74 ngx.req.set_header("JWT-Scope", jwt_obj.payload.scope) |
| paddy@0 | 75 ngx.req.set_header("JWT-Issuer", jwt_obj.payload.iss) |
| paddy@0 | 76 ngx.req.set_header("JWT-Subject", jwt_obj.payload.sub) |
| paddy@0 | 77 ngx.req.set_header("JWT-Expiration-Time", jwt_obj.payload.exp) |
| paddy@0 | 78 ngx.req.set_header("JWT-Not-Before", jwt_obj.payload.nbf) |
| paddy@0 | 79 ngx.req.set_header("JWT-Issued-At", jwt_obj.payload.iat) |
| paddy@0 | 80 return |
| paddy@0 | 81 end |
| paddy@0 | 82 end |
| paddy@0 | 83 end |
| paddy@0 | 84 end |
| paddy@0 | 85 |
| paddy@0 | 86 return M |