├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── kong └── plugins │ └── whispir-token-auth │ ├── handler.lua │ └── schema.lua ├── main.go └── run.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kong:0.10.1 2 | MAINTAINER Habor Huang, haborhuang@whispir.cc 3 | 4 | ENV KONG_VERSION 0.10.1 5 | ENV KONG_DATABASE cassandra 6 | ENV KONG_LUA_PACKAGE_PATH /kong-plugins/?.lua;; 7 | ENV KONG_CUSTOM_PLUGINS whispir-token-auth 8 | 9 | ADD kong/ /kong-plugins/kong/ 10 | ADD run.sh / 11 | 12 | RUN chmod +x run.sh 13 | 14 | # Clear entrypoint of base image 15 | ENTRYPOINT [] 16 | CMD ["/run.sh"] 17 | 18 | EXPOSE 8000 8443 8001 7946 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This project implements a custom plugin of Kong to query an auth server to validate a JWT. 4 | 5 | ## Start Kong 6 | 7 | ### Build Kong image 8 | 9 | The logs cannot be displayed with [official Docker image](https://hub.docker.com/_/kong/) 10 | when executing 'docker logs' command. So I wrap a run.sh to 'tail' the error.log. Build the 11 | image as following: 12 | 13 | ``` 14 | % docker build -t my-kong . 15 | ``` 16 | 17 | ### Start containers 18 | 19 | Start a Cassandra container: 20 | 21 | ``` 22 | $ docker run -d --name kong-database -p 9042:9042 cassandra:3.10 23 | ``` 24 | 25 | Start a Kong container: 26 | 27 | ``` 28 | $ docker run -d --rm --name kong \ 29 | --link kong-database:kong-database \ 30 | -e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \ 31 | -e "KONG_LOG_LEVEL=debug" \ 32 | -p 8000:8000 \ 33 | -p 8443:8443 \ 34 | -p 8001:8001 \ 35 | -p 7946:7946 \ 36 | -p 7946:7946/udp \ 37 | my-kong 38 | ``` 39 | 40 | ## Run with plugin 41 | 42 | ### Start a resource server 43 | 44 | After the Kong container started, the custom plugin has been already loaded. Run a resource 45 | server that should be accessed with a token e.g. the main.go in this project: 46 | 47 | ``` 48 | $ go run main.go 49 | 2017/04/28 15:42:24 listening 19000 50 | ``` 51 | 52 | ### Register API and Enable plugin 53 | 54 | Register the resource API: 55 | 56 | ``` 57 | $ curl -i -XPOST localhost:8001/apis/ \ 58 | --data 'name=demo' --data 'upstream_url=http://:19000' --data 'uris=/demo' 59 | ``` 60 | 61 | Then the API could be queried with /demo path prefix: 62 | 63 | ``` 64 | $ curl localhost:8000/demo/foo 65 | hello world 66 | ``` 67 | 68 | Enable the custom authorization plugin for this API with the auth server url specified by 69 | 'auth_server_url' configuration: 70 | 71 | ``` 72 | $ curl -i -XPOST localhost:8001/apis/demo/plugins \ 73 | --data 'name=whispir-token-auth' \ 74 | --data 'config.auth_server_url=' 75 | ``` 76 | 77 | My another project [auth-server](https://github.com/FlyingShit-XinHuang/auth-server) could be used as an auth server. The 'auth_server_url' could 78 | be 'http://<your server ip>:18080/info'. 79 | 80 | The resource API is protected now: 81 | 82 | ``` 83 | $ curl localhost:8000/demo/foo 84 | {"message":"Missing token"} 85 | ``` 86 | 87 | Query the auth server to generate a token and query the resource API with the 'Authorization' 88 | header: 89 | 90 | ``` 91 | $ curl localhost:8000/demo/foo \ 92 | -H "Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTMzNzAwNjUsIm5iZiI6MTQ5MzM2NjQ2NSwiaWF0IjoxNDkzMzY2NDY1LCJjbGllbnRfaWQiOiI1Si1iMHRNclRGUzNBeExuckNmSDVBIn0.XUMUYMrrtKRKS11fVOvy4Vr4whS9ffRIxOQ_psSubwo" 93 | hello world 94 | ``` 95 | -------------------------------------------------------------------------------- /kong/plugins/whispir-token-auth/handler.lua: -------------------------------------------------------------------------------- 1 | local http = require "socket.http" 2 | local ltn12 = require "ltn12" 3 | local cjson = require "cjson.safe" 4 | 5 | local BasePlugin = require "kong.plugins.base_plugin" 6 | local responses = require "kong.tools.responses" 7 | local cache = require "kong.tools.database_cache" 8 | 9 | local TokenAuthHandler = BasePlugin:extend() 10 | 11 | TokenAuthHandler.PRIORITY = 1000 12 | 13 | local KEY_PREFIX = "whispir_auth_token" 14 | local EXPIRES_ERR = "token expires" 15 | 16 | --- Get JWT from headers 17 | -- @param request ngx request object 18 | -- @return token JWT 19 | -- @return err 20 | local function extract_token(request) 21 | local auth_header = request.get_headers()["authorization"] 22 | if auth_header then 23 | local iterator, ierr = ngx.re.gmatch(auth_header, "\\s*[Bb]earer\\s+(.+)") 24 | if not iterator then 25 | return nil, ierr 26 | end 27 | 28 | local m, err = iterator() 29 | if err then 30 | return nil, err 31 | end 32 | 33 | if m and #m > 0 then 34 | return m[1] 35 | end 36 | end 37 | end 38 | 39 | --- Query auth server to validate token 40 | -- @param token Token to be validated 41 | -- @param conf Plugin configuration 42 | -- @return info Information associated with token 43 | -- @return err 44 | local function query_and_validate_token(token, conf) 45 | ngx.log(ngx.DEBUG, "get token info from: ", conf.auth_server_url) 46 | local response_body = {} 47 | local res, code, response_headers = http.request{ 48 | url = conf.auth_server_url, 49 | method = "GET", 50 | headers = { 51 | ["Authorization"] = "bearer " .. token 52 | }, 53 | sink = ltn12.sink.table(response_body), 54 | } 55 | 56 | if type(response_body) ~= "table" then 57 | return nil, "Unexpected response" 58 | end 59 | local resp = table.concat(response_body) 60 | ngx.log(ngx.DEBUG, "response body: ", resp) 61 | 62 | if code ~= 200 then 63 | return nil, resp 64 | end 65 | 66 | local decoded, err = cjson.decode(resp) 67 | if err then 68 | ngx.log(ngx.ERR, "failed to decode response body: ", err) 69 | return nil, err 70 | end 71 | 72 | if not decoded.expires_in then 73 | return nil, decoded.error or resp 74 | end 75 | 76 | if decoded.expires_in <= 0 then 77 | return nil, EXPIRES_ERR 78 | end 79 | 80 | decoded.expires_at = decoded.expires_in + os.time() 81 | return decoded 82 | end 83 | 84 | function TokenAuthHandler:new() 85 | TokenAuthHandler.super.new(self, "whispir-token-auth") 86 | end 87 | 88 | function TokenAuthHandler:access(conf) 89 | TokenAuthHandler.super.access(self) 90 | 91 | local token, err = extract_token(ngx.req) 92 | if err then 93 | ngx.log(ngx.ERR, "failed to extract token: ", err) 94 | return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) 95 | end 96 | ngx.log(ngx.DEBUG, "extracted token: ", token) 97 | 98 | local ttype = type(token) 99 | if ttype ~= "string" then 100 | if ttype == "nil" then 101 | return responses.send(401, "Missing token") 102 | end 103 | if ttype == "table" then 104 | return responses.send(401, "Multiple tokens") 105 | end 106 | return responses.send(401, "Unrecognized token") 107 | end 108 | 109 | local info 110 | info, err = cache.get_or_set(KEY_PREFIX .. ":" .. token, 3600, query_and_validate_token, token, conf) 111 | 112 | if err then 113 | ngx.log(ngx.ERR, "failed to validate token: ", err) 114 | if EXPIRES_ERR == err then 115 | return responses.send(401, EXPIRES_ERR) 116 | end 117 | return responses.send_HTTP_INTERNAL_SERVER_ERROR(err) 118 | end 119 | 120 | if info.expires_at < os.time() then 121 | return responses.send(401, EXPIRES_ERR) 122 | end 123 | ngx.log(ngx.DEBUG, "token will expire in ", info.expires_at - os.time(), " seconds") 124 | 125 | end 126 | 127 | return TokenAuthHandler -------------------------------------------------------------------------------- /kong/plugins/whispir-token-auth/schema.lua: -------------------------------------------------------------------------------- 1 | return { 2 | no_consumer = true, 3 | fields = { 4 | auth_server_url = {type = "url", required = true}, 5 | } 6 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "log" 6 | ) 7 | 8 | func main() { 9 | http.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) { 10 | log.Println(r.Header) 11 | w.Write([]byte("hello world\n")) 12 | }) 13 | 14 | log.Println("listening 19000") 15 | http.ListenAndServe(":19000", nil) 16 | return 17 | } -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | kong start && tail -f /usr/local/kong/logs/error.log --------------------------------------------------------------------------------