├── .gitignore ├── example ├── haproxy │ ├── Dockerfile-ubuntu │ ├── pem │ │ ├── pubkey.pem │ │ └── test.com.pem │ ├── lib │ │ ├── base64.lua │ │ └── jwtverify.lua │ ├── haproxy.cfg │ └── install.sh └── web │ ├── Dockerfile │ ├── package.json │ └── index.js ├── docker-compose.ubuntu.example.yml ├── Vagrantfile ├── lib ├── base64.lua └── jwtverify.lua ├── haproxy-example.cfg ├── README.md ├── install.sh └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant -------------------------------------------------------------------------------- /example/haproxy/Dockerfile-ubuntu: -------------------------------------------------------------------------------- 1 | FROM haproxytech/haproxy-ubuntu:2.6 2 | WORKDIR /usr/src 3 | COPY . . 4 | RUN ./install.sh luaoauth -------------------------------------------------------------------------------- /example/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | WORKDIR /home/node/app 3 | EXPOSE 80 4 | COPY . . 5 | RUN npm install 6 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /example/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.16.4" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/haproxy/pem/pubkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvIL8bebCh+pi68Rt0CCu 3 | 104VqR10kuD0E1yzwaywvaEiyhfUeDDKAyKC8yS5ilu9xyWK/pg/84RiWq7WoqhU 4 | m8L06jtknn/ZCOuyUdkn1QcdOG10lbbrUF1AOduTIvFYyT4zHrIcKt6MyeQUO0kH 5 | cXQU7lvM2C62BboAasZFupDts1m1kPZMWaiSjLrE1eruhl8NrfipiPWMZJSJoYCQ 6 | cmtN3REXk9z8X7ZPgcMJ9hNN+Kv0fTYLZI4wS4TpHscVfbK18cL4uLrTCcip7jNe 7 | y2KZ/YdbeHgmmcQAdiB4veH4I2dAyqIdsy8Jk+KTs3Ae8qp+S3XtC8z/uXMbN7lR 8 | AwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /docker-compose.ubuntu.example.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3" 3 | services: 4 | 5 | server1: 6 | image: example/web:latest 7 | build: 8 | context: example/web 9 | environment: 10 | - "name=server1" 11 | 12 | haproxy: 13 | image: example/haproxy-ubuntu 14 | build: 15 | context: example/haproxy 16 | dockerfile: Dockerfile-ubuntu 17 | volumes: 18 | - ./example/haproxy/haproxy.cfg:/etc/haproxy/haproxy.cfg 19 | - ./example/haproxy/pem/pubkey.pem:/etc/haproxy/pem/pubkey.pem 20 | - ./example/haproxy/pem/test.com.pem:/etc/haproxy/pem/test.com.pem 21 | ports: 22 | - "80:80" 23 | - "443:443" 24 | - "8080:8080" 25 | - "9000:9000" 26 | depends_on: 27 | - server1 -------------------------------------------------------------------------------- /example/web/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const port = 80 4 | 5 | var hamsters = [ 6 | "robo-hamster", 7 | "space-hamster", 8 | "commando-hamster", 9 | "pirate-hmaster" 10 | ] 11 | 12 | app.get('/api/myapp', function (req, res) { 13 | console.log('GET /api/myapp') 14 | res.send(hamsters) 15 | }) 16 | 17 | app.post('/api/myapp/:name', function (req, res) { 18 | console.log('POST /api/myapp') 19 | hamsters.push(req.params.name) 20 | res.send(hamsters) 21 | }) 22 | 23 | app.delete('/api/myapp/:name', function(req, res) { 24 | console.log('DELETE /api/myapp') 25 | var index = hamsters.indexOf(req.params.name) 26 | 27 | if (index > -1) { 28 | hamsters.splice(index, 1) 29 | } 30 | 31 | res.send(hamsters) 32 | }) 33 | 34 | app.listen(port, () => console.log(`Listening on port ${port}`)) -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | 3 | # ------------------------------------------------ 4 | # NOTE: Comment out OSes you don't want to create 5 | # ------------------------------------------------ 6 | 7 | config.vm.define "ubuntu20" do |server| 8 | server.vm.box = "ubuntu/focal64" 9 | server.vm.hostname = "ubuntu20" 10 | server.vm.network "private_network", ip: "192.168.56.30" 11 | end 12 | 13 | # config.vm.define "centos8" do |server| 14 | # server.vm.box = "centos/8" 15 | # server.vm.hostname = "centos8" 16 | # server.vm.network "private_network", ip: "192.168.56.31" 17 | # end 18 | 19 | # config.vm.define "centos7" do |server| 20 | # server.vm.box = "centos/7" 21 | # server.vm.hostname = "centos7" 22 | # server.vm.network "private_network", ip: "192.168.56.32" 23 | # end 24 | end 25 | -------------------------------------------------------------------------------- /lib/base64.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- base64.lua 3 | -- 4 | -- URL safe base64 encoder/decoder 5 | -- 6 | 7 | -- https://github.com/diegonehab/luasocket 8 | local mime = require 'mime' 9 | local _M = {} 10 | 11 | --- base64 encoder 12 | -- 13 | -- @param s String to encode (can be binary data) 14 | -- @return Encoded string 15 | function _M.encode(s) 16 | local u 17 | local padding_len = 2 - ((#s-1) % 3) 18 | 19 | if padding_len > 0 then 20 | u = mime.b64(s):sub(1, - padding_len - 1) 21 | else 22 | u = mime.b64(s) 23 | end 24 | 25 | if u then 26 | return u:gsub('[+]', '-'):gsub('[/]', '_') 27 | else 28 | return nil 29 | end 30 | end 31 | 32 | --- base64 decoder 33 | -- 34 | -- @param s String to decode 35 | -- @return Decoded string (can be binary data) 36 | function _M.decode(s) 37 | local e = s:gsub('[-]', '+'):gsub('[_]', '/') 38 | local u, _ = mime.unb64(e .. string.rep('=', 3 - ((#s - 1) % 4))) 39 | 40 | return u 41 | end 42 | 43 | return _M 44 | -------------------------------------------------------------------------------- /example/haproxy/lib/base64.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- base64.lua 3 | -- 4 | -- URL safe base64 encoder/decoder 5 | -- 6 | 7 | -- https://github.com/diegonehab/luasocket 8 | local mime = require 'mime' 9 | local _M = {} 10 | 11 | --- base64 encoder 12 | -- 13 | -- @param s String to encode (can be binary data) 14 | -- @return Encoded string 15 | function _M.encode(s) 16 | local u 17 | local padding_len = 2 - ((#s-1) % 3) 18 | 19 | if padding_len > 0 then 20 | u = mime.b64(s):sub(1, - padding_len - 1) 21 | else 22 | u = mime.b64(s) 23 | end 24 | 25 | if u then 26 | return u:gsub('[+]', '-'):gsub('[/]', '_') 27 | else 28 | return nil 29 | end 30 | end 31 | 32 | --- base64 decoder 33 | -- 34 | -- @param s String to decode 35 | -- @return Decoded string (can be binary data) 36 | function _M.decode(s) 37 | local e = s:gsub('[-]', '+'):gsub('[_]', '/') 38 | local u, _ = mime.unb64(e .. string.rep('=', 3 - ((#s - 1) % 4))) 39 | 40 | return u 41 | end 42 | 43 | return _M 44 | -------------------------------------------------------------------------------- /example/haproxy/haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 3 | ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS 4 | ssl-default-bind-options ssl-min-ver TLSv1.1 5 | chroot /var/lib/haproxy 6 | user haproxy 7 | group haproxy 8 | lua-load /usr/local/share/lua/5.4/jwtverify.lua 9 | 10 | # Replace the Auth0 URL with your own: 11 | setenv OAUTH_ISSUER https://youraccount.auth0.com/ 12 | setenv OAUTH_AUDIENCE https://api.mywebsite.com 13 | setenv OAUTH_PUBKEY_PATH /etc/haproxy/pem/pubkey.pem 14 | 15 | defaults 16 | log global 17 | mode http 18 | option httplog 19 | timeout connect 10s 20 | timeout client 30s 21 | timeout server 30s 22 | option http-buffer-request 23 | 24 | frontend api_gateway 25 | bind :443 ssl crt /etc/haproxy/pem/test.com.pem alpn h2,http1.1 26 | default_backend apiservers 27 | http-request deny unless { req.hdr(authorization) -m found } 28 | http-request lua.jwtverify 29 | http-request deny unless { var(txn.authorized) -m bool } 30 | http-request deny if { path_beg /api/myapp } { method GET } ! { var(txn.oauth.scope) -m sub read:myapp } 31 | http-request deny if { path_beg /api/myapp } { method POST PUT DELETE } ! { var(txn.oauth.scope) -m sub write:myapp } 32 | 33 | backend apiservers 34 | balance roundrobin 35 | server server1 server1:80 36 | -------------------------------------------------------------------------------- /haproxy-example.cfg: -------------------------------------------------------------------------------- 1 | global 2 | daemon 3 | lua-load /usr/local/share/lua/5.4/jwtverify.lua 4 | 5 | # Set env variables used by Lua file... 6 | 7 | # If using RS256 signature: Path to public key certificate tokens are signed with (get from your token issuer, like auth0.com): 8 | setenv OAUTH_PUBKEY_PATH /etc/haproxy/pem/pubkey.pem 9 | 10 | # If using HS256 signature: HMAC secret key 11 | # setenv OAUTH_HMAC_SECRET E57lT71DHOHK1eeOlMjatvUtUg6ejFZ8 12 | 13 | # OPTIONAL: OAuth issuer 14 | setenv OAUTH_ISSUER https://youraccount.auth0.com/ 15 | 16 | # OPTIONAL: OAuth audience - should match what you set on the Auth0 website for your API 17 | setenv OAUTH_AUDIENCE https://api.mywebsite.com 18 | 19 | defaults 20 | timeout connect 5s 21 | timeout client 5s 22 | timeout server 5s 23 | mode http 24 | 25 | frontend api_gateway 26 | # Good practice to secure communication when passing tokens 27 | # bind :443 ssl crt /etc/haproxy/pem/test.com.pem alpn h2,http1.1 28 | bind :80 29 | 30 | # Deny if no Authorization header sent 31 | http-request deny unless { req.hdr(authorization) -m found } 32 | 33 | # Invoke the jwtverify Lua file 34 | http-request lua.jwtverify 35 | 36 | # Deny unless jwtverify set 'authorized' to true 37 | http-request deny unless { var(txn.authorized) -m bool } 38 | 39 | # OPTIONAL: Deny if GET request, but JWT does not contain 'read:myservice' scope 40 | http-request deny if { path_beg /api/myservice } { method GET } ! { var(txn.oauth.scope) -m sub read:myservice } 41 | 42 | # OPTIONAL: Deny if POST, PUT, or DELETE request, but JWT does not contain 'write:myservice' scope 43 | http-request deny if { path_beg /api/myservice } { method POST PUT DELETE } ! { var(txn.oauth.scope) -m sub write:myservice } 44 | 45 | default_backend apiservers 46 | 47 | backend apiservers 48 | balance roundrobin 49 | server server1 127.0.0.1:8080 50 | 51 | -------------------------------------------------------------------------------- /example/haproxy/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SOURCE_DIR=/usr/src 3 | LUA_VERSION=5.4.4 4 | CWD=$(pwd) 5 | 6 | install_luaoauth_var=false 7 | rhel_based=false 8 | debian_based=false 9 | lua_installed=false 10 | lua_dep_dir=/usr/local/share/lua/5.4/ 11 | 12 | if [ -f /etc/redhat-release ]; then 13 | rhel_based=true 14 | elif [ -f /etc/debian_version ]; then 15 | debian_based=true 16 | fi 17 | 18 | cd $SOURCE_DIR 19 | 20 | display_working() { 21 | pid=$1 22 | spin='-\|/' 23 | i=0 24 | while kill -0 $pid 2>/dev/null 25 | do 26 | i=$(( (i+1) %4 )) 27 | printf "\r${spin:$i:1}" 28 | sleep .1 29 | done 30 | } 31 | 32 | download_rhel_lua() { 33 | printf "\r[+] Downloading Lua\n" 34 | curl -sLO https://www.lua.org/ftp/lua-$LUA_VERSION.tar.gz 35 | tar xf lua-$LUA_VERSION.tar.gz && rm lua-$LUA_VERSION.tar.gz 36 | } 37 | 38 | install_yum_deps() { 39 | printf "\r[+] Installing yum dependencies\n" 40 | yum -y install gcc openssl-devel readline-devel systemd-devel unzip >/dev/null 2>&1 41 | } 42 | 43 | build_lua() { 44 | printf "\r[+] Building Lua\n" 45 | cd $SOURCE_DIR/lua-$LUA_VERSION 46 | make linux test >/dev/null 47 | } 48 | 49 | install_rhel_lua() { 50 | printf "\r[+] Installing Lua\n" 51 | cd $SOURCE_DIR/lua-$LUA_VERSION 52 | make install >/dev/null 53 | } 54 | 55 | install_deb_lua() { 56 | printf "\r[+] Installing Lua\n" 57 | apt-get update >/dev/null 2>&1 58 | apt-get install -y software-properties-common unzip build-essential libssl-dev lua5.4 liblua5.4-dev >/dev/null 2>&1 59 | } 60 | 61 | install_luaoauth_deps_debian() { 62 | printf "\r[+] Installing haproxy-lua-oauth dependencies\n" 63 | 64 | if [ ! -e $lua_dep_dir ]; then 65 | mkdir -p $lua_dep_dir; 66 | fi; 67 | 68 | apt-get update >/dev/null 2>&1 69 | apt-get install -y build-essential liblua5.4-dev libssl-dev unzip >/dev/null 2>&1 70 | 71 | cd $SOURCE_DIR 72 | 73 | curl -sLO https://github.com/rxi/json.lua/archive/refs/heads/master.zip 74 | unzip -qo master.zip && rm master.zip 75 | cp json.lua-master/json.lua $lua_dep_dir 76 | 77 | curl -sLO https://github.com/lunarmodules/luasocket/archive/refs/heads/master.zip 78 | unzip -qo master.zip && rm master.zip 79 | cd luasocket-master/ 80 | make clean all install-both LUAINC=/usr/include/lua5.4/ >/dev/null 81 | cd .. 82 | 83 | curl -sLO https://github.com/wahern/luaossl/archive/refs/heads/master.zip 84 | unzip -qo master.zip && rm master.zip 85 | cd luaossl-master/ 86 | make install >/dev/null 87 | cd .. 88 | } 89 | 90 | install_luaoauth_deps_rhel() { 91 | printf "\r[+] Installing haproxy-lua-oauth dependencies\n" 92 | 93 | if [ ! -e $lua_dep_dir ]; then 94 | mkdir -p $lua_dep_dir; 95 | fi; 96 | 97 | dnf config-manager --set-enabled powertools 98 | dnf update >/dev/null 2>&1 99 | dnf install -y gcc openssl-devel lua-devel make readline-devel systemd-devel unzip >/dev/null 2>&1 100 | 101 | cd $SOURCE_DIR 102 | 103 | curl -sLO https://github.com/rxi/json.lua/archive/refs/heads/master.zip 104 | unzip -qo master.zip && rm master.zip 105 | mv json.lua-master/json.lua $lua_dep_dir 106 | 107 | curl -sLO https://github.com/lunarmodules/luasocket/archive/refs/heads/master.zip 108 | unzip -qo master.zip && rm master.zip 109 | cd luasocket-master/ 110 | make clean all install-both LUAINC=/usr/include/ >/dev/null 111 | cd .. 112 | 113 | curl -sLO https://github.com/wahern/luaossl/archive/rel-20181207.zip 114 | unzip -qo rel-20181207.zip && rm rel-20181207.zip 115 | cd luaossl-rel-20181207/ 116 | make install >/dev/null 117 | cd .. 118 | } 119 | 120 | install_luaoauth() { 121 | printf "\r[+] Installing haproxy-lua-oauth\n" 122 | if [ ! -e $lua_dep_dir ]; then 123 | mkdir -p $lua_dep_dir; 124 | fi; 125 | 126 | cp $CWD/lib/*.lua $lua_dep_dir 127 | } 128 | 129 | case $1 in 130 | luaoauth) 131 | install_luaoauth_var=true 132 | ;; 133 | *) 134 | echo "Usage: install.sh luaoauth" 135 | esac 136 | 137 | if $install_luaoauth_var; then 138 | if $rhel_based; then 139 | download_and_install_luaoauth=(install_luaoauth_deps_rhel install_luaoauth) 140 | elif $debian_based; then 141 | download_and_install_luaoauth=(install_luaoauth_deps_debian install_luaoauth) 142 | fi 143 | 144 | for func in ${download_and_install_luaoauth[*]}; do 145 | $func & 146 | display_working $! 147 | done 148 | fi 149 | -------------------------------------------------------------------------------- /example/haproxy/pem/test.com.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | XfTnEJm6zTwcOYWLbrko+Wzw0A== 32 | -----END CERTIFICATE----- 33 | -----BEGIN PRIVATE KEY----- 34 | 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 83 | FxYTinsG64nGWqDnjhc4YSBODpfSsw== 84 | -----END PRIVATE KEY----- 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth 2 library for HAProxy 2 | 3 | This is a Lua library for HAProxy that will verify OAuth 2 JWT tokens. 4 | 5 | ## Install 6 | 7 | The `jwtverify.lua` file has these dependencies: 8 | 9 | * base64 (included in this repository) 10 | * [lua-json](https://github.com/rxi/json.lua) 11 | * [luaossl](https://github.com/wahern/luaossl) 12 | * [luasocket](https://github.com/diegonehab/luasocket) 13 | 14 | Install like so: 15 | 16 | ``` 17 | git clone https://github.com/haproxytech/haproxy-lua-oauth.git 18 | cd haproxy-lua-oauth 19 | chmod +x ./install.sh 20 | sudo ./install.sh luaoauth 21 | ``` 22 | 23 | This installs jwtverify.lua and its dependencies to **/usr/local/share/lua/5.4/jwtverify.lua**. 24 | 25 | ## Usage 26 | 27 | 1. Sign up for an account with an OAuth token provider, such as https://auth0.com 28 | 1. Create a new API on the Auth0 website 29 | 1. Create a new "Machine to Machine Application" on the Auth0 website, optionally granting it "scopes" 30 | 1. Download the public key certificate for your application on the Auth0 website via *Applications > My App > Settings > Show Advanced Settings > Certificates > Download Certificate*. Auth0 signs tokens using this key. Convert it using `openssl x509 -pubkey -noout -in ./mycert.pem > pubkey.pem`. 31 | 1. Update the HAProxy configuration file by: 32 | * Copy *haproxy-example.cfg* to **/etc/haproxy/haproxy.cfg** and restart HAProxy via `sudo systemctl restart haproxy` 33 | * *or* run it from this directory via `sudo haproxy -f ./haproxy-example.cfg` 34 | 1. Get a JSON web token (JWT) from your authentication server by following the *Quick Start* on the Auth0 website, under the Applications tab, for your Machine to Machine application. 35 | 1. Make requests to your API and attach the JWT in the Authorization header. You should get a successful response. 36 | 37 | ## Supported Signing Algorithms 38 | 39 | * RS256 40 | * HS256 41 | * HS512 42 | 43 | ## Support for multiple audiences 44 | 45 | This library support specifying multiple audience values in the JWT token. They should be specified as a JSON array of strings. 46 | You can also accept multiple audience values in the `OAUTH_AUDIENCE` environment variable in the **haproxy.cfg** file. Separate each value 47 | with a space and surround it with double quotes: 48 | 49 | ``` 50 | setenv OAUTH_AUDIENCE "https://api.mywebsite.com https://api2.mywebsite.com" 51 | ``` 52 | 53 | ## Output variables 54 | 55 | After calling `http-request lua.jwtverify`, you get access to variables for each of the claims in the token. 56 | 57 | *Examples* 58 | 59 | * `var(txn.oauth.aud)` 60 | * `var(txn.oauth.clientId)` 61 | * `var(txn.oauth.iss)` 62 | * `var(txn.oauth.scope)` 63 | 64 | For example, you could track rate limiting based on the clientId or set different rate limit thresholds based on the scope. 65 | 66 | ## Example 67 | 68 | Try it out using the Docker Compose. 69 | 70 | 1. Sign up for a free account at https://auth0.com/ and create a new API. 71 | 1. Give the API any name, such as "My OAuth Test" and set the identifier to "https://api.mywebsite.com". 72 | 1. Once created, go to the API's "Permissions" tab and add permissions (aka scopes) that grant users different levels of access. The colon syntax is just a personal style, and colons do not mean anything special. 73 | 74 | | permission | description | 75 | |-------------|-----------------------| 76 | | read:myapp | Read access to my app | 77 | | write:myapp | Write access to myapp | 78 | 79 | 1. Now that you have an API defined in Auth0, add an application that is allowed to authenticate to it. Go to the "Applications" tab and add a new "Machine to Machine Application" and select the API you just created. Give it the "read:myapp" and "write:myapp"permissions (or only one or the other). 80 | 1. On the Settings page for the new application, go to **Advanced Settings > Certificates** and download the certificate in PEM format. HAProxy will validate the access tokens against this certificate, which was signed by the OAuth provider, Auth0. 81 | 82 | 1. Convert it first using `openssl x509 -pubkey -noout -in ./mycert.pem > pubkey.pem` and save **pubkey.pem** to **/example/haproxy/pem/pubkey.pem**. 83 | 1. Edit **example/haproxy/haproxy.cfg**: 84 | 85 | * replace the `OAUTH_ISSUER` variable in the global section with the Auth0 domain URL with your own, such as https://myaccount.auth0.com/. 86 | * replace the `OAUTH_AUDIENCE` variable with your API name in Auth0, such as "https://api.mywebsite.com". 87 | * replace the `OAUTH_PUBKEY_PATH` variable with the path to your PEM certificate. (also update the docker-compose file) 88 | 89 | 1. Create the environment with Docker Compose: 90 | ``` 91 | $ docker compose -f docker-compose.ubuntu.example.yml build 92 | $ docker compose -f docker-compose.ubuntu.example.yml up 93 | ``` 94 | 1. Get a JSON web token (JWT) from your authentication server by going to your application on the Auth0 website and following the *Quick Start*. 95 | 1. Make requests to https://localhost/api/myapp and attach the JWT in the Authorization header. You should get a successful response. 96 | 97 | ``` 98 | $ curl --request GET \ 99 | -k \ 100 | --url https://localhost/api/myapp \ 101 | --header 'authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlJEVkNSVFZHTmpZNU5rVTJSVUV3TnpoRk56UkJRalU0TjBFeU5EWTNSRU01TWtaRFJqTkNNUSJ9.eyJpc3MiOiJodHRwczovL25pY2tyYW00NC5hdXRoMC5jb20vIiwic3ViIjoicm9DTHRDTlZycW0zNmVYTzJxcE84cjEzeFBmQno1NklAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vYXBpLm15d2Vic2l0ZS5jb20iLCJpYXQiOjE2NDgzMTQ2NjAsImV4cCI6MTY0ODQwMTA2MCwiYXpwIjoicm9DTHRDTlZycW0zNmVYTzJxcE84cjEzeFBmQno1NkkiLCJzY29wZSI6InJlYWQ6bXlhcHAgd3JpdGU6bXlhcHAiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.tEhJ0hKlqy9KRrS00we1Z6Y0CwGg5tAOmZ3qQYLYEwl1uymZ8OfJD9iGgPe5QhLJCTD-iwC18hWSwBMzNRLrjcjp1__hHOOyJRRoqekezS7NoHCMOKGLRis5EcfXMyb58yVxwrKIovHSRaEf0emg5NovQ2bdI3UpMThXnzlLhIH_SX5yRUtTxQ_qvO7xS9lZBNVYG9lYlNtU_Ih6dKCKNRUrMm8xsj2jLyR5_v3LcxgwzhK2VF01DZ9wyEgfHgs3H2AP6yJEZkmd9B1chO5Xf3f4klujsxvAb6RqTCwpGWmjRPY6SENkY2QX-PHOYVAc4zPvuauwx9Ojd4khA_KKfA' 102 | ``` 103 | 104 | A successful response: 105 | 106 | ``` 107 | ["robo-hamster","space-hamster","commando-hamster","pirate-hmaster"] 108 | ``` -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SOURCE_DIR=/usr/src 3 | LUA_VERSION=5.4.4 4 | CWD=$(pwd) 5 | 6 | install_luaoauth_var="false" 7 | rhel_based="false" 8 | debian_based="false" 9 | alpine_based="false" 10 | lua_installed="false" 11 | lua_dep_dir=/usr/local/share/lua/5.4/ 12 | 13 | if [ -f /etc/redhat-release ]; then 14 | echo "Red Hat based system detected" 15 | rhel_based="true" 16 | elif [ -f /etc/debian_version ]; then 17 | echo "Debian based system detected" 18 | debian_based="true" 19 | elif [ -f /etc/alpine-release ]; then 20 | echo "Alpine based system detected" 21 | alpine_based="true" 22 | fi 23 | 24 | if [ ! -e $SOURCE_DIR ]; then 25 | mkdir -p $SOURCE_DIR; 26 | fi; 27 | 28 | cd $SOURCE_DIR 29 | 30 | display_working() { 31 | pid=$1 32 | spin='-\|/' 33 | i=0 34 | while kill -0 "$pid" 2>/dev/null 35 | do 36 | i=$(( (i+1) %4 )) 37 | printf "\r${spin:$i:1}" 38 | sleep .1 39 | done 40 | } 41 | 42 | # ---------------------------- 43 | # Functions not currently used 44 | # ---------------------------- 45 | 46 | download_rhel_lua() { 47 | printf "\r[+] Downloading Lua\n" 48 | cd $SOURCE_DIR 49 | curl -sLO https://www.lua.org/ftp/lua-$LUA_VERSION.tar.gz 50 | tar xf lua-$LUA_VERSION.tar.gz && rm lua-$LUA_VERSION.tar.gz 51 | } 52 | 53 | install_yum_deps() { 54 | printf "\r[+] Installing yum dependencies\n" 55 | yum -y install dnf gcc openssl-devel readline-devel systemd-devel unzip >/dev/null 2>&1 56 | } 57 | 58 | build_lua() { 59 | printf "\r[+] Building Lua\n" 60 | cd $SOURCE_DIR/lua-$LUA_VERSION 61 | make linux test >/dev/null 62 | } 63 | 64 | install_rhel_lua() { 65 | printf "\r[+] Installing Lua\n" 66 | cd $SOURCE_DIR/lua-$LUA_VERSION 67 | make install >/dev/null 68 | } 69 | 70 | install_deb_lua() { 71 | printf "\r[+] Installing Lua\n" 72 | apt-get update >/dev/null 2>&1 73 | apt-get install -y software-properties-common unzip build-essential libssl-dev lua5.4 liblua5.4-dev >/dev/null 2>&1 74 | } 75 | 76 | install_alpine_lua() { 77 | printf "\r[+] Installing Lua\n" 78 | apk add --no-cache lua5.4 lua5.4-dev openssl-dev gcc libc-dev git make automake libtool curl unzip >/dev/null 2>&1 79 | #?? build-base libc-dev liblua5.4-dev unzip 80 | } 81 | 82 | # ---------------------------- 83 | # ---------------------------- 84 | 85 | install_luaoauth_deps_alpine() { 86 | printf "\r[+] Installing haproxy-lua-oauth dependencies\n" 87 | 88 | if [ ! -e $lua_dep_dir ]; then 89 | mkdir -p $lua_dep_dir; 90 | fi; 91 | 92 | apt-get update >/dev/null 2>&1 93 | apk add --no-cache lua5.4 lua5.4-dev openssl-dev gcc libc-dev git make automake libtool curl unzip >/dev/null 2>&1 94 | 95 | cd $SOURCE_DIR 96 | 97 | curl -sLO https://github.com/rxi/json.lua/archive/refs/heads/master.zip 98 | unzip -qo master.zip && rm master.zip 99 | mv json.lua-master/json.lua $lua_dep_dir 100 | 101 | curl -sLO https://github.com/lunarmodules/luasocket/archive/refs/heads/master.zip 102 | unzip -qo master.zip && rm master.zip 103 | cd luasocket-master/ 104 | make clean all install-both LUAINC=/usr/include/lua5.4/ >/dev/null 105 | cd .. 106 | 107 | curl -sLO https://github.com/wahern/luaossl/archive/refs/heads/master.zip 108 | unzip -qo master.zip && rm master.zip 109 | cd luaossl-master/ 110 | make install >/dev/null 111 | cd .. 112 | } 113 | 114 | install_luaoauth_deps_debian() { 115 | printf "\r[+] Installing haproxy-lua-oauth dependencies\n" 116 | 117 | if [ ! -e $lua_dep_dir ]; then 118 | mkdir -p $lua_dep_dir; 119 | fi; 120 | 121 | apt-get update >/dev/null 2>&1 122 | apt-get install -y build-essential liblua5.4-dev libssl-dev unzip >/dev/null 2>&1 123 | 124 | cd $SOURCE_DIR 125 | 126 | curl -sLO https://github.com/rxi/json.lua/archive/refs/heads/master.zip 127 | unzip -qo master.zip && rm master.zip 128 | cp json.lua-master/json.lua $lua_dep_dir 129 | 130 | curl -sLO https://github.com/lunarmodules/luasocket/archive/refs/heads/master.zip 131 | unzip -qo master.zip && rm master.zip 132 | cd luasocket-master/ 133 | make clean all install-both LUAINC=/usr/include/lua5.4/ >/dev/null 134 | cd .. 135 | 136 | curl -sLO https://github.com/wahern/luaossl/archive/refs/heads/master.zip 137 | unzip -qo master.zip && rm master.zip 138 | cd luaossl-master/ 139 | make install >/dev/null 140 | cd .. 141 | } 142 | 143 | install_luaoauth_deps_rhel() { 144 | printf "\r[+] Installing haproxy-lua-oauth dependencies\n" 145 | 146 | if [ ! -e $lua_dep_dir ]; then 147 | mkdir -p $lua_dep_dir; 148 | fi; 149 | 150 | yum -y install dnf 151 | dnf -y install dnf-plugins-core 152 | dnf config-manager --set-enabled powertools 153 | dnf update >/dev/null 2>&1 154 | dnf install -y gcc openssl-devel lua-devel make readline-devel systemd-devel unzip >/dev/null 2>&1 155 | 156 | cd $SOURCE_DIR 157 | 158 | curl -sLO https://github.com/rxi/json.lua/archive/refs/heads/master.zip 159 | unzip -qo master.zip && rm master.zip 160 | mv json.lua-master/json.lua $lua_dep_dir 161 | 162 | curl -sLO https://github.com/lunarmodules/luasocket/archive/refs/heads/master.zip 163 | unzip -qo master.zip && rm master.zip 164 | cd luasocket-master/ 165 | make clean all install-both LUAINC=/usr/include/ >/dev/null 166 | cd .. 167 | 168 | curl -sLO https://github.com/wahern/luaossl/archive/rel-20181207.zip 169 | unzip -qo rel-20181207.zip && rm rel-20181207.zip 170 | cd luaossl-rel-20181207/ 171 | make install >/dev/null 172 | cd .. 173 | } 174 | 175 | install_luaoauth() { 176 | printf "\r[+] Installing haproxy-lua-oauth\n" 177 | if [ ! -e $lua_dep_dir ]; then 178 | mkdir -p $lua_dep_dir; 179 | fi; 180 | 181 | cp "$CWD"/lib/*.lua $lua_dep_dir 182 | } 183 | 184 | case $1 in 185 | luaoauth) 186 | install_luaoauth_var="true" 187 | ;; 188 | *) 189 | echo "Usage: install.sh luaoauth" 190 | exit 1 191 | ;; 192 | esac 193 | 194 | echo "$rhel_based" 195 | 196 | if $install_luaoauth_var; then 197 | if $rhel_based; then 198 | echo "RHEL based system detected" 199 | download_and_install_luaoauth="install_luaoauth_deps_rhel install_luaoauth" 200 | elif $debian_based; then 201 | echo "Debian based system detected" 202 | download_and_install_luaoauth="install_luaoauth_deps_debian install_luaoauth" 203 | elif $alpine_based; then 204 | echo "Alpine based system detected" 205 | download_and_install_luaoauth="install_luaoauth_deps_alpine install_luaoauth" 206 | fi 207 | 208 | for func in $download_and_install_luaoauth; do 209 | "$func" & 210 | display_working "$!" 211 | done 212 | fi 213 | 214 | -------------------------------------------------------------------------------- /example/haproxy/lib/jwtverify.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- JWT Validation implementation for HAProxy Lua host 3 | -- 4 | -- Copyright (c) 2019. Adis Nezirovic 5 | -- Copyright (c) 2019. Baptiste Assmann 6 | -- Copyright (c) 2019. Nick Ramirez 7 | -- Copyright (c) 2019. HAProxy Technologies LLC 8 | -- 9 | -- Licensed under the Apache License, Version 2.0 (the "License"); 10 | -- you may not use this file except in compliance with the License. 11 | -- You may obtain a copy of the License at 12 | -- 13 | -- http://www.apache.org/licenses/LICENSE-2.0 14 | -- 15 | -- Unless required by applicable law or agreed to in writing, software 16 | -- distributed under the License is distributed on an "AS IS" BASIS, 17 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | -- See the License for the specific language governing permissions and 19 | -- limitations under the License. 20 | -- 21 | -- Use HAProxy 'lua-load' to load optional configuration file which 22 | -- should contain config table. 23 | -- Default/fallback config 24 | if not config then 25 | config = { 26 | debug = true, 27 | publicKey = nil, 28 | issuer = nil, 29 | audience = nil, 30 | hmacSecret = nil 31 | } 32 | end 33 | 34 | -- search these paths for *.lua and *.so files on CentOS/RHEL 35 | package.path = package.path .. ';/usr/local/share/lua/5.4/?.lua' 36 | package.cpath = package.cpath .. ';/usr/local/lib/lua/5.4/?.so;/usr/local/lib/lua/5.4/?/?.so' 37 | 38 | local json = require 'json' 39 | local base64 = require 'base64' 40 | local openssl = { 41 | pkey = require 'openssl.pkey', 42 | digest = require 'openssl.digest', 43 | x509 = require 'openssl.x509', 44 | hmac = require 'openssl.hmac' 45 | } 46 | 47 | local function log(msg) 48 | if config.debug then 49 | core.Debug(tostring(msg)) 50 | end 51 | end 52 | 53 | local function dump(o) 54 | if type(o) == 'table' then 55 | local s = '{ ' 56 | for k,v in pairs(o) do 57 | if type(k) ~= 'number' then k = '"'..k..'"' end 58 | s = s .. '['..k..'] = ' .. dump(v) .. ',' 59 | end 60 | return s .. '} ' 61 | else 62 | return tostring(o) 63 | end 64 | end 65 | 66 | -- Loops through array to find the given string. 67 | -- items: array of strings 68 | -- test_str: string to search for 69 | local function contains(items, test_str) 70 | for _,item in pairs(items) do 71 | 72 | -- strip whitespace 73 | item = item:gsub("%s+", "") 74 | test_str = test_str:gsub("%s+", "") 75 | 76 | if item == test_str then 77 | return true 78 | end 79 | end 80 | 81 | return false 82 | end 83 | 84 | local function readAll(file) 85 | log("Reading file " .. file) 86 | local f = assert(io.open(file, "rb")) 87 | local content = f:read("*all") 88 | f:close() 89 | return content 90 | end 91 | 92 | local function decodeJwt(authorizationHeader) 93 | local headerFields = core.tokenize(authorizationHeader, " .") 94 | 95 | if #headerFields ~= 4 then 96 | log("Improperly formated Authorization header. Should be 'Bearer' followed by 3 token sections.") 97 | return nil 98 | end 99 | 100 | if headerFields[1] ~= 'Bearer' then 101 | log("Improperly formated Authorization header. Missing 'Bearer' property.") 102 | return nil 103 | end 104 | 105 | local token = {} 106 | token.header = headerFields[2] 107 | token.headerdecoded = json.decode(base64.decode(token.header)) 108 | 109 | token.payload = headerFields[3] 110 | token.payloaddecoded = json.decode(base64.decode(token.payload)) 111 | 112 | token.signature = headerFields[4] 113 | token.signaturedecoded = base64.decode(token.signature) 114 | 115 | log('Decoded JWT header: ' .. dump(token.headerdecoded)) 116 | log('Decoded JWT payload: ' .. dump(token.payloaddecoded)) 117 | 118 | return token 119 | end 120 | 121 | local function algorithmIsValid(token) 122 | if token.headerdecoded.alg == nil then 123 | log("No 'alg' provided in JWT header.") 124 | return false 125 | elseif token.headerdecoded.alg ~= 'HS256' and token.headerdecoded.alg ~= 'HS512' and token.headerdecoded.alg ~= 'RS256' then 126 | log("HS256, HS512 and RS256 supported. Incorrect alg in JWT: " .. token.headerdecoded.alg) 127 | return false 128 | end 129 | 130 | return true 131 | end 132 | 133 | local function rs256SignatureIsValid(token, publicKey) 134 | local digest = openssl.digest.new('SHA256') 135 | digest:update(token.header .. '.' .. token.payload) 136 | local vkey = openssl.pkey.new(publicKey) 137 | local isVerified = vkey:verify(token.signaturedecoded, digest) 138 | return isVerified 139 | end 140 | 141 | local function hs256SignatureIsValid(token, secret) 142 | local hmac = openssl.hmac.new(secret, 'SHA256') 143 | local checksum = hmac:final(token.header .. '.' .. token.payload) 144 | return checksum == token.signaturedecoded 145 | end 146 | 147 | local function hs512SignatureIsValid(token, secret) 148 | local hmac = openssl.hmac.new(secret, 'SHA512') 149 | local checksum = hmac:final(token.header .. '.' .. token.payload) 150 | return checksum == token.signaturedecoded 151 | end 152 | 153 | local function expirationIsValid(token) 154 | return os.difftime(token.payloaddecoded.exp, core.now().sec) > 0 155 | end 156 | 157 | local function issuerIsValid(token, expectedIssuer) 158 | return token.payloaddecoded.iss == expectedIssuer 159 | end 160 | 161 | -- Checks if the audience in the token is listed in the 162 | -- OAUTH_AUDIENCE environment variable. Both the token audience 163 | -- and the environment variable can contain multiple audience values, 164 | -- separated by commas. Each value will be checked. 165 | local function audienceIsValid(token, expectedAudienceParam) 166 | 167 | -- Convert OAUTH_AUDIENCE environment variable to a table, 168 | -- even if it contains only one value 169 | local expectedAudiences = expectedAudienceParam 170 | if type(expectedAudiences) == "string" then 171 | -- split multiple values using a space as the delimiter 172 | expectedAudiences = core.tokenize(expectedAudienceParam, " ") 173 | end 174 | 175 | -- Convert 'aud' claim to a table, even if it contains only one value 176 | local receivedAudiences = token.payloaddecoded.aud 177 | if type(token.payloaddecoded.aud) == "string" then 178 | receivedAudiences ={} 179 | receivedAudiences[1] = token.payloaddecoded.aud 180 | end 181 | 182 | for _, receivedAudience in ipairs(receivedAudiences) do 183 | if contains(expectedAudiences, receivedAudience) then 184 | return true 185 | end 186 | end 187 | 188 | return false 189 | end 190 | 191 | local function setVariablesFromPayload(txn, decodedPayload) 192 | for key, value in pairs(decodedPayload) do 193 | txn:set_var("txn.oauth." .. key, dump(value)) 194 | end 195 | end 196 | 197 | local function jwtverify(txn) 198 | local pem = config.publicKey 199 | local issuer = config.issuer 200 | local audience = config.audience 201 | local hmacSecret = config.hmacSecret 202 | 203 | -- 1. Decode and parse the JWT 204 | local token = decodeJwt(txn.sf:req_hdr("Authorization")) 205 | 206 | if token == nil then 207 | log("Token could not be decoded.") 208 | goto out 209 | end 210 | 211 | -- Set an HAProxy variable for each field in the token payload 212 | setVariablesFromPayload(txn, token.payloaddecoded) 213 | 214 | -- 2. Verify the signature algorithm is supported (HS256, HS512, RS256) 215 | if algorithmIsValid(token) == false then 216 | log("Algorithm not valid.") 217 | goto out 218 | end 219 | 220 | -- 3. Verify the signature with the certificate 221 | if token.headerdecoded.alg == 'RS256' then 222 | if rs256SignatureIsValid(token, pem) == false then 223 | log("Signature not valid.") 224 | goto out 225 | end 226 | elseif token.headerdecoded.alg == 'HS256' then 227 | if hs256SignatureIsValid(token, hmacSecret) == false then 228 | log("Signature not valid.") 229 | goto out 230 | end 231 | elseif token.headerdecoded.alg == 'HS512' then 232 | if hs512SignatureIsValid(token, hmacSecret) == false then 233 | log("Signature not valid.") 234 | goto out 235 | end 236 | end 237 | 238 | -- 4. Verify that the token is not expired 239 | if expirationIsValid(token) == false then 240 | log("Token is expired.") 241 | goto out 242 | end 243 | 244 | -- 5. Verify the issuer 245 | if issuer ~= nil and issuerIsValid(token, issuer) == false then 246 | log("Issuer not valid.") 247 | goto out 248 | end 249 | 250 | -- 6. Verify the audience 251 | if audience ~= nil and audienceIsValid(token, audience) == false then 252 | log("Audience not valid.") 253 | goto out 254 | end 255 | 256 | -- 8. Set authorized variable 257 | log("req.authorized = true") 258 | txn.set_var(txn, "txn.authorized", true) 259 | 260 | -- exit 261 | do return end 262 | 263 | -- way out. Display a message when running in debug mode 264 | ::out:: 265 | log("req.authorized = false") 266 | txn.set_var(txn, "txn.authorized", false) 267 | end 268 | 269 | -- Called after the configuration is parsed. 270 | -- Loads the OAuth public key for validating the JWT signature. 271 | core.register_init(function() 272 | config.issuer = os.getenv("OAUTH_ISSUER") 273 | config.audience = os.getenv("OAUTH_AUDIENCE") 274 | 275 | -- when using an RS256 signature 276 | local publicKeyPath = os.getenv("OAUTH_PUBKEY_PATH") 277 | local pem = readAll(publicKeyPath) 278 | config.publicKey = pem 279 | 280 | -- when using an HS256 or HS512 signature 281 | config.hmacSecret = os.getenv("OAUTH_HMAC_SECRET") 282 | 283 | log("PublicKeyPath: " .. publicKeyPath) 284 | log("Issuer: " .. (config.issuer or "")) 285 | log("Audience: " .. (config.audience or "")) 286 | end) 287 | 288 | -- Called on a request. 289 | core.register_action('jwtverify', {'http-req'}, jwtverify, 0) 290 | -------------------------------------------------------------------------------- /lib/jwtverify.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- JWT Validation implementation for HAProxy Lua host 3 | -- 4 | -- Copyright (c) 2019. Adis Nezirovic 5 | -- Copyright (c) 2019. Baptiste Assmann 6 | -- Copyright (c) 2019. Nick Ramirez 7 | -- Copyright (c) 2019. HAProxy Technologies LLC 8 | -- 9 | -- Licensed under the Apache License, Version 2.0 (the "License"); 10 | -- you may not use this file except in compliance with the License. 11 | -- You may obtain a copy of the License at 12 | -- 13 | -- http://www.apache.org/licenses/LICENSE-2.0 14 | -- 15 | -- Unless required by applicable law or agreed to in writing, software 16 | -- distributed under the License is distributed on an "AS IS" BASIS, 17 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | -- See the License for the specific language governing permissions and 19 | -- limitations under the License. 20 | -- 21 | -- Use HAProxy 'lua-load' to load optional configuration file which 22 | -- should contain config table. 23 | -- Default/fallback config 24 | if not config then 25 | config = { 26 | debug = true, 27 | publicKey = nil, 28 | issuer = nil, 29 | audience = nil, 30 | hmacSecret = nil 31 | } 32 | end 33 | 34 | -- search these paths for *.lua and *.so files on CentOS/RHEL 35 | package.path = package.path .. ';/usr/local/share/lua/5.4/?.lua' 36 | package.cpath = package.cpath .. ';/usr/local/lib/lua/5.4/?.so;/usr/local/lib/lua/5.4/?/?.so' 37 | 38 | local json = require 'json' 39 | local base64 = require 'base64' 40 | local openssl = { 41 | pkey = require 'openssl.pkey', 42 | digest = require 'openssl.digest', 43 | x509 = require 'openssl.x509', 44 | hmac = require 'openssl.hmac' 45 | } 46 | 47 | local function log(msg) 48 | if config.debug then 49 | core.Debug(tostring(msg)) 50 | end 51 | end 52 | 53 | local function dump(o) 54 | if type(o) == 'table' then 55 | local s = '{ ' 56 | for k,v in pairs(o) do 57 | if type(k) ~= 'number' then k = '"'..k..'"' end 58 | s = s .. '['..k..'] = ' .. dump(v) .. ',' 59 | end 60 | return s .. '} ' 61 | else 62 | return tostring(o) 63 | end 64 | end 65 | 66 | -- Loops through array to find the given string. 67 | -- items: array of strings 68 | -- test_str: string to search for 69 | local function contains(items, test_str) 70 | for _,item in pairs(items) do 71 | 72 | -- strip whitespace 73 | item = item:gsub("%s+", "") 74 | test_str = test_str:gsub("%s+", "") 75 | 76 | if item == test_str then 77 | return true 78 | end 79 | end 80 | 81 | return false 82 | end 83 | 84 | local function readAll(file) 85 | log("Reading file " .. file) 86 | local f = assert(io.open(file, "rb")) 87 | local content = f:read("*all") 88 | f:close() 89 | return content 90 | end 91 | 92 | local function decodeJwt(authorizationHeader) 93 | local headerFields = core.tokenize(authorizationHeader, " .") 94 | 95 | if #headerFields ~= 4 then 96 | log("Improperly formated Authorization header. Should be 'Bearer' followed by 3 token sections.") 97 | return nil 98 | end 99 | 100 | if headerFields[1] ~= 'Bearer' then 101 | log("Improperly formated Authorization header. Missing 'Bearer' property.") 102 | return nil 103 | end 104 | 105 | local token = {} 106 | token.header = headerFields[2] 107 | token.headerdecoded = json.decode(base64.decode(token.header)) 108 | 109 | token.payload = headerFields[3] 110 | token.payloaddecoded = json.decode(base64.decode(token.payload)) 111 | 112 | token.signature = headerFields[4] 113 | token.signaturedecoded = base64.decode(token.signature) 114 | 115 | log('Decoded JWT header: ' .. dump(token.headerdecoded)) 116 | log('Decoded JWT payload: ' .. dump(token.payloaddecoded)) 117 | 118 | return token 119 | end 120 | 121 | local function algorithmIsValid(token) 122 | if token.headerdecoded.alg == nil then 123 | log("No 'alg' provided in JWT header.") 124 | return false 125 | elseif token.headerdecoded.alg ~= 'HS256' and token.headerdecoded.alg ~= 'HS512' and token.headerdecoded.alg ~= 'RS256' then 126 | log("HS256, HS512 and RS256 supported. Incorrect alg in JWT: " .. token.headerdecoded.alg) 127 | return false 128 | end 129 | 130 | return true 131 | end 132 | 133 | local function rs256SignatureIsValid(token, publicKey) 134 | local digest = openssl.digest.new('SHA256') 135 | digest:update(token.header .. '.' .. token.payload) 136 | local vkey = openssl.pkey.new(publicKey) 137 | local isVerified = vkey:verify(token.signaturedecoded, digest) 138 | return isVerified 139 | end 140 | 141 | local function hs256SignatureIsValid(token, secret) 142 | local hmac = openssl.hmac.new(secret, 'SHA256') 143 | local checksum = hmac:final(token.header .. '.' .. token.payload) 144 | return checksum == token.signaturedecoded 145 | end 146 | 147 | local function hs512SignatureIsValid(token, secret) 148 | local hmac = openssl.hmac.new(secret, 'SHA512') 149 | local checksum = hmac:final(token.header .. '.' .. token.payload) 150 | return checksum == token.signaturedecoded 151 | end 152 | 153 | local function expirationIsValid(token) 154 | return os.difftime(token.payloaddecoded.exp, core.now().sec) > 0 155 | end 156 | 157 | local function issuerIsValid(token, expectedIssuer) 158 | return token.payloaddecoded.iss == expectedIssuer 159 | end 160 | 161 | -- Checks if the audience in the token is listed in the 162 | -- OAUTH_AUDIENCE environment variable. Both the token audience 163 | -- and the environment variable can contain multiple audience values, 164 | -- separated by commas. Each value will be checked. 165 | local function audienceIsValid(token, expectedAudienceParam) 166 | 167 | -- Convert OAUTH_AUDIENCE environment variable to a table, 168 | -- even if it contains only one value 169 | local expectedAudiences = expectedAudienceParam 170 | if type(expectedAudiences) == "string" then 171 | -- split multiple values using a space as the delimiter 172 | expectedAudiences = core.tokenize(expectedAudienceParam, " ") 173 | end 174 | 175 | -- Convert 'aud' claim to a table, even if it contains only one value 176 | local receivedAudiences = token.payloaddecoded.aud 177 | if type(token.payloaddecoded.aud) == "string" then 178 | receivedAudiences ={} 179 | receivedAudiences[1] = token.payloaddecoded.aud 180 | end 181 | 182 | for _, receivedAudience in ipairs(receivedAudiences) do 183 | if contains(expectedAudiences, receivedAudience) then 184 | return true 185 | end 186 | end 187 | 188 | return false 189 | end 190 | 191 | local function setVariablesFromPayload(txn, decodedPayload) 192 | for key, value in pairs(decodedPayload) do 193 | txn:set_var("txn.oauth." .. key, dump(value)) 194 | end 195 | end 196 | 197 | local function jwtverify(txn) 198 | local pem = config.publicKey 199 | local issuer = config.issuer 200 | local audience = config.audience 201 | local hmacSecret = config.hmacSecret 202 | 203 | -- 1. Decode and parse the JWT 204 | local token = decodeJwt(txn.sf:req_hdr("Authorization")) 205 | 206 | if token == nil then 207 | log("Token could not be decoded.") 208 | goto out 209 | end 210 | 211 | -- Set an HAProxy variable for each field in the token payload 212 | setVariablesFromPayload(txn, token.payloaddecoded) 213 | 214 | -- 2. Verify the signature algorithm is supported (HS256, HS512, RS256) 215 | if algorithmIsValid(token) == false then 216 | log("Algorithm not valid.") 217 | goto out 218 | end 219 | 220 | -- 3. Verify the signature with the certificate 221 | if token.headerdecoded.alg == 'RS256' then 222 | if rs256SignatureIsValid(token, pem) == false then 223 | log("Signature not valid.") 224 | goto out 225 | end 226 | elseif token.headerdecoded.alg == 'HS256' then 227 | if hs256SignatureIsValid(token, hmacSecret) == false then 228 | log("Signature not valid.") 229 | goto out 230 | end 231 | elseif token.headerdecoded.alg == 'HS512' then 232 | if hs512SignatureIsValid(token, hmacSecret) == false then 233 | log("Signature not valid.") 234 | goto out 235 | end 236 | end 237 | 238 | -- 4. Verify that the token is not expired 239 | if expirationIsValid(token) == false then 240 | log("Token is expired.") 241 | goto out 242 | end 243 | 244 | -- 5. Verify the issuer 245 | if issuer ~= nil and issuerIsValid(token, issuer) == false then 246 | log("Issuer not valid.") 247 | goto out 248 | end 249 | 250 | -- 6. Verify the audience 251 | if audience ~= nil and audienceIsValid(token, audience) == false then 252 | log("Audience not valid.") 253 | goto out 254 | end 255 | 256 | -- 8. Set authorized variable 257 | log("req.authorized = true") 258 | txn.set_var(txn, "txn.authorized", true) 259 | 260 | -- exit 261 | do return end 262 | 263 | -- way out. Display a message when running in debug mode 264 | ::out:: 265 | log("req.authorized = false") 266 | txn.set_var(txn, "txn.authorized", false) 267 | end 268 | 269 | -- Called after the configuration is parsed. 270 | -- Loads the OAuth public key for validating the JWT signature. 271 | core.register_init(function() 272 | config.issuer = os.getenv("OAUTH_ISSUER") 273 | config.audience = os.getenv("OAUTH_AUDIENCE") 274 | 275 | -- when using an RS256 signature 276 | local publicKeyPath = os.getenv("OAUTH_PUBKEY_PATH") 277 | if publicKeyPath ~= nil then 278 | local pem = readAll(publicKeyPath) 279 | config.publicKey = pem 280 | end 281 | 282 | -- when using an HS256 or HS512 signature 283 | config.hmacSecret = os.getenv("OAUTH_HMAC_SECRET") 284 | 285 | log("PublicKeyPath: " .. (publicKeyPath or "")) 286 | log("Issuer: " .. (config.issuer or "")) 287 | log("Audience: " .. (config.audience or "")) 288 | end) 289 | 290 | -- Called on a request. 291 | core.register_action('jwtverify', {'http-req'}, jwtverify, 0) 292 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------