Skip to content

Commit

Permalink
Refactor and re-did the TokenVerificationFilter to use an OAuth token…
Browse files Browse the repository at this point in the history
… instrospection endpoint instead of checking it agasint postgres, added validations and descriptive errors.

Cleanup Dockerfile to only include the needed dependencies.

Updated example.env with needed env variables

Updated README.md with new instructions

Signed-off-by: Alfredo Gutierrez <[email protected]>
  • Loading branch information
AlfredoG87 committed Feb 7, 2024
1 parent 5d9ccdb commit 7dbcd03
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 108 deletions.
7 changes: 2 additions & 5 deletions envoy-auth-layer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ COPY /filters/ /filters/
# Install Lua and Luarocks
RUN apt-get update && apt-get install -y lua5.1 luarocks git

# Install PostgreSQL client and development headers, required for luasql-postgres
RUN apt-get install -y postgresql-client libpq-dev

# Install Lua modules
RUN luarocks install lua-cjson
RUN luarocks install luasql-postgres PGSQL_INCDIR=/usr/include/postgresql/

RUN luarocks install sha2
# Install http socket module
RUN luarocks install luasocket
87 changes: 46 additions & 41 deletions envoy-auth-layer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,47 +29,53 @@ This is an implementation of EnvoyProxy filters for authentication and authoriza
2. Token Extraction
3. Token Hashing
4. Payload Params Extraction
5. Token Validation using Postgres
5. Token Validation using JWT
6. Proxy Routing Configuration (using EnvoyProxy itself)

it includes a Dockerfile for building the image and a docker-compose file for running the container.

## Pre-requisites

### Postgres
```
docker run --name postgres-envoy-test -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d postgres
```

Run init script to create the database and the table

```
docker exec -it postgres-envoy-test bash
psql -U postgres
CREATE DATABASE thegraphauth;
\c thegraphauth;
CREATE SCHEMA auth
AUTHORIZATION postgres;
CREATE TABLE IF NOT EXISTS auth.subgraph_token
(
id integer,
email character varying(255) NOT NULL,
subgraph_name character varying(255) NOT NULL,
token_hash character varying(65) NOT NULL
);
INSERT INTO auth.permissions(
id, token, method, param_name)
VALUES (1, 'Bearer 12345', 'subgraph_create', 'test');
INSERT INTO auth.permissions(
id, token, method, param_name)
VALUES (1, 'Bearer 12345', 'subgraph_deploy', 'test');
```
### OAUTH 2.0 Token Server
This auth-token validation proxy layer relies on an OAuth 2.0 token server for token issuace and validation. The token server should be able to issue and validate the token using the `client_id` and `client_secret` provided in the `.env` file.

So make sure to have a token server running that is previously configured with a Client ID and Client Secret, and the `/token` and `/token/introspection` endpoints are accessible.

### Token structure

Make sure that the access token has the following claims:

```json
{
"iss": "http://host.docker.internal:8080/realms/HederaTheGraph",
"resource_access": {
"htg-auth-layer": {
"roles": [
"subgraph_create",
"subgraph_deploy"
]
}
},
"subgraph_access": "<CSV of subgraph names>",
"email_verified": true,
"active": true,
"email": "[email protected]",
"client_id": "htg-auth-layer"
}
```


### Configure KeyCloak if using it as the token server
1. For local testing you can install it using a docker container
2. Create a realm for the Hedera-The-Graph
3. Create a client for the auth-layer
4. Create a client scope for the auth-layer.
5. Map UserAttribute "subgraph_access" to the client scope.
6. Create custom roles for that client: `subgraph_create` and `subgraph_deploy`
7. Create a user and assign the roles to the user, set password and verify email.
8. Add user attribute to the user "subgraph_access" and set the value to the subgraph names that the user can access. (CSV, ie: "subgraph1,subgraph2")
9. Get a Token using the `/token` endpoint and use it for testing the auth-layer.
10. Validate the token using the `/token/introspection` endpoint.

## Usage

Expand All @@ -86,12 +92,11 @@ docker build -t envoy-auth-layer .
Add Postgres or Redis credentials to the .env file

```
# Postgres
DB_USER=postgres
DB_PASSWORD=mysecretpassword
DB_HOST=host.docker.internal
DB_PORT=5432
DB_NAME=thegraphauth
# OAuth
CLIENT_ID=<clientId>
CLIENT_SECRET=<client_secret>
TOKEN_INTROSPECTION_URL=http://host.docker.internal:8080/realms/HederaTheGraph/protocol/openid-connect/token/introspect
```

### Configure the details of the service to be proxied on the envoy.yam
Expand Down
12 changes: 4 additions & 8 deletions envoy-auth-layer/example.env
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
# Postgres
DB_USER=postgres
DB_PASSWORD=mysecretpassword
DB_HOST=host.docker.internal
DB_PORT=5432
DB_NAME=thegraphauth
# Redis
REDIS_HOST=host.docker.internal
# OAuth
CLIENT_ID=htg-auth-layer
CLIENT_SECRET=0cyYtDVVbVvaZjrDViiw4p2kegTy9Q5X
TOKEN_INTROSPECTION_URL=http://host.docker.internal:8080/realms/HederaTheGraph/protocol/openid-connect/token/introspect
178 changes: 124 additions & 54 deletions envoy-auth-layer/filters/TokenVerificationFilter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,26 @@


local cjson = require("cjson")
local luasql = require("luasql.postgres")
local sha256 = require("sha2")
local http = require("socket.http")
local ltn12 = require("ltn12")

-- Configuration: Replace these with your actual details
local introspectionUrl = os.getenv("TOKEN_INTROSPECTION_URL") or nil
local clientId = os.getenv("CLIENT_ID") or nil
local clientSecret = os.getenv("CLIENT_SECRET") or nil

local function extractToken(request_handle)
return request_handle:headers():get("Authorization")
end

local function to_hex(str)
return (str:gsub('.', function (c)
return string.format('%02x', string.byte(c))
end))
end

local function sha256_hash(input)
local hash = sha256.sha256(input)
return to_hex(hash)
local function extractToken(request_handle)

local authHeader = request_handle:headers():get("Authorization")
if not authHeader then
return nil
end

-- Remove the "Bearer " prefix from the token
return authHeader:gsub("Bearer%s+", "")

end

local function parseJsonBody(body)
Expand All @@ -43,22 +46,6 @@ local function parseJsonBody(body)
return jsonBody.method, jsonBody.params and jsonBody.params.name, nil
end

local function escapeLiteral(conn, literal)
-- Use the connection's escape method to safely escape literals
return conn:escape(literal)
end

local function getDbConnection()
local env = luasql.postgres()
local db_user = os.getenv("DB_USER") or "postgres"
local db_password = os.getenv("DB_PASSWORD") or ""
local db_name = os.getenv("DB_NAME") or "thegraphauth"
local db_host = os.getenv("DB_HOST") or "host.docker.internal"
local db_port = tonumber(os.getenv("DB_PORT")) or 5432

return env:connect(db_name, db_user, db_password, db_host, db_port)
end

local function verifyValidMethod(method)
if((method == "subgraph_deploy") or (method == "subgraph_create") or (method == "subgraph_remove")) then
return true, nil
Expand All @@ -67,40 +54,121 @@ local function verifyValidMethod(method)
return false, "Invalid method"
end

local function checkTokenPermissions(token, subgraphName)
local conn, err = getDbConnection()
if not conn then
return false, "Database connection error: " .. err
-- Function to check if a value exists in a table
local function contains(table, value)
for _, v in ipairs(table) do
if v == value then
return true
end
end
return false
end

-- Escape parameters
token = escapeLiteral(conn, token)
subgraphName = escapeLiteral(conn, subgraphName)

-- Build and execute the query
local query = string.format("SELECT-- FROM auth.subgraph_token WHERE token_hash = '%s' AND subgraph_name = '%s'", token, subgraphName)
local cursor, error = conn:execute(query)
-- Function to check if subgraphName is present in the "subgraph_access" claim
local function checkSubgraphAccessClaim(result, subgraphName)
local subgraphAccessClaim = result.subgraph_access
if subgraphAccessClaim then
local subgraphList = {}
-- Split the comma-separated string into a table
for value in subgraphAccessClaim:gmatch("[^,]+") do
table.insert(subgraphList, value)
end
return contains(subgraphList, subgraphName)
end
return false
end

if not cursor then
conn:close()
return false, "Database error: " .. error
-- Function to check if the "method" parameter is included in roles
local function checkMethodInRoles(result, method)
local roles = result.resource_access[clientId].roles
if roles then
return contains(roles, method)
end
return false
end

local result = cursor:numrows() > 0
cursor:close()
conn:close()
-- Variable to store the user associated with the token
local tokenUser = "";

local function checkTokenPermissions(token, subgraphName, method)

-- Prepare the HTTP request body
local requestBody = "token=" .. token .. "&client_id=" .. clientId .. "&client_secret=" .. clientSecret

-- Prepare the HTTP request headers
local headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
["Content-Length"] = string.len(requestBody)
}

-- Prepare a table to collect the response
local responseBody = {}

-- Perform the HTTP POST request
local response, statusCode, responseHeaders, statusText = http.request{
method = "POST",
url = introspectionUrl,
headers = headers,
source = ltn12.source.string(requestBody),
sink = ltn12.sink.table(responseBody)
}

-- Check if the request was successful
if statusCode == 200 then
-- Concatenate the response body table into a string
responseBody = table.concat(responseBody)

-- Parse the JSON response
local result = cjson.decode(responseBody)

-- Check if the token is active
if result.active then

-- check if the token claims has resource roles collection.
if not result.resource_access[clientId] then
return false, "Client roles not found in token"
end

-- check if the token claims has subgraph_access claim.
if not result.subgraph_access then
return false, "subgraph_access claim not found in token"
end

local subgraphAccessGranted = checkSubgraphAccessClaim(result, subgraphName)
local methodAccessGranted = checkMethodInRoles(result, method)

print("Token introspection successful for user: " .. result.email)
-- Set the token user for logging purposes
tokenUser = result.email

if not result.email_verified then
return false, "Email not verified for user: " .. tokenUser
end

if not subgraphAccessGranted then
return false, "Subgraph access not granted for '" .. subgraphName .. "'"
end

if not methodAccessGranted then
return false, "Access denied for method: " .. method
end
else
return false, "Token is invalid or expired."
end
else
return false, "Failed to introspect token. Status: " .. tostring(statusCode)
end

return result, nil
return true, nil
end



-- This function is called for each request, and is the entry point for the filter
function envoy_on_request(request_handle)
local token = extractToken(request_handle)
local hashed_token = sha256_hash(token)

print("Token: " .. token)
print("Hashed token: " .. hashed_token)

if not hashed_token then
if not token then
request_handle:respond({[":status"] = "401"}, "No token provided")
return
end
Expand All @@ -122,10 +190,11 @@ function envoy_on_request(request_handle)
return
end

local hasPermission, permissionError = checkTokenPermissions(hashed_token, subgraphName)
local hasPermission, permissionError = checkTokenPermissions(token, subgraphName, method)

if permissionError then
request_handle:logErr(permissionError)
request_handle:respond({[":status"] = "500"}, "Internal Server Error")
request_handle:respond({[":status"] = "401"}, permissionError)
return
end

Expand All @@ -134,5 +203,6 @@ function envoy_on_request(request_handle)
return
end

print("Token is authorized for method: ".. method .. " and subgraph: " .. subgraphName.. " by the user".. tokenUser)
-- The request is authorized and processing continues
end

0 comments on commit 7dbcd03

Please sign in to comment.