Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using multiple keys and selecting between them via kid #40

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.vagrant
.vagrant
tmp
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ with a space and surround it with double quotes:
setenv OAUTH_AUDIENCE "https://api.mywebsite.com https://api2.mywebsite.com"
```

## Support for multiple keys

This library support specifying multiple keys values in the JWT token. They should be specified as a JSON array of strings.
You can also accept multiple audience values in the `OAUTH_KID` and `OAUTH_PUBKEY_PATH` environment variables in the **haproxy.cfg** file. Separate each value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace "audience values" with "key identifier values"?

with a space and surround it with double quotes:

```
setenv OAUTH_PUBKEY_PATH "/etc/haproxy/pem/pubkey.pem /etc/haproxy/pem/pubkey2.pem"
setenv OAUTH_KID "key1 key2"
```

Make sure that the order of the paths to the keys matches the index of the relevant identifier.

## Output variables

After calling `http-request lua.jwtverify`, you get access to variables for each of the claims in the token.
Expand All @@ -74,16 +87,16 @@ Try it out using the Docker Compose.
| permission | description |
|-------------|-----------------------|
| read:myapp | Read access to my app |
| write:myapp | Write access to myapp |
| write:myapp | Write access to myapp |

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).
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.

1. Convert it first using `openssl x509 -pubkey -noout -in ./mycert.pem > pubkey.pem` and save **pubkey.pem** to **/example/haproxy/pem/pubkey.pem**.
1. Edit **example/haproxy/haproxy.cfg**:
1. Edit **example/haproxy/haproxy.cfg**:

* replace the `OAUTH_ISSUER` variable in the global section with the Auth0 domain URL with your own, such as https://myaccount.auth0.com/.
* replace the `OAUTH_AUDIENCE` variable with your API name in Auth0, such as "https://api.mywebsite.com".
* replace the `OAUTH_ISSUER` variable in the global section with the Auth0 domain URL with your own, such as https://myaccount.auth0.com/.
* replace the `OAUTH_AUDIENCE` variable with your API name in Auth0, such as "https://api.mywebsite.com".
* replace the `OAUTH_PUBKEY_PATH` variable with the path to your PEM certificate. (also update the docker-compose file)

1. Create the environment with Docker Compose:
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.ubuntu.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ services:
volumes:
- ./example/haproxy/haproxy.cfg:/etc/haproxy/haproxy.cfg
- ./example/haproxy/pem/pubkey.pem:/etc/haproxy/pem/pubkey.pem
- ./example/haproxy/pem/pubkey2.pem:/etc/haproxy/pem/pubkey2.pem
- ./example/haproxy/pem/test.com.pem:/etc/haproxy/pem/test.com.pem
- ./lib/jwtverify.lua:/usr/local/share/lua/5.4/jwtverify.lua
Copy link
Contributor

@NickMRamirez NickMRamirez Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the existing repo, jwtverify.lua gets installed by example/haproxy/install.sh line 120:

install_luaoauth() {
    printf "\r[+] Installing haproxy-lua-oauth\n"
    if [ ! -e $lua_dep_dir ]; then
        mkdir -p $lua_dep_dir;
    fi;

    cp $CWD/lib/*.lua $lua_dep_dir
}

which tests the install script. What's the reason to overwrite that installed lua file with a Docker volume?

ports:
- "80:80"
- "443:443"
Expand Down
6 changes: 4 additions & 2 deletions example/haproxy/haproxy.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ global
# Replace the Auth0 URL with your own:
setenv OAUTH_ISSUER https://youraccount.auth0.com/
setenv OAUTH_AUDIENCE https://api.mywebsite.com
setenv OAUTH_PUBKEY_PATH /etc/haproxy/pem/pubkey.pem
# Note that that you can use multiple keys, just make sure that kid length matches the number of keys
setenv OAUTH_PUBKEY_PATH "/etc/haproxy/pem/pubkey.pem /etc/haproxy/pem/pubkey2.pem"
setenv OAUTH_KID "key1 key2"

defaults
log global
Expand All @@ -29,7 +31,7 @@ frontend api_gateway
http-request deny unless { var(txn.authorized) -m bool }
http-request deny if { path_beg /api/myapp } { method GET } ! { var(txn.oauth.scope) -m sub read:myapp }
http-request deny if { path_beg /api/myapp } { method POST PUT DELETE } ! { var(txn.oauth.scope) -m sub write:myapp }

backend apiservers
balance roundrobin
server server1 server1:80
19 changes: 12 additions & 7 deletions example/haproxy/pem/pubkey.pem
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvIL8bebCh+pi68Rt0CCu
104VqR10kuD0E1yzwaywvaEiyhfUeDDKAyKC8yS5ilu9xyWK/pg/84RiWq7WoqhU
m8L06jtknn/ZCOuyUdkn1QcdOG10lbbrUF1AOduTIvFYyT4zHrIcKt6MyeQUO0kH
cXQU7lvM2C62BboAasZFupDts1m1kPZMWaiSjLrE1eruhl8NrfipiPWMZJSJoYCQ
cmtN3REXk9z8X7ZPgcMJ9hNN+Kv0fTYLZI4wS4TpHscVfbK18cL4uLrTCcip7jNe
y2KZ/YdbeHgmmcQAdiB4veH4I2dAyqIdsy8Jk+KTs3Ae8qp+S3XtC8z/uXMbN7lR
AwIDAQAB
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2Z1w2ZZ0oHnpxYX3Nnee
zVwI4fHtOQTdNz9vek6QZlkl4JY9yxcVfG8ssJq0k55po4MelioNyybpR4+9AE/Q
PwGUm7xEsAjlP/BAmpTz5PAeBAMd1sn+frHgKaICfQO6feob9/JaLo2ixRN0zzPd
5dhUrRaW/Az5a4mcXLSEGUAtIgtCg+ZnXra5Bn503xSOqOJkp8R2qnozmsVidAeL
/bOMe/Yb7QpDjJgv565G3gbbzcLn6+IG8IWnXNxLD7C6mcPA0yS3MrJUpRFRzWrW
MMPWOvBHWCm/2NnNQGHGSpImQ/BZ1DOoFqXO6DRuZpYiBzE/H74ojaatfoxSzpJz
618j4u3CcTKwYafkdXXbUoSMK9FWXdCsSVppXqBLIOtaS2MZMtnuUP23EucoK65l
BSUiuQ7gztzuDTKSLjlm3oMS2Z6Z3j0e0dHXnNZQTZKvhjjTyi65n3Xq7EBhNsbp
r6zN4RGbimoliRvyuNQpY9ottbB5Md2PkNRx2WwMtOEMCNCDvmSMwJ7SeUEcs0n+
J7v4WyEA5TH2OYdwRPOfyAbZbxyP8ZEICCe6Xhn8QKVZO5nTIHzuQuHW5z8Q9gA/
3waj/ksN9CGP211lRCxDf2iINw1EkPYjeWM5MAr1N1BwyhItqbP3DAIsajFb6CtY
dZKcoUh45cl6d6DaXgWq4L8CAwEAAQ==
-----END PUBLIC KEY-----
14 changes: 14 additions & 0 deletions example/haproxy/pem/pubkey2.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAu9TovLhy2PFHJ6U3+lDj
8uiqtigAJ+cdBTbe4NotSJiNKikZlUSzJElIwAYXeHpRsDuCml76e0axB22XWGZ1
QPII/2Etd6ZjWu5mp27i9EqpJHnd5xpdeNnBUf1KWH2uVUJjPVEZWAU1rqVl/FhI
owRyjqq3KtZo36u4ZD3264SOMXzIZIn4+dDuwNavGUen0mug+r3istTa5fQy8DVu
DhSU24MBLKQwAlNOWfUUf/h9SqpE25w2mehJwhJ+qXP0OwlzCw0tJIjcSkycrB02
+xF9ucALZzZgX72et242gIak0p7NkRcjuYWPMhhmvrkVXgz4XSfszHIlgnvD+hG9
EZOTgq2hL7O1BHB4FIyQrZBtCT/Q7pcjlHb0VMiRW2tv4GYW1tiSnf2Tww3nJOwf
YetCSuT0zhamEC7LEQFlju9ZZvQThTtXrEYhryp+Tw2UxEsUtiiVFcncX0St4lw8
XBzQkIUOsUDdVHenpdfPqTLsFZ1CwydX7DmQX1tVDHY2J2jQGK2ZBJrNWPD6SlWA
7hDMaTZykTiVdQXGczVqHJi8YUB8YwBffLOHxF+TISF6Nj+6eB9DXmXmAyVsHaYu
5TEZccPOGvSRKzUJo/vwusZ8pmSZigaaD42M+xqWKxYlvx1Mli8lBtA9Q3jMotSl
YEAQiWgiAK/Vev1vo3i2sf8CAwEAAQ==
-----END PUBLIC KEY-----
74 changes: 62 additions & 12 deletions lib/jwtverify.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ if not config then
config = {
Copy link
Contributor

@NickMRamirez NickMRamirez Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please copy the updated lib/jwtverify.lua to example/haproxy/lib/jwtverify.lua? It's the same file, but copied into the example.

debug = true,
publicKey = nil,
kid = nil,
issuer = nil,
audience = nil,
hmacSecret = nil
Expand Down Expand Up @@ -130,7 +131,23 @@ local function algorithmIsValid(token)
return true
end

local function rs256SignatureIsValid(token, publicKey)
local function rs256SignatureIsValid(token, keys, kids)
-- Check if a kid if provided, if so verify it exists in the kids array
local token_kid = token.headerdecoded.kid
local publicKey
if kids ~= nil then
if not contains(kids, token_kid) then
log("The kid provided in the token (" .. token_kid .. ") does not match the kid provided in the configuration.")
return false
end

-- get the key from the keys list at the index of the correct kid
publicKey = keys[token_kid]
else
-- if no kid is provided, use the first key in the list
publicKey = keys[next(keys)]
end

local digest = openssl.digest.new('SHA256')
digest:update(token.header .. '.' .. token.payload)
local vkey = openssl.pkey.new(publicKey)
Expand Down Expand Up @@ -160,10 +177,10 @@ end

-- Checks if the audience in the token is listed in the
-- OAUTH_AUDIENCE environment variable. Both the token audience
-- and the environment variable can contain multiple audience values,
-- and the environment variable can contain multiple audience values,
-- separated by commas. Each value will be checked.
local function audienceIsValid(token, expectedAudienceParam)

-- Convert OAUTH_AUDIENCE environment variable to a table,
-- even if it contains only one value
local expectedAudiences = expectedAudienceParam
Expand All @@ -172,8 +189,14 @@ local function audienceIsValid(token, expectedAudienceParam)
expectedAudiences = core.tokenize(expectedAudienceParam, " ")
end

-- Convert 'aud' claim to a table, even if it contains only one value
local receivedAudiences = token.payloaddecoded.aud

-- Check if 'aud' exists and handle cases where it's missing
if receivedAudiences == nil then
return false
end

-- Convert 'aud' claim to a table, even if it contains only one value
if type(token.payloaddecoded.aud) == "string" then
receivedAudiences ={}
receivedAudiences[1] = token.payloaddecoded.aud
Expand All @@ -195,7 +218,8 @@ local function setVariablesFromPayload(txn, decodedPayload)
end

local function jwtverify(txn)
local pem = config.publicKey
local keys = config.publicKeys
local kid = config.kid
local issuer = config.issuer
local audience = config.audience
local hmacSecret = config.hmacSecret
Expand All @@ -219,7 +243,7 @@ local function jwtverify(txn)

-- 3. Verify the signature with the certificate
if token.headerdecoded.alg == 'RS256' then
if rs256SignatureIsValid(token, pem) == false then
if rs256SignatureIsValid(token, keys, kid) == false then
log("Signature not valid.")
goto out
end
Expand Down Expand Up @@ -271,18 +295,44 @@ end
core.register_init(function()
config.issuer = os.getenv("OAUTH_ISSUER")
config.audience = os.getenv("OAUTH_AUDIENCE")


-- when using multiple keys, parse the kid list
local kid = os.getenv("OAUTH_KID")
if kid ~= nil then
config.kid = core.tokenize(kid, " ")
end

-- when using an RS256 signature
local publicKeyPath = os.getenv("OAUTH_PUBKEY_PATH")
local publicKeyPath = os.getenv("OAUTH_PUBKEY_PATH")
if publicKeyPath ~= nil then
local pem = readAll(publicKeyPath)
config.publicKey = pem
-- tokenize the path in case multiple keys are provided
keyPaths = core.tokenize(publicKeyPath, " ")

-- Check if there is more than one file path then we must have kid identifiers
if #keyPaths > 1 and config.kid == nil then
log("Multiple public keys provided but no key identifiers.")
return
end

-- Make sure that the kid size matches the keyPaths size
if config.kid ~= nil and #config.kid ~= #keyPaths then
log("The number of keys does not match the number of key identifiers.")
return
end

-- Read all the keys and store them in the config
config.publicKeys = {}
for i, keyPath in ipairs(keyPaths) do
local pem = readAll(keyPath)
config.publicKeys[config.kid[i]] = pem
end
end

-- when using an HS256 or HS512 signature
config.hmacSecret = os.getenv("OAUTH_HMAC_SECRET")

log("PublicKeyPath: " .. (publicKeyPath or "<none>"))
log("KeyIdentifiers: " .. (kid or "<none>"))
log("Issuer: " .. (config.issuer or "<none>"))
log("Audience: " .. (config.audience or "<none>"))
end)
Expand Down
Loading