| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102 |
- <?php
- declare(strict_types=1);
- namespace App\Auth;
- use App\ApiClient\ApiException;
- use App\ApiClient\AuthClient;
- use Psr\Http\Message\ResponseInterface;
- use Psr\Http\Message\ServerRequestInterface;
- use Psr\Log\LoggerInterface;
- /**
- * OIDC bring-up.
- *
- * - `GET /login/oidc` — calls `OidcAuthenticator::authenticate()`. With
- * no `code` query, the underlying library redirects to the IdP and
- * `exit`s, so this method does not return in that case.
- * - `GET /oidc/callback` — same call; this time the library sees the
- * `code` and `state`, exchanges the code, validates the ID token,
- * and we hydrate the session.
- *
- * If the resolved user has an "empty" role (the api's
- * `OIDC_DEFAULT_ROLE=none` case surfaces as `role = "none"` in the
- * upsert response), redirect to `/no-access` — they're authenticated
- * but unauthorized.
- */
- final class OidcController
- {
- public function __construct(
- private readonly OidcAuthenticator $authenticator,
- private readonly AuthClient $auth,
- private readonly SessionManager $sessions,
- private readonly LoggerInterface $logger,
- private readonly bool $enabled,
- ) {
- }
- public function initiate(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
- {
- if (!$this->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);
- }
- }
|