authManager->handle($request); return $this->buildJsonResponse($response); } /** * Identify user for identity-first login flow */ #[AnonymousRoute('/auth/identify', name: 'auth.identify', methods: ['POST'])] public function identify(string $session, string $identity): JsonResponse { if (empty($session) || empty($identity)) { return new JsonResponse( ['error' => 'Session and identity are required', 'error_code' => 'invalid_request'], JsonResponse::HTTP_BAD_REQUEST ); } $request = AuthenticationRequest::identify($session, trim($identity)); $response = $this->authManager->handle($request); return $this->buildJsonResponse($response); } /** * Start a challenge for methods that require it (SMS, email, TOTP) */ #[AnonymousRoute('/auth/challenge', name: 'auth.challenge', methods: ['POST'])] public function challenge(string $session, string $method): JsonResponse { if (empty($session) || empty($method)) { return new JsonResponse( ['error' => 'Session ID and method are required', 'error_code' => 'invalid_request'], JsonResponse::HTTP_BAD_REQUEST ); } $request = AuthenticationRequest::challenge($session, $method); $response = $this->authManager->handle($request); return $this->buildJsonResponse($response); } /** * Verify a credential or challenge response */ #[AnonymousRoute('/auth/verify', name: 'auth.verify', methods: ['POST'])] public function verify(string $session, string $method, string $response): JsonResponse { if (empty($session) || empty($method)) { return new JsonResponse( ['error' => 'Session ID and method are required', 'error_code' => 'invalid_request'], JsonResponse::HTTP_BAD_REQUEST ); } $request = AuthenticationRequest::verify($session, $method, $response); $authResponse = $this->authManager->handle($request); return $this->buildJsonResponse($authResponse); } /** * Begin redirect-based authentication (OIDC/SAML) */ #[AnonymousRoute('/auth/redirect', name: 'auth.redirect', methods: ['POST'])] public function redirect(Request $request): JsonResponse { $data = $this->getRequestData($request); $sessionId = $data['session'] ?? ''; $method = $data['method'] ?? ''; $returnUrl = $data['return_url'] ?? '/'; if (empty($sessionId) || empty($method)) { return new JsonResponse( ['error' => 'Session ID and method are required', 'error_code' => 'invalid_request'], JsonResponse::HTTP_BAD_REQUEST ); } $scheme = $request->isSecure() ? 'https' : 'http'; $host = $request->getHost(); $callbackUrl = "{$scheme}://{$host}/auth/callback/{$method}"; $authRequest = AuthenticationRequest::redirect($sessionId, $method, $callbackUrl, $returnUrl); $response = $this->authManager->handle($authRequest); return $this->buildJsonResponse($response); } /** * Handle callback from identity provider (OIDC/SAML) */ #[AnonymousRoute('/auth/callback/{provider}', name: 'auth.callback', methods: ['GET', 'POST'])] public function callback(Request $request, string $provider): JsonResponse|RedirectResponse { $params = $request->isMethod('POST') ? $request->request->all() : $request->query->all(); $sessionId = $params['state'] ?? null; if (!$sessionId) { return $this->redirectWithError('Missing state parameter'); } $authRequest = AuthenticationRequest::callback($sessionId, $provider, $params); $response = $this->authManager->handle($authRequest); if ($response->isSuccess()) { $returnUrl = $response->returnUrl ?? '/'; $httpResponse = new RedirectResponse($returnUrl); if ($response->hasTokens()) { return $this->setTokenCookies($httpResponse, $response->tokens, $request->isSecure()); } return $httpResponse; } if ($response->isPending()) { return new RedirectResponse('/login/mfa?session=' . urlencode($response->sessionId)); } return $this->redirectWithError($response->errorMessage ?? 'Authentication failed'); } /** * Get current session status */ #[AnonymousRoute('/auth/status', name: 'auth.status', methods: ['GET'])] public function status(Request $request): JsonResponse { $sessionId = $request->query->get('session', ''); if (empty($sessionId)) { return new JsonResponse( ['error' => 'Session ID is required', 'error_code' => 'invalid_request'], JsonResponse::HTTP_BAD_REQUEST ); } $authRequest = AuthenticationRequest::status($sessionId); $response = $this->authManager->handle($authRequest); return $this->buildJsonResponse($response); } /** * Cancel authentication session */ #[AnonymousRoute('/auth/session', name: 'auth.session.cancel', methods: ['DELETE'])] public function cancel(Request $request): JsonResponse { $sessionId = $request->query->get('session', ''); $authRequest = AuthenticationRequest::cancel($sessionId); $this->authManager->handle($authRequest); return new JsonResponse(['status' => 'cancelled', 'message' => 'Session cancelled']); } // ========================================================================= // Token Operations // ========================================================================= /** * Refresh access token */ #[AnonymousRoute('/auth/refresh', name: 'auth.refresh', methods: ['POST'])] public function refresh(Request $request): JsonResponse { $refreshToken = $request->cookies->get('refreshToken'); if (!$refreshToken) { return new JsonResponse( ['error' => 'Refresh token required', 'error_code' => 'missing_token'], JsonResponse::HTTP_UNAUTHORIZED ); } $authRequest = AuthenticationRequest::refresh($refreshToken); $response = $this->authManager->handle($authRequest); if ($response->isFailed()) { $httpResponse = new JsonResponse($response->toArray(), $response->httpStatus); return $this->clearTokenCookies($httpResponse); } $httpResponse = new JsonResponse(['status' => 'success', 'message' => 'Token refreshed']); if ($response->tokens && isset($response->tokens['access'])) { $httpResponse->headers->setCookie( Cookie::create('accessToken') ->withValue($response->tokens['access']) ->withExpires(time() + 900) ->withPath('/') ->withSecure($request->isSecure()) ->withHttpOnly(true) ->withSameSite(Cookie::SAMESITE_STRICT) ); } return $httpResponse; } /** * Logout current device */ #[AuthenticatedRoute('/auth/logout', name: 'auth.logout', methods: ['POST'])] public function logout(Request $request): JsonResponse { $token = $request->cookies->get('accessToken'); $authRequest = AuthenticationRequest::logout($token, false); $this->authManager->handle($authRequest); $response = new JsonResponse(['status' => 'success', 'message' => 'Logged out successfully']); return $this->clearTokenCookies($response); } /** * Logout all devices */ #[AuthenticatedRoute('/auth/logout-all', name: 'auth.logout.all', methods: ['POST'])] public function logoutAll(Request $request): JsonResponse { $token = $request->cookies->get('accessToken'); $authRequest = AuthenticationRequest::logout($token, true); $this->authManager->handle($authRequest); $response = new JsonResponse(['status' => 'success', 'message' => 'Logged out from all devices']); return $this->clearTokenCookies($response); } // ========================================================================= // Response Helpers // ========================================================================= /** * Build JSON response from AuthenticationResponse */ private function buildJsonResponse(AuthenticationResponse $response): JsonResponse { $httpResponse = new JsonResponse($response->toArray(), $response->httpStatus); // Set token cookies if present if ($response->hasTokens()) { return $this->setTokenCookies($httpResponse, $response->tokens, true); } return $httpResponse; } /** * Set authentication token cookies */ private function setTokenCookies(JsonResponse|RedirectResponse $response, array $tokens, bool $secure = true): JsonResponse|RedirectResponse { if (isset($tokens['access'])) { $response->headers->setCookie( Cookie::create('accessToken') ->withValue($tokens['access']) ->withExpires(time() + 900) ->withPath('/') ->withSecure($secure) ->withHttpOnly(true) ->withSameSite(Cookie::SAMESITE_STRICT) ); } if (isset($tokens['refresh'])) { $response->headers->setCookie( Cookie::create('refreshToken') ->withValue($tokens['refresh']) ->withExpires(time() + 604800) ->withPath('/auth/refresh') ->withSecure($secure) ->withHttpOnly(true) ->withSameSite(Cookie::SAMESITE_STRICT) ); } return $response; } /** * Clear authentication token cookies */ private function clearTokenCookies(JsonResponse $response): JsonResponse { $response->headers->clearCookie('accessToken', '/'); $response->headers->clearCookie('refreshToken', '/auth/refresh'); return $response; } /** * Redirect with error message */ private function redirectWithError(string $error): RedirectResponse { return new RedirectResponse('/login?error=' . urlencode($error)); } /** * Get request data from JSON body or form data */ private function getRequestData(Request $request): array { $contentType = $request->headers->get('Content-Type', ''); if (str_contains($contentType, 'application/json')) { try { return $request->toArray(); } catch (\Throwable) { return []; } } return $request->request->all(); } }