attributes->get('_route'); } public function authenticate(Request $request): Passport { try { $accessToken = $this->client->fetchAccessToken($request); } catch (AuthorizationRequestException|IdentityProviderException $e) { $this->logger->error('Login via myep failed: no access token', ['exception' => $e]); // Audited here rather than from a failure event: this is the only point that still // knows why the sign-in did not happen. Downstream there is only the German message // the login page shows. $this->audit->signInRefused('no_access_token'); throw new CustomUserMessageAuthenticationException('Anmeldung über myep fehlgeschlagen.'); } try { $userinfo = $this->client->fetchUserinfo($accessToken); } catch (UserinfoUnavailableException $e) { $this->logger->error('Login via myep failed: userinfo unreadable', ['exception' => $e]); $this->audit->signInRefused('userinfo_unreadable'); throw new CustomUserMessageAuthenticationException('Anmeldung über myep fehlgeschlagen.'); } // The guide is explicit that profile.communication.email comes from the CRM address // record and is NOT necessarily the login email -- one address can hold several // contacts. The top-level claim is the identity, and it is what User is keyed on. $email = $userinfo['email'] ?? null; if (!\is_string($email) || '' === $email) { $this->logger->error('Login via myep failed: userinfo carried no email claim'); $this->audit->signInRefused('no_email_claim'); throw new CustomUserMessageAuthenticationException('Anmeldung über myep fehlgeschlagen.'); } $roles = Role::eligibleOnly($this->claimedRoles($userinfo)); if ([] === $roles) { $this->logger->info('Login via myep refused: no eligible role', ['email' => $email]); $this->audit->signInRefused('no_eligible_role', $email); throw new CustomUserMessageAuthenticationException( 'Für E&P Platz ist eine Mitarbeiter- oder Administrator-Rolle erforderlich.', ); } $user = $this->createOrUpdate($email, $this->nameFrom($userinfo, $email), $roles); // Resolved here rather than by the user provider: the account may have just been // created in this very request, and handing the provider an email would mean a second // lookup of a row we already hold. return new SelfValidatingPassport( new UserBadge($user->getUserIdentifier(), static fn (): User => $user), ); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { // Whatever they were trying to reach before the firewall intervened. $targetPath = $this->getTargetPath($request->getSession(), $firewallName); return new RedirectResponse($targetPath ?? $this->urlGenerator->generate('app_dashboard')); } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { $request->getSession()->getFlashBag()->add('error', $exception->getMessageKey()); return new RedirectResponse($this->urlGenerator->generate('app_login')); } /** * @param array $userinfo * * @return list */ private function claimedRoles(array $userinfo): array { $claimed = $userinfo['roles'] ?? []; if (!\is_array($claimed)) { return []; } return array_values(array_filter($claimed, \is_string(...))); } /** * @param array $userinfo */ private function nameFrom(array $userinfo, string $fallback): string { $profile = $userinfo['profile'] ?? null; if (\is_array($profile)) { $name = trim(\sprintf('%s %s', $profile['first_name'] ?? '', $profile['last_name'] ?? '')); if ('' !== $name) { return mb_substr($name, 0, 100); } } // The profile scope may not have been granted, and a nameless account would render as a // blank holder on the floor plan -- the email at least identifies somebody. return mb_substr($fallback, 0, 100); } /** * @param list $roles */ private function createOrUpdate(string $email, string $name, array $roles): User { $user = $this->users->findOneBy(['email' => $email]); $isNew = null === $user; if (null === $user) { $user = new User($email, $name); $this->em->persist($user); } else { $user->setName($name); } // Filtered the same way the claim was, so the comparison below is between two sets of // the same shape -- getRoles() would add ROLE_USER to one side and not the other. $previousRoles = Role::eligibleOnly($user->getRoles()); // myep leads: a role it no longer reports is withdrawn here, not kept because it was // granted once. $user->setRoles($roles); $this->em->flush(); // After the flush, so nothing is recorded that a failing write would have undone. Both // records show "anonymous" as their actor, correctly: they happen while the person is // being authenticated and before any token exists, which is why they name the account. if ($isNew) { $this->audit->accountProvisioned($user); } elseif ($previousRoles !== $roles) { $this->audit->rolesChanged($user, $previousRoles, $roles); } return $user; } }