fix(auth): Store app passwords locally for multi-user BasicAuth background sync
Previously, the multi-user BasicAuth mode attempted to retrieve app passwords
via OAuth client_credentials grant, which Nextcloud OIDC doesn't support.
This fix implements local storage for app passwords:
- Add app_passwords table via Alembic migration (002)
- Add store/get/delete methods to RefreshTokenStorage
- Add management API endpoints for app password provisioning:
- POST /api/v1/users/{user_id}/app-password
- GET /api/v1/users/{user_id}/app-password
- DELETE /api/v1/users/{user_id}/app-password
- Update oauth_sync.py to read from local storage
- Update Astrolabe to send app passwords to MCP server after validation
- Add app-hook to configure mcp_server_url in Nextcloud
The flow is now:
1. User creates app password in Nextcloud Security settings
2. User enters it in Astrolabe Personal Settings
3. Astrolabe validates against Nextcloud, then sends to MCP server
4. MCP server stores encrypted app password locally
5. Background sync uses locally stored password
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,24 +94,78 @@ class CredentialsController extends Controller {
|
||||
], Http::STATUS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Store encrypted app password
|
||||
// Store encrypted app password locally in Nextcloud
|
||||
try {
|
||||
$this->tokenStorage->storeBackgroundSyncPassword($userId, $appPassword);
|
||||
$this->logger->info("Successfully stored app password for user: $userId");
|
||||
|
||||
return new JSONResponse([
|
||||
'success' => true,
|
||||
'message' => 'App password saved successfully'
|
||||
], Http::STATUS_OK);
|
||||
$this->logger->info("Stored app password locally for user: $userId");
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Failed to store app password for user $userId", [
|
||||
$this->logger->error("Failed to store app password locally for user $userId", [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return new JSONResponse([
|
||||
'success' => false,
|
||||
'error' => 'Failed to save app password'
|
||||
'error' => 'Failed to save app password locally'
|
||||
], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
// Send app password to MCP server for background sync
|
||||
// Get MCP server URL from system config (set in config.php)
|
||||
$mcpServerUrl = $this->config->getSystemValue('mcp_server_url', '');
|
||||
if (empty($mcpServerUrl)) {
|
||||
$this->logger->warning("MCP server URL not configured, app password stored locally only");
|
||||
return new JSONResponse([
|
||||
'success' => true,
|
||||
'message' => 'App password saved locally (MCP server not configured)'
|
||||
], Http::STATUS_OK);
|
||||
}
|
||||
|
||||
try {
|
||||
$httpClient = $this->httpClientService->newClient();
|
||||
|
||||
// Send to MCP server with BasicAuth (user proves ownership of password)
|
||||
$mcpEndpoint = rtrim($mcpServerUrl, '/') . '/api/v1/users/' . urlencode($userId) . '/app-password';
|
||||
|
||||
$this->logger->debug("Sending app password to MCP server: $mcpEndpoint");
|
||||
|
||||
$response = $httpClient->post($mcpEndpoint, [
|
||||
'auth' => [$userId, $appPassword],
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
],
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
$body = json_decode($response->getBody(), true);
|
||||
|
||||
if ($statusCode === 200 && ($body['success'] ?? false)) {
|
||||
$this->logger->info("Successfully provisioned app password to MCP server for user: $userId");
|
||||
return new JSONResponse([
|
||||
'success' => true,
|
||||
'message' => 'App password saved successfully'
|
||||
], Http::STATUS_OK);
|
||||
} else {
|
||||
$error = $body['error'] ?? 'Unknown error';
|
||||
$this->logger->error("MCP server rejected app password for user $userId: $error");
|
||||
// Still return success since it was stored locally
|
||||
return new JSONResponse([
|
||||
'success' => true,
|
||||
'message' => 'App password saved locally (MCP server sync failed)',
|
||||
'warning' => $error
|
||||
], Http::STATUS_OK);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error("Failed to send app password to MCP server for user $userId", [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
// Still return success since it was stored locally
|
||||
return new JSONResponse([
|
||||
'success' => true,
|
||||
'message' => 'App password saved locally (MCP server unreachable)',
|
||||
'warning' => $e->getMessage()
|
||||
], Http::STATUS_OK);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user