Api Key Exposure in Hapi with Saml
Api Key Exposure in Hapi with Saml — how this specific combination creates or exposes the vulnerability
Hapi is a rich framework for building services and APIs in Node.js. When Hapi applications integrate SAML for authentication, developers often focus on the login flow and profile mapping while overlooking how API keys or bearer tokens may be exposed in SAML flows or related endpoints. Api key exposure in this context occurs when application logic leaks keys through SAML responses, assertions, or redirect URLs, or when keys are embedded in pages or scripts that the SAML identity provider (IdP) can access or relay back to unintended parties.
Consider a Hapi server using the hapi-saml plugin or a similar SAML strategy where the framework validates SAML assertions and then establishes its own authorization tokens or API keys for downstream services. If the server embeds an API key in a redirect URL or in a SAML attribute that is not strictly necessary, the IdP or any observer in the SAML path can see the key. For example, a common misconfiguration is to pass an API key as a query parameter in the RelayState field:
// Example unsafe Hapi route with API key in RelayState
server.route({
method: 'GET',
path: '/saml/login',
handler: (request, h) => {
const apiKey = process.env.DATA_API_KEY;
const relayState = `/api/data?key=${encodeURIComponent(apiKey)}`;
return h.redirect(`https://idp.example.com/sso/saml?RelayState=${relayState}`);
}
});
In this pattern, the API key travels in plaintext through the IdP and may be recorded in logs or browser history. SAML bindings (HTTP Redirect or POST) can inadvertently surface sensitive data if the assertion or RelayState is not carefully constrained. Even when assertions are signed, attributes sent to the service provider can contain user identifiers that, when combined with an exposed key, allow an attacker to impersonate services or escalate access.
Another exposure vector arises when Hapi uses API keys to authorize requests to backend APIs and those keys are included in error messages or debug output triggered during SAML processing. If a SAML response fails validation and the server returns detailed errors to the user, keys may be reflected in HTML or JSON responses. Additionally, if the Hapi application caches or logs RelayState or SAML attributes, keys can persist in systems that are not designed for secrets management.
The risk is compounded when SAML endpoints are also used for unauthenticated LLM services or public status endpoints, as scanning tools can discover these routes and see whether API keys are present in responses. Findings often include references to insecure transport, missing validation on SAML attributes, and improper handling of RelayState, all of which can lead to key leakage. Remediation focuses on removing keys from URLs and SAML attributes, using short-lived tokens instead, and ensuring that SAML bindings and assertions adhere to the principle of minimal data disclosure.
Saml-Specific Remediation in Hapi — concrete code fixes
Secure integration of SAML in Hapi requires keeping API keys out of SAML flows and using backend-to-backend mechanisms for delegation. Instead of embedding keys in RelayState or SAML attributes, use opaque references that the server can map to secrets stored securely. Below are concrete, safe patterns for Hapi with SAML.
1. Avoid exposing keys in RelayState
RelayState should contain only non-sensitive, non-identifying values such as a route identifier or a short-lived session token. Store sensitive values server-side, keyed by the RelayState value:
// Secure Hapi route avoiding key exposure in RelayState
const storedKeys = new Map(); // In production, use a secure cache or KMS-backed store
server.route({
method: 'GET',
path: '/saml/login',
handler: (request, h) => {
const sessionId = require('crypto').randomUUID();
const apiKey = process.env.DATA_API_KEY;
storedKeys.set(sessionId, apiKey);
const relayState = `/api/data?sessionId=${encodeURIComponent(sessionId)}`;
return h.redirect(`https://idp.example.com/sso/saml?RelayState=${relayState}`);
}
});
server.route({
method: 'GET',
path: '/api/data',
handler: (request, h) => {
const { sessionId } = request.query;
const apiKey = storedKeys.get(sessionId);
if (!apiKey) {
return h.response({ error: 'Invalid session' }).code(400);
}
// Use apiKey to call downstream service
return { message: 'Data retrieved', keyUsed: sessionId.slice(0, 8) + '...' };
}
});
2. Validate and limit SAML attributes
Only request the attributes you need and validate them strictly. Do not trust SAML attributes for access control without additional verification. Configure the SAML strategy to request minimal attributes and enforce schema checks in Hapi.
const Hapi = require('@hapi/hapi');
const Saml = require('hapi-saml');
const init = async () => {
const server = Hapi.server({ port: 4000, host: 'localhost' });
await server.register({
plugin: Saml,
options: {
path: '/sso/saml',
entryPoint: 'https://idp.example.com/sso/saml',
metadata: require('./idp-metadata.xml'),
wantMessagesSigned: true,
wantAssertionsSigned: true,
requestedAttributes: [
{ name: 'email', friendlyName: 'Email', required: true },
{ name: 'role', friendlyName: 'Role' }
],
validateInResponseTo: true,
parseRequestBodyExtensions: false,
disableRequestedAuthnContext: true
}
});
server.auth.strategy('saml', 'saml');
server.auth.default('saml');
server.route({
method: 'GET',
path: '/profile',
handler: (request, h) => {
const { email, role } = request.auth.credentials;
// Use server-side mapping to determine permissions, never trust role blindly
return { email, authorized: role === 'admin' || role === 'support' };
}
});
await server.start();
console.log('Server running on %s', server.info.uri);
};
init().catch(err => console.error(err));
3. Secure error handling and logging
Ensure SAML processing errors do not reflect API keys or sensitive data. Use generic messages and audit logs that exclude secrets:
server.ext('onPreResponse', (request, h) => {
const response = request.response;
if (response.isBoom && response.output.payload.statusCode === 500) {
// Log securely without exposing keys
console.error('SAML processing error:', { requestId: request.id, path: request.path });
response.output.payload.message = 'Internal error';
}
return h.continue;
});
4. Use the middleBrick CLI to verify your configuration
Run middlebrick scan <your-api-url> to check whether API keys or secrets appear in responses, redirects, or metadata. The scanner flags exposure of keys in URLs, headers, and SAML attributes, helping you align with secure SAML integration practices.