ADR-059: Delegated Authentication for iDempiere Chat Integration

Status

Proposed - 2025-12-16

Context

Users logged into iDempiere Chat (web interface) want to use iDempiere Hub (MCP Server, Chat API, CLI) without re-entering credentials. The challenge is how to authenticate users who are already authenticated in iDempiere Chat.

Problem Statement

Scenario:

  1. User logs into iDempiere Chat with username/password
  2. User's session contains tenant context (clientId, roleId, orgId, warehouseId)
  3. User clicks "Connect to AI Assistant" to use iDempiere Hub MCP Server
  4. Challenge: How does iDempiere Hub authenticate without asking for password again?

Requirements

  1. No password prompt - User already authenticated in iDempiere Chat
  2. Preserve user context - Maintain tenant, role, org, warehouse from chat session
  3. Secure - No credential sharing, token validation required
  4. Auditable - Track which user accessed what data
  5. Affordable - Minimal infrastructure changes to iDempiere
  6. Scalable - Support multiple users, multiple tenants

Current State

Decision

We will implement Service Token with Impersonation (M2M) as the primary approach for delegated authentication, with JWT Token Delegation via X-Headers as a simpler fallback option.

How it works:

┌─────────────────────────────────────────────────────────────┐
│ User in iDempiere Chat                                      │
│ User ID: 12345, Tenant: 1000014, Role: 1000000            │
└────────────────────┬────────────────────────────────────────┘
                     │
                     │ 1. Click "Connect to AI Assistant"
                     │
                     ▼
┌────────────────────────────────────────────────────────────┐
│ iDempiere Chat Backend (M2M Service)                       │
│ POST /api/v1/auth/service-tokens                           │
│ Authorization: Bearer <CHAT_M2M_TOKEN>                     │
│ {                                                           │
│   "impersonate_user_id": 12345,  ← User who clicked       │
│   "client_id": 1000014,           ← User's tenant          │
│   "role_id": 1000000,                                      │
│   "org_id": 1000000,                                       │
│   "scopes": ["registry:read", "query:execute"],           │
│   "expires_in": 3600                                       │
│ }                                                           │
└────────────────────┬───────────────────────────────────────┘
                     │
                     │ 2. iDempiere validates M2M token
                     │    Checks impersonation permission
                     │
                     ▼
┌────────────────────────────────────────────────────────────┐
│ iDempiere Authorization Server                             │
│ Generates JWT with claims:                                 │
│ {                                                           │
│   "iss": "iDempiere",                                      │
│   "sub": "service:idempiere-chat",    ← M2M service       │
│   "impersonate_user_id": 12345,       ← Actual user       │
│   "client_id": 1000014,                ← User's tenant     │
│   "role_id": 1000000,                                      │
│   "org_id": 1000000,                                       │
│   "scopes": ["registry:read", "query:execute"]            │
│ }                                                           │
└────────────────────┬───────────────────────────────────────┘
                     │
                     │ 3. Return service token
                     │
                     ▼
┌────────────────────────────────────────────────────────────┐
│ iDempiere Chat sends to Hub                                │
│ POST https://hub.company.com/api/v1/connect                │
│ {                                                           │
│   "service_token": "eyJraWQiOiJpZ...",                     │
│   "user_id": 12345,                                        │
│   "tenant_id": 1000014                                     │
│ }                                                           │
└────────────────────┬───────────────────────────────────────┘
                     │
                     │ 4. Hub validates and uses token
                     │
                     ▼
┌────────────────────────────────────────────────────────────┐
│ iDempiere Hub MCP Server                                   │
│ • Validate service token                                  │
│ • Extract user context from claims                        │
│ • Use token for iDempiere API calls                       │
│ • Audit: "service:idempiere-chat on behalf of user 12345"│
└────────────────────────────────────────────────────────────┘

Implementation Requirements:

  1. Create M2M Service Account in iDempiere

    INSERT INTO AD_User (Name, Value, IsSystemUser)
    VALUES ('iDempiere Chat Service', 'service_idempiere_chat', 'Y');
    
    -- Grant impersonation permission
    INSERT INTO AD_User_Permission (AD_User_ID, Permission_Type, Permission_Value)
    VALUES (service_user_id, 'IMPERSONATE', '*');
    
  2. Generate M2M Token for iDempiere Chat

    # One-time setup
    curl -X POST https://idempiere.company.com/api/v1/auth/m2m-tokens \
      -H "Content-Type: application/json" \
      -d '{
        "service_name": "idempiere-chat",
        "permissions": ["impersonate"],
        "expires_in": 31536000
      }'
    
    # Store M2M token in iDempiere Chat config
    CHAT_M2M_TOKEN=eyJraWQiOiJpZ...
    
  3. Add Service Token Endpoint to iDempiere

    @Path("/auth/service-tokens")
    @POST
    public Response generateServiceToken(
        @HeaderParam("Authorization") String m2mToken,
        ServiceTokenRequest request
    ) {
        // Validate M2M token
        JwtClaims m2mClaims = jwtValidator.validate(extractBearer(m2mToken));
        if (!m2mClaims.hasClaim("service") ||
            !hasPermission(m2mClaims, "impersonate")) {
            throw new ForbiddenException("No impersonation permission");
        }
    
        // Validate user has access to requested tenant
        User user = userService.getUser(request.getImpersonateUserId());
        if (!user.hasAccessToClient(request.getClientId())) {
            throw new ForbiddenException("User lacks tenant access");
        }
    
        // Generate service token
        String token = jwtGenerator.generate(JwtClaims.builder()
            .issuer("iDempiere")
            .subject("service:" + m2mClaims.getSubject())
            .claim("impersonate_user_id", request.getImpersonateUserId())
            .claim("client_id", request.getClientId())
            .claim("role_id", request.getRoleId())
            .claim("org_id", request.getOrgId())
            .claim("scopes", request.getScopes())
            .expiresIn(request.getExpiresIn())
            .build());
    
        return Response.ok(new ServiceTokenResponse().token(token)).build();
    }
    
  4. iDempiere Chat Integration

    // In iDempiere Chat frontend
    async function connectToHub() {
      const userContext = getCurrentUserContext();
    
      // Call backend to get service token
      const response = await fetch('/api/hub/connect', {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${userSessionToken}` },
        body: JSON.stringify({
          user_id: userContext.userId,
          client_id: userContext.clientId,
          role_id: userContext.roleId,
          org_id: userContext.orgId
        })
      });
    
      const { hubToken, mcpUrl } = await response.json();
    
      // Configure Claude Desktop MCP
      await configureMCP({
        url: mcpUrl,
        headers: { 'Authorization': `Bearer ${hubToken}` }
      });
    }
    
    // In iDempiere Chat backend
    @Path("/api/hub/connect")
    @POST
    public Response connectToHub(
        @HeaderParam("Authorization") String userToken,
        HubConnectRequest request
    ) {
        // Validate user's session token
        JwtClaims userClaims = jwtValidator.validate(extractBearer(userToken));
    
        // Request service token from iDempiere
        ServiceTokenRequest serviceTokenReq = new ServiceTokenRequest()
            .impersonateUserId(request.getUserId())
            .clientId(request.getClientId())
            .roleId(request.getRoleId())
            .orgId(request.getOrgId())
            .scopes(Arrays.asList("registry:read", "query:execute"))
            .expiresIn(3600);
    
        String serviceToken = idempiereClient.generateServiceToken(
            chatM2MToken,
            serviceTokenReq
        );
    
        return Response.ok(new HubConnectResponse()
            .hubToken(serviceToken)
            .mcpUrl("https://hub.company.com/mcp/streaming")
        ).build();
    }
    
  5. iDempiere Hub Token Validation

    @ServerRequestFilter
    public void validateServiceToken(ContainerRequestContext ctx) {
        String authHeader = ctx.getHeaderString("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7);
    
            JwtClaims claims = jwtValidator.validate(token);
    
            // Extract user context from service token
            int impersonatedUserId = claims.getClaimValue("impersonate_user_id", Integer.class);
            int clientId = claims.getClaimValue("client_id", Integer.class);
            int roleId = claims.getClaimValue("role_id", Integer.class);
            int orgId = claims.getClaimValue("org_id", Integer.class);
    
            // Set tenant context
            TenantContext.set(clientId, roleId, orgId, 0,
                "User " + impersonatedUserId + " via service:" + claims.getSubject());
    
            // Store for audit logging
            ctx.setProperty("impersonated_user_id", impersonatedUserId);
            ctx.setProperty("service_name", claims.getSubject());
    
            logger.info("Service token validated: {} on behalf of user {}",
                claims.getSubject(), impersonatedUserId);
        }
    }
    

Approach 2: JWT Token Delegation via X-Headers - FALLBACK

Simpler approach for internal services or prototyping:

// iDempiere Chat Frontend
fetch('https://hub.company.com/api/v1/connect', {
  headers: {
    'X-iDempiere-Token': currentUserToken,  // User's existing JWT
    'X-Tenant-Id': userContext.clientId.toString(),
    'X-Role-Id': userContext.roleId.toString(),
    'X-Org-Id': userContext.orgId.toString()
  }
});

// iDempiere Hub Backend
@ServerRequestFilter
public void validateDelegatedToken(ContainerRequestContext ctx) {
    String delegatedToken = ctx.getHeaderString("X-iDempiere-Token");
    String tenantId = ctx.getHeaderString("X-Tenant-Id");

    if (delegatedToken != null) {
        // Validate JWT signature with iDempiere's public key
        JwtClaims claims = jwtValidator.validate(delegatedToken);

        // Verify tenant matches
        int tokenClientId = claims.getClaimValue("clientId", Integer.class);
        if (tokenClientId != Integer.parseInt(tenantId)) {
            throw new UnauthorizedException("Tenant mismatch");
        }

        // Use delegated token for iDempiere API calls
        TenantContext.set(Integer.parseInt(tenantId), ...);
        config.setToken(delegatedToken);
    }
}

Alternatives Considered

1. OAuth2 Authorization Code Flow

Pros:

Cons:

Decision: Rejected - too complex for internal service integration

2. Session Token Exchange

Pros:

Cons:

Decision: Rejected - security concerns outweigh simplicity

3. Shared Database Session Store

Pros:

Cons:

Decision: Rejected - violates microservices principles

Decision Rationale

Why Service Token with Impersonation?

  1. Affordable ✅

    • Minimal changes to iDempiere (one new endpoint)
    • Reuses existing JWT infrastructure
    • No OAuth2 server needed
  2. Secure ✅

    • M2M authentication (chat service authenticates, not user password)
    • Scoped permissions (limit what hub can do)
    • Clear audit trail (service + impersonated user)
  3. Scalable ✅

    • Works across distributed systems
    • Supports multiple chat instances
    • Token-based (stateless)
  4. Auditable ✅

    • Logs show: "service:idempiere-chat on behalf of user 12345"
    • Clear separation of service vs user actions
    • Compliance-friendly
  5. Developer-Friendly ✅

    • Standard JWT pattern
    • Well-understood M2M flow
    • Easy to test and debug

Implementation Phases

Phase 1: Prototype with X-Headers (1 week)

Phase 2: Production with Service Tokens (2 weeks)

Phase 3: Enhanced Security (future)

Consequences

Positive

Negative

Neutral

Implementation Checklist

iDempiere Backend

iDempiere Chat

iDempiere Hub

Testing

Documentation

Migration Path

From Current State (Password-Based)

Before: User must enter password for iDempiere Hub

User → iDempiere Hub → Enter username/password → Authenticate

After: User authenticated via iDempiere Chat session

User in Chat → Click "Connect to AI" → Auto-authenticated in Hub (no password)

Migration Steps:

  1. Phase 1: Both methods supported

    • Old: Direct password authentication
    • New: Delegated service token authentication
  2. Phase 2: Deprecate password authentication for chat users

    • Show warning: "Please connect via iDempiere Chat"
  3. Phase 3: Remove password prompt for chat-originated requests

    • Detect origin via referrer or service token
    • Redirect to delegated auth flow

Security Considerations

  1. M2M Token Storage

    • Store in environment variables or secrets manager
    • Never commit to version control
    • Rotate periodically (every 90 days)
  2. Service Token Scopes

    • Request minimal scopes needed
    • Validate scopes on each operation
    • Deny by default
  3. Token Expiration

    • Short-lived service tokens (3600s = 1 hour)
    • Implement refresh flow in chat backend
    • Auto-refresh before expiration
  4. Audit Logging

    • Log all service token generations
    • Log all impersonated operations
    • Include service name, user ID, tenant, operation
    • Monitor for suspicious patterns
  5. Rate Limiting

    • Limit service token generation (100 req/min per service)
    • Limit API calls per service token (1000 req/hour)
    • Alert on anomalies

Monitoring and Observability

Metrics to Track:

Logs to Capture:

INFO  - Service token generated: service=idempiere-chat, user=12345, tenant=1000014, scopes=[registry:read,query:execute]
INFO  - Service token validated: service=idempiere-chat, impersonated_user=12345, operation=listTables
WARN  - Service token validation failed: service=idempiere-chat, reason=expired
ERROR - Impersonation permission denied: service=idempiere-chat, user=12345, tenant=1000014

References

License

See LICENSE for details.

Path: /docs/developers/architecture/idempiere-hub/059-delegated-authentication-for-chat-integration