Created
March 11, 2026 19:47
-
-
Save aaguiarz/8b3e34f1e5fbba878ec337272335a5e5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- Kong Pre-function Plugin - Auth0 Token Vault Access Token Exchange | |
| -- Reference: https://auth0.com/docs/secure/tokens/token-vault/access-token-exchange-with-token-vault | |
| -- | |
| -- This plugin performs Auth0 Token Vault access token exchange: | |
| -- 1. Client sends Auth0 access token | |
| -- 2. Kong exchanges it for the external provider's token via Auth0 | |
| -- 3. Kong forwards the request with the external token | |
| -- | |
| -- Token validation is handled by Auth0 during the exchange (no local JWT validation) | |
| -- | |
| -- Configure per Kong Service for each external API (Google, GitHub, Slack, etc.) | |
| -- Connection is automatically determined from the Kong service host | |
| -- | |
| -- Required Environment Variables: | |
| -- AUTH0_DOMAIN - Your Auth0 tenant domain (e.g., "your-tenant.auth0.com") | |
| -- AUTH0_CLIENT_ID - Custom API Client ID (linked to your backend API) | |
| -- AUTH0_CLIENT_SECRET - Custom API Client secret | |
| local http = require "resty.http" | |
| local cjson = require "cjson.safe" | |
| local resty_sha256 = require "resty.sha256" | |
| local str = require "resty.string" | |
| -- Configuration | |
| local AUTH0_DOMAIN = os.getenv("AUTH0_DOMAIN") | |
| local AUTH0_CLIENT_ID = os.getenv("AUTH0_CLIENT_ID") | |
| local AUTH0_CLIENT_SECRET = os.getenv("AUTH0_CLIENT_SECRET") | |
| -- Host to Auth0 Connection mapping | |
| -- Map external service hosts to their Auth0 connection names | |
| local HOST_TO_CONNECTION = { | |
| -- Google services | |
| ["www.googleapis.com"] = "google-oauth2", | |
| ["gmail.googleapis.com"] = "google-oauth2", | |
| ["calendar.google.com"] = "google-oauth2", | |
| ["drive.google.com"] = "google-oauth2", | |
| -- GitHub | |
| ["api.github.com"] = "github", | |
| } | |
| -- Validate required configuration | |
| if not AUTH0_DOMAIN or not AUTH0_CLIENT_ID or not AUTH0_CLIENT_SECRET then | |
| kong.log.err("Missing Auth0 configuration: AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET required") | |
| return kong.response.exit(500, { | |
| error = "configuration_error", | |
| error_description = "Auth0 credentials not configured" | |
| }) | |
| end | |
| -- Generate SHA256 hash of input for cache keys | |
| local function sha256_hash(input) | |
| local sha = resty_sha256:new() | |
| sha:update(input) | |
| local digest = sha:final() | |
| return str.to_hex(digest) | |
| end | |
| -- Perform Token Vault access token exchange | |
| local function exchange_token(subject_token, connection) | |
| local httpc = http.new() | |
| httpc:set_timeout(5000) | |
| -- Token exchange request per Auth0 Token Vault spec | |
| local request_body = { | |
| client_id = AUTH0_CLIENT_ID, | |
| client_secret = AUTH0_CLIENT_SECRET, | |
| subject_token = subject_token, | |
| grant_type = "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token", | |
| subject_token_type = "urn:ietf:params:oauth:token-type:access_token", | |
| requested_token_type = "http://auth0.com/oauth/token-type/federated-connection-access-token", | |
| connection = connection | |
| } | |
| local res, err = httpc:request_uri("https://" .. AUTH0_DOMAIN .. "/oauth/token", { | |
| method = "POST", | |
| body = cjson.encode(request_body), | |
| headers = { ["Content-Type"] = "application/json" }, | |
| ssl_verify = true | |
| }) | |
| httpc:close() | |
| if not res then | |
| kong.log.err("Token exchange request failed: ", err) | |
| return nil, "network_error", "Failed to connect to Auth0" | |
| end | |
| local response_data = cjson.decode(res.body) | |
| if res.status ~= 200 then | |
| kong.log.err("Token exchange failed: ", res.status, " - ", res.body) | |
| return nil, | |
| response_data and response_data.error or "token_exchange_failed", | |
| response_data and response_data.error_description or "Token exchange failed" | |
| end | |
| if not response_data or not response_data.access_token then | |
| return nil, "invalid_response", "Missing access_token in response" | |
| end | |
| return response_data.access_token, nil, nil, response_data.expires_in | |
| end | |
| -- Determine Auth0 connection from Kong service host | |
| local service = kong.router.get_service() | |
| if not service or not service.host then | |
| kong.log.err("Could not determine Kong service host") | |
| return kong.response.exit(500, { | |
| error = "configuration_error", | |
| error_description = "Could not determine target service" | |
| }) | |
| end | |
| local connection = HOST_TO_CONNECTION[service.host] | |
| if not connection then | |
| kong.log.warn("No connection mapping found for host: ", service.host) | |
| return kong.response.exit(400, { | |
| error = "connection_not_found", | |
| error_description = "Could not determine Auth0 connection for host: " .. service.host | |
| }) | |
| end | |
| kong.log.debug("Auth0 Token Vault exchange - connection: ", connection, " for host: ", service.host) | |
| -- Extract Auth0 access token | |
| local auth_header = kong.request.get_header("authorization") | |
| if not auth_header then | |
| return kong.response.exit(401, { | |
| error = "unauthorized", | |
| error_description = "Authorization header required" | |
| }) | |
| end | |
| local subject_token = auth_header:match("^Bearer%s+(.+)$") | |
| if not subject_token then | |
| return kong.response.exit(401, { | |
| error = "invalid_token", | |
| error_description = "Invalid Authorization header format" | |
| }) | |
| end | |
| -- Generate cache key using SHA256 hash of token + connection | |
| -- This ensures only the exchanged token is cached, not the Auth0 token | |
| local cache_key = "token_vault:" .. sha256_hash(subject_token .. ":" .. connection) | |
| -- Try to get cached token from Kong's shared cache | |
| local external_token, err = kong.cache:get(cache_key, nil, function() | |
| -- Cache miss - perform token exchange | |
| kong.log.debug("Cache miss - exchanging token for connection: ", connection) | |
| local token, error_code, error_message, expires_in = exchange_token(subject_token, connection) | |
| if not token then | |
| kong.log.err("Token exchange failed for ", connection, ": ", error_code, " - ", error_message) | |
| -- Return nil to not cache errors | |
| return nil | |
| end | |
| -- Subtract 60 seconds as a safety buffer before expiration | |
| local ttl = expires_in and (expires_in - 60) or 300 | |
| kong.log.info("Token exchanged for connection: ", connection, " with TTL: ", ttl, "s") | |
| -- Return token and TTL for Kong cache | |
| return { token = token }, nil, ttl | |
| end) | |
| if err then | |
| kong.log.err("Cache error: ", err) | |
| return kong.response.exit(500, { | |
| error = "cache_error", | |
| error_description = "Failed to retrieve cached token" | |
| }) | |
| end | |
| if not external_token or not external_token.token then | |
| return kong.response.exit(401, { | |
| error = "token_exchange_failed", | |
| error_description = "Failed to exchange token", | |
| connection = connection | |
| }) | |
| end | |
| -- Replace Auth0 token with external provider token | |
| kong.service.request.set_header("Authorization", "Bearer " .. external_token.token) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment