fix(auth): show specific error messages for denied OAuth logins

Add OauthLoginException, which carries a translation key for the message
shown on the login page. The exception message only goes to the log.
OauthLoginService now throws it for an unverified email, an account that
is already linked to another identity, and disabled registration.
OauthController shows the matching message for each case. Other
HttpExceptions fall back to a generic OAuth failure message.

The email verification check now runs before the linked-identity check,
so an unverified email cannot reveal that an account is already linked.

Add translations for the new auth.failed.oauth* keys in all locales and
cover the new error messages with feature tests.
This commit is contained in:
Andras Bacsai
2026-10-02 16:07:48 +02:00
parent 4e4659dad7
commit 8521f973dd
24 changed files with 186 additions and 11 deletions
+19
View File
@@ -0,0 +1,19 @@
<?php
namespace App\Exceptions;
use Symfony\Component\HttpKernel\Exception\HttpException;
/**
* A denied OAuth login with a translation key for the message shown on the
* login page. The exception message is only written to the log.
*/
class OauthLoginException extends HttpException
{
public function __construct(
string $message,
public readonly string $userMessageKey = 'auth.failed.oauth'
) {
parent::__construct(403, $message);
}
}
+6 -1
View File
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Exceptions\OauthLoginException;
use App\Models\OauthSetting;
use App\Services\Auth\OauthLoginService;
use Illuminate\Support\Facades\Log;
@@ -32,7 +33,11 @@ class OauthController extends Controller
} catch (\Exception $e) {
$this->logCallbackFailure($provider, $e);
$errorCode = $e instanceof HttpException ? 'auth.failed' : 'auth.failed.callback';
$errorCode = match (true) {
$e instanceof OauthLoginException => $e->userMessageKey,
$e instanceof HttpException => 'auth.failed.oauth',
default => 'auth.failed.callback',
};
return redirect()->route('login')->withErrors([__($errorCode)]);
}
+13 -10
View File
@@ -3,6 +3,7 @@
namespace App\Services\Auth;
use App\Auth\Oidc\OidcUser;
use App\Exceptions\OauthLoginException;
use App\Models\OauthIdentity;
use App\Models\OauthSetting;
use App\Models\Team;
@@ -130,20 +131,22 @@ class OauthLoginService
}
$user = User::whereEmail($email)->first();
if ($user?->oauthIdentities()->exists()) {
throw new HttpException(403, 'OAuth identity cannot be linked to this account');
}
// Before OAuth identities existed, OAuth sign-in matched users by email only.
// Users from that time keep signing in without email delivery, so their
// first identity links without a provider verification claim.
// first identity links without a provider verification claim. This check
// runs first, so an unverified email cannot reveal a linked account.
if ($user?->created_before_oauth_identities !== true && ! $this->hasVerifiedEmail($provider, $rawClaims, $email)) {
throw new HttpException(403, 'OAuth provider did not verify the email address');
throw new OauthLoginException('OAuth provider did not verify the email address', 'auth.failed.oauth_email_unverified');
}
if ($user?->oauthIdentities()->exists()) {
throw new OauthLoginException('OAuth identity cannot be linked to this account', 'auth.failed.oauth_already_linked');
}
if (! $user) {
if (! $this->canCreateUser($oauthSetting)) {
throw new HttpException(403, 'Registration is disabled');
throw new OauthLoginException('Registration is disabled', 'auth.registration_disabled');
}
$user = $this->createUser($oauthUser->name ?: $email, $email, $oauthSetting);
@@ -202,7 +205,7 @@ class OauthLoginService
}
if ($oauthSetting->require_email_verified && ! $emailVerified) {
throw new HttpException(403, 'OIDC provider did not verify the email address');
throw new OauthLoginException('OIDC provider did not verify the email address', 'auth.failed.oauth_email_unverified');
}
$rawClaims = is_array($oauthUser->user ?? null) ? $oauthUser->user : [];
@@ -234,16 +237,16 @@ class OauthLoginService
// guard is independent of the require_email_verified toggle, which
// only governs the broader login flow.
if ($user && ! $emailVerified) {
throw new HttpException(403, 'OIDC provider must verify the email address before linking to an existing account');
throw new OauthLoginException('OIDC provider must verify the email address before linking to an existing account', 'auth.failed.oauth_email_unverified');
}
if ($user?->oauthIdentities()->exists()) {
throw new HttpException(403, 'OAuth identity cannot be linked to this account');
throw new OauthLoginException('OAuth identity cannot be linked to this account', 'auth.failed.oauth_already_linked');
}
if (! $user) {
if (! $this->canCreateUser($oauthSetting)) {
throw new HttpException(403, 'Registration is disabled');
throw new OauthLoginException('Registration is disabled', 'auth.registration_disabled');
}
$user = $this->createUser($oauthUser->name ?: $email, $email, $oauthSetting);
+3
View File
@@ -21,6 +21,9 @@
"auth.reset_password": "إعادة تعيين كلمة المرور",
"auth.failed": "هذه البيانات لا تتطابق مع سجلاتنا.",
"auth.failed.callback": "فشل في معالجة استدعاء من مزود تسجيل الدخول.",
"auth.failed.oauth": "فشل تسجيل الدخول باستخدام هذا المزود. تواصل مع المسؤول إذا استمرت المشكلة.",
"auth.failed.oauth_already_linked": "هذا الحساب يستخدم بالفعل طريقة تسجيل دخول أخرى. سجّل الدخول باستخدام تلك الطريقة.",
"auth.failed.oauth_email_unverified": "لم يتحقق مزود تسجيل الدخول من بريدك الإلكتروني. تحقق منه لدى المزود ثم حاول مرة أخرى.",
"auth.failed.password": "كلمة المرور المقدمة غير صحيحة.",
"auth.failed.email": "لا يمكننا العثور على مستخدم بهذا البريد الإلكتروني.",
"auth.throttle": "عدد محاولات تسجيل الدخول كثيرة جدًا. يرجى المحاولة مرة أخرى في :seconds ثانية.",
+3
View File
@@ -21,6 +21,9 @@
"auth.reset_password": "Şifrənin bərpası",
"auth.failed": "Bu məlumatlar bizim qeydlərimizlə uyğun gəlmir.",
"auth.failed.callback": "Giriş təminatçısından geri çağırma işlənə bilmədi.",
"auth.failed.oauth": "Bu təminatçı ilə giriş uğursuz oldu. Problem davam edərsə, administratorla əlaqə saxlayın.",
"auth.failed.oauth_already_linked": "Bu hesab artıq başqa giriş üsulundan istifadə edir. Həmin üsulla daxil olun.",
"auth.failed.oauth_email_unverified": "Giriş təminatçınız e-poçt ünvanınızı təsdiqləməyib. Onu təminatçıda təsdiqləyin və yenidən cəhd edin.",
"auth.failed.password": "Daxil etdiyiniz şifrə yanlışdır.",
"auth.failed.email": "Bu e-poçt ünvanı ilə istifadəçi tapılmadı.",
"auth.throttle": "Çox sayda uğursuz giriş cəhdi. Zəhmət olmasa :seconds saniyə sonra yenidən cəhd edin.",
+3
View File
@@ -20,6 +20,9 @@
"auth.reset_password": "Obnovit heslo",
"auth.failed": "Tyto údaje neodpovídají našim záznamům.",
"auth.failed.callback": "Nepodařilo se zpracovat zpětné volání od poskytovatele přihlášení.",
"auth.failed.oauth": "Přihlášení přes tohoto poskytovatele se nezdařilo. Pokud problém přetrvává, kontaktujte správce.",
"auth.failed.oauth_already_linked": "Tento účet již používá jinou metodu přihlášení. Přihlaste se touto metodou.",
"auth.failed.oauth_email_unverified": "Váš poskytovatel přihlášení neověřil vaši e-mailovou adresu. Ověřte ji u poskytovatele a zkuste to znovu.",
"auth.failed.password": "Zadané heslo je nesprávné.",
"auth.failed.email": "Nemůžeme najít uživatele s touto e-mailovou adresou.",
"auth.throttle": "Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds sekund.",
+3
View File
@@ -22,6 +22,9 @@
"auth.reset_password": "Passwort zurücksetzen",
"auth.failed": "Diese Anmeldedaten wurden nicht gefunden.",
"auth.failed.callback": "Fehlerhafte Verarbeitung der Antwort des Anmeldeanbieters.",
"auth.failed.oauth": "Die Anmeldung über diesen Anbieter ist fehlgeschlagen. Wende dich an deinen Administrator, wenn das Problem weiter besteht.",
"auth.failed.oauth_already_linked": "Dieses Konto verwendet bereits eine andere Anmeldemethode. Melde dich mit dieser Methode an.",
"auth.failed.oauth_email_unverified": "Dein Anmeldeanbieter hat deine E-Mail-Adresse nicht bestätigt. Bestätige sie beim Anbieter und versuche es erneut.",
"auth.failed.password": "Das angegebene Passwort ist inkorrekt.",
"auth.failed.email": "Wir können keinen Benutzer mit dieser E-Mail Adresse finden.",
"auth.throttle": "Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.",
+3
View File
@@ -24,6 +24,9 @@
"auth.reset_password": "Reset password",
"auth.failed": "These credentials do not match our records.",
"auth.failed.callback": "Failed to process callback from login provider.",
"auth.failed.oauth": "Sign-in with this provider failed. Contact your administrator if the problem continues.",
"auth.failed.oauth_already_linked": "This account already uses another sign-in method. Sign in with that method.",
"auth.failed.oauth_email_unverified": "Your sign-in provider did not verify your email address. Verify it with the provider and try again.",
"auth.failed.password": "The provided password is incorrect.",
"auth.failed.email": "If an account exists with this email address, you will receive a password reset link shortly.",
"auth.throttle": "Too many login attempts. Please try again in :seconds seconds.",
+3
View File
@@ -20,6 +20,9 @@
"auth.reset_password": "Cambiar contraseña",
"auth.failed": "Las credenciales no coinciden con nuestro registro..",
"auth.failed.callback": "Falló el proceso de inicio de sesión con el proveedor.",
"auth.failed.oauth": "El inicio de sesión con este proveedor falló. Contacta a tu administrador si el problema continúa.",
"auth.failed.oauth_already_linked": "Esta cuenta ya usa otro método de inicio de sesión. Inicia sesión con ese método.",
"auth.failed.oauth_email_unverified": "Tu proveedor de inicio de sesión no verificó tu dirección de correo. Verifícala con el proveedor e inténtalo de nuevo.",
"auth.failed.password": "La contraseña es incorrecta.",
"auth.failed.email": "No encontramos un usuario con ese correo.",
"auth.throttle": "Demasiados intentos. Por favor inténtalo en :seconds segundos.",
+3
View File
@@ -20,6 +20,9 @@
"auth.reset_password": "بازیابی رمز عبور",
"auth.failed": "این اطلاعات با سوابق ما مطابقت ندارد.",
"auth.failed.callback": "پردازش بازگشت از ارائه‌دهنده ورود با شکست مواجه شد.",
"auth.failed.oauth": "ورود با این ارائه‌دهنده ناموفق بود. اگر مشکل ادامه داشت با مدیر تماس بگیرید.",
"auth.failed.oauth_already_linked": "این حساب از قبل از روش ورود دیگری استفاده می‌کند. با همان روش وارد شوید.",
"auth.failed.oauth_email_unverified": "ارائه‌دهنده ورود شما آدرس ایمیل شما را تأیید نکرده است. آن را نزد ارائه‌دهنده تأیید کنید و دوباره تلاش کنید.",
"auth.failed.password": "رمز عبور ارائه شده نادرست است.",
"auth.failed.email": "ما نمی توانیم کاربر با آدرس ایمیل مورد نظر را پیدا کنیم.",
"auth.throttle": "تعداد تلاش‌های ورود بیش از حد است. لطفاً در :seconds ثانیه دوباره تلاش کنید.",
+3
View File
@@ -21,6 +21,9 @@
"auth.reset_password": "Réinitialiser le mot de passe",
"auth.failed": "Aucune correspondance n'a été trouvée pour les informations d'identification renseignées.",
"auth.failed.callback": "Erreur lors du processus de retour de la plateforme de connexion.",
"auth.failed.oauth": "La connexion avec ce fournisseur a échoué. Contactez votre administrateur si le problème persiste.",
"auth.failed.oauth_already_linked": "Ce compte utilise déjà une autre méthode de connexion. Connectez-vous avec cette méthode.",
"auth.failed.oauth_email_unverified": "Votre fournisseur de connexion n'a pas vérifié votre adresse e-mail. Vérifiez-la auprès du fournisseur et réessayez.",
"auth.failed.password": "Le mot de passe renseigné est incorrect.",
"auth.failed.email": "Aucun utilisateur avec cette adresse email n'a été trouvé.",
"auth.throttle": "Trop de tentatives de connexion. Merci de réessayer dans :seconds secondes.",
+3
View File
@@ -21,6 +21,9 @@
"auth.reset_password": "Reset kata sandi",
"auth.failed": "Kredensial ini tidak cocok dengan catatan kami.",
"auth.failed.callback": "Gagal memproses callback dari penyedia login.",
"auth.failed.oauth": "Login dengan penyedia ini gagal. Hubungi administrator jika masalah berlanjut.",
"auth.failed.oauth_already_linked": "Akun ini sudah menggunakan metode login lain. Login dengan metode tersebut.",
"auth.failed.oauth_email_unverified": "Penyedia login Anda belum memverifikasi alamat email Anda. Verifikasi di penyedia, lalu coba lagi.",
"auth.failed.password": "Kata sandi yang diberikan salah.",
"auth.failed.email": "Kami tidak dapat menemukan pengguna dengan alamat e-mail tersebut.",
"auth.throttle": "Terlalu banyak percobaan login. Silakan coba lagi dalam :seconds detik.",
+3
View File
@@ -21,6 +21,9 @@
"auth.reset_password": "Reimposta password",
"auth.failed": "Queste credenziali non corrispondono ai nostri record.",
"auth.failed.callback": "Errore durante l'elaborazione del callback dal provider di accesso.",
"auth.failed.oauth": "L'accesso con questo provider non è riuscito. Contatta l'amministratore se il problema persiste.",
"auth.failed.oauth_already_linked": "Questo account usa già un altro metodo di accesso. Accedi con quel metodo.",
"auth.failed.oauth_email_unverified": "Il tuo provider di accesso non ha verificato il tuo indirizzo email. Verificalo presso il provider e riprova.",
"auth.failed.password": "La password fornita non è corretta.",
"auth.failed.email": "Non possiamo trovare un utente con questo indirizzo email.",
"auth.throttle": "Troppi tentativi di accesso. Per favore riprova tra :seconds secondi.",
+3
View File
@@ -20,6 +20,9 @@
"auth.reset_password": "パスワードをリセット",
"auth.failed": "これらの資格情報は記録と一致しません。",
"auth.failed.callback": "ログインプロバイダーからのコールバックの処理に失敗しました。",
"auth.failed.oauth": "このプロバイダーでのログインに失敗しました。問題が続く場合は管理者に連絡してください。",
"auth.failed.oauth_already_linked": "このアカウントは既に別のログイン方法を使用しています。その方法でログインしてください。",
"auth.failed.oauth_email_unverified": "ログインプロバイダーがメールアドレスを確認していません。プロバイダーで確認してから再度お試しください。",
"auth.failed.password": "提供されたパスワードが正しくありません。",
"auth.failed.email": "そのメールアドレスのユーザーが見つかりません。",
"auth.throttle": "ログイン試行回数が多すぎます。:seconds秒後にもう一度お試しください。",
+3
View File
@@ -21,6 +21,9 @@
"auth.reset_password": "Tilbakestill passord",
"auth.failed": "Disse legitimasjonene samsvarer ikke med våre registre.",
"auth.failed.callback": "Klarte ikke å behandle tilbakekall fra innloggingsleverandør.",
"auth.failed.oauth": "Innlogging med denne leverandøren mislyktes. Kontakt administratoren hvis problemet vedvarer.",
"auth.failed.oauth_already_linked": "Denne kontoen bruker allerede en annen innloggingsmetode. Logg inn med den metoden.",
"auth.failed.oauth_email_unverified": "Innloggingsleverandøren din har ikke bekreftet e-postadressen din. Bekreft den hos leverandøren og prøv igjen.",
"auth.failed.password": "Det oppgitte passordet er feil.",
"auth.failed.email": "Vi finner ingen bruker med den e-postadressen.",
"auth.throttle": "For mange innloggingsforsøk. Vennligst prøv igjen om :seconds sekunder.",
+3
View File
@@ -23,6 +23,9 @@
"auth.reset_password": "Zresetuj hasło",
"auth.failed": "Podane dane nie zgadzają się z naszymi rekordami.",
"auth.failed.callback": "Nie udało się przeprocesować callbacku od dostawcy logowania.",
"auth.failed.oauth": "Logowanie przez tego dostawcę nie powiodło się. Skontaktuj się z administratorem, jeśli problem będzie się powtarzał.",
"auth.failed.oauth_already_linked": "To konto używa już innej metody logowania. Zaloguj się tą metodą.",
"auth.failed.oauth_email_unverified": "Twój dostawca logowania nie zweryfikował Twojego adresu e-mail. Zweryfikuj go u dostawcy i spróbuj ponownie.",
"auth.failed.password": "Podane hasło jest nieprawidłowe.",
"auth.failed.email": "Nie znaleziono użytkownika z takim adresem email.",
"auth.throttle": "Zbyt wiele prób logowania. Spróbuj ponownie za :seconds sekund.",
+3
View File
@@ -22,6 +22,9 @@
"auth.reset_password": "Redefinir senha",
"auth.failed": "Essas credenciais não correspondem aos nossos registros.",
"auth.failed.callback": "Falha ao processar o callback do provedor de login.",
"auth.failed.oauth": "O login com este provedor falhou. Entre em contato com o administrador se o problema continuar.",
"auth.failed.oauth_already_linked": "Esta conta já usa outro método de login. Entre com esse método.",
"auth.failed.oauth_email_unverified": "Seu provedor de login não verificou seu endereço de e-mail. Verifique-o no provedor e tente novamente.",
"auth.failed.password": "A senha fornecida está incorreta.",
"auth.failed.email": "Não encontramos nenhum usuário com esse endereço de e-mail.",
"auth.throttle": "Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.",
+3
View File
@@ -22,6 +22,9 @@
"auth.reset_password": "Redefinir senha",
"auth.failed": "Essas credenciais não correspondem aos nossos registros.",
"auth.failed.callback": "Falha ao processar o callback do provedor de login.",
"auth.failed.oauth": "O início de sessão com este fornecedor falhou. Contacte o administrador se o problema continuar.",
"auth.failed.oauth_already_linked": "Esta conta já usa outro método de início de sessão. Inicie sessão com esse método.",
"auth.failed.oauth_email_unverified": "O seu fornecedor de início de sessão não verificou o seu endereço de email. Verifique-o no fornecedor e tente novamente.",
"auth.failed.password": "A senha fornecida está incorreta.",
"auth.failed.email": "Não encontramos um usuário com esse endereço de e-mail.",
"auth.throttle": "Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.",
+3
View File
@@ -20,6 +20,9 @@
"auth.reset_password": "Resetare parolă",
"auth.failed": "Autentificare nereușită. Vă rugăm să verificați datele introduse.",
"auth.failed.callback": "A apărut o eroare în timpul autentificării cu furnizorul extern.",
"auth.failed.oauth": "Autentificarea cu acest furnizor a eșuat. Contactați administratorul dacă problema persistă.",
"auth.failed.oauth_already_linked": "Acest cont folosește deja o altă metodă de autentificare. Autentificați-vă cu acea metodă.",
"auth.failed.oauth_email_unverified": "Furnizorul de autentificare nu v-a verificat adresa de e-mail. Verificați-o la furnizor și încercați din nou.",
"auth.failed.password": "Parola furnizată este incorectă.",
"auth.failed.email": "Nu putem găsi un utilizator cu această adresă de e-mail.",
"auth.throttle": "Prea multe încercări de autentificare. Vă rugăm să încercați din nou în :seconds secunde.",
+3
View File
@@ -22,6 +22,9 @@
"auth.reset_password": "Şifreyi Sıfırla",
"auth.failed": "Bu kimlik bilgileri kayıtlarımızla eşleşmiyor.",
"auth.failed.callback": "Giriş sağlayıcıdan gelen istek işlenemedi.",
"auth.failed.oauth": "Bu sağlayıcı ile giriş başarısız oldu. Sorun devam ederse yöneticinize başvurun.",
"auth.failed.oauth_already_linked": "Bu hesap zaten başka bir giriş yöntemi kullanıyor. O yöntemle giriş yapın.",
"auth.failed.oauth_email_unverified": "Giriş sağlayıcınız e-posta adresinizi doğrulamadı. Sağlayıcıda doğrulayın ve tekrar deneyin.",
"auth.failed.password": "Sağlanan şifre yanlış.",
"auth.failed.email": "Bu e-posta adresiyle bir kullanıcı bulamıyoruz.",
"auth.throttle": "Çok fazla giriş denemesi. Lütfen :seconds saniye sonra tekrar deneyin.",
+3
View File
@@ -20,6 +20,9 @@
"auth.reset_password": "Đặt lại mật khẩu",
"auth.failed": "Thông tin đăng nhập không khớp với bất kỳ tài khoản nào.",
"auth.failed.callback": "Xử lý thông tin từ nhà cung cấp đăng nhập thất bại.",
"auth.failed.oauth": "Đăng nhập bằng nhà cung cấp này thất bại. Hãy liên hệ quản trị viên nếu sự cố vẫn tiếp diễn.",
"auth.failed.oauth_already_linked": "Tài khoản này đã dùng một phương thức đăng nhập khác. Hãy đăng nhập bằng phương thức đó.",
"auth.failed.oauth_email_unverified": "Nhà cung cấp đăng nhập chưa xác minh địa chỉ email của bạn. Hãy xác minh với nhà cung cấp và thử lại.",
"auth.failed.password": "Mật khẩu bạn cung cấp không chính xác.",
"auth.failed.email": "Không có người dùng nào đã đăng ký với email đó.",
"auth.throttle": "Quá nhiều lần đăng nhập thất bại. Vui lòng thử lại sau :seconds giây.",
+3
View File
@@ -22,6 +22,9 @@
"auth.reset_password": "重置密码",
"auth.failed": "这些凭据与我们的记录不符",
"auth.failed.callback": "处理第三方登录的回调时出错",
"auth.failed.oauth": "使用此提供商登录失败。如果问题持续存在,请联系管理员。",
"auth.failed.oauth_already_linked": "此账户已使用其他登录方式。请使用该方式登录。",
"auth.failed.oauth_email_unverified": "你的登录提供商未验证你的邮箱地址。请在提供商处验证后重试。",
"auth.failed.password": "密码错误",
"auth.failed.email": "该账户未注册",
"auth.throttle": "登录次数过多,请在 :seconds 秒后重试",
+3
View File
@@ -20,6 +20,9 @@
"auth.reset_password": "重設密碼",
"auth.failed": "這些憑證與我們的記錄不符。",
"auth.failed.callback": "無法處理來自登入提供者的回呼。",
"auth.failed.oauth": "使用此提供者登入失敗。如果問題持續,請聯絡管理員。",
"auth.failed.oauth_already_linked": "此帳戶已使用其他登入方式。請使用該方式登入。",
"auth.failed.oauth_email_unverified": "您的登入提供者未驗證您的電子郵件地址。請在提供者處驗證後再試一次。",
"auth.failed.password": "密碼錯誤。",
"auth.failed.email": "找不到該電子郵件地址的使用者。",
"auth.throttle": "登入嘗試次數太多。請在 :seconds 秒後重試。",
+88
View File
@@ -718,3 +718,91 @@ it('sends the instance callback url without saving it when a forged host starts
expect($query['redirect_uri'])->toBe('https://coolify.example.com/auth/github/callback')
->and(OauthSetting::where('provider', 'github')->value('redirect_uri'))->toBeNull();
});
function mockGoogleCallbackUser(array $oauthUser): void
{
$provider = Mockery::mock();
$provider->shouldReceive('setConfig')->andReturnSelf();
$provider->shouldReceive('with')->andReturnSelf();
$provider->shouldReceive('user')->andReturn((object) $oauthUser);
Socialite::shouldReceive('driver')->with('google')->andReturn($provider);
}
describe('callback error messages', function () {
beforeEach(function () {
config()->set('app.maintenance.driver', 'file');
});
it('tells a user that the account already uses another sign-in method', function () {
$user = User::factory()->create(['email' => 'linked@example.com']);
OauthIdentity::create([
'user_id' => $user->id,
'provider' => 'github',
'issuer' => 'github',
'provider_user_id' => 'github-user-id',
'email' => 'linked@example.com',
]);
mockGoogleCallbackUser([
'email' => 'linked@example.com',
'name' => 'Linked User',
'id' => 'google-user-id',
'user' => ['email_verified' => true, 'hd' => 'example.com'],
]);
$this->from('/login')->get(route('auth.callback', 'google'))
->assertRedirect('/login');
expect(session('errors')->first())->toBe(__('auth.failed.oauth_already_linked'));
$this->assertGuest();
});
it('tells a user that the provider did not verify the email before it reveals a linked account', function () {
$user = User::factory()->create(['email' => 'linked@example.com']);
OauthIdentity::create([
'user_id' => $user->id,
'provider' => 'github',
'issuer' => 'github',
'provider_user_id' => 'github-user-id',
'email' => 'linked@example.com',
]);
mockGoogleCallbackUser([
'email' => 'linked@example.com',
'name' => 'Unverified User',
'id' => 'google-user-id',
'user' => ['email_verified' => false, 'hd' => 'example.com'],
]);
$this->from('/login')->get(route('auth.callback', 'google'))
->assertRedirect('/login');
expect(session('errors')->first())->toBe(__('auth.failed.oauth_email_unverified'));
$this->assertGuest();
});
it('tells a new user that registration is disabled', function () {
mockGoogleCallbackUser([
'email' => 'new@example.com',
'name' => 'New User',
'id' => 'google-user-id',
'user' => ['email_verified' => true, 'hd' => 'example.com'],
]);
$this->from('/login')->get(route('auth.callback', 'google'))
->assertRedirect('/login');
expect(session('errors')->first())->toBe(__('auth.registration_disabled'));
$this->assertGuest();
});
it('shows a generic OAuth message instead of the password message for other denied logins', function () {
mockGoogleCallbackUser([
'email' => 'outside@example.org',
'name' => 'Outside User',
'id' => 'google-user-id',
'user' => ['email_verified' => true, 'hd' => 'example.org'],
]);
$this->from('/login')->get(route('auth.callback', 'google'))
->assertRedirect('/login');
expect(session('errors')->first())->toBe(__('auth.failed.oauth'))
->not->toBe(__('auth.failed'));
$this->assertGuest();
});
});