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:
- User logs into iDempiere Chat with username/password
- User's session contains tenant context (clientId, roleId, orgId, warehouseId)
- User clicks "Connect to AI Assistant" to use iDempiere Hub MCP Server
- Challenge: How does iDempiere Hub authenticate without asking for password again?
Requirements
- No password prompt - User already authenticated in iDempiere Chat
- Preserve user context - Maintain tenant, role, org, warehouse from chat session
- Secure - No credential sharing, token validation required
- Auditable - Track which user accessed what data
- Affordable - Minimal infrastructure changes to iDempiere
- Scalable - Support multiple users, multiple tenants
Current State
- iDempiere Chat: Users authenticate with username/password → JWT token
- iDempiere Hub: Requires authentication via
POST /auth/tokenswith credentials - Gap: No way to delegate existing authentication from Chat to Hub
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.
Approach 1: Service Token with Impersonation (M2M) - RECOMMENDED
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:
-
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', '*'); -
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... -
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(); } -
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(); } -
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:
- Industry standard
- Explicit user consent
- Token refresh support
Cons:
- ❌ Requires full OAuth2 server implementation in iDempiere
- ❌ High complexity (authorization server, client registration, etc.)
- ❌ Overhead for internal service integration
- ❌ Not affordable for current scope
Decision: Rejected - too complex for internal service integration
2. Session Token Exchange
Pros:
- Simple - exchange session cookie for API token
- No OAuth2 needed
Cons:
- ❌ Session hijacking risk (cookies exposed)
- ❌ No scope control
- ❌ Tight coupling between chat and hub
- ❌ Doesn't work for cross-domain scenarios
Decision: Rejected - security concerns outweigh simplicity
3. Shared Database Session Store
Pros:
- Very simple - read session from database
Cons:
- ❌ Tight coupling to database
- ❌ No API-first approach
- ❌ Doesn't scale across distributed systems
- ❌ Violates service separation
Decision: Rejected - violates microservices principles
Decision Rationale
Why Service Token with Impersonation?
-
Affordable ✅
- Minimal changes to iDempiere (one new endpoint)
- Reuses existing JWT infrastructure
- No OAuth2 server needed
-
Secure ✅
- M2M authentication (chat service authenticates, not user password)
- Scoped permissions (limit what hub can do)
- Clear audit trail (service + impersonated user)
-
Scalable ✅
- Works across distributed systems
- Supports multiple chat instances
- Token-based (stateless)
-
Auditable ✅
- Logs show: "service:idempiere-chat on behalf of user 12345"
- Clear separation of service vs user actions
- Compliance-friendly
-
Developer-Friendly ✅
- Standard JWT pattern
- Well-understood M2M flow
- Easy to test and debug
Implementation Phases
Phase 1: Prototype with X-Headers (1 week)
- Use JWT delegation via X-Headers
- Validate concept
- Test user flows
- Goal: Prove delegated auth works
Phase 2: Production with Service Tokens (2 weeks)
- Implement M2M token generation
- Add
/auth/service-tokensendpoint to iDempiere - Update iDempiere Chat to use service tokens
- Add audit logging
- Goal: Production-ready delegated auth
Phase 3: Enhanced Security (future)
- Add scope-based permissions
- Implement token rotation
- Add rate limiting
- Goal: Enterprise-grade security
Consequences
Positive
- ✅ No password prompts - Users authenticate once in iDempiere Chat
- ✅ Secure M2M - Chat service authenticates, not user credentials
- ✅ User context preserved - Tenant, role, org carried through token
- ✅ Clear audit trail - Service + impersonated user in logs
- ✅ Affordable - Minimal infrastructure changes
- ✅ Scalable - Token-based, works across distributed systems
- ✅ Testable - Standard JWT validation, easy to mock
Negative
- ❌ New endpoint required - Must add
/auth/service-tokensto iDempiere - ❌ M2M token management - Must securely store chat service M2M token
- ❌ Token expiration - Need to handle refresh (3600s default)
- ❌ Scope system - Need to implement permission scopes
- ❌ Breaking change - Requires iDempiere 11+ with JWT support
Neutral
- ⚪ Not OAuth2 - Simpler but less standard than OAuth2
- ⚪ Service-specific - Designed for iDempiere Chat, not generic OAuth2 clients
Implementation Checklist
iDempiere Backend
- [ ] Add
AD_User_Permissiontable for service permissions - [ ] Add
/auth/service-tokensendpoint - [ ] Implement M2M token validation
- [ ] Add impersonation permission checks
- [ ] Implement JWT generation with impersonation claims
- [ ] Add audit logging for impersonated operations
iDempiere Chat
- [ ] Generate and store M2M token for chat service
- [ ] Add backend endpoint
/api/hub/connect - [ ] Call iDempiere
/auth/service-tokensendpoint - [ ] Pass service token to frontend
- [ ] Configure MCP client with service token
iDempiere Hub
- [ ] Add service token validation filter
- [ ] Extract impersonated user ID from claims
- [ ] Set tenant context from token claims
- [ ] Add audit logging (service + user)
- [ ] Document MCP configuration with service tokens
Testing
- [ ] Unit tests for token validation
- [ ] Integration tests for delegated auth flow
- [ ] Security tests (invalid tokens, tenant mismatch)
- [ ] Audit log verification
- [ ] Performance tests (token generation latency)
Documentation
- [ ] Update DELEGATED.md
- [ ] Update MCP server configuration guide
- [ ] Add deployment guide (M2M token generation)
- [ ] Add troubleshooting guide
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:
-
Phase 1: Both methods supported
- Old: Direct password authentication
- New: Delegated service token authentication
-
Phase 2: Deprecate password authentication for chat users
- Show warning: "Please connect via iDempiere Chat"
-
Phase 3: Remove password prompt for chat-originated requests
- Detect origin via referrer or service token
- Redirect to delegated auth flow
Security Considerations
-
M2M Token Storage
- Store in environment variables or secrets manager
- Never commit to version control
- Rotate periodically (every 90 days)
-
Service Token Scopes
- Request minimal scopes needed
- Validate scopes on each operation
- Deny by default
-
Token Expiration
- Short-lived service tokens (3600s = 1 hour)
- Implement refresh flow in chat backend
- Auto-refresh before expiration
-
Audit Logging
- Log all service token generations
- Log all impersonated operations
- Include service name, user ID, tenant, operation
- Monitor for suspicious patterns
-
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:
idempiere.auth.service_tokens.generated- Service tokens generatedidempiere.auth.service_tokens.validated- Service tokens validatedidempiere.auth.service_tokens.failed- Failed validationsidempiere.auth.impersonation.operations- Operations on behalf of usersidempiere.auth.service_tokens.latency- Token generation latency
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
- DELEGATED.md - Complete implementation guide
- STRATEGY.md - Base authentication architecture
- OAuth 2.0 RFC 6749 - OAuth2 specification
- JWT RFC 7519 - JWT specification
- ADR-010: MCP Server Architecture
License
See LICENSE for details.