1
0

OidcController.php 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Auth;
  4. use App\ApiClient\ApiException;
  5. use App\ApiClient\AuthClient;
  6. use Psr\Http\Message\ResponseInterface;
  7. use Psr\Http\Message\ServerRequestInterface;
  8. use Psr\Log\LoggerInterface;
  9. /**
  10. * OIDC bring-up.
  11. *
  12. * - `GET /login/oidc` — calls `OidcAuthenticator::authenticate()`. With
  13. * no `code` query, the underlying library redirects to the IdP and
  14. * `exit`s, so this method does not return in that case.
  15. * - `GET /oidc/callback` — same call; this time the library sees the
  16. * `code` and `state`, exchanges the code, validates the ID token,
  17. * and we hydrate the session.
  18. *
  19. * If the resolved user has an "empty" role (the api's
  20. * `OIDC_DEFAULT_ROLE=none` case surfaces as `role = "none"` in the
  21. * upsert response), redirect to `/no-access` — they're authenticated
  22. * but unauthorized.
  23. */
  24. final class OidcController
  25. {
  26. public function __construct(
  27. private readonly OidcAuthenticator $authenticator,
  28. private readonly AuthClient $auth,
  29. private readonly SessionManager $sessions,
  30. private readonly LoggerInterface $logger,
  31. private readonly bool $enabled,
  32. ) {
  33. }
  34. public function initiate(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
  35. {
  36. if (!$this->enabled) {
  37. return $response->withStatus(404);
  38. }
  39. // authenticate() will redirect-and-exit on the initiate path; only
  40. // on the callback path does it return normally. We delegate.
  41. return $this->finishOrFail($request, $response);
  42. }
  43. public function callback(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
  44. {
  45. if (!$this->enabled) {
  46. return $response->withStatus(404);
  47. }
  48. return $this->finishOrFail($request, $response);
  49. }
  50. private function finishOrFail(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
  51. {
  52. try {
  53. $claims = $this->authenticator->authenticate();
  54. } catch (OidcException $e) {
  55. $this->logger->error('oidc handshake failed', ['error' => $e->getMessage()]);
  56. $this->sessions->flash('error', 'Sign-in via Microsoft failed. Please try again.');
  57. return $response->withStatus(302)->withHeader('Location', '/login');
  58. }
  59. try {
  60. $user = $this->auth->upsertOidc(
  61. subject: $claims->subject,
  62. email: $claims->email,
  63. displayName: $claims->displayName,
  64. groups: $claims->groups,
  65. );
  66. } catch (ApiException $e) {
  67. $this->logger->error('oidc upsert failed', ['error' => $e->getMessage()]);
  68. $this->sessions->flash('error', 'API unreachable; please retry.');
  69. return $response->withStatus(302)->withHeader('Location', '/login');
  70. }
  71. if ($user->role === 'none' || $user->role === '') {
  72. $this->logger->warning('oidc user has no role assigned', ['subject' => $claims->subject]);
  73. return $response->withStatus(302)->withHeader('Location', '/no-access');
  74. }
  75. $this->sessions->regenerateId();
  76. $this->sessions->setUser(new UserContext(
  77. userId: $user->userId,
  78. displayName: $user->displayName !== '' ? $user->displayName : ($claims->email ?? $claims->subject),
  79. role: $user->role,
  80. email: $user->email ?? $claims->email,
  81. source: UserContext::SOURCE_OIDC,
  82. ));
  83. $next = $this->sessions->consumeNext() ?? '/app/dashboard';
  84. return $response->withStatus(302)->withHeader('Location', $next);
  85. }
  86. }