Skip to main content
Glama
cbcoutinho

Nextcloud MCP Server

by cbcoutinho
IdpTokenRefresher.php5.31 kB
<?php declare(strict_types=1); namespace OCA\Astrolabe\Service; use OCP\Http\Client\IClientService; use OCP\IConfig; use Psr\Log\LoggerInterface; /** * Refreshes OAuth tokens directly with the Identity Provider. * * Works with both Nextcloud OIDC and external IdPs like Keycloak. * Uses OIDC discovery to find the token endpoint automatically. * * This service is only used for confidential clients (with client_secret). * Public clients without client_secret cannot refresh tokens. */ class IdpTokenRefresher { private $config; private $httpClient; private $logger; private $mcpServerClient; public function __construct( IConfig $config, IClientService $clientService, LoggerInterface $logger, McpServerClient $mcpServerClient, ) { $this->config = $config; $this->httpClient = $clientService->newClient(); $this->logger = $logger; $this->mcpServerClient = $mcpServerClient; } /** * Get Nextcloud base URL for constructing internal OIDC endpoint URLs. * * @return string Base URL (e.g., "https://nextcloud.example.com") */ private function getNextcloudBaseUrl(): string { // Prefer explicit CLI URL override $baseUrl = $this->config->getSystemValue('overwrite.cli.url', ''); 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'; } /** * Refresh access token using refresh token. * * Calls IdP's token endpoint directly (NOT MCP server). * * @param string $refreshToken The refresh token * @return array|null New token data or null on failure */ public function refreshAccessToken(string $refreshToken): ?array { // Check if confidential client secret is configured $clientSecret = $this->config->getSystemValue('astrolabe_client_secret', ''); if (empty($clientSecret)) { $this->logger->warning('Cannot refresh: no client secret configured. Confidential client required for token refresh.'); return null; } try { // Get MCP server URL $mcpServerUrl = $this->config->getSystemValue('mcp_server_url', ''); if (empty($mcpServerUrl)) { throw new \Exception('MCP server URL not configured'); } // Query MCP server to discover which IdP it's configured to use $statusResponse = $this->httpClient->get($mcpServerUrl . '/api/v1/status'); $statusData = json_decode($statusResponse->getBody(), true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \RuntimeException('Invalid status response from MCP server'); } // Determine OIDC discovery URL and token endpoint $useInternalNextcloud = !isset($statusData['oidc']['discovery_url']); if (!$useInternalNextcloud) { // External IdP configured - use OIDC discovery $discoveryUrl = $statusData['oidc']['discovery_url']; $this->logger->info('IdpTokenRefresher: Using external IdP', [ 'discovery_url' => $discoveryUrl, ]); $discoveryResponse = $this->httpClient->get($discoveryUrl); $discovery = json_decode($discoveryResponse->getBody(), true); if (json_last_error() !== JSON_ERROR_NONE || !isset($discovery['token_endpoint'])) { throw new \RuntimeException('Invalid OIDC discovery response'); } $tokenEndpoint = $discovery['token_endpoint']; } else { // Nextcloud's OIDC app - use internal URL $tokenEndpoint = $this->getNextcloudBaseUrl() . '/apps/oidc/token'; $this->logger->info('IdpTokenRefresher: Using Nextcloud OIDC app', [ 'token_endpoint' => $tokenEndpoint, ]); } // Call IdP's token endpoint with refresh_token grant $postData = [ 'grant_type' => 'refresh_token', 'refresh_token' => $refreshToken, 'client_id' => $this->mcpServerClient->getClientId(), 'client_secret' => $clientSecret, ]; $this->logger->info('IdpTokenRefresher: Requesting token refresh'); $response = $this->httpClient->post($tokenEndpoint, [ 'body' => http_build_query($postData), 'headers' => [ 'Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json', ], ]); $tokenData = json_decode($response->getBody(), true); if (json_last_error() !== JSON_ERROR_NONE || !isset($tokenData['access_token'])) { throw new \RuntimeException('Invalid token response from IdP'); } // Validate refresh_token is present (required for token rotation) if (!isset($tokenData['refresh_token'])) { $this->logger->error( 'IdpTokenRefresher: No refresh token in response - token rotation will fail', [ 'has_access_token' => isset($tokenData['access_token']), 'response_keys' => array_keys($tokenData), ] ); return null; } $this->logger->info('IdpTokenRefresher: Token refresh successful'); return $tokenData; } catch (\Exception $e) { $this->logger->error('IdpTokenRefresher: Token refresh failed', [ 'error' => $e->getMessage(), ]); return null; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cbcoutinho/nextcloud-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server