fix(astrolabe): fix OAuth flow and settings UI for hybrid mode
In hybrid mode (multi_user_basic + offline_access), users need BOTH: - OAuth token for Astrolabe→MCP API calls - App password for MCP→Nextcloud background sync Changes: - Personal.php: Pass correct oauthUrl pointing to Astrolabe's OAuth controller instead of MCP server's browser OAuth. Check both OAuth token AND app password status in hybrid mode. - personal.php template: Show two-step workflow UI requiring both credentials before showing "Active" status. Each step shows completion badges. - IdpTokenRefresher.php: Use http://localhost for internal token refresh requests (consistent with OAuthController). External URLs like localhost:8080 don't work from inside the container. Fixes 401 errors when searching in Astrolabe with hybrid deployment. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
f16f852b23
commit
c95459234b
+11
-17
@@ -38,25 +38,19 @@ class IdpTokenRefresher {
|
|||||||
/**
|
/**
|
||||||
* Get Nextcloud base URL for constructing internal OIDC endpoint URLs.
|
* Get Nextcloud base URL for constructing internal OIDC endpoint URLs.
|
||||||
*
|
*
|
||||||
* @return string Base URL (e.g., "https://nextcloud.example.com")
|
* IMPORTANT: This method returns the INTERNAL URL for server-to-server
|
||||||
|
* requests within the container. External URLs (like localhost:8080) won't
|
||||||
|
* work from inside the container since localhost refers to the container itself.
|
||||||
|
*
|
||||||
|
* For internal requests, we always use http://localhost (port 80) since
|
||||||
|
* Nextcloud's web server is accessible at that address inside the container.
|
||||||
|
*
|
||||||
|
* @return string Base URL for internal requests (e.g., "http://localhost")
|
||||||
*/
|
*/
|
||||||
private function getNextcloudBaseUrl(): string {
|
private function getNextcloudBaseUrl(): string {
|
||||||
// Prefer explicit CLI URL override
|
// For internal requests within the container, always use http://localhost
|
||||||
$baseUrl = $this->config->getSystemValue('overwrite.cli.url', '');
|
// The web server is accessible at port 80 inside the container.
|
||||||
|
// External URLs like http://localhost:8080 won't work from inside the container.
|
||||||
if (!empty($baseUrl)) {
|
|
||||||
return rtrim($baseUrl, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to first trusted domain with protocol
|
|
||||||
$trustedDomains = $this->config->getSystemValue('trusted_domains', []);
|
|
||||||
if (!empty($trustedDomains)) {
|
|
||||||
$protocol = $this->config->getSystemValue('overwriteprotocol', 'https');
|
|
||||||
return $protocol . '://' . $trustedDomains[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Last resort: localhost (log warning)
|
|
||||||
$this->logger->warning('IdpTokenRefresher: No Nextcloud URL configured, using localhost fallback');
|
|
||||||
return 'http://localhost';
|
return 'http://localhost';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+38
-50
@@ -79,60 +79,48 @@ class Personal implements ISettings {
|
|||||||
// Check if user has MCP OAuth token
|
// Check if user has MCP OAuth token
|
||||||
$token = $this->tokenStorage->getUserToken($userId);
|
$token = $this->tokenStorage->getUserToken($userId);
|
||||||
|
|
||||||
// For multi_user_basic mode with app password support, check if user has app password
|
// For multi_user_basic mode with app password support (hybrid mode)
|
||||||
|
// User needs BOTH:
|
||||||
|
// 1. OAuth token for Astrolabe→MCP API calls (stored in McpTokenStorage)
|
||||||
|
// 2. App password for MCP→Nextcloud background sync
|
||||||
if ($authMode === 'multi_user_basic' && $supportsAppPasswords) {
|
if ($authMode === 'multi_user_basic' && $supportsAppPasswords) {
|
||||||
// Check if user has already provided an app password
|
// Check both credentials
|
||||||
$hasBackgroundAccess = $this->tokenStorage->hasBackgroundSyncAccess($userId);
|
$hasOAuthToken = ($token !== null && !$this->tokenStorage->isExpired($token));
|
||||||
|
$hasAppPassword = $this->tokenStorage->hasBackgroundSyncAccess($userId);
|
||||||
|
$backgroundSyncType = $this->tokenStorage->getBackgroundSyncType($userId);
|
||||||
|
$backgroundSyncProvisionedAt = $this->tokenStorage->getBackgroundSyncProvisionedAt($userId);
|
||||||
|
|
||||||
if (!$hasBackgroundAccess) {
|
// OAuth URL for Astrolabe's own OAuth controller (NOT MCP server's browser OAuth)
|
||||||
// No app password yet - show app password entry form
|
$oauthUrl = $this->urlGenerator->linkToRoute('astrolabe.oauth.initiateOAuth');
|
||||||
return new TemplateResponse(
|
|
||||||
Application::APP_ID,
|
|
||||||
'settings/personal',
|
|
||||||
[
|
|
||||||
'serverUrl' => $this->client->getPublicServerUrl(), // Changed from server_url to serverUrl
|
|
||||||
'serverStatus' => $serverStatus,
|
|
||||||
'auth_mode' => $authMode,
|
|
||||||
'authMode' => $authMode, // Add camelCase version for template
|
|
||||||
'supports_app_passwords' => $supportsAppPasswords,
|
|
||||||
'supportsAppPasswords' => $supportsAppPasswords, // Add camelCase version
|
|
||||||
'session' => null, // No session yet
|
|
||||||
'hasBackgroundAccess' => false, // FIXED: Add missing parameter
|
|
||||||
'backgroundAccessGranted' => false, // FIXED: Add missing parameter
|
|
||||||
'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false,
|
|
||||||
'hasToken' => false, // No OAuth token in multi_user_basic mode
|
|
||||||
'requesttoken' => \OCP\Util::callRegister(),
|
|
||||||
],
|
|
||||||
TemplateResponse::RENDER_AS_BLANK
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// User has app password - show active status
|
|
||||||
$backgroundSyncType = $this->tokenStorage->getBackgroundSyncType($userId);
|
|
||||||
$backgroundSyncProvisionedAt = $this->tokenStorage->getBackgroundSyncProvisionedAt($userId);
|
|
||||||
|
|
||||||
$parameters = [
|
$parameters = [
|
||||||
'userId' => $userId,
|
'userId' => $userId,
|
||||||
'serverStatus' => $serverStatus,
|
'serverUrl' => $this->client->getPublicServerUrl(),
|
||||||
'session' => null, // No user session for app passwords
|
'serverStatus' => $serverStatus,
|
||||||
'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false,
|
'auth_mode' => $authMode,
|
||||||
'backgroundAccessGranted' => true, // App password grants background access
|
'authMode' => $authMode,
|
||||||
'serverUrl' => $this->client->getPublicServerUrl(),
|
'supports_app_passwords' => $supportsAppPasswords,
|
||||||
'hasToken' => false, // No OAuth token
|
'supportsAppPasswords' => $supportsAppPasswords,
|
||||||
'hasBackgroundAccess' => true,
|
'session' => null, // No session in hybrid mode
|
||||||
'backgroundSyncType' => $backgroundSyncType,
|
'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false,
|
||||||
'backgroundSyncProvisionedAt' => $backgroundSyncProvisionedAt,
|
// OAuth token status (for Astrolabe→MCP API calls)
|
||||||
'authMode' => $authMode,
|
'hasToken' => $hasOAuthToken,
|
||||||
'supportsAppPasswords' => $supportsAppPasswords,
|
'hasOAuthToken' => $hasOAuthToken,
|
||||||
'requesttoken' => \OCP\Util::callRegister(),
|
'oauthUrl' => $oauthUrl,
|
||||||
];
|
// App password status (for MCP→Nextcloud background sync)
|
||||||
|
'hasBackgroundAccess' => $hasAppPassword,
|
||||||
|
'backgroundAccessGranted' => $hasAppPassword,
|
||||||
|
'backgroundSyncType' => $backgroundSyncType,
|
||||||
|
'backgroundSyncProvisionedAt' => $backgroundSyncProvisionedAt,
|
||||||
|
'requesttoken' => \OCP\Util::callRegister(),
|
||||||
|
];
|
||||||
|
|
||||||
return new TemplateResponse(
|
return new TemplateResponse(
|
||||||
Application::APP_ID,
|
Application::APP_ID,
|
||||||
'settings/personal',
|
'settings/personal',
|
||||||
$parameters,
|
$parameters,
|
||||||
TemplateResponse::RENDER_AS_BLANK
|
TemplateResponse::RENDER_AS_BLANK
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// For OAuth modes, if no token or token is expired, show OAuth authorization UI
|
// For OAuth modes, if no token or token is expired, show OAuth authorization UI
|
||||||
elseif (!$token || $this->tokenStorage->isExpired($token)) {
|
elseif (!$token || $this->tokenStorage->isExpired($token)) {
|
||||||
|
|||||||
+126
-41
@@ -43,7 +43,17 @@ style('astrolabe', 'astrolabe-personalSettings');
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<h2><?php p($l->t('Background Sync Access')); ?></h2>
|
<h2><?php p($l->t('Background Sync Access')); ?></h2>
|
||||||
|
|
||||||
<?php if ($_['hasBackgroundAccess'] || $_['backgroundAccessGranted']): ?>
|
<?php
|
||||||
|
// In hybrid mode (multi_user_basic + app passwords), user needs BOTH OAuth AND app password
|
||||||
|
// to be "fully configured". Check both credentials in hybrid mode.
|
||||||
|
$isHybridMode = isset($_['authMode']) && $_['authMode'] === 'multi_user_basic' && !empty($_['supportsAppPasswords']);
|
||||||
|
$hasOAuthToken = !empty($_['hasOAuthToken']);
|
||||||
|
$hasBackgroundAccess = $_['hasBackgroundAccess'] || $_['backgroundAccessGranted'];
|
||||||
|
|
||||||
|
// In hybrid mode, both credentials required; otherwise just background access
|
||||||
|
$isFullyConfigured = $isHybridMode ? ($hasOAuthToken && $hasBackgroundAccess) : $hasBackgroundAccess;
|
||||||
|
?>
|
||||||
|
<?php if ($isFullyConfigured): ?>
|
||||||
<!-- Already configured -->
|
<!-- Already configured -->
|
||||||
<div class="mcp-background-status">
|
<div class="mcp-background-status">
|
||||||
<p>
|
<p>
|
||||||
@@ -110,54 +120,129 @@ style('astrolabe', 'astrolabe-personalSettings');
|
|||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<!-- Not configured - show provisioning options -->
|
<!-- Not configured - show provisioning options -->
|
||||||
<p class="mcp-help-text">
|
<?php if (isset($_['authMode']) && $_['authMode'] === 'multi_user_basic' && !empty($_['supportsAppPasswords'])): ?>
|
||||||
<?php p($l->t('Enable background sync to allow the MCP server to access your Nextcloud data for background operations like content indexing.')); ?>
|
<!-- Hybrid mode: User needs BOTH OAuth AND app password -->
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="mcp-grant-section">
|
|
||||||
<h4><?php p($l->t('Option 1: OAuth Refresh Token (Recommended for Future)')); ?></h4>
|
|
||||||
<p class="mcp-help-text">
|
<p class="mcp-help-text">
|
||||||
<?php p($l->t('When Nextcloud fully supports OAuth for app APIs. Currently waiting for upstream PR to merge.')); ?>
|
<?php p($l->t('To use semantic search, you need to complete two setup steps:')); ?>
|
||||||
</p>
|
|
||||||
<a href="<?php p($_['serverUrl']); ?>/oauth/login?next=<?php p(urlencode($urlGenerator->getAbsoluteURL($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'astrolabe'])))); ?>" class="button">
|
|
||||||
<span class="icon icon-confirm"></span>
|
|
||||||
<?php p($l->t('Authorize via OAuth')); ?>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mcp-grant-section">
|
|
||||||
<h4><?php p($l->t('Option 2: App Password (Works Today - Recommended)')); ?></h4>
|
|
||||||
<p class="mcp-help-text">
|
|
||||||
<?php p($l->t('Generate an app password in Security settings and provide it below. This is the recommended interim solution.')); ?>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mcp-app-password-steps">
|
<!-- Step 1: OAuth Authorization (for Astrolabe→MCP API calls) -->
|
||||||
<p><strong><?php p($l->t('Step 1:')); ?></strong>
|
<div class="mcp-grant-section">
|
||||||
<a href="<?php p($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'security'])); ?>" target="_blank">
|
<h4>
|
||||||
<?php p($l->t('Generate app password in Security settings')); ?>
|
<?php if (!empty($_['hasOAuthToken'])): ?>
|
||||||
|
<span class="badge badge-success"><span class="icon icon-checkmark-white"></span> <?php p($l->t('Complete')); ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge badge-warning"><?php p($l->t('Required')); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php p($l->t('Step 1: Authorize Search Access')); ?>
|
||||||
|
</h4>
|
||||||
|
<p class="mcp-help-text">
|
||||||
|
<?php p($l->t('Authorize Astrolabe to perform searches on your behalf.')); ?>
|
||||||
|
</p>
|
||||||
|
<?php if (empty($_['hasOAuthToken'])): ?>
|
||||||
|
<a href="<?php p($_['oauthUrl']); ?>" class="button primary">
|
||||||
|
<span class="icon icon-confirm"></span>
|
||||||
|
<?php p($l->t('Authorize')); ?>
|
||||||
</a>
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<p><span class="icon icon-checkmark"></span> <?php p($l->t('Search access authorized.')); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: App Password (for MCP→Nextcloud background sync) -->
|
||||||
|
<div class="mcp-grant-section">
|
||||||
|
<h4>
|
||||||
|
<?php if (!empty($_['hasBackgroundAccess'])): ?>
|
||||||
|
<span class="badge badge-success"><span class="icon icon-checkmark-white"></span> <?php p($l->t('Complete')); ?></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge badge-warning"><?php p($l->t('Required')); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php p($l->t('Step 2: Enable Background Indexing')); ?>
|
||||||
|
</h4>
|
||||||
|
<p class="mcp-help-text">
|
||||||
|
<?php p($l->t('Provide an app password to allow background indexing of your content.')); ?>
|
||||||
|
</p>
|
||||||
|
<?php if (empty($_['hasBackgroundAccess'])): ?>
|
||||||
|
<div class="mcp-app-password-steps">
|
||||||
|
<p>
|
||||||
|
<a href="<?php p($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'security'])); ?>" target="_blank">
|
||||||
|
<?php p($l->t('Generate app password in Security settings')); ?>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.credentials.storeAppPassword')); ?>" id="mcp-app-password-form">
|
||||||
|
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
|
||||||
|
<div class="mcp-input-group">
|
||||||
|
<input type="password" name="appPassword" id="mcp-app-password-input"
|
||||||
|
placeholder="xxxxx-xxxxx-xxxxx-xxxxx-xxxxx"
|
||||||
|
pattern="[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}"
|
||||||
|
required>
|
||||||
|
<button type="submit" class="button primary" id="mcp-save-app-password-button">
|
||||||
|
<span class="icon icon-checkmark"></span>
|
||||||
|
<?php p($l->t('Save')); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="mcp-help-text">
|
||||||
|
<?php p($l->t('The app password will be validated and securely encrypted before storage.')); ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<p><span class="icon icon-checkmark"></span> <?php p($l->t('Background indexing enabled.')); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<!-- Standard OAuth or BasicAuth mode -->
|
||||||
|
<p class="mcp-help-text">
|
||||||
|
<?php p($l->t('Enable background sync to allow the MCP server to access your Nextcloud data for background operations like content indexing.')); ?>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mcp-grant-section">
|
||||||
|
<h4><?php p($l->t('Option 1: OAuth Refresh Token (Recommended for Future)')); ?></h4>
|
||||||
|
<p class="mcp-help-text">
|
||||||
|
<?php p($l->t('When Nextcloud fully supports OAuth for app APIs. Currently waiting for upstream PR to merge.')); ?>
|
||||||
|
</p>
|
||||||
|
<a href="<?php p($_['oauthUrl'] ?? ($_['serverUrl'] . '/oauth/login?next=' . urlencode($urlGenerator->getAbsoluteURL($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'astrolabe']))))); ?>" class="button">
|
||||||
|
<span class="icon icon-confirm"></span>
|
||||||
|
<?php p($l->t('Authorize via OAuth')); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mcp-grant-section">
|
||||||
|
<h4><?php p($l->t('Option 2: App Password (Works Today - Recommended)')); ?></h4>
|
||||||
|
<p class="mcp-help-text">
|
||||||
|
<?php p($l->t('Generate an app password in Security settings and provide it below. This is the recommended interim solution.')); ?>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p><strong><?php p($l->t('Step 2:')); ?></strong> <?php p($l->t('Enter the app password below:')); ?></p>
|
<div class="mcp-app-password-steps">
|
||||||
|
<p><strong><?php p($l->t('Step 1:')); ?></strong>
|
||||||
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.credentials.storeAppPassword')); ?>" id="mcp-app-password-form">
|
<a href="<?php p($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'security'])); ?>" target="_blank">
|
||||||
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
|
<?php p($l->t('Generate app password in Security settings')); ?>
|
||||||
<div class="mcp-input-group">
|
</a>
|
||||||
<input type="password" name="appPassword" id="mcp-app-password-input"
|
|
||||||
placeholder="xxxxx-xxxxx-xxxxx-xxxxx-xxxxx"
|
|
||||||
pattern="[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}"
|
|
||||||
required>
|
|
||||||
<button type="submit" class="button primary" id="mcp-save-app-password-button">
|
|
||||||
<span class="icon icon-checkmark"></span>
|
|
||||||
<?php p($l->t('Save')); ?>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="mcp-help-text">
|
|
||||||
<?php p($l->t('The app password will be validated and securely encrypted before storage.')); ?>
|
|
||||||
</p>
|
</p>
|
||||||
</form>
|
|
||||||
|
<p><strong><?php p($l->t('Step 2:')); ?></strong> <?php p($l->t('Enter the app password below:')); ?></p>
|
||||||
|
|
||||||
|
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.credentials.storeAppPassword')); ?>" id="mcp-app-password-form">
|
||||||
|
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
|
||||||
|
<div class="mcp-input-group">
|
||||||
|
<input type="password" name="appPassword" id="mcp-app-password-input"
|
||||||
|
placeholder="xxxxx-xxxxx-xxxxx-xxxxx-xxxxx"
|
||||||
|
pattern="[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}"
|
||||||
|
required>
|
||||||
|
<button type="submit" class="button primary" id="mcp-save-app-password-button">
|
||||||
|
<span class="icon icon-checkmark"></span>
|
||||||
|
<?php p($l->t('Save')); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="mcp-help-text">
|
||||||
|
<?php p($l->t('The app password will be validated and securely encrypted before storage.')); ?>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user