enabled) { return $response->withStatus(404); } // authenticate() will redirect-and-exit on the initiate path; only // on the callback path does it return normally. We delegate. return $this->finishOrFail($request, $response); } public function callback(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { if (!$this->enabled) { return $response->withStatus(404); } return $this->finishOrFail($request, $response); } private function finishOrFail(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { try { $claims = $this->authenticator->authenticate(); } catch (OidcException $e) { $this->logger->error('oidc handshake failed', ['error' => $e->getMessage()]); $this->sessions->flash('error', 'Sign-in via Microsoft failed. Please try again.'); return $response->withStatus(302)->withHeader('Location', '/login'); } try { $user = $this->auth->upsertOidc( subject: $claims->subject, email: $claims->email, displayName: $claims->displayName, groups: $claims->groups, ); } catch (ApiException $e) { $this->logger->error('oidc upsert failed', ['error' => $e->getMessage()]); $this->sessions->flash('error', 'API unreachable; please retry.'); return $response->withStatus(302)->withHeader('Location', '/login'); } if ($user->role === 'none' || $user->role === '') { $this->logger->warning('oidc user has no role assigned', ['subject' => $claims->subject]); return $response->withStatus(302)->withHeader('Location', '/no-access'); } $this->sessions->regenerateId(); $this->sessions->setUser(new UserContext( userId: $user->userId, displayName: $user->displayName !== '' ? $user->displayName : ($claims->email ?? $claims->subject), role: $user->role, email: $user->email ?? $claims->email, source: UserContext::SOURCE_OIDC, )); $next = $this->sessions->consumeNext() ?? '/app/dashboard'; return $response->withStatus(302)->withHeader('Location', $next); } }