From cc2a2ddca3926230d5ff529483af61d3e1a1f6ef Mon Sep 17 00:00:00 2001 From: jubnl Date: Fri, 10 Apr 2026 06:22:37 +0200 Subject: [PATCH] remove(oauth): drop browser-initiated DCR registration flow OAuthRegisterPage and its server routes (GET /api/oauth/register/validate, POST /api/oauth/register) are superseded by the RFC 7591 machine-to-machine DCR endpoint (POST /oauth/register). Claude.ai and compliant MCP clients register via RFC 7591, then go through the standard /oauth/authorize consent screen for scope selection. --- client/src/App.tsx | 2 - client/src/api/client.ts | 9 -- client/src/pages/OAuthRegisterPage.tsx | 214 ------------------------- server/src/routes/oauth.ts | 70 -------- 4 files changed, 295 deletions(-) delete mode 100644 client/src/pages/OAuthRegisterPage.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index a97034f2..06640508 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,7 +13,6 @@ import AtlasPage from './pages/AtlasPage' import SharedTripPage from './pages/SharedTripPage' import InAppNotificationsPage from './pages/InAppNotificationsPage.tsx' import OAuthAuthorizePage from './pages/OAuthAuthorizePage' -import OAuthRegisterPage from './pages/OAuthRegisterPage' import { ToastContainer } from './components/shared/Toast' import { TranslationProvider, useTranslation } from './i18n' import { authApi } from './api/client' @@ -167,7 +166,6 @@ export default function App() { } /> {/* OAuth 2.1 consent page — intentionally outside ProtectedRoute */} } /> - } /> apiClient.post('/oauth/authorize', body).then(r => r.data), - register: { - /** Validate DCR params — called by registration page on load */ - validate: (params: { redirect_uri: string; client_name?: string; scope?: string; state?: string }) => - apiClient.get('/oauth/register/validate', { params }).then(r => r.data), - /** Submit registration approval or cancellation */ - submit: (body: { client_name: string; redirect_uri: string; scopes: string[]; state?: string; approved: boolean }) => - apiClient.post('/oauth/register', body).then(r => r.data), - }, - clients: { list: () => apiClient.get('/oauth/clients').then(r => r.data), create: (data: { name: string; redirect_uris: string[]; allowed_scopes: string[] }) => diff --git a/client/src/pages/OAuthRegisterPage.tsx b/client/src/pages/OAuthRegisterPage.tsx deleted file mode 100644 index 64bfbd39..00000000 --- a/client/src/pages/OAuthRegisterPage.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import React, { useEffect, useState } from 'react' -import { useAuthStore } from '../store/authStore' -import { oauthApi } from '../api/client' -import { ALL_SCOPES } from '../api/oauthScopes' -import ScopeGroupPicker from '../components/OAuth/ScopeGroupPicker' -import { Lock, ShieldCheck, AlertTriangle, Loader2, LogIn } from 'lucide-react' - -interface ValidateResult { - valid: boolean - error?: string - error_description?: string - client_name?: string - requested_scopes?: string[] - loginRequired?: boolean -} - -type PageState = 'loading' | 'login_required' | 'ready' | 'error' | 'done' - -export default function OAuthRegisterPage(): React.ReactElement { - const { isLoading: authLoading, loadUser } = useAuthStore() - const [pageState, setPageState] = useState('loading') - const [validation, setValidation] = useState(null) - const [selectedScopes, setSelectedScopes] = useState([]) - const [submitting, setSubmitting] = useState(false) - const [errorMsg, setErrorMsg] = useState(null) - - const params = new URLSearchParams(window.location.search) - const redirectUri = params.get('redirect_uri') || '' - const clientName = params.get('client_name') || '' - const scope = params.get('scope') || '' - const state = params.get('state') || '' - - useEffect(() => { - loadUser({ silent: true }).catch(() => {}) - }, [loadUser]) - - useEffect(() => { - if (authLoading) return - validateRequest() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [authLoading]) - - async function validateRequest() { - setPageState('loading') - try { - const result: ValidateResult = await oauthApi.register.validate({ - redirect_uri: redirectUri, - client_name: clientName, - scope, - state, - }) - setValidation(result) - - if (!result.valid) { - setPageState('error') - setErrorMsg(result.error_description || result.error || 'Invalid registration request') - return - } - - if (result.loginRequired) { - setPageState('login_required') - return - } - - // Pre-check the scopes the client requested; fall back to read-only defaults - const requested = result.requested_scopes ?? [] - setSelectedScopes( - requested.length > 0 - ? requested - : ALL_SCOPES.filter(s => s.endsWith(':read')), - ) - setPageState('ready') - } catch { - setPageState('error') - setErrorMsg('Failed to validate registration request. Please try again.') - } - } - - function handleLoginRedirect() { - const next = '/oauth/register?' + params.toString() - window.location.href = '/login?redirect=' + encodeURIComponent(next) - } - - async function submitRegistration(approved: boolean) { - setSubmitting(true) - try { - const result = await oauthApi.register.submit({ - client_name: validation?.client_name || clientName || 'MCP Client', - redirect_uri: redirectUri, - scopes: approved ? selectedScopes : [], - state, - approved, - }) - setPageState('done') - window.location.href = result.redirect - } catch { - setPageState('error') - setErrorMsg('Registration failed. Please try again.') - setSubmitting(false) - } - } - - // ---- Render states ---- - - if (pageState === 'loading' || pageState === 'done') { - return ( -
-
- -

Loading…

-
-
- ) - } - - if (pageState === 'error') { - return ( -
-
- -

Registration Error

-

{errorMsg}

-
-
- ) - } - - if (pageState === 'login_required') { - return ( -
-
-
- -

Sign in to continue

-

- {clientName || 'This application'} wants to register for access to your TREK account. Please sign in first. -

-
- -
-
- ) - } - - // pageState === 'ready' - const displayName = validation?.client_name || clientName || 'MCP Client' - - return ( -
-
- - {/* Left panel — identity + actions */} -
-
-
- -
-
-

Client Registration

-

- {displayName} -

-

- This application wants to access your TREK account. Choose which permissions to grant. -

-
- -
-

Will redirect to

-

{redirectUri}

-
-
- -
-

- Only grant access to applications you trust. You can revoke this at any time in Settings. -

- - -
-
- - {/* Right panel — scope picker */} -
-
-

- Select permissions -

- -
-
- -
-
- ) -} diff --git a/server/src/routes/oauth.ts b/server/src/routes/oauth.ts index bc58e2a4..f76b42cb 100644 --- a/server/src/routes/oauth.ts +++ b/server/src/routes/oauth.ts @@ -358,76 +358,6 @@ oauthApiRouter.post('/authorize', requireCookieAuth, (req: Request, res: Respons return res.json({ redirect: url.toString() }); }); -// ---- Browser-initiated dynamic client registration ---- - -// SPA calls this on load to validate DCR params before rendering scope selection UI -oauthApiRouter.get('/register/validate', validateLimiter, optionalAuth, (req: Request, res: Response) => { - if (!isAddonEnabled(ADDON_IDS.MCP)) return res.status(404).end(); - - const { redirect_uri, client_name, scope } = req.query as Record; - const userId = (req as OptionalAuthRequest).user?.id ?? null; - - if (!redirect_uri) { - return res.json({ valid: false, error: 'invalid_request', error_description: 'redirect_uri is required' }); - } - - if (!isValidRedirectUri(redirect_uri)) { - return res.json({ valid: false, error: 'invalid_redirect_uri', error_description: 'redirect_uri must use HTTPS (localhost is exempt)' }); - } - - // Anti-fingerprinting: don't expose details to unauthenticated callers - if (userId === null) { - return res.json({ valid: true, loginRequired: true }); - } - - const resolvedName = (client_name || '').trim().slice(0, 100) || 'MCP Client'; - const requestedScopes = (scope || '').split(' ').filter(s => (ALL_SCOPES as string[]).includes(s)); - - return res.json({ valid: true, client_name: resolvedName, requested_scopes: requestedScopes }); -}); - -// User submits DCR approval (or cancel) — requires cookie auth -oauthApiRouter.post('/register', requireCookieAuth, (req: Request, res: Response) => { - if (!isAddonEnabled(ADDON_IDS.MCP)) return res.status(403).json({ error: 'MCP is not enabled' }); - - const { user } = req as AuthRequest; - const { client_name, redirect_uri, scopes, state, approved } = req.body as { - client_name: string; - redirect_uri: string; - scopes: string[]; - state?: string; - approved?: boolean; - }; - const ip = getClientIp(req); - - // Validate redirect_uri before constructing any redirect URL - if (!redirect_uri || !isValidRedirectUri(redirect_uri)) { - return res.status(400).json({ error: 'invalid_request', error_description: 'Invalid redirect_uri' }); - } - - if (approved === false) { - const url = new URL(redirect_uri); - url.searchParams.set('error', 'access_denied'); - url.searchParams.set('error_description', 'User cancelled the registration'); - if (state) url.searchParams.set('state', state); - return res.json({ redirect: url.toString() }); - } - - const result = createOAuthClient( - user.id, client_name, [redirect_uri], scopes, ip, - { isPublic: true, createdVia: 'browser-registration' }, - ); - if (result.error) return res.status(result.status || 400).json({ error: result.error }); - - const newClientId = result.client!.client_id as string; - saveConsent(newClientId, user.id, scopes, ip); - - const url = new URL(redirect_uri); - url.searchParams.set('client_id', newClientId); - if (state) url.searchParams.set('state', state); - return res.json({ redirect: url.toString() }); -}); - // ---- OAuth client CRUD ---- oauthApiRouter.get('/clients', authenticate, (req: Request, res: Response) => {