A secure authentication proxy system designed to hide API keys and manage authentication for Single Page Applications (SPAs).
- Frontend Interception: Automatically intercepts
fetchandXMLHttpRequestto reroute requests through the proxy. - Backend Policy Engine: Flexible JavaScript-based policies to authorize requests, verify tokens (JWT), and inject API keys.
- OAuth2 Proxy Integration: Seamlessly works with
oauth2-proxyfor OIDC authentication. - Cookie Decryption: Helper utilities to decrypt
oauth2-proxycookies within policies.
The system uses a split-proxy architecture to segregate concerns:
- OAuth2 Proxy: Protects the Frontend (SPA). Handles user login/logout and sets a session cookie.
- Auth Proxy Backend: Protects the API. Verifies the session cookie (shared via domain) or JWT, validates policies, and injects API keys before forwarding to the real API.
- Auth Proxy Frontend: Runs in the SPA. Intercepts API requests and routes them to the Auth Proxy Backend.
yarn add @appmana-public/auth-proxy-frontendyarn add @appmana-public/auth-proxy-backendYou can use the bundled version directly in your HTML:
<script src="https://unpkg.com/@appmana-public/auth-proxy-frontend/dist/auth-proxy.global.js"></script>
<script>
AppManaAuthProxy.configureAuthProxy({
domains: ["generativelanguage.googleapis.com"],
proxyUrl: "https://auth-proxy.yourdomain.com",
// Optional: Custom token retrieval
// getAuthToken: () => 'my-token'
});
</script>import { configureAuthProxy } from "@appmana-public/auth-proxy-frontend";
configureAuthProxy({
domains: ["generativelanguage.googleapis.com"],
proxyUrl: "https://auth-proxy.yourdomain.com",
});import { GoogleGenerativeAI } from "@google/generative-ai";
// ⚠️ INSECURE: API Key exposed in frontend code
const genAI = new GoogleGenerativeAI("YOUR_API_KEY");
const model = genAI.getGenerativeModel({ model: "gemini-pro" });
const result = await model.generateContent("Hello!");import { GoogleGenerativeAI } from "@google/generative-ai";
import { configureAuthProxy } from "@appmana-public/auth-proxy-frontend";
// 1. Configure Proxy
configureAuthProxy({
domains: ["generativelanguage.googleapis.com"], // Intercept requests to this domain
proxyUrl: "https://auth-proxy.yourdomain.com",
});
// 2. Initialize Client without API Key (or with a dummy one if required by library validation)
// The proxy will inject the real key.
const genAI = new GoogleGenerativeAI("dummy-key");
const model = genAI.getGenerativeModel({ model: "gemini-pro" });
// 3. Make requests as usual
const result = await model.generateContent("Hello!");Start the backend server:
node node_modules/@appmana-public/auth-proxy-backend/build/index.js \
--policy ./policy.js \
--upstream https://api.yourdomain.com \
--allowed-domains "*.yourdomain.com" "localhost:*"Note: When running on localhost with oauth2-proxy, ensure both services run on the same domain (e.g. localhost) so cookies are shared. The frontend must be configured with credentials: 'include' (handled automatically by @appmana-public/auth-proxy-frontend when configured properly).
You can configure the backend via command line arguments, environment variables (prefixed with AUTH_PROXY_), or a JSON config file.
| Argument | Env Var | Description |
|---|---|---|
--policy |
AUTH_PROXY_POLICY |
Path to policy file(s). Supports globs (e.g. ./policies/*.js). |
--port |
AUTH_PROXY_PORT |
Port to listen on (default: 3000). |
--upstream |
AUTH_PROXY_UPSTREAM |
Default upstream URL. Used if X-Proxy-Target-Url is missing. |
--allowed-domains |
AUTH_PROXY_ALLOWED_DOMAINS |
Whitelist of allowed proxy targets (e.g. *.example.com). Supports globs. |
--authorize |
N/A | JSON string config for simple authorization (repeatable). |
--print-frontend-config |
N/A | Print frontend <script> tag based on --authorize domains and exit. |
--config |
N/A | Path to JSON config file. |
For many use cases (checking Issuer, Audience, and Allowed Domains), you don't need to write a JavaScript policy file. You can use the --authorize argument.
Example: Google Generative AI + Keycloak
This example configures the proxy to:
- Verify tokens issued by your Keycloak.
- Allow access to Google Generative AI.
- Allow access to your internal API.
node node_modules/@appmana-public/auth-proxy-backend/build/index.js \
--authorize '{"issuer": "https://auth.yourdomain.com/realms/myrealm", "audience": "my-app", "domains": ["generativelanguage.googleapis.com"]}' \
--authorize '{"issuer": "https://auth.yourdomain.com/realms/myrealm", "audience": "my-app", "domains": ["api.internal.com"]}' \
--port 3000Generate Frontend Config
You can generate the required frontend initialization script based on your --authorize arguments:
node node_modules/@appmana-public/auth-proxy-backend/build/index.js \
--authorize '{"domains": ["generativelanguage.googleapis.com"]}' \
--print-frontend-configOutput:
<script>
// Generated by @appmana-public/auth-proxy-backend
(async () => {
const module = await import("https://unpkg.com/@appmana-public/auth-proxy-frontend/dist/auth-proxy.global.js");
// Or use local import if available
if (module && module.AppManaAuthProxy) {
module.AppManaAuthProxy.configureAuthProxy({
domains: ["generativelanguage.googleapis.com"],
proxyUrl: window.location.origin, // Assuming auth proxy handles this domain
});
}
})();
</script>To get IDE autocomplete and type checking in your policy files, you can use JSDoc to reference the types exported by @appmana-public/auth-proxy-backend.
policy.js:
/**
* @typedef {import('@appmana-public/auth-proxy-backend').PolicyContext} PolicyContext
* @typedef {import('@appmana-public/auth-proxy-backend').PolicyResult} PolicyResult
*/
/**
* @param {PolicyContext} context
* @returns {Promise<PolicyResult>}
*/
module.exports = async (context) => {
const { request, user, utils } = context;
// IDE will now provide autocomplete for request, user, and utils
console.log(request.method, request.url);
return { allow: true };
};Decrypts oauth2-proxy cookie and checks if email is @appmana-public.com.
// policy.js
module.exports = async (context) => {
const { request, utils } = context;
const { cipher, parseCookies, joinCookieValues, jwt } = utils;
const cookieHeader = request.headers["cookie"];
if (!cookieHeader) return { allow: false };
const cookies = parseCookies(cookieHeader);
const encryptedCookie = joinCookieValues(cookies, "_oauth2_proxy");
if (!encryptedCookie) return { allow: false };
if (cipher) {
try {
const decrypted = cipher.decrypt(encryptedCookie);
// Extract JWT or use email directly from decrypted content
// The decrypted content structure depends on oauth2-proxy version/config
const email = decrypted.email || decrypted.e; // simplistic check
if (email && email.endsWith("@appmana-public.com")) {
return {
allow: true,
modifiedRequest: {
headers: {
Authorization: `Bearer ${process.env.BACKEND_API_KEY}`,
},
},
};
}
} catch (e) {
console.error("Cookie decryption failed", e);
}
}
return { allow: false };
};Verifies a JWT in the Authorization header and checks for 'admin' role.
// policy.js
module.exports = async (context) => {
const { request, utils } = context;
const { jwt } = utils;
const authHeader = request.headers["authorization"];
if (!authHeader || !authHeader.startsWith("Bearer ")) return { allow: false };
const token = authHeader.substring(7);
const decoded = jwt.verify(token);
if (decoded && decoded.roles && decoded.roles.includes("admin")) {
return {
allow: true,
modifiedRequest: {
headers: {
"X-User-Role": "admin",
},
},
};
}
return { allow: false };
};Build the backend image:
docker build -f appmana-auth-proxy/auth-proxy-backend/Dockerfile -t ghcr.io/appmana-public/auth-proxy-backend:latest .A complete example with Nginx, OAuth2 Proxy, and Auth Proxy Backend is available in k8s/deployment.yaml.
appmana-auth-proxy/auth-proxy-frontend: Frontend packageappmana-auth-proxy/auth-proxy-backend: Backend packageappmana-auth-proxy/auth-proxy-integration-tests: End-to-end tests
- Node.js: v18+
- Yarn: v4+ (Berry)
- Docker: Required for integration tests (runs Keycloak and OAuth2 Proxy containers).
Run unit tests for individual packages:
# Backend
yarn workspace @appmana-public/auth-proxy-backend test
# Frontend
yarn workspace @appmana-public/auth-proxy-frontend testThe integration tests verify the full flow including Keycloak, OAuth2 Proxy, and the Auth Proxy.
Run all integration tests:
yarn workspace @appmana-public/auth-proxy-integration-tests testRun specific suites:
# Run only Keycloak Integration Test (Full OIDC flow)
yarn workspace @appmana-public/auth-proxy-integration-tests run test:keycloak
# Run only Basic tests
yarn workspace @appmana-public/auth-proxy-integration-tests run test:basicThis repository is configured to publish packages to NPM and Docker images to GitHub Container Registry (GHCR) automatically via GitHub Actions.
NPM Publishing:
- Ensure you have properly versioned your packages (Semantic Versioning).
- Push to
main. - The
publish.ymlworkflow will runyarn workspaces foreach ... npm publish.- It skips private packages.
- It tolerates existing versions (skips if version already exists on registry).
- It requires
NPM_TOKENsecret in GitHub.
Docker Publishing:
- The
publish.ymlworkflow builds the backend Docker image. - Pushes to
ghcr.io/appmana/auth-proxy-backend:latest(and git sha tag). - Requires
GITHUB_TOKEN(automatic) for authentication.