Skip to content

Bug: jwt.claims.principal_id not set for routes without rlsModule #1439

Description

@theothersideofgod

Summary

Routes without rlsModule (e.g., meta-schema routes for database provisioning) skip authentication entirely, causing jwt.claims.principal_id to not be set. This violates the agent-principal-identity.md specification which states:

Both claims are always set.

Affected Code

graphql/server/src/middleware/auth.ts (lines 57-79):

if (!rlsModule) {
  log.info('[auth] No RLS module configured, skipping auth');
  return next();  // Skips auth even if Bearer token is present
}

graphql/server/src/middleware/graphile.ts (lines 320-330):

// When req.token is not set, falls back to anonSettings
const anonSettings: Record<string, string> = {
  role: anonRole,
  ...context,  // Does not include principal_id
};

Impact

When a request hits a route without rlsModule:

  1. auth.ts skips authentication entirely, even if Bearer token is present
  2. req.token is undefined
  3. graphile.ts uses anonSettings which has no principal_id
  4. jwt.claims.principal_id is not set in PostgreSQL
  5. Any SPRT policy using current_principal_id() fails

Specification Reference

From constructive-db/docs/spec/agent-principal-identity.md:

Section 3.1 (Two JWT Claims, Always Set):

// graphile.ts middleware
pgSettings['jwt.claims.principal_id'] = req.token.principal_id ?? req.token.user_id;

Section 8.1 (Normal User flow):

Bearer token -> authenticate() -> {user_id: human, principal_id: NULL}
Middleware:
  jwt.claims.principal_id  = human  (fallback: ?? user_id)

The spec assumes authenticate() is always called when a Bearer token is present.

Suggested Fix

In auth.ts, when there's no rlsModule but a Bearer token is present, attempt platform-level authentication:

if (!rlsModule) {
  const { authorization = '' } = req.headers;
  const [authType, authToken] = authorization.split(' ');

  if (authType?.toLowerCase() === 'bearer' && authToken) {
    try {
      const authQuery = `SELECT * FROM constructive_auth_private.authenticate($1)`;
      const result = await pool.query(authQuery, [authToken]);
      if (result?.rowCount && result.rowCount > 0) {
        req.token = result.rows[0];
      }
    } catch (e: any) {
      log.warn(`[auth] Platform auth failed: ${e.message}`);
    }
  }
  return next();
}

Affected Routes

  • Meta-schema routes (X-Meta-Schema header)
  • Database provisioning endpoints
  • Any route without a configured rlsModule

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions