HIGH api key exposurehapisaml

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.

Frequently Asked Questions

Why is putting an API key in the RelayState field unsafe with SAML in Hapi?
The RelayState value is transmitted to the Identity Provider and can be logged or viewed by the IdP. If it contains an API key, the key is exposed to third parties and may appear in browser history or server logs, leading to unauthorized access.
How can I map SAML attributes to roles in Hapi without exposing credentials?
Request only necessary SAML attributes, validate them strictly, and map them to internal roles server-side. Do not embed credentials in attributes or rely on attribute values alone for authorization; use server-side mappings and short-lived tokens instead.