Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
kdhttps committed Sep 15, 2019
1 parent d9ce2df commit 3d3085d
Show file tree
Hide file tree
Showing 13 changed files with 967 additions and 0 deletions.
42 changes: 42 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Compiled Lua sources
luac.out

# luarocks build files
*.src.rock
*.zip
*.tar.gz

# Object files
*.o
*.os
*.ko
*.obj
*.elf

# Precompiled Headers
*.gch
*.pch

# Libraries
*.lib
*.a
*.la
*.lo
*.def
*.exp

# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib

# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex

.idea
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[submodule "third-party/lua-resty-lrucache"]
path = third-party/lua-resty-lrucache
url = [email protected]:openresty/lua-resty-lrucache.git
[submodule "third-party/lua-resty-jwt"]
path = third-party/lua-resty-jwt
url = [email protected]:cdbattags/lua-resty-jwt.git
[submodule "third-party/lua-resty-hmac"]
path = third-party/lua-resty-hmac
url = [email protected]:jkeys089/lua-resty-hmac.git
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# jwt-auth

JWT Authenticaion plugin for Kong Lua plugin

# Setup

## Clone

Plugin use some third party libs so need to clone repository recursively.
With version 2.13 of Git and later
```
git clone --recurse-submodules [email protected]:3gthtech/jwt-auth.git
```

With version 1.9 of Git up until version 2.12
```
git clone --recursive [email protected]:3gthtech/jwt-auth.git
```

## Kong Configuration

### Take a file from your local clone repo `/third-party` folder.

```
$ sudo cp -R /third-party/lua-resty-lrucache/lib/resty/ /usr/local/share/lua/5.1/resty
$ sudo cp /third-party/lua-resty-lrucache/lib/resty/lrucache.lua /usr/local/share/lua/5.1/resty
$ sudo cp -a /third-party/lua-resty-jwt/lib/resty/. /usr/local/share/lua/5.1/resty
$ sudo cp -a /third-party/lua-resty-hmac/lib/resty/. /usr/local/share/lua/5.1/resty
```

### Install our `jwt-auth` with kong plugin

```
$ sudo cp -R /kong/plugins/jwt-auth /usr/local/share/lua/5.1/kong/plugins
```

Enable plugin by adding plugin name in `/etc/kong/kong.conf`

```
plugins = bundled, jwt-auth
```

`bundled` is for kong default plugins. After adding this name, restart kong `sudo kong restart`.

# Configuration

1. Add service

```
$ curl --fail -sS -X POST --url http://localhost:8001/services/ --header 'content-type: application/json' --data '{"name":"demo-service","url":"http://backend"}'
```

2. Add Route

```
$ curl --fail -i -sS -X POST --url http://localhost:8001/services/demo-service/routes --data 'hosts[]=backend.com'
```

Before add plugin check proxy with your upstream API

```
curl -i -sS -X GET --url http://localhost:8000/v1/user --header 'Host: backend.com'
```

3. Add Plugin

```
$ curl -v -i -sS -X POST --url http://localhost:8001/plugins/ --header 'content-type: application/json;charset=UTF-8' --data '{"config":{"check_jwt_expire":60,"login_endpoint":"/tokens","refresh_token_endpoint":"/tokens","upstream_url":"http://localhost:8081"},"name":"jwt-auth","service":{"id":"dc862e47-4475-4cb0-ac48-9f9d4f1eb869"}}'
```

Schema details

| Title | Description |
|-------|-------------|
| upstream_url | Your upstream service example: `http://localhost:8081` |
| login_endpoint | Authotization endpoint from your upstream service from where you get JWT. Example: `/tokens` |
| refresh_token_endpoint | Refresh token endpoint from your upstream service from where you get new JWT by refresh token. Example: `/tokens` |
| check_jwt_expire | Time in second to check old JWT expire time before it expired. This time in seconds. Default value is 60 seconds. |

# Usage

Request to login_endpoint through kong proxy to upstream service.

```
$ curl -sS -X POST --url http://localhost:8000/tokens --header 'Host: backend.com' --header 'content-type: application/json' --data '{"email":"[email protected]", "password": "admin"}'
```

Get a new token from this and request to access protected resources.

```
curl -i -sS -X GET --url http://localhost:8000/v1/user --header 'Host: backend.com' --header 'Authorization: Bearer <your_jwt_token>'
```

# Test

[Test cases](./t)
177 changes: 177 additions & 0 deletions kong/plugins/jwt-auth/access.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
local validators = require "resty.jwt-validators"
local jwt = require "resty.jwt"
local cjson = require "cjson.safe"
local pl_pretty = require "pl.pretty"
local http = require "resty.http"

local EXPIRE_DELTA = 20
local MAX_PENDING_SLEEPS = 40
local PENDING_EXPIRE = 0.2
local PENDING_TABLE = {}

local lrucache = require "resty.lrucache.pureffi"
-- it is shared by all the requests served by each nginx worker process:
local worker_cache, err = lrucache.new(10000) -- allow up to 10000 items in the cache
if not worker_cache then
return error("failed to create the cache: " .. (err or "unknown"))
end

local function unexpected_error(...)
local pending_key = kong.ctx.plugin.pending_key
if pending_key then
worker_cache:delete(pending_key)
end
kong.log.err(...)
kong.response.exit(502, { message = "An unexpected error ocurred" })
end

local function get_token(authorization)
if authorization and #authorization > 0 then
local from, to, err = ngx.re.find(authorization, "\\s*[Bb]earer\\s+(.+)", "jo", nil, 1)
if from then
return authorization:sub(from, to) -- Return token
end
if err then
return unexpected_error(err)
end
end

return nil
end

-- lru cache get operation with `pending` state support
local function worker_cache_get_pending(key)
for i = 1, MAX_PENDING_SLEEPS do
local token_data, stale_data = worker_cache:get(key)

if not token_data or stale_data then
return
end

if token_data == PENDING_TABLE then
kong.log.debug("sleep 5ms")
ngx.sleep(0.005) -- 5ms
else
return token_data
end
end
end

local function set_pending_state(key)
kong.ctx.plugin.pending_key = key
worker_cache:set(key, PENDING_TABLE, PENDING_EXPIRE)
end

local function clear_pending_state(key)
kong.ctx.plugin.pending_key = nil
worker_cache:delete(key)
end

local function set_cache(token, body)
set_pending_state(token)

local status, err, exp
local jwt_obj = jwt:load_jwt(token)

if not jwt_obj.valid then
clear_pending_state(token)
return kong.response.exit(401, { message = "Invalid token" })
end

local payload = jwt_obj.payload
exp = payload.exp
payload.refresh_token = body.refreshToken

kong.log.debug("save token in cache")
worker_cache:set(token, payload,
exp - ngx.now() - EXPIRE_DELTA)
end

local function request_to_upstream_set_cache(url, jsonBody)
local httpc = http.new()
local headers = {
["Content-Type"] = "application/json"
}

local res, err = httpc:request_uri(url, {
method = "POST",
body = jsonBody,
headers = headers,
ssl_verify = false
})

if err then
return error("failed to create the cache: " .. (err or "unknown"))
end

if res.status >= 300 then
ngx.status = res.status
ngx.header.content_type = "application/json; charset=utf-8"
ngx.say(res.body)
return ngx.exit(res.status)
end

local json, err = cjson.decode(res.body)
if err then
ngx.log(ngx.ERR, err)
return nil, "JSON decode error: " .. err
end

set_cache(json.token, json)
ngx.status = ngx.HTTP_OK
ngx.header.content_type = "application/json; charset=utf-8"
ngx.say(res.body)
return ngx.exit(ngx.HTTP_OK)
end

local function access_handler(self, conf)
local authorization = ngx.var.http_authorization
local token = get_token(authorization)

local method = ngx.req.get_method()
local path = ngx.var.uri
kong.log.debug("path : ", path)

if method == "POST" and path == conf.login_endpoint then
ngx.req.read_body()
local body = ngx.req.get_body_data()
if not body then
ngx.log(ngx.ERR, "body not found")
end

ngx.log(ngx.DEBUG, "Request Body :::: ", body)
cjson.decode_array_with_array_mt(true)
local req_body = cjson.decode(body)
cjson.decode_array_with_array_mt(false)

if req_body.email and req_body.password then
req_body = cjson.encode({ email = req_body.email, password = req_body.password })
else
req_body = cjson.encode({})
end

return request_to_upstream_set_cache(conf.upstream_url .. conf.login_endpoint, req_body)
end

if not token then
return kong.response.exit(401, { message = "Failed to get bearer token from Authorization header" })
end

local token_data = worker_cache_get_pending(token)
if token_data then
if token_data.exp - conf.check_jwt_expire <= ngx.now() then
kong.log.debug("Requesting for new token")
clear_pending_state(token)
local req_body = cjson.encode({ grantType = "refresh_token", refreshToken = token_data.refresh_token})
return request_to_upstream_set_cache(conf.upstream_url .. conf.refresh_token_endpoint, req_body)
end
kong.log.debug("Allow Success, Request already validate fron cache")
return
else
return kong.response.exit(401, { message = "Bad token" })
end
end

return function(self, conf)
access_handler(self, conf)
end
22 changes: 22 additions & 0 deletions kong/plugins/jwt-auth/handler.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
local BasePlugin = require "kong.plugins.base_plugin"
local access = require "kong.plugins.jwt-auth.access"

local handler = BasePlugin:extend()
handler.PRIORITY = 999

-- Your plugin handler's constructor. If you are extending the
-- Base Plugin handler, it's only role is to instanciate itself
-- with a name. The name is your plugin name as it will be printed in the logs.
function handler:new()
handler.super.new(self, "jwt-auth")
end

function handler:access(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
handler.super.access(self)

access(self, config)
end

return handler
9 changes: 9 additions & 0 deletions kong/plugins/jwt-auth/schema.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
return {
no_consumer = true,
fields = {
upstream_url = { required = true, type = "string" },
login_endpoint = { required = true, type = "string" },
refresh_token_endpoint = { required = true, type = "string" },
check_jwt_expire = { required = true, type = "number", default = 60 }
}
}
28 changes: 28 additions & 0 deletions t/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM ubuntu:xenial

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get -qqy update && \
apt-get -qqy install libluajit-5.1-dev && \
ln -s /usr/bin/luajit /usr/bin/lua && \
apt-get -qqy install curl git unzip net-tools uuid-runtime

RUN apt-get -qqy update && apt-get -qqy install luarocks && \
luarocks install busted && \
luarocks install LuaSocket && \
luarocks install json-lua

RUN apt-get -qqy install apt-transport-https ca-certificates software-properties-common && \
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add && \
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" && \
apt-get -qqy update && \
apt-get -qqy install docker-ce && \
apt-get -qqy install python python-dev python-distribute python-pip && \
pip install --upgrade pip==9.0.3 && \
pip install docker-compose

RUN cd /opt && git clone -q https://github.com/vishnubob/wait-for-it.git

RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

ENV DEBIAN_FRONTEND teletype
Loading

0 comments on commit 3d3085d

Please sign in to comment.