mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 15:07:32 +00:00
Merge branch 'main' into logout-page
This commit is contained in:
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- qa
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -41,7 +42,7 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
driver-opts: 'image=moby/buildkit:v0.11.6'
|
driver: docker-container
|
||||||
|
|
||||||
- name: Login Public
|
- name: Login Public
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
2
apps/login/constants/csp.js
Normal file
2
apps/login/constants/csp.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const DEFAULT_CSP =
|
||||||
|
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;";
|
@@ -172,17 +172,23 @@
|
|||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Willkommen {user}!",
|
"title": "Willkommen {user}!",
|
||||||
"description": "Sie sind angemeldet.",
|
"description": "Sie sind angemeldet.",
|
||||||
"continue": "Weiter"
|
"continue": "Weiter",
|
||||||
|
"error": {
|
||||||
|
"title": "Fehler",
|
||||||
|
"description": "Ein Fehler ist aufgetreten."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "Keine Benutzer-ID angegeben!",
|
"userIdMissing": "Keine Benutzer-ID angegeben!",
|
||||||
"success": "Erfolgreich verifiziert",
|
"successTitle": "Benutzer verifiziert",
|
||||||
|
"successDescription": "Der Benutzer wurde erfolgreich verifiziert.",
|
||||||
"setupAuthenticator": "Authentifikator einrichten",
|
"setupAuthenticator": "Authentifikator einrichten",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Benutzer verifizieren",
|
"title": "Benutzer verifizieren",
|
||||||
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",
|
"description": "Geben Sie den Code ein, der in der Bestätigungs-E-Mail angegeben ist.",
|
||||||
"noCodeReceived": "Keinen Code erhalten?",
|
"noCodeReceived": "Keinen Code erhalten?",
|
||||||
"resendCode": "Code erneut senden",
|
"resendCode": "Code erneut senden",
|
||||||
|
"codeSent": "Ein Code wurde gerade an Ihre E-Mail-Adresse gesendet.",
|
||||||
"submit": "Weiter"
|
"submit": "Weiter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -193,7 +199,29 @@
|
|||||||
"allSetup": "Sie haben bereits einen Authentifikator eingerichtet!",
|
"allSetup": "Sie haben bereits einen Authentifikator eingerichtet!",
|
||||||
"linkWithIDP": "oder verknüpfe mit einem Identitätsanbieter"
|
"linkWithIDP": "oder verknüpfe mit einem Identitätsanbieter"
|
||||||
},
|
},
|
||||||
|
"device": {
|
||||||
|
"usercode": {
|
||||||
|
"title": "Gerätecode",
|
||||||
|
"description": "Geben Sie den Code ein.",
|
||||||
|
"submit": "Weiter"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"title": "{appName} möchte eine Verbindung herstellen:",
|
||||||
|
"disclaimer": "{appName} hat Zugriff auf:",
|
||||||
|
"description": "Durch Klicken auf Zulassen erlauben Sie {appName} und Zitadel, Ihre Informationen gemäß ihren jeweiligen Nutzungsbedingungen und Datenschutzrichtlinien zu verwenden. Sie können diesen Zugriff jederzeit widerrufen.",
|
||||||
|
"submit": "Zulassen",
|
||||||
|
"deny": "Ablehnen"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"openid": "Überprüfen Ihrer Identität.",
|
||||||
|
"email": "Zugriff auf Ihre E-Mail-Adresse.",
|
||||||
|
"profile": "Zugriff auf Ihre vollständigen Profilinformationen.",
|
||||||
|
"offline_access": "Erlauben Sie den Offline-Zugriff auf Ihr Konto."
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"noUserCode": "Kein Benutzercode angegeben!",
|
||||||
|
"noDeviceRequest": " Es wurde keine Geräteanforderung gefunden. Bitte überprüfen Sie die URL.",
|
||||||
"unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.",
|
"unknownContext": "Der Kontext des Benutzers konnte nicht ermittelt werden. Stellen Sie sicher, dass Sie zuerst den Benutzernamen eingeben oder einen loginName als Suchparameter angeben.",
|
||||||
"sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
"sessionExpired": "Ihre aktuelle Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
|
||||||
"failedLoading": "Daten konnten nicht geladen werden. Bitte versuchen Sie es erneut.",
|
"failedLoading": "Daten konnten nicht geladen werden. Bitte versuchen Sie es erneut.",
|
||||||
|
@@ -172,17 +172,23 @@
|
|||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Welcome {user}!",
|
"title": "Welcome {user}!",
|
||||||
"description": "You are signed in.",
|
"description": "You are signed in.",
|
||||||
"continue": "Continue"
|
"continue": "Continue",
|
||||||
|
"error": {
|
||||||
|
"title": "Error",
|
||||||
|
"description": "An error occurred while trying to sign in."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "No userId provided!",
|
"userIdMissing": "No userId provided!",
|
||||||
"success": "The user has been verified successfully.",
|
"successTitle": "User verified",
|
||||||
|
"successDescription": "The user has been verified successfully.",
|
||||||
"setupAuthenticator": "Setup authenticator",
|
"setupAuthenticator": "Setup authenticator",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Verify user",
|
"title": "Verify user",
|
||||||
"description": "Enter the Code provided in the verification email.",
|
"description": "Enter the Code provided in the verification email.",
|
||||||
"noCodeReceived": "Didn't receive a code?",
|
"noCodeReceived": "Didn't receive a code?",
|
||||||
"resendCode": "Resend code",
|
"resendCode": "Resend code",
|
||||||
|
"codeSent": "A code has just been sent to your email address.",
|
||||||
"submit": "Continue"
|
"submit": "Continue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -193,7 +199,29 @@
|
|||||||
"allSetup": "You have already setup an authenticator!",
|
"allSetup": "You have already setup an authenticator!",
|
||||||
"linkWithIDP": "or link with an Identity Provider"
|
"linkWithIDP": "or link with an Identity Provider"
|
||||||
},
|
},
|
||||||
|
"device": {
|
||||||
|
"usercode": {
|
||||||
|
"title": "Device code",
|
||||||
|
"description": "Enter the code displayed on your app or device.",
|
||||||
|
"submit": "Continue"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"title": "{appName} would like to connect",
|
||||||
|
"description": "{appName} will have access to:",
|
||||||
|
"disclaimer": "By clicking Allow, you allow {appName} and Zitadel to use your information in accordance with their respective terms of service and privacy policies. You can revoke this access at any time.",
|
||||||
|
"submit": "Allow",
|
||||||
|
"deny": "Deny"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"openid": "Verify your identity.",
|
||||||
|
"email": "View your email address.",
|
||||||
|
"profile": "View your full profile information.",
|
||||||
|
"offline_access": "Allow offline access to your account."
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"noUserCode": "No user code provided!",
|
||||||
|
"noDeviceRequest": "No device request found.",
|
||||||
"unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.",
|
"unknownContext": "Could not get the context of the user. Make sure to enter the username first or provide a loginName as searchParam.",
|
||||||
"sessionExpired": "Your current session has expired. Please login again.",
|
"sessionExpired": "Your current session has expired. Please login again.",
|
||||||
"failedLoading": "Failed to load data. Please try again.",
|
"failedLoading": "Failed to load data. Please try again.",
|
||||||
|
@@ -172,17 +172,23 @@
|
|||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "¡Bienvenido {user}!",
|
"title": "¡Bienvenido {user}!",
|
||||||
"description": "Has iniciado sesión.",
|
"description": "Has iniciado sesión.",
|
||||||
"continue": "Continuar"
|
"continue": "Continuar",
|
||||||
|
"error": {
|
||||||
|
"title": "Error",
|
||||||
|
"description": "Ocurrió un error al iniciar sesión."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "¡No se proporcionó userId!",
|
"userIdMissing": "¡No se proporcionó userId!",
|
||||||
"success": "¡Verificación exitosa!",
|
"successTitle": "Usuario verificado",
|
||||||
|
"successDescription": "El usuario ha sido verificado con éxito.",
|
||||||
"setupAuthenticator": "Configurar autenticador",
|
"setupAuthenticator": "Configurar autenticador",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Verificar usuario",
|
"title": "Verificar usuario",
|
||||||
"description": "Introduce el código proporcionado en el correo electrónico de verificación.",
|
"description": "Introduce el código proporcionado en el correo electrónico de verificación.",
|
||||||
"noCodeReceived": "¿No recibiste un código?",
|
"noCodeReceived": "¿No recibiste un código?",
|
||||||
"resendCode": "Reenviar código",
|
"resendCode": "Reenviar código",
|
||||||
|
"codeSent": "Se ha enviado un código a tu dirección de correo electrónico.",
|
||||||
"submit": "Continuar"
|
"submit": "Continuar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -193,7 +199,29 @@
|
|||||||
"allSetup": "¡Ya has configurado un autenticador!",
|
"allSetup": "¡Ya has configurado un autenticador!",
|
||||||
"linkWithIDP": "o vincúlalo con un proveedor de identidad"
|
"linkWithIDP": "o vincúlalo con un proveedor de identidad"
|
||||||
},
|
},
|
||||||
|
"device": {
|
||||||
|
"usercode": {
|
||||||
|
"title": "Código del dispositivo",
|
||||||
|
"description": "Introduce el código.",
|
||||||
|
"submit": "Continuar"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"title": "{appName} desea conectarse:",
|
||||||
|
"description": "{appName} tendrá acceso a:",
|
||||||
|
"disclaimer": "Al hacer clic en Permitir, autorizas a {appName} y a Zitadel a usar tu información de acuerdo con sus respectivos términos de servicio y políticas de privacidad. Puedes revocar este acceso en cualquier momento.",
|
||||||
|
"submit": "Permitir",
|
||||||
|
"deny": "Denegar"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"openid": "Verifica tu identidad.",
|
||||||
|
"email": "Accede a tu dirección de correo electrónico.",
|
||||||
|
"profile": "Accede a la información completa de tu perfil.",
|
||||||
|
"offline_access": "Permitir acceso sin conexión a tu cuenta."
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"noUserCode": "¡No se proporcionó código de usuario!",
|
||||||
|
"noDeviceRequest": "No se encontró ninguna solicitud de dispositivo.",
|
||||||
"unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.",
|
"unknownContext": "No se pudo obtener el contexto del usuario. Asegúrate de ingresar primero el nombre de usuario o proporcionar un loginName como parámetro de búsqueda.",
|
||||||
"sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo.",
|
"sessionExpired": "Tu sesión actual ha expirado. Por favor, inicia sesión de nuevo.",
|
||||||
"failedLoading": "No se pudieron cargar los datos. Por favor, inténtalo de nuevo.",
|
"failedLoading": "No se pudieron cargar los datos. Por favor, inténtalo de nuevo.",
|
||||||
|
@@ -172,17 +172,23 @@
|
|||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Benvenuto {user}!",
|
"title": "Benvenuto {user}!",
|
||||||
"description": "Sei connesso.",
|
"description": "Sei connesso.",
|
||||||
"continue": "Continua"
|
"continue": "Continua",
|
||||||
|
"error": {
|
||||||
|
"title": "Errore",
|
||||||
|
"description": "Si è verificato un errore durante il tentativo di accesso."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "Nessun userId fornito!",
|
"userIdMissing": "Nessun userId fornito!",
|
||||||
"success": "Verifica effettuata con successo!",
|
"successTitle": "Utente verificato",
|
||||||
|
"successDescription": "L'utente è stato verificato con successo.",
|
||||||
"setupAuthenticator": "Configura autenticatore",
|
"setupAuthenticator": "Configura autenticatore",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Verifica utente",
|
"title": "Verifica utente",
|
||||||
"description": "Inserisci il codice fornito nell'email di verifica.",
|
"description": "Inserisci il codice fornito nell'email di verifica.",
|
||||||
"noCodeReceived": "Non hai ricevuto un codice?",
|
"noCodeReceived": "Non hai ricevuto un codice?",
|
||||||
"resendCode": "Invia di nuovo il codice",
|
"resendCode": "Invia di nuovo il codice",
|
||||||
|
"codeSent": "Un codice è stato appena inviato al tuo indirizzo email.",
|
||||||
"submit": "Continua"
|
"submit": "Continua"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -193,7 +199,29 @@
|
|||||||
"allSetup": "Hai già configurato un autenticatore!",
|
"allSetup": "Hai già configurato un autenticatore!",
|
||||||
"linkWithIDP": "o collega con un Identity Provider"
|
"linkWithIDP": "o collega con un Identity Provider"
|
||||||
},
|
},
|
||||||
|
"device": {
|
||||||
|
"usercode": {
|
||||||
|
"title": "Codice dispositivo",
|
||||||
|
"description": "Inserisci il codice.",
|
||||||
|
"submit": "Continua"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"title": "{appName} desidera connettersi:",
|
||||||
|
"description": "{appName} avrà accesso a:",
|
||||||
|
"disclaimer": "Cliccando su Consenti, autorizzi {appName} e Zitadel a utilizzare le tue informazioni in conformità con i rispettivi termini di servizio e politiche sulla privacy. Puoi revocare questo accesso in qualsiasi momento.",
|
||||||
|
"submit": "Consenti",
|
||||||
|
"deny": "Nega"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"openid": "Verifica la tua identità.",
|
||||||
|
"email": "Accedi al tuo indirizzo email.",
|
||||||
|
"profile": "Accedi alle informazioni complete del tuo profilo.",
|
||||||
|
"offline_access": "Consenti l'accesso offline al tuo account."
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"noUserCode": "Nessun codice utente fornito!",
|
||||||
|
"noDeviceRequest": "Nessuna richiesta di dispositivo trovata.",
|
||||||
"unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.",
|
"unknownContext": "Impossibile ottenere il contesto dell'utente. Assicurati di inserire prima il nome utente o di fornire un loginName come parametro di ricerca.",
|
||||||
"sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso.",
|
"sessionExpired": "La tua sessione attuale è scaduta. Effettua nuovamente l'accesso.",
|
||||||
"failedLoading": "Impossibile caricare i dati. Riprova.",
|
"failedLoading": "Impossibile caricare i dati. Riprova.",
|
||||||
|
@@ -172,17 +172,23 @@
|
|||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Witaj {user}!",
|
"title": "Witaj {user}!",
|
||||||
"description": "Jesteś zalogowany.",
|
"description": "Jesteś zalogowany.",
|
||||||
"continue": "Kontynuuj"
|
"continue": "Kontynuuj",
|
||||||
|
"error": {
|
||||||
|
"title": "Błąd",
|
||||||
|
"description": "Nie można załadować danych. Sprawdź połączenie z internetem lub spróbuj ponownie później."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "Nie podano identyfikatora użytkownika!",
|
"userIdMissing": "Nie podano identyfikatora użytkownika!",
|
||||||
"success": "Użytkownik został pomyślnie zweryfikowany.",
|
"successTitle": "Weryfikacja zakończona",
|
||||||
|
"successDescription": "Użytkownik został pomyślnie zweryfikowany.",
|
||||||
"setupAuthenticator": "Skonfiguruj uwierzytelnianie",
|
"setupAuthenticator": "Skonfiguruj uwierzytelnianie",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Zweryfikuj użytkownika",
|
"title": "Zweryfikuj użytkownika",
|
||||||
"description": "Wprowadź kod z wiadomości weryfikacyjnej.",
|
"description": "Wprowadź kod z wiadomości weryfikacyjnej.",
|
||||||
"noCodeReceived": "Nie otrzymałeś kodu?",
|
"noCodeReceived": "Nie otrzymałeś kodu?",
|
||||||
"resendCode": "Wyślij kod ponownie",
|
"resendCode": "Wyślij kod ponownie",
|
||||||
|
"codeSent": "Kod został właśnie wysłany na twój adres e-mail.",
|
||||||
"submit": "Kontynuuj"
|
"submit": "Kontynuuj"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -193,7 +199,29 @@
|
|||||||
"allSetup": "Już skonfigurowałeś metodę uwierzytelniania!",
|
"allSetup": "Już skonfigurowałeś metodę uwierzytelniania!",
|
||||||
"linkWithIDP": "lub połącz z dostawcą tożsamości"
|
"linkWithIDP": "lub połącz z dostawcą tożsamości"
|
||||||
},
|
},
|
||||||
|
"device": {
|
||||||
|
"usercode": {
|
||||||
|
"title": "Kod urządzenia",
|
||||||
|
"description": "Wprowadź kod.",
|
||||||
|
"submit": "Kontynuuj"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"title": "{appName} chce się połączyć:",
|
||||||
|
"description": "{appName} będzie miało dostęp do:",
|
||||||
|
"disclaimer": "Klikając Zezwól, pozwalasz tej aplikacji i Zitadel na korzystanie z Twoich informacji zgodnie z ich odpowiednimi warunkami użytkowania i politykami prywatności. Możesz cofnąć ten dostęp w dowolnym momencie.",
|
||||||
|
"submit": "Zezwól",
|
||||||
|
"deny": "Odmów"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"openid": "Zweryfikuj swoją tożsamość.",
|
||||||
|
"email": "Uzyskaj dostęp do swojego adresu e-mail.",
|
||||||
|
"profile": "Uzyskaj dostęp do pełnych informacji o swoim profilu.",
|
||||||
|
"offline_access": "Zezwól na dostęp offline do swojego konta."
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"noUserCode": "Nie podano kodu użytkownika!",
|
||||||
|
"noDeviceRequest": "Nie znaleziono żądania urządzenia.",
|
||||||
"unknownContext": "Nie udało się pobrać kontekstu użytkownika. Upewnij się, że najpierw wprowadziłeś nazwę użytkownika lub podałeś login jako parametr wyszukiwania.",
|
"unknownContext": "Nie udało się pobrać kontekstu użytkownika. Upewnij się, że najpierw wprowadziłeś nazwę użytkownika lub podałeś login jako parametr wyszukiwania.",
|
||||||
"sessionExpired": "Twoja sesja wygasła. Zaloguj się ponownie.",
|
"sessionExpired": "Twoja sesja wygasła. Zaloguj się ponownie.",
|
||||||
"failedLoading": "Nie udało się załadować danych. Spróbuj ponownie.",
|
"failedLoading": "Nie udało się załadować danych. Spróbuj ponownie.",
|
||||||
|
@@ -172,17 +172,23 @@
|
|||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "Добро пожаловать, {user}!",
|
"title": "Добро пожаловать, {user}!",
|
||||||
"description": "Вы вошли в систему.",
|
"description": "Вы вошли в систему.",
|
||||||
"continue": "Продолжить"
|
"continue": "Продолжить",
|
||||||
|
"error": {
|
||||||
|
"title": "Ошибка",
|
||||||
|
"description": "Не удалось войти в систему. Проверьте свои данные и попробуйте снова."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "Не указан userId!",
|
"userIdMissing": "Не указан userId!",
|
||||||
"success": "Пользователь успешно подтверждён.",
|
"successTitle": "Пользователь подтверждён",
|
||||||
|
"successDescription": "Пользователь успешно подтверждён.",
|
||||||
"setupAuthenticator": "Настроить аутентификатор",
|
"setupAuthenticator": "Настроить аутентификатор",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "Подтверждение пользователя",
|
"title": "Подтверждение пользователя",
|
||||||
"description": "Введите код из письма подтверждения.",
|
"description": "Введите код из письма подтверждения.",
|
||||||
"noCodeReceived": "Не получили код?",
|
"noCodeReceived": "Не получили код?",
|
||||||
"resendCode": "Отправить код повторно",
|
"resendCode": "Отправить код повторно",
|
||||||
|
"codeSent": "Код отправлен на ваш email.",
|
||||||
"submit": "Продолжить"
|
"submit": "Продолжить"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -193,7 +199,29 @@
|
|||||||
"allSetup": "Аутентификатор уже настроен!",
|
"allSetup": "Аутентификатор уже настроен!",
|
||||||
"linkWithIDP": "или привязать через Identity Provider"
|
"linkWithIDP": "или привязать через Identity Provider"
|
||||||
},
|
},
|
||||||
|
"device": {
|
||||||
|
"usercode": {
|
||||||
|
"title": "Код устройства",
|
||||||
|
"description": "Введите код.",
|
||||||
|
"submit": "Продолжить"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"title": "{appName} хочет подключиться:",
|
||||||
|
"description": "{appName} получит доступ к:",
|
||||||
|
"disclaimer": "Нажимая «Разрешить», вы разрешаете этому приложению и Zitadel использовать вашу информацию в соответствии с их условиями использования и политиками конфиденциальности. Вы можете отозвать этот доступ в любое время.",
|
||||||
|
"submit": "Разрешить",
|
||||||
|
"deny": "Запретить"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"openid": "Проверка вашей личности.",
|
||||||
|
"email": "Доступ к вашему адресу электронной почты.",
|
||||||
|
"profile": "Доступ к полной информации вашего профиля.",
|
||||||
|
"offline_access": "Разрешить офлайн-доступ к вашему аккаунту."
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"noUserCode": "Не указан код пользователя!",
|
||||||
|
"noDeviceRequest": "Не найдена ни одна заявка на устройство.",
|
||||||
"unknownContext": "Не удалось получить контекст пользователя. Укажите имя пользователя или loginName в параметрах поиска.",
|
"unknownContext": "Не удалось получить контекст пользователя. Укажите имя пользователя или loginName в параметрах поиска.",
|
||||||
"sessionExpired": "Ваша сессия истекла. Войдите снова.",
|
"sessionExpired": "Ваша сессия истекла. Войдите снова.",
|
||||||
"failedLoading": "Ошибка загрузки данных. Попробуйте ещё раз.",
|
"failedLoading": "Ошибка загрузки данных. Попробуйте ещё раз.",
|
||||||
|
@@ -172,17 +172,23 @@
|
|||||||
"signedin": {
|
"signedin": {
|
||||||
"title": "欢迎 {user}!",
|
"title": "欢迎 {user}!",
|
||||||
"description": "您已登录。",
|
"description": "您已登录。",
|
||||||
"continue": "继续"
|
"continue": "继续",
|
||||||
|
"error": {
|
||||||
|
"title": "错误",
|
||||||
|
"description": "登录时发生错误。"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"userIdMissing": "未提供用户 ID!",
|
"userIdMissing": "未提供用户 ID!",
|
||||||
"success": "用户验证成功。",
|
"successTitle": "用户已验证",
|
||||||
|
"successDescription": "用户已成功验证。",
|
||||||
"setupAuthenticator": "设置认证器",
|
"setupAuthenticator": "设置认证器",
|
||||||
"verify": {
|
"verify": {
|
||||||
"title": "验证用户",
|
"title": "验证用户",
|
||||||
"description": "输入验证邮件中的验证码。",
|
"description": "输入验证邮件中的验证码。",
|
||||||
"noCodeReceived": "没有收到验证码?",
|
"noCodeReceived": "没有收到验证码?",
|
||||||
"resendCode": "重发验证码",
|
"resendCode": "重发验证码",
|
||||||
|
"codeSent": "刚刚发送了一封包含验证码的电子邮件。",
|
||||||
"submit": "继续"
|
"submit": "继续"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -193,7 +199,29 @@
|
|||||||
"allSetup": "您已经设置好了一个认证器!",
|
"allSetup": "您已经设置好了一个认证器!",
|
||||||
"linkWithIDP": "或将其与身份提供者关联"
|
"linkWithIDP": "或将其与身份提供者关联"
|
||||||
},
|
},
|
||||||
|
"device": {
|
||||||
|
"usercode": {
|
||||||
|
"title": "设备代码",
|
||||||
|
"description": "输入代码。",
|
||||||
|
"submit": "继续"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"title": "{appName} 想要连接:",
|
||||||
|
"description": "{appName} 将访问:",
|
||||||
|
"disclaimer": "点击“允许”即表示您允许此应用程序和 Zitadel 根据其各自的服务条款和隐私政策使用您的信息。您可以随时撤销此访问权限。",
|
||||||
|
"submit": "允许",
|
||||||
|
"deny": "拒绝"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"openid": "验证您的身份。",
|
||||||
|
"email": "访问您的电子邮件地址。",
|
||||||
|
"profile": "访问您的完整个人资料信息。",
|
||||||
|
"offline_access": "允许离线访问您的账户。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
"noUserCode": "未提供用户代码!",
|
||||||
|
"noDeviceRequest": "没有找到设备请求。",
|
||||||
"unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。",
|
"unknownContext": "无法获取用户的上下文。请先输入用户名或提供 loginName 作为搜索参数。",
|
||||||
"sessionExpired": "当前会话已过期,请重新登录。",
|
"sessionExpired": "当前会话已过期,请重新登录。",
|
||||||
"failedLoading": "加载数据失败,请再试一次。",
|
"failedLoading": "加载数据失败,请再试一次。",
|
||||||
|
@@ -6,6 +6,13 @@
|
|||||||
"data": {}
|
"data": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"service": "zitadel.settings.v2.SettingsService",
|
||||||
|
"method": "GetSecuritySettings",
|
||||||
|
"out": {
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"service": "zitadel.settings.v2.SettingsService",
|
"service": "zitadel.settings.v2.SettingsService",
|
||||||
"method": "GetLegalAndSupportSettings",
|
"method": "GetLegalAndSupportSettings",
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import createNextIntlPlugin from "next-intl/plugin";
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
|
import { DEFAULT_CSP } from "./constants/csp.js";
|
||||||
|
|
||||||
const withNextIntl = createNextIntlPlugin();
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
@@ -29,9 +30,9 @@ const secureHeaders = [
|
|||||||
// script-src va.vercel-scripts.com for analytics/vercel scripts
|
// script-src va.vercel-scripts.com for analytics/vercel scripts
|
||||||
{
|
{
|
||||||
key: "Content-Security-Policy",
|
key: "Content-Security-Policy",
|
||||||
value:
|
value: `${DEFAULT_CSP} frame-ancestors 'none'`,
|
||||||
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com; connect-src 'self'; child-src; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; img-src 'self' https://vercel.com;",
|
|
||||||
},
|
},
|
||||||
|
{ key: "X-Frame-Options", value: "deny" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const imageRemotePatterns = [
|
const imageRemotePatterns = [
|
||||||
|
@@ -373,7 +373,7 @@ On all pages, where the current user is shown, you can jump to this page. This w
|
|||||||
|
|
||||||
### /signedin
|
### /signedin
|
||||||
|
|
||||||
This is a success page which shows a completed login flow for a user, which did navigate to the login without a OIDC auth requrest.
|
This is a success page which shows a completed login flow for a user, which did navigate to the login without a OIDC auth requrest. From here device authorization flows are completed. It checks if the requestId param of starts with `device_` and then executes the `authorizeOrDenyDeviceAuthorization` command.
|
||||||
|
|
||||||
<img src="./screenshots/signedin.png" alt="/signedin" width="400px" />
|
<img src="./screenshots/signedin.png" alt="/signedin" width="400px" />
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { SessionsList } from "@/components/sessions-list";
|
import { SessionsList } from "@/components/sessions-list";
|
||||||
import { getAllSessionCookieIds } from "@/lib/cookies";
|
import { getAllSessionCookieIds } from "@/lib/cookies";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
getDefaultOrg,
|
getDefaultOrg,
|
||||||
|
@@ -5,8 +5,9 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
|||||||
import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getSessionCookieById } from "@/lib/cookies";
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
|
import { checkUserVerification } from "@/lib/verify-helper";
|
||||||
import {
|
import {
|
||||||
getActiveIdentityProviders,
|
getActiveIdentityProviders,
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function Page(props: {
|
export default async function Page(props: {
|
||||||
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
@@ -33,8 +35,8 @@ export default async function Page(props: {
|
|||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
const sessionWithData = sessionId
|
const sessionWithData = sessionId
|
||||||
? await loadSessionById(serviceUrl, sessionId, organization)
|
? await loadSessionById(sessionId, organization)
|
||||||
: await loadSessionByLoginname(serviceUrl, loginName, organization);
|
: await loadSessionByLoginname(loginName, organization);
|
||||||
|
|
||||||
async function getAuthMethodsAndUser(
|
async function getAuthMethodsAndUser(
|
||||||
serviceUrl: string,
|
serviceUrl: string,
|
||||||
@@ -67,7 +69,6 @@ export default async function Page(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadSessionByLoginname(
|
async function loadSessionByLoginname(
|
||||||
host: string,
|
|
||||||
loginName?: string,
|
loginName?: string,
|
||||||
organization?: string,
|
organization?: string,
|
||||||
) {
|
) {
|
||||||
@@ -82,11 +83,7 @@ export default async function Page(props: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSessionById(
|
async function loadSessionById(sessionId: string, organization?: string) {
|
||||||
host: string,
|
|
||||||
sessionId: string,
|
|
||||||
organization?: string,
|
|
||||||
) {
|
|
||||||
const recent = await getSessionCookieById({ sessionId, organization });
|
const recent = await getSessionCookieById({ sessionId, organization });
|
||||||
return getSession({
|
return getSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
@@ -97,20 +94,50 @@ export default async function Page(props: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionWithData) {
|
if (
|
||||||
|
!sessionWithData ||
|
||||||
|
!sessionWithData.factors ||
|
||||||
|
!sessionWithData.factors.user
|
||||||
|
) {
|
||||||
return <Alert>{tError("unknownContext")}</Alert>;
|
return <Alert>{tError("unknownContext")}</Alert>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
const branding = await getBrandingSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
organization: sessionWithData.factors?.user?.organizationId,
|
organization: sessionWithData.factors.user?.organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
const loginSettings = await getLoginSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
organization: sessionWithData.factors?.user?.organizationId,
|
organization: sessionWithData.factors.user?.organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// check if user was verified recently
|
||||||
|
const isUserVerified = await checkUserVerification(
|
||||||
|
sessionWithData.factors.user?.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUserVerified) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
loginName: sessionWithData.factors.user.loginName as string,
|
||||||
|
invite: "true",
|
||||||
|
send: "true", // set this to true to request a new code immediately
|
||||||
|
});
|
||||||
|
|
||||||
|
if (requestId) {
|
||||||
|
params.append("requestId", requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization || sessionWithData.factors.user.organizationId) {
|
||||||
|
params.append(
|
||||||
|
"organization",
|
||||||
|
organization ?? (sessionWithData.factors.user.organizationId as string),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(`/verify?` + params);
|
||||||
|
}
|
||||||
|
|
||||||
const identityProviders = await getActiveIdentityProviders({
|
const identityProviders = await getActiveIdentityProviders({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
orgId: sessionWithData.factors?.user?.organizationId,
|
orgId: sessionWithData.factors?.user?.organizationId,
|
||||||
@@ -157,13 +184,12 @@ export default async function Page(props: {
|
|||||||
></ChooseAuthenticatorToSetup>
|
></ChooseAuthenticatorToSetup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loginSettings?.allowExternalIdp && identityProviders && (
|
{loginSettings?.allowExternalIdp && !!identityProviders.length && (
|
||||||
<>
|
<>
|
||||||
{identityProviders.length && (
|
<div className="py-3 flex flex-col">
|
||||||
<div className="py-3 flex flex-col">
|
<p className="ztdl-p text-center">{t("linkWithIDP")}</p>
|
||||||
<p className="ztdl-p text-center">{t("linkWithIDP")}</p>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<SignInWithIdp
|
<SignInWithIdp
|
||||||
identityProviders={identityProviders}
|
identityProviders={identityProviders}
|
||||||
requestId={requestId}
|
requestId={requestId}
|
||||||
|
89
apps/login/src/app/(login)/device/consent/page.tsx
Normal file
89
apps/login/src/app/(login)/device/consent/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { ConsentScreen } from "@/components/consent";
|
||||||
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
|
import {
|
||||||
|
getBrandingSettings,
|
||||||
|
getDefaultOrg,
|
||||||
|
getDeviceAuthorizationRequest,
|
||||||
|
} from "@/lib/zitadel";
|
||||||
|
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||||
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
|
export default async function Page(props: {
|
||||||
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
const locale = getLocale();
|
||||||
|
const t = await getTranslations({ locale });
|
||||||
|
|
||||||
|
const userCode = searchParams?.user_code;
|
||||||
|
const requestId = searchParams?.requestId;
|
||||||
|
const organization = searchParams?.organization;
|
||||||
|
|
||||||
|
if (!userCode || !requestId) {
|
||||||
|
return <div>{t("error.noUserCode")}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({
|
||||||
|
serviceUrl,
|
||||||
|
userCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!deviceAuthorizationRequest) {
|
||||||
|
return <div>{t("error.noDeviceRequest")}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultOrganization;
|
||||||
|
if (!organization) {
|
||||||
|
const org: Organization | null = await getDefaultOrg({
|
||||||
|
serviceUrl,
|
||||||
|
});
|
||||||
|
if (org) {
|
||||||
|
defaultOrganization = org.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization: organization ?? defaultOrganization,
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
if (requestId) {
|
||||||
|
params.append("requestId", requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organization) {
|
||||||
|
params.append("organization", organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>
|
||||||
|
{t("device.request.title", {
|
||||||
|
appName: deviceAuthorizationRequest?.appName,
|
||||||
|
})}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="ztdl-p">
|
||||||
|
{t("device.request.description", {
|
||||||
|
appName: deviceAuthorizationRequest?.appName,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ConsentScreen
|
||||||
|
deviceAuthorizationRequestId={deviceAuthorizationRequest?.id}
|
||||||
|
scope={deviceAuthorizationRequest.scope}
|
||||||
|
appName={deviceAuthorizationRequest?.appName}
|
||||||
|
nextUrl={`/loginname?` + params}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
46
apps/login/src/app/(login)/device/page.tsx
Normal file
46
apps/login/src/app/(login)/device/page.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { DeviceCodeForm } from "@/components/device-code-form";
|
||||||
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
|
import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
|
||||||
|
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||||
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
|
export default async function Page(props: {
|
||||||
|
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
||||||
|
}) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
const locale = getLocale();
|
||||||
|
const t = await getTranslations({ locale, namespace: "device" });
|
||||||
|
|
||||||
|
const userCode = searchParams?.user_code;
|
||||||
|
const organization = searchParams?.organization;
|
||||||
|
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
let defaultOrganization;
|
||||||
|
if (!organization) {
|
||||||
|
const org: Organization | null = await getDefaultOrg({
|
||||||
|
serviceUrl,
|
||||||
|
});
|
||||||
|
if (org) {
|
||||||
|
defaultOrganization = org.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization: organization ?? defaultOrganization,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>{t("usercode.title")}</h1>
|
||||||
|
<p className="ztdl-p">{t("usercode.description")}</p>
|
||||||
|
<DeviceCodeForm userCode={userCode}></DeviceCodeForm>
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
@@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
|
|||||||
import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login";
|
import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to-login";
|
||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
|
@@ -4,7 +4,7 @@ import { linkingFailed } from "@/components/idps/pages/linking-failed";
|
|||||||
import { linkingSuccess } from "@/components/idps/pages/linking-success";
|
import { linkingSuccess } from "@/components/idps/pages/linking-success";
|
||||||
import { loginFailed } from "@/components/idps/pages/login-failed";
|
import { loginFailed } from "@/components/idps/pages/login-failed";
|
||||||
import { loginSuccess } from "@/components/idps/pages/login-success";
|
import { loginSuccess } from "@/components/idps/pages/login-success";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import {
|
import {
|
||||||
addHuman,
|
addHuman,
|
||||||
addIDPLink,
|
addIDPLink,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel";
|
import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
@@ -1,73 +0,0 @@
|
|||||||
import { Alert, AlertType } from "@/components/alert";
|
|
||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
|
||||||
import { InviteForm } from "@/components/invite-form";
|
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
|
||||||
import {
|
|
||||||
getBrandingSettings,
|
|
||||||
getDefaultOrg,
|
|
||||||
getLoginSettings,
|
|
||||||
getPasswordComplexitySettings,
|
|
||||||
} from "@/lib/zitadel";
|
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
|
|
||||||
export default async function Page(props: {
|
|
||||||
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
|
||||||
}) {
|
|
||||||
const searchParams = await props.searchParams;
|
|
||||||
const locale = getLocale();
|
|
||||||
const t = await getTranslations({ locale, namespace: "invite" });
|
|
||||||
|
|
||||||
let { firstname, lastname, email, organization } = searchParams;
|
|
||||||
|
|
||||||
const _headers = await headers();
|
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
|
||||||
|
|
||||||
if (!organization) {
|
|
||||||
const org = await getDefaultOrg({ serviceUrl });
|
|
||||||
if (!org) {
|
|
||||||
throw new Error("No default organization found");
|
|
||||||
}
|
|
||||||
|
|
||||||
organization = org.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
|
|
||||||
const passwordComplexitySettings = await getPasswordComplexitySettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DynamicTheme branding={branding}>
|
|
||||||
<div className="flex flex-col items-center space-y-4">
|
|
||||||
<h1>{t("title")}</h1>
|
|
||||||
<p className="ztdl-p">{t("description")}</p>
|
|
||||||
|
|
||||||
{!loginSettings?.allowRegister ? (
|
|
||||||
<Alert type={AlertType.ALERT}>{t("notAllowed")}</Alert>
|
|
||||||
) : (
|
|
||||||
<Alert type={AlertType.INFO}>{t("info")}</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{passwordComplexitySettings && loginSettings?.allowRegister && (
|
|
||||||
<InviteForm
|
|
||||||
organization={organization}
|
|
||||||
firstname={firstname}
|
|
||||||
lastname={lastname}
|
|
||||||
email={email}
|
|
||||||
></InviteForm>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DynamicTheme>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,81 +0,0 @@
|
|||||||
import { Alert, AlertType } from "@/components/alert";
|
|
||||||
import { Button, ButtonVariants } from "@/components/button";
|
|
||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
|
||||||
import { getBrandingSettings, getDefaultOrg, getUserByID } from "@/lib/zitadel";
|
|
||||||
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export default async function Page(props: {
|
|
||||||
searchParams: Promise<Record<string | number | symbol, string | undefined>>;
|
|
||||||
}) {
|
|
||||||
const searchParams = await props.searchParams;
|
|
||||||
const locale = getLocale();
|
|
||||||
const t = await getTranslations({ locale, namespace: "invite" });
|
|
||||||
|
|
||||||
let { userId, organization } = searchParams;
|
|
||||||
|
|
||||||
const _headers = await headers();
|
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
|
||||||
|
|
||||||
if (!organization) {
|
|
||||||
const org = await getDefaultOrg({ serviceUrl });
|
|
||||||
if (!org) {
|
|
||||||
throw new Error("No default organization found");
|
|
||||||
}
|
|
||||||
|
|
||||||
organization = org.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization,
|
|
||||||
});
|
|
||||||
|
|
||||||
let user: User | undefined;
|
|
||||||
let human: HumanUser | undefined;
|
|
||||||
if (userId) {
|
|
||||||
const userResponse = await getUserByID({
|
|
||||||
serviceUrl,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
if (userResponse) {
|
|
||||||
user = userResponse.user;
|
|
||||||
if (user?.type.case === "human") {
|
|
||||||
human = user.type.value as HumanUser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DynamicTheme branding={branding}>
|
|
||||||
<div className="flex flex-col items-center space-y-4">
|
|
||||||
<h1>{t("success.title")}</h1>
|
|
||||||
<p className="ztdl-p">{t("success.description")}</p>
|
|
||||||
{user && (
|
|
||||||
<UserAvatar
|
|
||||||
loginName={user.preferredLoginName}
|
|
||||||
displayName={human?.profile?.displayName}
|
|
||||||
showDropdown={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{human?.email?.isVerified ? (
|
|
||||||
<Alert type={AlertType.INFO}>{t("success.verified")}</Alert>
|
|
||||||
) : (
|
|
||||||
<Alert type={AlertType.INFO}>{t("success.notVerifiedYet")}</Alert>
|
|
||||||
)}
|
|
||||||
<div className="mt-8 flex w-full flex-row items-center justify-between">
|
|
||||||
<span></span>
|
|
||||||
<Link href="/invite">
|
|
||||||
<Button type="submit" variant={ButtonVariants.Primary}>
|
|
||||||
{t("success.submit")}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DynamicTheme>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,7 +1,7 @@
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
||||||
import { UsernameForm } from "@/components/username-form";
|
import { UsernameForm } from "@/components/username-form";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import {
|
import {
|
||||||
getActiveIdentityProviders,
|
getActiveIdentityProviders,
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
|
@@ -4,7 +4,7 @@ import { ChooseSecondFactor } from "@/components/choose-second-factor";
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getSessionCookieById } from "@/lib/cookies";
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
|
@@ -4,7 +4,7 @@ import { ChooseSecondFactorToSetup } from "@/components/choose-second-factor-to-
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getSessionCookieById } from "@/lib/cookies";
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
|
@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
|||||||
import { LoginOTP } from "@/components/login-otp";
|
import { LoginOTP } from "@/components/login-otp";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getSessionCookieById } from "@/lib/cookies";
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
|
@@ -4,7 +4,7 @@ import { Button, ButtonVariants } from "@/components/button";
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { TotpRegister } from "@/components/totp-register";
|
import { TotpRegister } from "@/components/totp-register";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
addOTPEmail,
|
addOTPEmail,
|
||||||
|
@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
|||||||
import { LoginPasskey } from "@/components/login-passkey";
|
import { LoginPasskey } from "@/components/login-passkey";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getSessionCookieById } from "@/lib/cookies";
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import { getBrandingSettings, getSession } from "@/lib/zitadel";
|
import { getBrandingSettings, getSession } from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
@@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { RegisterPasskey } from "@/components/register-passkey";
|
import { RegisterPasskey } from "@/components/register-passkey";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import { getBrandingSettings } from "@/lib/zitadel";
|
import { getBrandingSettings } from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
@@ -2,7 +2,7 @@ import { Alert } from "@/components/alert";
|
|||||||
import { ChangePasswordForm } from "@/components/change-password-form";
|
import { ChangePasswordForm } from "@/components/change-password-form";
|
||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
|
@@ -2,7 +2,7 @@ import { Alert } from "@/components/alert";
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { PasswordForm } from "@/components/password-form";
|
import { PasswordForm } from "@/components/password-form";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
|
@@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { SetPasswordForm } from "@/components/set-password-form";
|
import { SetPasswordForm } from "@/components/set-password-form";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { RegisterForm } from "@/components/register-form";
|
import { RegisterForm } from "@/components/register-form";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
getDefaultOrg,
|
getDefaultOrg,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
|
import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import {
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
getDefaultOrg,
|
getDefaultOrg,
|
||||||
|
@@ -1,69 +1,29 @@
|
|||||||
|
import { Alert, AlertType } from "@/components/alert";
|
||||||
import { Button, ButtonVariants } from "@/components/button";
|
import { Button, ButtonVariants } from "@/components/button";
|
||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { SelfServiceMenu } from "@/components/self-service-menu";
|
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getMostRecentCookieWithLoginname } from "@/lib/cookies";
|
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
|
||||||
import {
|
import {
|
||||||
createCallback,
|
getMostRecentCookieWithLoginname,
|
||||||
createResponse,
|
getSessionCookieById,
|
||||||
|
} from "@/lib/cookies";
|
||||||
|
import { completeDeviceAuthorization } from "@/lib/server/device";
|
||||||
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
|
import {
|
||||||
getBrandingSettings,
|
getBrandingSettings,
|
||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getSession,
|
getSession,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { create } from "@zitadel/client";
|
|
||||||
import {
|
|
||||||
CreateCallbackRequestSchema,
|
|
||||||
SessionSchema,
|
|
||||||
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
|
||||||
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
|
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
async function loadSession(
|
async function loadSessionById(
|
||||||
serviceUrl: string,
|
serviceUrl: string,
|
||||||
|
sessionId: string,
|
||||||
loginName: string,
|
organization?: string,
|
||||||
requestId?: string,
|
|
||||||
) {
|
) {
|
||||||
const recent = await getMostRecentCookieWithLoginname({ loginName });
|
const recent = await getSessionCookieById({ sessionId, organization });
|
||||||
|
|
||||||
if (requestId && requestId.startsWith("oidc_")) {
|
|
||||||
return createCallback({
|
|
||||||
serviceUrl,
|
|
||||||
req: create(CreateCallbackRequestSchema, {
|
|
||||||
authRequestId: requestId,
|
|
||||||
callbackKind: {
|
|
||||||
case: "session",
|
|
||||||
value: create(SessionSchema, {
|
|
||||||
sessionId: recent.id,
|
|
||||||
sessionToken: recent.token,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}).then(({ callbackUrl }) => {
|
|
||||||
return redirect(callbackUrl);
|
|
||||||
});
|
|
||||||
} else if (requestId && requestId.startsWith("saml_")) {
|
|
||||||
return createResponse({
|
|
||||||
serviceUrl,
|
|
||||||
req: create(CreateResponseRequestSchema, {
|
|
||||||
samlRequestId: requestId.replace("saml_", ""),
|
|
||||||
responseKind: {
|
|
||||||
case: "session",
|
|
||||||
value: {
|
|
||||||
sessionId: recent.id,
|
|
||||||
sessionToken: recent.token,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}).then(({ url }) => {
|
|
||||||
return redirect(url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return getSession({
|
return getSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
sessionId: recent.id,
|
sessionId: recent.id,
|
||||||
@@ -83,14 +43,45 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
const { loginName, requestId, organization } = searchParams;
|
const { loginName, requestId, organization, sessionId } = searchParams;
|
||||||
const sessionFactors = await loadSession(serviceUrl, loginName, requestId);
|
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
const branding = await getBrandingSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
organization,
|
organization,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// complete device authorization flow if device requestId is present
|
||||||
|
if (requestId && requestId.startsWith("device_")) {
|
||||||
|
const cookie = sessionId
|
||||||
|
? await getSessionCookieById({ sessionId, organization })
|
||||||
|
: await getMostRecentCookieWithLoginname({
|
||||||
|
loginName: loginName,
|
||||||
|
organization: organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
await completeDeviceAuthorization(requestId.replace("device_", ""), {
|
||||||
|
sessionId: cookie.id,
|
||||||
|
sessionToken: cookie.token,
|
||||||
|
}).catch((err) => {
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>{t("error.title")}</h1>
|
||||||
|
<p className="ztdl-p mb-6 block">{t("error.description")}</p>
|
||||||
|
<Alert>{err.message}</Alert>
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionFactors = sessionId
|
||||||
|
? await loadSessionById(serviceUrl, sessionId, organization)
|
||||||
|
: await loadMostRecentSession({
|
||||||
|
serviceUrl,
|
||||||
|
sessionParams: { loginName, organization },
|
||||||
|
});
|
||||||
|
|
||||||
let loginSettings;
|
let loginSettings;
|
||||||
if (!requestId) {
|
if (!requestId) {
|
||||||
loginSettings = await getLoginSettings({
|
loginSettings = await getLoginSettings({
|
||||||
@@ -110,12 +101,15 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
<UserAvatar
|
<UserAvatar
|
||||||
loginName={loginName ?? sessionFactors?.factors?.user?.loginName}
|
loginName={loginName ?? sessionFactors?.factors?.user?.loginName}
|
||||||
displayName={sessionFactors?.factors?.user?.displayName}
|
displayName={sessionFactors?.factors?.user?.displayName}
|
||||||
showDropdown
|
showDropdown={!(requestId && requestId.startsWith("device_"))}
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{sessionFactors?.id && (
|
{requestId && requestId.startsWith("device_") && (
|
||||||
<SelfServiceMenu sessionId={sessionFactors?.id} />
|
<Alert type={AlertType.INFO}>
|
||||||
|
You can now close this window and return to the device where you
|
||||||
|
started the authorization process to continue.
|
||||||
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loginSettings?.defaultRedirectUri && (
|
{loginSettings?.defaultRedirectUri && (
|
||||||
|
@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
|||||||
import { LoginPasskey } from "@/components/login-passkey";
|
import { LoginPasskey } from "@/components/login-passkey";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getSessionCookieById } from "@/lib/cookies";
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import { getBrandingSettings, getSession } from "@/lib/zitadel";
|
import { getBrandingSettings, getSession } from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
@@ -2,7 +2,7 @@ import { Alert } from "@/components/alert";
|
|||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { RegisterU2f } from "@/components/register-u2f";
|
import { RegisterU2f } from "@/components/register-u2f";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import { getBrandingSettings } from "@/lib/zitadel";
|
import { getBrandingSettings } from "@/lib/zitadel";
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
@@ -1,18 +1,12 @@
|
|||||||
import { Alert } from "@/components/alert";
|
import { Alert, AlertType } from "@/components/alert";
|
||||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
import { UserAvatar } from "@/components/user-avatar";
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
import { VerifyForm } from "@/components/verify-form";
|
import { VerifyForm } from "@/components/verify-form";
|
||||||
import { VerifyRedirectButton } from "@/components/verify-redirect-button";
|
import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify";
|
||||||
import { sendEmailCode } from "@/lib/server/verify";
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { getServiceUrlFromHeaders } from "@/lib/service";
|
|
||||||
import { loadMostRecentSession } from "@/lib/session";
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
import {
|
import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
|
||||||
getBrandingSettings,
|
|
||||||
getUserByID,
|
|
||||||
listAuthenticationMethodTypes,
|
|
||||||
} from "@/lib/zitadel";
|
|
||||||
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
|
||||||
import { getLocale, getTranslations } from "next-intl/server";
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
@@ -22,16 +16,11 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
const t = await getTranslations({ locale, namespace: "verify" });
|
const t = await getTranslations({ locale, namespace: "verify" });
|
||||||
const tError = await getTranslations({ locale, namespace: "error" });
|
const tError = await getTranslations({ locale, namespace: "error" });
|
||||||
|
|
||||||
const { userId, loginName, code, organization, requestId, invite } =
|
const { userId, loginName, code, organization, requestId, invite, send } =
|
||||||
searchParams;
|
searchParams;
|
||||||
|
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
const host = _headers.get("host");
|
|
||||||
|
|
||||||
if (!host || typeof host !== "string") {
|
|
||||||
throw new Error("No host found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const branding = await getBrandingSettings({
|
const branding = await getBrandingSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
@@ -43,10 +32,40 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
let human: HumanUser | undefined;
|
let human: HumanUser | undefined;
|
||||||
let id: string | undefined;
|
let id: string | undefined;
|
||||||
|
|
||||||
const doSend = invite !== "true";
|
const doSend = send === "true";
|
||||||
|
|
||||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||||
|
|
||||||
|
async function sendEmail(userId: string) {
|
||||||
|
const host = _headers.get("host");
|
||||||
|
|
||||||
|
if (!host || typeof host !== "string") {
|
||||||
|
throw new Error("No host found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invite === "true") {
|
||||||
|
await sendInviteEmailCode({
|
||||||
|
userId,
|
||||||
|
urlTemplate:
|
||||||
|
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
||||||
|
(requestId ? `&requestId=${requestId}` : ""),
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Could not send invitation email", error);
|
||||||
|
throw Error("Failed to send invitation email");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await sendEmailCode({
|
||||||
|
userId,
|
||||||
|
urlTemplate:
|
||||||
|
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||||
|
(requestId ? `&requestId=${requestId}` : ""),
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("Could not send verification email", error);
|
||||||
|
throw Error("Failed to send verification email");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("loginName" in searchParams) {
|
if ("loginName" in searchParams) {
|
||||||
sessionFactors = await loadMostRecentSession({
|
sessionFactors = await loadMostRecentSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
@@ -57,29 +76,11 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (doSend && sessionFactors?.factors?.user?.id) {
|
if (doSend && sessionFactors?.factors?.user?.id) {
|
||||||
await sendEmailCode({
|
await sendEmail(sessionFactors.factors.user.id);
|
||||||
serviceUrl,
|
|
||||||
userId: sessionFactors?.factors?.user?.id,
|
|
||||||
urlTemplate:
|
|
||||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
|
||||||
(requestId ? `&requestId=${requestId}` : ""),
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error("Could not resend verification email", error);
|
|
||||||
throw Error("Failed to send verification email");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if ("userId" in searchParams && userId) {
|
} else if ("userId" in searchParams && userId) {
|
||||||
if (doSend) {
|
if (doSend) {
|
||||||
await sendEmailCode({
|
await sendEmail(userId);
|
||||||
serviceUrl,
|
|
||||||
userId,
|
|
||||||
urlTemplate:
|
|
||||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
|
||||||
(requestId ? `&requestId=${requestId}` : ""),
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error("Could not resend verification email", error);
|
|
||||||
throw Error("Failed to send verification email");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userResponse = await getUserByID({
|
const userResponse = await getUserByID({
|
||||||
@@ -96,12 +97,8 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
|
|
||||||
id = userId ?? sessionFactors?.factors?.user?.id;
|
id = userId ?? sessionFactors?.factors?.user?.id;
|
||||||
|
|
||||||
let authMethods: AuthenticationMethodType[] | null = null;
|
if (!id) {
|
||||||
if (human?.email?.isVerified) {
|
throw Error("Failed to get user id");
|
||||||
const authMethodsResponse = await listAuthenticationMethodTypes(userId);
|
|
||||||
if (authMethodsResponse.authMethodTypes) {
|
|
||||||
authMethods = authMethodsResponse.authMethodTypes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
@@ -138,6 +135,12 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{id && send && (
|
||||||
|
<div className="py-4 w-full">
|
||||||
|
<Alert type={AlertType.INFO}>{t("verify.codeSent")}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{sessionFactors ? (
|
{sessionFactors ? (
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
@@ -155,27 +158,14 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{id &&
|
<VerifyForm
|
||||||
(human?.email?.isVerified ? (
|
loginName={loginName}
|
||||||
// show page for already verified users
|
organization={organization}
|
||||||
<VerifyRedirectButton
|
userId={id}
|
||||||
userId={id}
|
code={code}
|
||||||
loginName={loginName}
|
isInvite={invite === "true"}
|
||||||
organization={organization}
|
requestId={requestId}
|
||||||
requestId={requestId}
|
/>
|
||||||
authMethods={authMethods}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
// check if auth methods are set
|
|
||||||
<VerifyForm
|
|
||||||
loginName={loginName}
|
|
||||||
organization={organization}
|
|
||||||
userId={id}
|
|
||||||
code={code}
|
|
||||||
isInvite={invite === "true"}
|
|
||||||
requestId={requestId}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</DynamicTheme>
|
</DynamicTheme>
|
||||||
);
|
);
|
||||||
|
109
apps/login/src/app/(login)/verify/success/page.tsx
Normal file
109
apps/login/src/app/(login)/verify/success/page.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||||
|
import { UserAvatar } from "@/components/user-avatar";
|
||||||
|
import { getSessionCookieById } from "@/lib/cookies";
|
||||||
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
|
import { loadMostRecentSession } from "@/lib/session";
|
||||||
|
import {
|
||||||
|
getBrandingSettings,
|
||||||
|
getLoginSettings,
|
||||||
|
getSession,
|
||||||
|
getUserByID,
|
||||||
|
} from "@/lib/zitadel";
|
||||||
|
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
|
import { getLocale, getTranslations } from "next-intl/server";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
|
async function loadSessionById(
|
||||||
|
serviceUrl: string,
|
||||||
|
sessionId: string,
|
||||||
|
organization?: string,
|
||||||
|
) {
|
||||||
|
const recent = await getSessionCookieById({ sessionId, organization });
|
||||||
|
return getSession({
|
||||||
|
serviceUrl,
|
||||||
|
sessionId: recent.id,
|
||||||
|
sessionToken: recent.token,
|
||||||
|
}).then((response) => {
|
||||||
|
if (response?.session) {
|
||||||
|
return response.session;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page(props: { searchParams: Promise<any> }) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
const locale = getLocale();
|
||||||
|
const t = await getTranslations({ locale, namespace: "verify" });
|
||||||
|
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
const { loginName, requestId, organization, userId } = searchParams;
|
||||||
|
|
||||||
|
const branding = await getBrandingSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionFactors = await loadMostRecentSession({
|
||||||
|
serviceUrl,
|
||||||
|
sessionParams: { loginName, organization },
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warn("Error loading session:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
let loginSettings;
|
||||||
|
if (!requestId) {
|
||||||
|
loginSettings = await getLoginSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = userId ?? sessionFactors?.factors?.user?.id;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw Error("Failed to get user id");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResponse = await getUserByID({
|
||||||
|
serviceUrl,
|
||||||
|
userId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
let user: User | undefined;
|
||||||
|
let human: HumanUser | undefined;
|
||||||
|
|
||||||
|
if (userResponse) {
|
||||||
|
user = userResponse.user;
|
||||||
|
if (user?.type.case === "human") {
|
||||||
|
human = user.type.value as HumanUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DynamicTheme branding={branding}>
|
||||||
|
<div className="flex flex-col items-center space-y-4">
|
||||||
|
<h1>{t("successTitle")}</h1>
|
||||||
|
<p className="ztdl-p mb-6 block">{t("successDescription")}</p>
|
||||||
|
|
||||||
|
{sessionFactors ? (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={loginName ?? sessionFactors.factors?.user?.loginName}
|
||||||
|
displayName={sessionFactors.factors?.user?.displayName}
|
||||||
|
showDropdown
|
||||||
|
searchParams={searchParams}
|
||||||
|
></UserAvatar>
|
||||||
|
) : (
|
||||||
|
user && (
|
||||||
|
<UserAvatar
|
||||||
|
loginName={user.preferredLoginName}
|
||||||
|
displayName={human?.profile?.displayName}
|
||||||
|
showDropdown={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DynamicTheme>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,9 +1,9 @@
|
|||||||
import { getAllSessions } from "@/lib/cookies";
|
import { getAllSessions } from "@/lib/cookies";
|
||||||
import { idpTypeToSlug } from "@/lib/idp";
|
import { idpTypeToSlug } from "@/lib/idp";
|
||||||
import { loginWithOIDCandSession } from "@/lib/oidc";
|
import { loginWithOIDCAndSession } from "@/lib/oidc";
|
||||||
import { loginWithSAMLandSession } from "@/lib/saml";
|
import { loginWithSAMLAndSession } from "@/lib/saml";
|
||||||
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
|
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
|
||||||
import { constructUrl, getServiceUrlFromHeaders } from "@/lib/service";
|
import { constructUrl, getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
import { findValidSession } from "@/lib/session";
|
import { findValidSession } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
createCallback,
|
createCallback,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
getAuthRequest,
|
getAuthRequest,
|
||||||
getOrgsByDomain,
|
getOrgsByDomain,
|
||||||
getSAMLRequest,
|
getSAMLRequest,
|
||||||
|
getSecuritySettings,
|
||||||
listSessions,
|
listSessions,
|
||||||
startIdentityProviderFlow,
|
startIdentityProviderFlow,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
@@ -25,6 +26,7 @@ import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml
|
|||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { DEFAULT_CSP } from "../../../constants/csp";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
export const revalidate = false;
|
export const revalidate = false;
|
||||||
@@ -107,7 +109,7 @@ export async function GET(request: NextRequest) {
|
|||||||
if (requestId && sessionId) {
|
if (requestId && sessionId) {
|
||||||
if (requestId.startsWith("oidc_")) {
|
if (requestId.startsWith("oidc_")) {
|
||||||
// this finishes the login process for OIDC
|
// this finishes the login process for OIDC
|
||||||
return loginWithOIDCandSession({
|
return loginWithOIDCAndSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
authRequest: requestId.replace("oidc_", ""),
|
authRequest: requestId.replace("oidc_", ""),
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -117,7 +119,7 @@ export async function GET(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
} else if (requestId.startsWith("saml_")) {
|
} else if (requestId.startsWith("saml_")) {
|
||||||
// this finishes the login process for SAML
|
// this finishes the login process for SAML
|
||||||
return loginWithSAMLandSession({
|
return loginWithSAMLAndSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
samlRequest: requestId.replace("saml_", ""),
|
samlRequest: requestId.replace("saml_", ""),
|
||||||
sessionId,
|
sessionId,
|
||||||
@@ -293,17 +295,32 @@ export async function GET(request: NextRequest) {
|
|||||||
* This means that the user should not be prompted to enter their password again.
|
* This means that the user should not be prompted to enter their password again.
|
||||||
* Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction
|
* Instead, the server attempts to silently authenticate the user using an existing session or other authentication mechanisms that do not require user interaction
|
||||||
**/
|
**/
|
||||||
|
const securitySettings = await getSecuritySettings({
|
||||||
|
serviceUrl,
|
||||||
|
});
|
||||||
|
|
||||||
const selectedSession = await findValidSession({
|
const selectedSession = await findValidSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
sessions,
|
sessions,
|
||||||
authRequest,
|
authRequest,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!selectedSession || !selectedSession.id) {
|
const noSessionResponse = NextResponse.json(
|
||||||
return NextResponse.json(
|
{ error: "No active session found" },
|
||||||
{ error: "No active session found" },
|
{ status: 400 },
|
||||||
{ status: 400 },
|
);
|
||||||
|
|
||||||
|
if (securitySettings?.embeddedIframe?.enabled) {
|
||||||
|
securitySettings.embeddedIframe.allowedOrigins;
|
||||||
|
noSessionResponse.headers.set(
|
||||||
|
"Content-Security-Policy",
|
||||||
|
`${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`,
|
||||||
);
|
);
|
||||||
|
noSessionResponse.headers.delete("X-Frame-Options");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedSession || !selectedSession.id) {
|
||||||
|
return noSessionResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cookie = sessionCookies.find(
|
const cookie = sessionCookies.find(
|
||||||
@@ -311,10 +328,7 @@ export async function GET(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!cookie || !cookie.id || !cookie.token) {
|
if (!cookie || !cookie.id || !cookie.token) {
|
||||||
return NextResponse.json(
|
return noSessionResponse;
|
||||||
{ error: "No active session found" },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = {
|
const session = {
|
||||||
@@ -332,7 +346,19 @@ export async function GET(request: NextRequest) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
return NextResponse.redirect(callbackUrl);
|
|
||||||
|
const callbackResponse = NextResponse.redirect(callbackUrl);
|
||||||
|
|
||||||
|
if (securitySettings?.embeddedIframe?.enabled) {
|
||||||
|
securitySettings.embeddedIframe.allowedOrigins;
|
||||||
|
callbackResponse.headers.set(
|
||||||
|
"Content-Security-Policy",
|
||||||
|
`${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`,
|
||||||
|
);
|
||||||
|
callbackResponse.headers.delete("X-Frame-Options");
|
||||||
|
}
|
||||||
|
|
||||||
|
return callbackResponse;
|
||||||
} else {
|
} else {
|
||||||
// check for loginHint, userId hint and valid sessions
|
// check for loginHint, userId hint and valid sessions
|
||||||
let selectedSession = await findValidSession({
|
let selectedSession = await findValidSession({
|
||||||
@@ -499,7 +525,9 @@ export async function GET(request: NextRequest) {
|
|||||||
requestId: `saml_${samlRequest.id}`,
|
requestId: `saml_${samlRequest.id}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
// Device Authorization does not need to start here as it is handled on the /device endpoint
|
||||||
|
else {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "No authRequest nor samlRequest provided" },
|
{ error: "No authRequest nor samlRequest provided" },
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
|
28
apps/login/src/app/security/route.ts
Normal file
28
apps/login/src/app/security/route.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { createServiceForHost } from "@/lib/service";
|
||||||
|
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||||
|
import { Client } from "@zitadel/client";
|
||||||
|
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
const settingsService: Client<typeof SettingsService> =
|
||||||
|
await createServiceForHost(SettingsService, serviceUrl);
|
||||||
|
|
||||||
|
const settings = await settingsService
|
||||||
|
.getSecuritySettings({})
|
||||||
|
.then((resp) => (resp.settings ? resp.settings : undefined));
|
||||||
|
|
||||||
|
const response = NextResponse.json({ settings }, { status: 200 });
|
||||||
|
|
||||||
|
// Add Cache-Control header to cache the response for up to 1 hour
|
||||||
|
response.headers.set(
|
||||||
|
"Cache-Control",
|
||||||
|
"public, max-age=3600, stale-while-revalidate=86400",
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
48
apps/login/src/components/app-avatar.tsx
Normal file
48
apps/login/src/components/app-avatar.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { ColorShade, getColorHash } from "@/helpers/colors";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { getInitials } from "./avatar";
|
||||||
|
|
||||||
|
interface AvatarProps {
|
||||||
|
appName: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
shadow?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppAvatar({ appName, imageUrl, shadow }: AvatarProps) {
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
|
const credentials = getInitials(appName, appName);
|
||||||
|
|
||||||
|
const color: ColorShade = getColorHash(appName);
|
||||||
|
|
||||||
|
const avatarStyleDark = {
|
||||||
|
backgroundColor: color[900],
|
||||||
|
color: color[200],
|
||||||
|
};
|
||||||
|
|
||||||
|
const avatarStyleLight = {
|
||||||
|
backgroundColor: color[200],
|
||||||
|
color: color[900],
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-[100px] h-[100px] flex justify-center items-center cursor-default pointer-events-none group-focus:outline-none group-focus:ring-2 transition-colors duration-200 dark:group-focus:ring-offset-blue bg-primary-light-500 text-primary-light-contrast-500 hover:bg-primary-light-400 hover:dark:bg-primary-dark-500 group-focus:ring-primary-light-200 dark:group-focus:ring-primary-dark-400 dark:bg-primary-dark-300 dark:text-primary-dark-contrast-300 dark:text-blue rounded-full ${
|
||||||
|
shadow ? "shadow" : ""
|
||||||
|
}`}
|
||||||
|
style={resolvedTheme === "light" ? avatarStyleLight : avatarStyleDark}
|
||||||
|
>
|
||||||
|
{imageUrl ? (
|
||||||
|
<Image
|
||||||
|
height={48}
|
||||||
|
width={48}
|
||||||
|
alt="avatar"
|
||||||
|
className="w-full h-full border border-divider-light dark:border-divider-dark rounded-full"
|
||||||
|
src={imageUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className={`uppercase text-3xl`}>{credentials}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -12,7 +12,7 @@ interface AvatarProps {
|
|||||||
shadow?: boolean;
|
shadow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitials(name: string, loginName: string) {
|
export function getInitials(name: string, loginName: string) {
|
||||||
let credentials = "";
|
let credentials = "";
|
||||||
if (name) {
|
if (name) {
|
||||||
const split = name.split(" ");
|
const split = name.split(" ");
|
||||||
|
111
apps/login/src/components/consent.tsx
Normal file
111
apps/login/src/components/consent.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { completeDeviceAuthorization } from "@/lib/server/device";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Alert } from "./alert";
|
||||||
|
import { Button, ButtonVariants } from "./button";
|
||||||
|
import { Spinner } from "./spinner";
|
||||||
|
|
||||||
|
export function ConsentScreen({
|
||||||
|
scope,
|
||||||
|
nextUrl,
|
||||||
|
deviceAuthorizationRequestId,
|
||||||
|
appName,
|
||||||
|
}: {
|
||||||
|
scope?: string[];
|
||||||
|
nextUrl: string;
|
||||||
|
deviceAuthorizationRequestId: string;
|
||||||
|
appName?: string;
|
||||||
|
}) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function denyDeviceAuth() {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await completeDeviceAuthorization(
|
||||||
|
deviceAuthorizationRequestId,
|
||||||
|
)
|
||||||
|
.catch(() => {
|
||||||
|
setError("Could not register user");
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
return router.push("/device");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopes = scope?.filter((s) => !!s);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-4 w-full flex flex-col items-center space-y-4">
|
||||||
|
<ul className="list-disc space-y-2 w-full">
|
||||||
|
{scopes?.length === 0 && (
|
||||||
|
<span className="w-full text-sm flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light py-2 px-4 rounded-md transition-all">
|
||||||
|
{t("device.scope.openid")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{scopes?.map((s) => {
|
||||||
|
const translationKey = `device.scope.${s}`;
|
||||||
|
const description = t(translationKey, null);
|
||||||
|
|
||||||
|
// Check if the key itself is returned and provide a fallback
|
||||||
|
const resolvedDescription =
|
||||||
|
description === translationKey ? "" : description;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={s}
|
||||||
|
className="w-full text-sm flex flex-row items-center bg-background-light-400 dark:bg-background-dark-400 border border-divider-light py-2 px-4 rounded-md transition-all"
|
||||||
|
>
|
||||||
|
<span>{resolvedDescription}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p className="ztdl-p text-xs text-left">
|
||||||
|
{t("device.request.disclaimer", { appName: appName })}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="py-4">
|
||||||
|
<Alert>{error}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-4 flex w-full flex-row items-center">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
denyDeviceAuth();
|
||||||
|
}}
|
||||||
|
variant={ButtonVariants.Secondary}
|
||||||
|
data-testid="deny-button"
|
||||||
|
>
|
||||||
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
|
{t("device.request.deny")}
|
||||||
|
</Button>
|
||||||
|
<span className="flex-grow"></span>
|
||||||
|
|
||||||
|
<Link href={nextUrl}>
|
||||||
|
<Button
|
||||||
|
data-testid="submit-button"
|
||||||
|
type="submit"
|
||||||
|
className="self-end"
|
||||||
|
variant={ButtonVariants.Primary}
|
||||||
|
>
|
||||||
|
{t("device.request.submit")}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
97
apps/login/src/components/device-code-form.tsx
Normal file
97
apps/login/src/components/device-code-form.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Alert } from "@/components/alert";
|
||||||
|
import { getDeviceAuthorizationRequest } from "@/lib/server/oidc";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { BackButton } from "./back-button";
|
||||||
|
import { Button, ButtonVariants } from "./button";
|
||||||
|
import { TextInput } from "./input";
|
||||||
|
import { Spinner } from "./spinner";
|
||||||
|
|
||||||
|
type Inputs = {
|
||||||
|
userCode: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DeviceCodeForm({ userCode }: { userCode?: string }) {
|
||||||
|
const t = useTranslations("verify");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState } = useForm<Inputs>({
|
||||||
|
mode: "onBlur",
|
||||||
|
defaultValues: {
|
||||||
|
userCode: userCode || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
async function submitCodeAndContinue(value: Inputs): Promise<boolean | void> {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const response = await getDeviceAuthorizationRequest(value.userCode)
|
||||||
|
.catch(() => {
|
||||||
|
setError("Could not continue the request");
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response || !response.deviceAuthorizationRequest?.id) {
|
||||||
|
setError("Could not continue the request");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return router.push(
|
||||||
|
`/device/consent?` +
|
||||||
|
new URLSearchParams({
|
||||||
|
requestId: `device_${response.deviceAuthorizationRequest.id}`,
|
||||||
|
user_code: value.userCode,
|
||||||
|
}).toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form className="w-full">
|
||||||
|
<div className="mt-4">
|
||||||
|
<TextInput
|
||||||
|
type="text"
|
||||||
|
autoComplete="one-time-code"
|
||||||
|
{...register("userCode", { required: "This field is required" })}
|
||||||
|
label="Code"
|
||||||
|
data-testid="code-text-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="py-4" data-testid="error">
|
||||||
|
<Alert>{error}</Alert>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex w-full flex-row items-center">
|
||||||
|
<BackButton />
|
||||||
|
<span className="flex-grow"></span>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="self-end"
|
||||||
|
variant={ButtonVariants.Primary}
|
||||||
|
disabled={loading || !formState.isValid}
|
||||||
|
onClick={handleSubmit(submitCodeAndContinue)}
|
||||||
|
data-testid="submit-button"
|
||||||
|
>
|
||||||
|
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
||||||
|
{t("verify.submit")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -3,27 +3,34 @@
|
|||||||
import { Logo } from "@/components/logo";
|
import { Logo } from "@/components/logo";
|
||||||
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
|
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
import { AppAvatar } from "./app-avatar";
|
||||||
import { ThemeWrapper } from "./theme-wrapper";
|
import { ThemeWrapper } from "./theme-wrapper";
|
||||||
|
|
||||||
export function DynamicTheme({
|
export function DynamicTheme({
|
||||||
branding,
|
branding,
|
||||||
children,
|
children,
|
||||||
|
appName,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
branding?: BrandingSettings;
|
branding?: BrandingSettings;
|
||||||
|
appName?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ThemeWrapper branding={branding}>
|
<ThemeWrapper branding={branding}>
|
||||||
<div className="rounded-lg bg-background-light-400 dark:bg-background-dark-500 px-8 py-12">
|
<div className="rounded-lg bg-background-light-400 dark:bg-background-dark-500 px-8 py-12">
|
||||||
<div className="mx-auto flex flex-col items-center space-y-4">
|
<div className="mx-auto flex flex-col items-center space-y-4">
|
||||||
<div className="relative">
|
<div className="relative flex flex-row items-center justify-center gap-8">
|
||||||
{branding && (
|
{branding && (
|
||||||
<Logo
|
<>
|
||||||
lightSrc={branding.lightTheme?.logoUrl}
|
<Logo
|
||||||
darkSrc={branding.darkTheme?.logoUrl}
|
lightSrc={branding.lightTheme?.logoUrl}
|
||||||
height={150}
|
darkSrc={branding.darkTheme?.logoUrl}
|
||||||
width={150}
|
height={appName ? 100 : 150}
|
||||||
/>
|
width={appName ? 100 : 150}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{appName && <AppAvatar appName={appName} />}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -83,6 +83,16 @@ export function RegisterPasskey({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("error" in resp && resp.error) {
|
||||||
|
setError(resp.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("passkeyId" in resp)) {
|
||||||
|
setError("An error on registering passkey");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const passkeyId = resp.passkeyId;
|
const passkeyId = resp.passkeyId;
|
||||||
const options: CredentialCreationOptions =
|
const options: CredentialCreationOptions =
|
||||||
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
|
(resp.publicKeyCredentialCreationOptions as CredentialCreationOptions) ??
|
||||||
@@ -92,6 +102,7 @@ export function RegisterPasskey({
|
|||||||
setError("An error on registering passkey");
|
setError("An error on registering passkey");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
options.publicKey.challenge = coerceToArrayBuffer(
|
options.publicKey.challenge = coerceToArrayBuffer(
|
||||||
options.publicKey.challenge,
|
options.publicKey.challenge,
|
||||||
"challenge",
|
"challenge",
|
||||||
|
@@ -74,7 +74,7 @@ export function SignInWithIdp({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full space-y-2 text-sm">
|
<div className="flex flex-col w-full space-y-2 text-sm">
|
||||||
{identityProviders?.map(renderIDPButton)}
|
{!!identityProviders.length && identityProviders?.map(renderIDPButton)}
|
||||||
{state?.error && (
|
{state?.error && (
|
||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
<Alert>{state?.error}</Alert>
|
<Alert>{state?.error}</Alert>
|
||||||
|
@@ -63,6 +63,11 @@ export function VerifyForm({
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response && "error" in response && response?.error) {
|
||||||
|
setError(response.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,90 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
sendVerificationRedirectWithoutCheck,
|
|
||||||
SendVerificationRedirectWithoutCheckCommand,
|
|
||||||
} from "@/lib/server/verify";
|
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Alert, AlertType } from "./alert";
|
|
||||||
import { BackButton } from "./back-button";
|
|
||||||
import { Button, ButtonVariants } from "./button";
|
|
||||||
import { Spinner } from "./spinner";
|
|
||||||
|
|
||||||
export function VerifyRedirectButton({
|
|
||||||
userId,
|
|
||||||
loginName,
|
|
||||||
requestId,
|
|
||||||
authMethods,
|
|
||||||
organization,
|
|
||||||
}: {
|
|
||||||
userId?: string;
|
|
||||||
loginName?: string;
|
|
||||||
requestId: string;
|
|
||||||
authMethods: AuthenticationMethodType[] | null;
|
|
||||||
organization?: string;
|
|
||||||
}) {
|
|
||||||
const t = useTranslations("verify");
|
|
||||||
const [error, setError] = useState<string>("");
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
async function submitAndContinue(): Promise<boolean | void> {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
let command = {
|
|
||||||
organization,
|
|
||||||
requestId,
|
|
||||||
} as SendVerificationRedirectWithoutCheckCommand;
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
command = {
|
|
||||||
...command,
|
|
||||||
userId,
|
|
||||||
} as SendVerificationRedirectWithoutCheckCommand;
|
|
||||||
} else if (loginName) {
|
|
||||||
command = {
|
|
||||||
...command,
|
|
||||||
loginName,
|
|
||||||
} as SendVerificationRedirectWithoutCheckCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
await sendVerificationRedirectWithoutCheck(command)
|
|
||||||
.catch(() => {
|
|
||||||
setError("Could not verify");
|
|
||||||
return;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Alert type={AlertType.INFO}>{t("success")}</Alert>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="py-4">
|
|
||||||
<Alert>{error}</Alert>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-8 flex w-full flex-row items-center">
|
|
||||||
<BackButton />
|
|
||||||
<span className="flex-grow"></span>
|
|
||||||
{authMethods?.length === 0 && (
|
|
||||||
<Button
|
|
||||||
onClick={() => submitAndContinue()}
|
|
||||||
type="submit"
|
|
||||||
className="self-end"
|
|
||||||
variant={ButtonVariants.Primary}
|
|
||||||
>
|
|
||||||
{loading && <Spinner className="h-5 w-5 mr-2" />}
|
|
||||||
{t("setupAuthenticator")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -5,6 +5,33 @@ type FinishFlowCommand =
|
|||||||
}
|
}
|
||||||
| { loginName: string };
|
| { loginName: string };
|
||||||
|
|
||||||
|
function goToSignedInPage(
|
||||||
|
props:
|
||||||
|
| { sessionId: string; organization?: string; requestId?: string }
|
||||||
|
| { organization?: string; loginName: string; requestId?: string },
|
||||||
|
) {
|
||||||
|
const params = new URLSearchParams({});
|
||||||
|
|
||||||
|
if ("loginName" in props && props.loginName) {
|
||||||
|
params.append("loginName", props.loginName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("sessionId" in props && props.sessionId) {
|
||||||
|
params.append("sessionId", props.sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.organization) {
|
||||||
|
params.append("organization", props.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
// required to show conditional UI for device flow
|
||||||
|
if (props.requestId) {
|
||||||
|
params.append("requestId", props.requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/signedin?` + params;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* for client: redirects user back to an OIDC or SAML application or to a success page when using requestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName
|
* for client: redirects user back to an OIDC or SAML application or to a success page when using requestId, check if a default redirect and redirect to it, or just redirect to a success page with the loginName
|
||||||
* @param command
|
* @param command
|
||||||
@@ -14,7 +41,25 @@ export async function getNextUrl(
|
|||||||
command: FinishFlowCommand & { organization?: string },
|
command: FinishFlowCommand & { organization?: string },
|
||||||
defaultRedirectUri?: string,
|
defaultRedirectUri?: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if ("sessionId" in command && "requestId" in command) {
|
// finish Device Authorization Flow
|
||||||
|
if (
|
||||||
|
"requestId" in command &&
|
||||||
|
command.requestId.startsWith("device_") &&
|
||||||
|
("loginName" in command || "sessionId" in command)
|
||||||
|
) {
|
||||||
|
return goToSignedInPage({
|
||||||
|
...command,
|
||||||
|
organization: command.organization,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish SAML or OIDC flow
|
||||||
|
if (
|
||||||
|
"sessionId" in command &&
|
||||||
|
"requestId" in command &&
|
||||||
|
(command.requestId.startsWith("saml_") ||
|
||||||
|
command.requestId.startsWith("oidc_"))
|
||||||
|
) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
sessionId: command.sessionId,
|
sessionId: command.sessionId,
|
||||||
requestId: command.requestId,
|
requestId: command.requestId,
|
||||||
@@ -31,13 +76,5 @@ export async function getNextUrl(
|
|||||||
return defaultRedirectUri;
|
return defaultRedirectUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
return goToSignedInPage(command);
|
||||||
loginName: command.loginName,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (command.organization) {
|
|
||||||
params.append("organization", command.organization);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `/signedin?` + params;
|
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,10 @@ export type Cookie = {
|
|||||||
|
|
||||||
type SessionCookie<T> = Cookie & T;
|
type SessionCookie<T> = Cookie & T;
|
||||||
|
|
||||||
async function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) {
|
async function setSessionHttpOnlyCookie<T>(
|
||||||
|
sessions: SessionCookie<T>[],
|
||||||
|
sameSite: boolean | "lax" | "strict" | "none" = true,
|
||||||
|
) {
|
||||||
const cookiesList = await cookies();
|
const cookiesList = await cookies();
|
||||||
|
|
||||||
return cookiesList.set({
|
return cookiesList.set({
|
||||||
@@ -28,6 +31,8 @@ async function setSessionHttpOnlyCookie<T>(sessions: SessionCookie<T>[]) {
|
|||||||
value: JSON.stringify(sessions),
|
value: JSON.stringify(sessions),
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: "/",
|
path: "/",
|
||||||
|
sameSite: process.env.NODE_ENV === "production" ? sameSite : "lax",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +47,15 @@ export async function setLanguageCookie(language: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addSessionToCookie<T>(
|
export async function addSessionToCookie<T>({
|
||||||
session: SessionCookie<T>,
|
session,
|
||||||
cleanup: boolean = false,
|
cleanup,
|
||||||
): Promise<any> {
|
sameSite,
|
||||||
|
}: {
|
||||||
|
session: SessionCookie<T>;
|
||||||
|
cleanup?: boolean;
|
||||||
|
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
|
||||||
|
}): Promise<any> {
|
||||||
const cookiesList = await cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
@@ -79,17 +89,23 @@ export async function addSessionToCookie<T>(
|
|||||||
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
|
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
|
||||||
: true,
|
: true,
|
||||||
);
|
);
|
||||||
return setSessionHttpOnlyCookie(filteredSessions);
|
return setSessionHttpOnlyCookie(filteredSessions, sameSite);
|
||||||
} else {
|
} else {
|
||||||
return setSessionHttpOnlyCookie(currentSessions);
|
return setSessionHttpOnlyCookie(currentSessions, sameSite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSessionCookie<T>(
|
export async function updateSessionCookie<T>({
|
||||||
id: string,
|
id,
|
||||||
session: SessionCookie<T>,
|
session,
|
||||||
cleanup: boolean = false,
|
cleanup,
|
||||||
): Promise<any> {
|
sameSite,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
session: SessionCookie<T>;
|
||||||
|
cleanup?: boolean;
|
||||||
|
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
|
||||||
|
}): Promise<any> {
|
||||||
const cookiesList = await cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
@@ -108,19 +124,24 @@ export async function updateSessionCookie<T>(
|
|||||||
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
|
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
|
||||||
: true,
|
: true,
|
||||||
);
|
);
|
||||||
return setSessionHttpOnlyCookie(filteredSessions);
|
return setSessionHttpOnlyCookie(filteredSessions, sameSite);
|
||||||
} else {
|
} else {
|
||||||
return setSessionHttpOnlyCookie(sessions);
|
return setSessionHttpOnlyCookie(sessions, sameSite);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw "updateSessionCookie<T>: session id now found";
|
throw "updateSessionCookie<T>: session id now found";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeSessionFromCookie<T>(
|
export async function removeSessionFromCookie<T>({
|
||||||
session: SessionCookie<T>,
|
session,
|
||||||
cleanup: boolean = false,
|
cleanup,
|
||||||
): Promise<any> {
|
sameSite,
|
||||||
|
}: {
|
||||||
|
session: SessionCookie<T>;
|
||||||
|
cleanup?: boolean;
|
||||||
|
sameSite?: boolean | "lax" | "strict" | "none" | undefined;
|
||||||
|
}): Promise<any> {
|
||||||
const cookiesList = await cookies();
|
const cookiesList = await cookies();
|
||||||
const stringifiedCookie = cookiesList.get("sessions");
|
const stringifiedCookie = cookiesList.get("sessions");
|
||||||
|
|
||||||
@@ -136,9 +157,9 @@ export async function removeSessionFromCookie<T>(
|
|||||||
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
|
? timestampDate(timestampFromMs(Number(session.expirationTs))) > now
|
||||||
: true,
|
: true,
|
||||||
);
|
);
|
||||||
return setSessionHttpOnlyCookie(filteredSessions);
|
return setSessionHttpOnlyCookie(filteredSessions, sameSite);
|
||||||
} else {
|
} else {
|
||||||
return setSessionHttpOnlyCookie(reducedSessions);
|
return setSessionHttpOnlyCookie(reducedSessions, sameSite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -8,10 +8,10 @@ import {
|
|||||||
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
} from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { constructUrl } from "./service";
|
import { constructUrl } from "./service-url";
|
||||||
import { isSessionValid } from "./session";
|
import { isSessionValid } from "./session";
|
||||||
|
|
||||||
type LoginWithOIDCandSession = {
|
type LoginWithOIDCAndSession = {
|
||||||
serviceUrl: string;
|
serviceUrl: string;
|
||||||
authRequest: string;
|
authRequest: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -19,14 +19,14 @@ type LoginWithOIDCandSession = {
|
|||||||
sessionCookies: Cookie[];
|
sessionCookies: Cookie[];
|
||||||
request: NextRequest;
|
request: NextRequest;
|
||||||
};
|
};
|
||||||
export async function loginWithOIDCandSession({
|
export async function loginWithOIDCAndSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
authRequest,
|
authRequest,
|
||||||
sessionId,
|
sessionId,
|
||||||
sessions,
|
sessions,
|
||||||
sessionCookies,
|
sessionCookies,
|
||||||
request,
|
request,
|
||||||
}: LoginWithOIDCandSession) {
|
}: LoginWithOIDCAndSession) {
|
||||||
console.log(
|
console.log(
|
||||||
`Login with session: ${sessionId} and authRequest: ${authRequest}`,
|
`Login with session: ${sessionId} and authRequest: ${authRequest}`,
|
||||||
);
|
);
|
||||||
|
@@ -5,10 +5,10 @@ import { create } from "@zitadel/client";
|
|||||||
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
|
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
|
||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { constructUrl } from "./service";
|
import { constructUrl } from "./service-url";
|
||||||
import { isSessionValid } from "./session";
|
import { isSessionValid } from "./session";
|
||||||
|
|
||||||
type LoginWithSAMLandSession = {
|
type LoginWithSAMLAndSession = {
|
||||||
serviceUrl: string;
|
serviceUrl: string;
|
||||||
samlRequest: string;
|
samlRequest: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -17,14 +17,14 @@ type LoginWithSAMLandSession = {
|
|||||||
request: NextRequest;
|
request: NextRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loginWithSAMLandSession({
|
export async function loginWithSAMLAndSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
samlRequest,
|
samlRequest,
|
||||||
sessionId,
|
sessionId,
|
||||||
sessions,
|
sessions,
|
||||||
sessionCookies,
|
sessionCookies,
|
||||||
request,
|
request,
|
||||||
}: LoginWithSAMLandSession) {
|
}: LoginWithSAMLAndSession) {
|
||||||
console.log(
|
console.log(
|
||||||
`Login with session: ${sessionId} and samlRequest: ${samlRequest}`,
|
`Login with session: ${sessionId} and samlRequest: ${samlRequest}`,
|
||||||
);
|
);
|
||||||
|
@@ -4,7 +4,7 @@ import { createServerTransport } from "@zitadel/client/node";
|
|||||||
import { createUserServiceClient } from "@zitadel/client/v2";
|
import { createUserServiceClient } from "@zitadel/client/v2";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { getSessionCookieById } from "./cookies";
|
import { getSessionCookieById } from "./cookies";
|
||||||
import { getServiceUrlFromHeaders } from "./service";
|
import { getServiceUrlFromHeaders } from "./service-url";
|
||||||
import { getSession } from "./zitadel";
|
import { getSession } from "./zitadel";
|
||||||
|
|
||||||
const transport = async (serviceUrl: string, token: string) => {
|
const transport = async (serviceUrl: string, token: string) => {
|
||||||
|
@@ -4,6 +4,7 @@ import { addSessionToCookie, updateSessionCookie } from "@/lib/cookies";
|
|||||||
import {
|
import {
|
||||||
createSessionForUserIdAndIdpIntent,
|
createSessionForUserIdAndIdpIntent,
|
||||||
createSessionFromChecks,
|
createSessionFromChecks,
|
||||||
|
getSecuritySettings,
|
||||||
getSession,
|
getSession,
|
||||||
setSession,
|
setSession,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
@@ -20,7 +21,7 @@ import {
|
|||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
|
|
||||||
type CustomCookieData = {
|
type CustomCookieData = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -65,7 +66,7 @@ export async function createSessionAndUpdateCookie(command: {
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
sessionId: createdSession.sessionId,
|
sessionId: createdSession.sessionId,
|
||||||
sessionToken: createdSession.sessionToken,
|
sessionToken: createdSession.sessionToken,
|
||||||
}).then((response) => {
|
}).then(async (response) => {
|
||||||
if (response?.session && response.session?.factors?.user?.loginName) {
|
if (response?.session && response.session?.factors?.user?.loginName) {
|
||||||
const sessionCookie: CustomCookieData = {
|
const sessionCookie: CustomCookieData = {
|
||||||
id: createdSession.sessionId,
|
id: createdSession.sessionId,
|
||||||
@@ -91,9 +92,14 @@ export async function createSessionAndUpdateCookie(command: {
|
|||||||
response.session.factors.user.organizationId;
|
response.session.factors.user.organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return addSessionToCookie(sessionCookie).then(() => {
|
const securitySettings = await getSecuritySettings({ serviceUrl });
|
||||||
return response.session as Session;
|
const sameSite = securitySettings?.embeddedIframe?.enabled
|
||||||
});
|
? "none"
|
||||||
|
: true;
|
||||||
|
|
||||||
|
await addSessionToCookie({ session: sessionCookie, sameSite });
|
||||||
|
|
||||||
|
return response.session as Session;
|
||||||
} else {
|
} else {
|
||||||
throw "could not get session or session does not have loginName";
|
throw "could not get session or session does not have loginName";
|
||||||
}
|
}
|
||||||
@@ -167,7 +173,10 @@ export async function createSessionForIdpAndUpdateCookie(
|
|||||||
sessionCookie.organization = session.factors.user.organizationId;
|
sessionCookie.organization = session.factors.user.organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return addSessionToCookie(sessionCookie).then(() => {
|
const securitySettings = await getSecuritySettings({ serviceUrl });
|
||||||
|
const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true;
|
||||||
|
|
||||||
|
return addSessionToCookie({ session: sessionCookie, sameSite }).then(() => {
|
||||||
return session as Session;
|
return session as Session;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -217,32 +226,44 @@ export async function setSessionAndUpdateCookie(
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
sessionId: sessionCookie.id,
|
sessionId: sessionCookie.id,
|
||||||
sessionToken: sessionCookie.token,
|
sessionToken: sessionCookie.token,
|
||||||
}).then((response) => {
|
}).then(async (response) => {
|
||||||
if (response?.session && response.session.factors?.user?.loginName) {
|
if (
|
||||||
const { session } = response;
|
!response?.session ||
|
||||||
const newCookie: CustomCookieData = {
|
!response.session.factors?.user?.loginName
|
||||||
id: sessionCookie.id,
|
) {
|
||||||
token: updatedSession.sessionToken,
|
|
||||||
creationTs: sessionCookie.creationTs,
|
|
||||||
expirationTs: sessionCookie.expirationTs,
|
|
||||||
// just overwrite the changeDate with the new one
|
|
||||||
changeTs: updatedSession.details?.changeDate
|
|
||||||
? `${timestampMs(updatedSession.details.changeDate)}`
|
|
||||||
: "",
|
|
||||||
loginName: session.factors?.user?.loginName ?? "",
|
|
||||||
organization: session.factors?.user?.organizationId ?? "",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sessionCookie.requestId) {
|
|
||||||
newCookie.requestId = sessionCookie.requestId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateSessionCookie(sessionCookie.id, newCookie).then(() => {
|
|
||||||
return { challenges: updatedSession.challenges, ...session };
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw "could not get session or session does not have loginName";
|
throw "could not get session or session does not have loginName";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { session } = response;
|
||||||
|
const newCookie: CustomCookieData = {
|
||||||
|
id: sessionCookie.id,
|
||||||
|
token: updatedSession.sessionToken,
|
||||||
|
creationTs: sessionCookie.creationTs,
|
||||||
|
expirationTs: sessionCookie.expirationTs,
|
||||||
|
// just overwrite the changeDate with the new one
|
||||||
|
changeTs: updatedSession.details?.changeDate
|
||||||
|
? `${timestampMs(updatedSession.details.changeDate)}`
|
||||||
|
: "",
|
||||||
|
loginName: session.factors?.user?.loginName ?? "",
|
||||||
|
organization: session.factors?.user?.organizationId ?? "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sessionCookie.requestId) {
|
||||||
|
newCookie.requestId = sessionCookie.requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const securitySettings = await getSecuritySettings({ serviceUrl });
|
||||||
|
const sameSite = securitySettings?.embeddedIframe?.enabled
|
||||||
|
? "none"
|
||||||
|
: true;
|
||||||
|
|
||||||
|
return updateSessionCookie({
|
||||||
|
id: sessionCookie.id,
|
||||||
|
session: newCookie,
|
||||||
|
sameSite,
|
||||||
|
}).then(() => {
|
||||||
|
return { challenges: updatedSession.challenges, ...session };
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw "Session not be set";
|
throw "Session not be set";
|
||||||
|
20
apps/login/src/lib/server/device.ts
Normal file
20
apps/login/src/lib/server/device.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { authorizeOrDenyDeviceAuthorization } from "@/lib/zitadel";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
|
|
||||||
|
export async function completeDeviceAuthorization(
|
||||||
|
deviceAuthorizationId: string,
|
||||||
|
session?: { sessionId: string; sessionToken: string },
|
||||||
|
) {
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
// without the session, device auth request is denied
|
||||||
|
return authorizeOrDenyDeviceAuthorization({
|
||||||
|
serviceUrl,
|
||||||
|
deviceAuthorizationId,
|
||||||
|
session,
|
||||||
|
});
|
||||||
|
}
|
@@ -8,7 +8,7 @@ import {
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import { checkEmailVerification } from "../verify-helper";
|
import { checkEmailVerification } from "../verify-helper";
|
||||||
import { createSessionForIdpAndUpdateCookie } from "./cookie";
|
import { createSessionForIdpAndUpdateCookie } from "./cookie";
|
||||||
|
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
import { addHumanUser, createInviteCode } from "@/lib/zitadel";
|
import { addHumanUser, createInviteCode } from "@/lib/zitadel";
|
||||||
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
|
|
||||||
type InviteUserCommand = {
|
type InviteUserCommand = {
|
||||||
email: string;
|
email: string;
|
||||||
|
@@ -8,8 +8,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
|
|||||||
|
|
||||||
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||||
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import { checkInvite } from "../verify-helper";
|
|
||||||
import {
|
import {
|
||||||
getActiveIdentityProviders,
|
getActiveIdentityProviders,
|
||||||
getIDPByID,
|
getIDPByID,
|
||||||
@@ -254,37 +253,27 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
|||||||
userId: session.factors?.user?.id,
|
userId: session.factors?.user?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// this can be expected to be an invite as users created in console have a password set.
|
// always resend invite if user has no auth method set
|
||||||
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
|
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
|
||||||
// redirect to /verify invite if no auth method is set and email is not verified
|
const params = new URLSearchParams({
|
||||||
const inviteCheck = checkInvite(
|
loginName: session.factors?.user?.loginName as string,
|
||||||
session,
|
send: "true", // set this to true to request a new code immediately
|
||||||
humanUser,
|
invite: "true",
|
||||||
session.factors.user.organizationId,
|
|
||||||
command.requestId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (inviteCheck?.redirect) {
|
|
||||||
return inviteCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
const paramsAuthenticatorSetup = new URLSearchParams({
|
|
||||||
loginName: session.factors?.user?.loginName,
|
|
||||||
userId: session.factors?.user?.id, // verify needs user id
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (command.requestId) {
|
||||||
|
params.append("requestId", command.requestId);
|
||||||
|
}
|
||||||
|
|
||||||
if (command.organization || session.factors?.user?.organizationId) {
|
if (command.organization || session.factors?.user?.organizationId) {
|
||||||
paramsAuthenticatorSetup.append(
|
params.append(
|
||||||
"organization",
|
"organization",
|
||||||
command.organization ?? session.factors?.user?.organizationId,
|
command.organization ??
|
||||||
|
(session.factors?.user?.organizationId as string),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (command.requestId) {
|
return { redirect: `/verify?` + params };
|
||||||
paramsAuthenticatorSetup.append("requestId", command.requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { redirect: "/authenticator/set?" + paramsAuthenticatorSetup };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (methods.authMethodTypes.length == 1) {
|
if (methods.authMethodTypes.length == 1) {
|
||||||
|
15
apps/login/src/lib/server/oidc.ts
Normal file
15
apps/login/src/lib/server/oidc.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
|
|
||||||
|
export async function getDeviceAuthorizationRequest(userCode: string) {
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
return zitadelGetDeviceAuthorizationRequest({
|
||||||
|
serviceUrl,
|
||||||
|
userCode,
|
||||||
|
});
|
||||||
|
}
|
@@ -13,7 +13,7 @@ import {
|
|||||||
getSessionCookieById,
|
getSessionCookieById,
|
||||||
getSessionCookieByLoginName,
|
getSessionCookieByLoginName,
|
||||||
} from "../cookies";
|
} from "../cookies";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import { getLoginSettings } from "../zitadel";
|
import { getLoginSettings } from "../zitadel";
|
||||||
|
|
||||||
export type SetOTPCommand = {
|
export type SetOTPCommand = {
|
||||||
|
@@ -5,10 +5,12 @@ import {
|
|||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getSession,
|
getSession,
|
||||||
getUserByID,
|
getUserByID,
|
||||||
|
listAuthenticationMethodTypes,
|
||||||
registerPasskey,
|
registerPasskey,
|
||||||
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
|
verifyPasskeyRegistration as zitadelVerifyPasskeyRegistration,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
import { create, Duration } from "@zitadel/client";
|
import { create, Duration, Timestamp, timestampDate } from "@zitadel/client";
|
||||||
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import {
|
import {
|
||||||
RegisterPasskeyResponse,
|
RegisterPasskeyResponse,
|
||||||
@@ -22,8 +24,11 @@ import {
|
|||||||
getSessionCookieById,
|
getSessionCookieById,
|
||||||
getSessionCookieByLoginName,
|
getSessionCookieByLoginName,
|
||||||
} from "../cookies";
|
} from "../cookies";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import { checkEmailVerification } from "../verify-helper";
|
import {
|
||||||
|
checkEmailVerification,
|
||||||
|
checkUserVerification,
|
||||||
|
} from "../verify-helper";
|
||||||
import { setSessionAndUpdateCookie } from "./cookie";
|
import { setSessionAndUpdateCookie } from "./cookie";
|
||||||
|
|
||||||
type VerifyPasskeyCommand = {
|
type VerifyPasskeyCommand = {
|
||||||
@@ -37,9 +42,25 @@ type RegisterPasskeyCommand = {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isSessionValid(session: Partial<Session>): {
|
||||||
|
valid: boolean;
|
||||||
|
verifiedAt?: Timestamp;
|
||||||
|
} {
|
||||||
|
const validPassword = session?.factors?.password?.verifiedAt;
|
||||||
|
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
|
||||||
|
const stillValid = session.expirationDate
|
||||||
|
? timestampDate(session.expirationDate) > new Date()
|
||||||
|
: true;
|
||||||
|
|
||||||
|
const verifiedAt = validPassword || validPasskey;
|
||||||
|
const valid = !!((validPassword || validPasskey) && stillValid);
|
||||||
|
|
||||||
|
return { valid, verifiedAt };
|
||||||
|
}
|
||||||
|
|
||||||
export async function registerPasskeyLink(
|
export async function registerPasskeyLink(
|
||||||
command: RegisterPasskeyCommand,
|
command: RegisterPasskeyCommand,
|
||||||
): Promise<RegisterPasskeyResponse> {
|
): Promise<RegisterPasskeyResponse | { error: string }> {
|
||||||
const { sessionId } = command;
|
const { sessionId } = command;
|
||||||
|
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
@@ -57,6 +78,36 @@ export async function registerPasskeyLink(
|
|||||||
sessionToken: sessionCookie.token,
|
sessionToken: sessionCookie.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!session?.session?.factors?.user?.id) {
|
||||||
|
return { error: "Could not determine user from session" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionValid = isSessionValid(session.session);
|
||||||
|
|
||||||
|
if (!sessionValid) {
|
||||||
|
const authmethods = await listAuthenticationMethodTypes({
|
||||||
|
serviceUrl,
|
||||||
|
userId: session.session.factors.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// if the user has no authmethods set, we need to check if the user was verified
|
||||||
|
if (authmethods.authMethodTypes.length !== 0) {
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
"You have to authenticate or have a valid User Verification Check",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a verification was done earlier
|
||||||
|
const hasValidUserVerificationCheck = await checkUserVerification(
|
||||||
|
session.session.factors.user.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasValidUserVerificationCheck) {
|
||||||
|
return { error: "User Verification Check has to be done" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [hostname, port] = host.split(":");
|
const [hostname, port] = host.split(":");
|
||||||
|
|
||||||
if (!hostname) {
|
if (!hostname) {
|
||||||
|
@@ -32,11 +32,12 @@ import {
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import {
|
import {
|
||||||
checkEmailVerification,
|
checkEmailVerification,
|
||||||
checkMFAFactors,
|
checkMFAFactors,
|
||||||
checkPasswordChangeRequired,
|
checkPasswordChangeRequired,
|
||||||
|
checkUserVerification,
|
||||||
} from "../verify-helper";
|
} from "../verify-helper";
|
||||||
|
|
||||||
type ResetPasswordCommand = {
|
type ResetPasswordCommand = {
|
||||||
@@ -297,6 +298,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
|
|||||||
return { redirect: url };
|
return { redirect: url };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this function lets users with code set a password or users with valid User Verification Check
|
||||||
export async function changePassword(command: {
|
export async function changePassword(command: {
|
||||||
code?: string;
|
code?: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -316,11 +318,39 @@ export async function changePassword(command: {
|
|||||||
}
|
}
|
||||||
const userId = user.userId;
|
const userId = user.userId;
|
||||||
|
|
||||||
|
if (user.state === UserState.INITIAL) {
|
||||||
|
return { error: "User Initial State is not supported" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the user has no password set in order to set a password
|
||||||
|
if (!command.code) {
|
||||||
|
const authmethods = await listAuthenticationMethodTypes({
|
||||||
|
serviceUrl,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// if the user has no authmethods set, we need to check if the user was verified
|
||||||
|
if (authmethods.authMethodTypes.length !== 0) {
|
||||||
|
return {
|
||||||
|
error:
|
||||||
|
"You have to provide a code or have a valid User Verification Check",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if a verification was done earlier
|
||||||
|
const hasValidUserVerificationCheck = await checkUserVerification(
|
||||||
|
user.userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasValidUserVerificationCheck) {
|
||||||
|
return { error: "User Verification Check has to be done" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return setUserPassword({
|
return setUserPassword({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId,
|
userId,
|
||||||
password: command.password,
|
password: command.password,
|
||||||
user,
|
|
||||||
code: command.code,
|
code: command.code,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
} from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import { checkEmailVerification } from "../verify-helper";
|
import { checkEmailVerification } from "../verify-helper";
|
||||||
|
|
||||||
type RegisterUserCommand = {
|
type RegisterUserCommand = {
|
||||||
|
@@ -4,6 +4,7 @@ import { setSessionAndUpdateCookie } from "@/lib/server/cookie";
|
|||||||
import {
|
import {
|
||||||
deleteSession,
|
deleteSession,
|
||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
|
getSecuritySettings,
|
||||||
humanMFAInitSkipped,
|
humanMFAInitSkipped,
|
||||||
listAuthenticationMethodTypes,
|
listAuthenticationMethodTypes,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
@@ -19,7 +20,7 @@ import {
|
|||||||
getSessionCookieByLoginName,
|
getSessionCookieByLoginName,
|
||||||
removeSessionFromCookie,
|
removeSessionFromCookie,
|
||||||
} from "../cookies";
|
} from "../cookies";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
|
|
||||||
export async function skipMFAAndContinueWithNextUrl({
|
export async function skipMFAAndContinueWithNextUrl({
|
||||||
userId,
|
userId,
|
||||||
@@ -209,8 +210,11 @@ export async function clearSession(options: ClearSessionOptions) {
|
|||||||
sessionToken: session.token,
|
sessionToken: session.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const securitySettings = await getSecuritySettings({ serviceUrl });
|
||||||
|
const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true;
|
||||||
|
|
||||||
if (deletedSession) {
|
if (deletedSession) {
|
||||||
return removeSessionFromCookie(session);
|
return removeSessionFromCookie({ session, sameSite });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,9 +234,12 @@ export async function cleanupSession({ sessionId }: CleanupSessionCommand) {
|
|||||||
sessionToken: sessionCookie.token,
|
sessionToken: sessionCookie.token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const securitySettings = await getSecuritySettings({ serviceUrl });
|
||||||
|
const sameSite = securitySettings?.embeddedIframe?.enabled ? "none" : true;
|
||||||
|
|
||||||
if (!deleteResponse) {
|
if (!deleteResponse) {
|
||||||
throw new Error("Could not delete session");
|
throw new Error("Could not delete session");
|
||||||
}
|
}
|
||||||
|
|
||||||
return removeSessionFromCookie(sessionCookie);
|
return removeSessionFromCookie({ session: sessionCookie, sameSite });
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { userAgent } from "next/server";
|
import { userAgent } from "next/server";
|
||||||
import { getSessionCookieById } from "../cookies";
|
import { getSessionCookieById } from "../cookies";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
|
|
||||||
type RegisterU2FCommand = {
|
type RegisterU2FCommand = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
@@ -1,25 +1,26 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
createInviteCode,
|
||||||
getLoginSettings,
|
getLoginSettings,
|
||||||
getSession,
|
getSession,
|
||||||
getUserByID,
|
getUserByID,
|
||||||
listAuthenticationMethodTypes,
|
listAuthenticationMethodTypes,
|
||||||
resendEmailCode,
|
|
||||||
resendInviteCode,
|
|
||||||
verifyEmail,
|
verifyEmail,
|
||||||
verifyInviteCode,
|
verifyInviteCode,
|
||||||
verifyTOTPRegistration,
|
verifyTOTPRegistration,
|
||||||
sendEmailCode as zitadelSendEmailCode,
|
sendEmailCode as zitadelSendEmailCode,
|
||||||
} from "@/lib/zitadel";
|
} from "@/lib/zitadel";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { create } from "@zitadel/client";
|
import { create } from "@zitadel/client";
|
||||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||||
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { cookies, headers } from "next/headers";
|
||||||
import { headers } from "next/headers";
|
|
||||||
import { getNextUrl } from "../client";
|
import { getNextUrl } from "../client";
|
||||||
import { getSessionCookieByLoginName } from "../cookies";
|
import { getSessionCookieByLoginName } from "../cookies";
|
||||||
import { getServiceUrlFromHeaders } from "../service";
|
import { getOrSetFingerprintId } from "../fingerprint";
|
||||||
|
import { getServiceUrlFromHeaders } from "../service-url";
|
||||||
import { loadMostRecentSession } from "../session";
|
import { loadMostRecentSession } from "../session";
|
||||||
import { checkMFAFactors } from "../verify-helper";
|
import { checkMFAFactors } from "../verify-helper";
|
||||||
import { createSessionAndUpdateCookie } from "./cookie";
|
import { createSessionAndUpdateCookie } from "./cookie";
|
||||||
@@ -69,14 +70,16 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
verificationCode: command.code,
|
verificationCode: command.code,
|
||||||
}).catch(() => {
|
}).catch((error) => {
|
||||||
|
console.warn(error);
|
||||||
return { error: "Could not verify invite" };
|
return { error: "Could not verify invite" };
|
||||||
})
|
})
|
||||||
: await verifyEmail({
|
: await verifyEmail({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
verificationCode: command.code,
|
verificationCode: command.code,
|
||||||
}).catch(() => {
|
}).catch((error) => {
|
||||||
|
console.warn(error);
|
||||||
return { error: "Could not verify email" };
|
return { error: "Could not verify email" };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,20 +92,26 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let session: Session | undefined;
|
let session: Session | undefined;
|
||||||
let user: User | undefined;
|
const userResponse = await getUserByID({
|
||||||
|
serviceUrl,
|
||||||
|
userId: command.userId,
|
||||||
|
});
|
||||||
|
|
||||||
if ("loginName" in command) {
|
if (!userResponse || !userResponse.user) {
|
||||||
const sessionCookie = await getSessionCookieByLoginName({
|
return { error: "Could not load user" };
|
||||||
loginName: command.loginName,
|
}
|
||||||
organization: command.organization,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.warn("Ignored error:", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sessionCookie) {
|
const user = userResponse.user;
|
||||||
return { error: "Could not load session cookie" };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const sessionCookie = await getSessionCookieByLoginName({
|
||||||
|
loginName:
|
||||||
|
"loginName" in command ? command.loginName : user.preferredLoginName,
|
||||||
|
organization: command.organization,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warn("Ignored error:", error); // checked later
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sessionCookie) {
|
||||||
session = await getSession({
|
session = await getSession({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
sessionId: sessionCookie.id,
|
sessionId: sessionCookie.id,
|
||||||
@@ -112,65 +121,9 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
return response.session;
|
return response.session;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const userResponse = await getUserByID({
|
|
||||||
serviceUrl,
|
|
||||||
userId: session?.factors?.user?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userResponse?.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
user = userResponse.user;
|
|
||||||
} else {
|
|
||||||
const userResponse = await getUserByID({
|
|
||||||
serviceUrl,
|
|
||||||
userId: command.userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userResponse || !userResponse.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
user = userResponse.user;
|
|
||||||
|
|
||||||
const checks = create(ChecksSchema, {
|
|
||||||
user: {
|
|
||||||
search: {
|
|
||||||
case: "loginName",
|
|
||||||
value: userResponse.user.preferredLoginName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
session = await createSessionAndUpdateCookie({
|
|
||||||
checks,
|
|
||||||
requestId: command.requestId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
// load auth methods for user
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization: user.details?.resourceOwner,
|
|
||||||
});
|
|
||||||
|
|
||||||
const authMethodResponse = await listAuthenticationMethodTypes({
|
const authMethodResponse = await listAuthenticationMethodTypes({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
@@ -186,6 +139,26 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
authMethodResponse.authMethodTypes &&
|
authMethodResponse.authMethodTypes &&
|
||||||
authMethodResponse.authMethodTypes.length == 0
|
authMethodResponse.authMethodTypes.length == 0
|
||||||
) {
|
) {
|
||||||
|
if (!sessionCookie) {
|
||||||
|
const checks = create(ChecksSchema, {
|
||||||
|
user: {
|
||||||
|
search: {
|
||||||
|
case: "loginName",
|
||||||
|
value: userResponse.user.preferredLoginName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
session = await createSessionAndUpdateCookie({
|
||||||
|
checks,
|
||||||
|
requestId: command.requestId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return { error: "Could not create session" };
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
});
|
});
|
||||||
@@ -193,9 +166,62 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
|||||||
if (session.factors?.user?.loginName) {
|
if (session.factors?.user?.loginName) {
|
||||||
params.set("loginName", session.factors?.user?.loginName);
|
params.set("loginName", session.factors?.user?.loginName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set hash of userId and userAgentId to prevent attacks, checks are done for users with invalid sessions and invalid userAgentId
|
||||||
|
const cookiesList = await cookies();
|
||||||
|
const userAgentId = await getOrSetFingerprintId();
|
||||||
|
|
||||||
|
const verificationCheck = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(`${user.userId}:${userAgentId}`)
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
await cookiesList.set({
|
||||||
|
name: "verificationCheck",
|
||||||
|
value: verificationCheck,
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
maxAge: 300, // 5 minutes
|
||||||
|
});
|
||||||
|
|
||||||
return { redirect: `/authenticator/set?${params}` };
|
return { redirect: `/authenticator/set?${params}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if no session found only show success page,
|
||||||
|
// if user is invited, recreate invite flow to not depend on session
|
||||||
|
if (!session?.factors?.user?.id) {
|
||||||
|
const verifySuccessParams = new URLSearchParams({});
|
||||||
|
|
||||||
|
if (command.userId) {
|
||||||
|
verifySuccessParams.set("userId", command.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
("loginName" in command && command.loginName) ||
|
||||||
|
user.preferredLoginName
|
||||||
|
) {
|
||||||
|
verifySuccessParams.set(
|
||||||
|
"loginName",
|
||||||
|
"loginName" in command && command.loginName
|
||||||
|
? command.loginName
|
||||||
|
: user.preferredLoginName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (command.requestId) {
|
||||||
|
verifySuccessParams.set("requestId", command.requestId);
|
||||||
|
}
|
||||||
|
if (command.organization) {
|
||||||
|
verifySuccessParams.set("organization", command.organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { redirect: `/verify/success?${verifySuccessParams}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSettings = await getLoginSettings({
|
||||||
|
serviceUrl,
|
||||||
|
organization: user.details?.resourceOwner,
|
||||||
|
});
|
||||||
|
|
||||||
// redirect to mfa factor if user has one, or redirect to set one up
|
// redirect to mfa factor if user has one, or redirect to set one up
|
||||||
const mfaFactorCheck = await checkMFAFactors(
|
const mfaFactorCheck = await checkMFAFactors(
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
@@ -254,193 +280,50 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
|
|||||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||||
|
|
||||||
return command.isInvite
|
return command.isInvite
|
||||||
? resendInviteCode({ serviceUrl, userId: command.userId })
|
? createInviteCode({
|
||||||
: resendEmailCode({
|
serviceUrl,
|
||||||
|
userId: command.userId,
|
||||||
|
urlTemplate:
|
||||||
|
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
||||||
|
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
||||||
|
}).catch((error) => {
|
||||||
|
if (error.code === 9) {
|
||||||
|
return { error: "User is already verified!" };
|
||||||
|
}
|
||||||
|
return { error: "Could not resend invite" };
|
||||||
|
})
|
||||||
|
: zitadelSendEmailCode({
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
urlTemplate:
|
urlTemplate:
|
||||||
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
`${host.includes("localhost") ? "http://" : "https://"}${host}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||||
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type sendEmailCommand = {
|
type SendEmailCommand = {
|
||||||
serviceUrl: string;
|
|
||||||
|
|
||||||
userId: string;
|
userId: string;
|
||||||
urlTemplate: string;
|
urlTemplate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function sendEmailCode(command: sendEmailCommand) {
|
export async function sendEmailCode(command: SendEmailCommand) {
|
||||||
|
const _headers = await headers();
|
||||||
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
return zitadelSendEmailCode({
|
return zitadelSendEmailCode({
|
||||||
serviceUrl: command.serviceUrl,
|
serviceUrl,
|
||||||
userId: command.userId,
|
userId: command.userId,
|
||||||
urlTemplate: command.urlTemplate,
|
urlTemplate: command.urlTemplate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SendVerificationRedirectWithoutCheckCommand = {
|
export async function sendInviteEmailCode(command: SendEmailCommand) {
|
||||||
organization?: string;
|
|
||||||
requestId?: string;
|
|
||||||
} & (
|
|
||||||
| { userId: string; loginName?: never }
|
|
||||||
| { userId?: never; loginName: string }
|
|
||||||
);
|
|
||||||
|
|
||||||
export async function sendVerificationRedirectWithoutCheck(
|
|
||||||
command: SendVerificationRedirectWithoutCheckCommand,
|
|
||||||
) {
|
|
||||||
const _headers = await headers();
|
const _headers = await headers();
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
if (!("loginName" in command || "userId" in command)) {
|
return createInviteCode({
|
||||||
return { error: "No userId, nor loginname provided" };
|
|
||||||
}
|
|
||||||
|
|
||||||
let session: Session | undefined;
|
|
||||||
let user: User | undefined;
|
|
||||||
|
|
||||||
if ("loginName" in command) {
|
|
||||||
const sessionCookie = await getSessionCookieByLoginName({
|
|
||||||
loginName: command.loginName,
|
|
||||||
organization: command.organization,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.warn("Ignored error:", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sessionCookie) {
|
|
||||||
return { error: "Could not load session cookie" };
|
|
||||||
}
|
|
||||||
|
|
||||||
session = await getSession({
|
|
||||||
serviceUrl,
|
|
||||||
sessionId: sessionCookie.id,
|
|
||||||
sessionToken: sessionCookie.token,
|
|
||||||
}).then((response) => {
|
|
||||||
if (response?.session) {
|
|
||||||
return response.session;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const userResponse = await getUserByID({
|
|
||||||
serviceUrl,
|
|
||||||
userId: session?.factors?.user?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userResponse?.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
user = userResponse.user;
|
|
||||||
} else if ("userId" in command) {
|
|
||||||
const userResponse = await getUserByID({
|
|
||||||
serviceUrl,
|
|
||||||
userId: command.userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userResponse?.user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
user = userResponse.user;
|
|
||||||
|
|
||||||
const checks = create(ChecksSchema, {
|
|
||||||
user: {
|
|
||||||
search: {
|
|
||||||
case: "loginName",
|
|
||||||
value: userResponse.user.preferredLoginName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
session = await createSessionAndUpdateCookie({
|
|
||||||
checks,
|
|
||||||
requestId: command.requestId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session?.factors?.user?.id) {
|
|
||||||
return { error: "Could not create session for user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return { error: "Could not load user" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const authMethodResponse = await listAuthenticationMethodTypes({
|
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId: user.userId,
|
userId: command.userId,
|
||||||
|
urlTemplate: command.urlTemplate,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
|
||||||
return { error: "Could not load possible authenticators" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// if no authmethods are found on the user, redirect to set one up
|
|
||||||
if (
|
|
||||||
authMethodResponse &&
|
|
||||||
authMethodResponse.authMethodTypes &&
|
|
||||||
authMethodResponse.authMethodTypes.length == 0
|
|
||||||
) {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
sessionId: session.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (session.factors?.user?.loginName) {
|
|
||||||
params.set("loginName", session.factors?.user?.loginName);
|
|
||||||
}
|
|
||||||
return { redirect: `/authenticator/set?${params}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginSettings = await getLoginSettings({
|
|
||||||
serviceUrl,
|
|
||||||
organization: user.details?.resourceOwner,
|
|
||||||
});
|
|
||||||
|
|
||||||
// redirect to mfa factor if user has one, or redirect to set one up
|
|
||||||
const mfaFactorCheck = await checkMFAFactors(
|
|
||||||
serviceUrl,
|
|
||||||
session,
|
|
||||||
loginSettings,
|
|
||||||
authMethodResponse.authMethodTypes,
|
|
||||||
command.organization,
|
|
||||||
command.requestId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (mfaFactorCheck?.redirect) {
|
|
||||||
return mfaFactorCheck;
|
|
||||||
}
|
|
||||||
|
|
||||||
// login user if no additional steps are required
|
|
||||||
if (command.requestId && session.id) {
|
|
||||||
const nextUrl = await getNextUrl(
|
|
||||||
{
|
|
||||||
sessionId: session.id,
|
|
||||||
requestId: command.requestId,
|
|
||||||
organization:
|
|
||||||
command.organization ?? session.factors?.user?.organizationId,
|
|
||||||
},
|
|
||||||
loginSettings?.defaultRedirectUri,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { redirect: nextUrl };
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = await getNextUrl(
|
|
||||||
{
|
|
||||||
loginName: session.factors.user.loginName,
|
|
||||||
organization: session.factors?.user?.organizationId,
|
|
||||||
},
|
|
||||||
loginSettings?.defaultRedirectUri,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { redirect: url };
|
|
||||||
}
|
}
|
||||||
|
58
apps/login/src/lib/service-url.ts
Normal file
58
apps/login/src/lib/service-url.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the service url and region from the headers if used in a multitenant context (host, x-zitadel-forward-host header)
|
||||||
|
* or falls back to the ZITADEL_API_URL for a self hosting deployment
|
||||||
|
* or falls back to the host header for a self hosting deployment using custom domains
|
||||||
|
* @param headers
|
||||||
|
* @returns the service url and region from the headers
|
||||||
|
* @throws if the service url could not be determined
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): {
|
||||||
|
serviceUrl: string;
|
||||||
|
} {
|
||||||
|
let instanceUrl;
|
||||||
|
|
||||||
|
const forwardedHost = headers.get("x-zitadel-forward-host");
|
||||||
|
// use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself
|
||||||
|
if (forwardedHost) {
|
||||||
|
instanceUrl = forwardedHost;
|
||||||
|
instanceUrl = instanceUrl.startsWith("http://")
|
||||||
|
? instanceUrl
|
||||||
|
: `https://${instanceUrl}`;
|
||||||
|
} else if (process.env.ZITADEL_API_URL) {
|
||||||
|
instanceUrl = process.env.ZITADEL_API_URL;
|
||||||
|
} else {
|
||||||
|
const host = headers.get("host");
|
||||||
|
|
||||||
|
if (host) {
|
||||||
|
const [hostname, port] = host.split(":");
|
||||||
|
if (hostname !== "localhost") {
|
||||||
|
instanceUrl = host.startsWith("http") ? host : `https://${host}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instanceUrl) {
|
||||||
|
throw new Error("Service URL could not be determined");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
serviceUrl: instanceUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function constructUrl(request: NextRequest, path: string) {
|
||||||
|
const forwardedProto = request.headers.get("x-forwarded-proto")
|
||||||
|
? `${request.headers.get("x-forwarded-proto")}:`
|
||||||
|
: request.nextUrl.protocol;
|
||||||
|
|
||||||
|
const forwardedHost =
|
||||||
|
request.headers.get("x-zitadel-forward-host") ??
|
||||||
|
request.headers.get("x-forwarded-host") ??
|
||||||
|
request.headers.get("host");
|
||||||
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
||||||
|
return new URL(`${basePath}${path}`, `${forwardedProto}//${forwardedHost}`);
|
||||||
|
}
|
@@ -7,8 +7,6 @@ import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
|
|||||||
import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||||
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
|
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
|
||||||
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
|
||||||
import { NextRequest } from "next/server";
|
|
||||||
import { systemAPIToken } from "./api";
|
import { systemAPIToken } from "./api";
|
||||||
|
|
||||||
type ServiceClass =
|
type ServiceClass =
|
||||||
@@ -66,59 +64,3 @@ export async function createServiceForHost<T extends ServiceClass>(
|
|||||||
|
|
||||||
return createClientFor<T>(service)(transport);
|
return createClientFor<T>(service)(transport);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts the service url and region from the headers if used in a multitenant context (host, x-zitadel-forward-host header)
|
|
||||||
* or falls back to the ZITADEL_API_URL for a self hosting deployment
|
|
||||||
* or falls back to the host header for a self hosting deployment using custom domains
|
|
||||||
* @param headers
|
|
||||||
* @returns the service url and region from the headers
|
|
||||||
* @throws if the service url could not be determined
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): {
|
|
||||||
serviceUrl: string;
|
|
||||||
} {
|
|
||||||
let instanceUrl;
|
|
||||||
|
|
||||||
const forwardedHost = headers.get("x-zitadel-forward-host");
|
|
||||||
// use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself
|
|
||||||
if (forwardedHost) {
|
|
||||||
instanceUrl = forwardedHost;
|
|
||||||
instanceUrl = instanceUrl.startsWith("http://")
|
|
||||||
? instanceUrl
|
|
||||||
: `https://${instanceUrl}`;
|
|
||||||
} else if (process.env.ZITADEL_API_URL) {
|
|
||||||
instanceUrl = process.env.ZITADEL_API_URL;
|
|
||||||
} else {
|
|
||||||
const host = headers.get("host");
|
|
||||||
|
|
||||||
if (host) {
|
|
||||||
const [hostname, port] = host.split(":");
|
|
||||||
if (hostname !== "localhost") {
|
|
||||||
instanceUrl = host.startsWith("http") ? host : `https://${host}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!instanceUrl) {
|
|
||||||
throw new Error("Service URL could not be determined");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
serviceUrl: instanceUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function constructUrl(request: NextRequest, path: string) {
|
|
||||||
const forwardedProto = request.headers.get("x-forwarded-proto")
|
|
||||||
? `${request.headers.get("x-forwarded-proto")}:`
|
|
||||||
: request.nextUrl.protocol;
|
|
||||||
|
|
||||||
const forwardedHost =
|
|
||||||
request.headers.get("x-zitadel-forward-host") ??
|
|
||||||
request.headers.get("x-forwarded-host") ??
|
|
||||||
request.headers.get("host");
|
|
||||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
|
||||||
return new URL(`${basePath}${path}`, `${forwardedProto}//${forwardedHost}`);
|
|
||||||
}
|
|
||||||
|
@@ -4,7 +4,10 @@ import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings
|
|||||||
import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
import { PasswordExpirySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||||
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
import { HumanUser } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||||
|
import crypto from "crypto";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { getFingerprintIdCookie } from "./fingerprint";
|
||||||
import { getUserByID } from "./zitadel";
|
import { getUserByID } from "./zitadel";
|
||||||
|
|
||||||
export function checkPasswordChangeRequired(
|
export function checkPasswordChangeRequired(
|
||||||
@@ -44,7 +47,7 @@ export function checkPasswordChangeRequired(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkInvite(
|
export function checkEmailVerified(
|
||||||
session: Session,
|
session: Session,
|
||||||
humanUser?: HumanUser,
|
humanUser?: HumanUser,
|
||||||
organization?: string,
|
organization?: string,
|
||||||
@@ -54,7 +57,7 @@ export function checkInvite(
|
|||||||
const paramsVerify = new URLSearchParams({
|
const paramsVerify = new URLSearchParams({
|
||||||
loginName: session.factors?.user?.loginName as string,
|
loginName: session.factors?.user?.loginName as string,
|
||||||
userId: session.factors?.user?.id as string, // verify needs user id
|
userId: session.factors?.user?.id as string, // verify needs user id
|
||||||
invite: "true", // TODO: check - set this to true as we dont expect old email verification method here
|
send: "true", // we request a new email code once the page is loaded
|
||||||
});
|
});
|
||||||
|
|
||||||
if (organization || session.factors?.user?.organizationId) {
|
if (organization || session.factors?.user?.organizationId) {
|
||||||
@@ -84,6 +87,7 @@ export function checkEmailVerification(
|
|||||||
) {
|
) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
loginName: session.factors?.user?.loginName as string,
|
loginName: session.factors?.user?.loginName as string,
|
||||||
|
send: "true", // set this to true as we dont expect old email codes to be valid anymore
|
||||||
});
|
});
|
||||||
|
|
||||||
if (requestId) {
|
if (requestId) {
|
||||||
@@ -248,3 +252,38 @@ export async function checkMFAFactors(
|
|||||||
return { redirect: `/mfa/set?` + params };
|
return { redirect: `/mfa/set?` + params };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function checkUserVerification(userId: string): Promise<boolean> {
|
||||||
|
// check if a verification was done earlier
|
||||||
|
const cookiesList = await cookies();
|
||||||
|
|
||||||
|
// only read cookie to prevent issues on page.tsx
|
||||||
|
const fingerPrintCookie = await getFingerprintIdCookie();
|
||||||
|
|
||||||
|
if (!fingerPrintCookie || !fingerPrintCookie.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verificationCheck = crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(`${userId}:${fingerPrintCookie.value}`)
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
|
const cookieValue = await cookiesList.get("verificationCheck")?.value;
|
||||||
|
|
||||||
|
if (!cookieValue) {
|
||||||
|
console.warn(
|
||||||
|
"User verification check cookie not found. User verification check failed.",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookieValue !== verificationCheck) {
|
||||||
|
console.warn(
|
||||||
|
`User verification check failed. Expected ${verificationCheck} but got ${cookieValue}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@@ -29,11 +29,7 @@ import {
|
|||||||
SearchQuery,
|
SearchQuery,
|
||||||
SearchQuerySchema,
|
SearchQuerySchema,
|
||||||
} from "@zitadel/proto/zitadel/user/v2/query_pb";
|
} from "@zitadel/proto/zitadel/user/v2/query_pb";
|
||||||
import {
|
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||||
SendInviteCodeSchema,
|
|
||||||
User,
|
|
||||||
UserState,
|
|
||||||
} from "@zitadel/proto/zitadel/user/v2/user_pb";
|
|
||||||
import {
|
import {
|
||||||
AddHumanUserRequest,
|
AddHumanUserRequest,
|
||||||
ResendEmailCodeRequest,
|
ResendEmailCodeRequest,
|
||||||
@@ -92,6 +88,21 @@ export async function getLoginSettings({
|
|||||||
return useCache ? cacheWrapper(callback) : callback;
|
return useCache ? cacheWrapper(callback) : callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSecuritySettings({
|
||||||
|
serviceUrl,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string;
|
||||||
|
}) {
|
||||||
|
const settingsService: Client<typeof SettingsService> =
|
||||||
|
await createServiceForHost(SettingsService, serviceUrl);
|
||||||
|
|
||||||
|
const callback = settingsService
|
||||||
|
.getSecuritySettings({})
|
||||||
|
.then((resp) => (resp.settings ? resp.settings : undefined));
|
||||||
|
|
||||||
|
return useCache ? cacheWrapper(callback) : callback;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getLockoutSettings({
|
export async function getLockoutSettings({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
orgId,
|
orgId,
|
||||||
@@ -491,21 +502,6 @@ export async function verifyInviteCode({
|
|||||||
return userService.verifyInviteCode({ userId, verificationCode }, {});
|
return userService.verifyInviteCode({ userId, verificationCode }, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resendInviteCode({
|
|
||||||
serviceUrl,
|
|
||||||
userId,
|
|
||||||
}: {
|
|
||||||
serviceUrl: string;
|
|
||||||
userId: string;
|
|
||||||
}) {
|
|
||||||
const userService: Client<typeof UserService> = await createServiceForHost(
|
|
||||||
UserService,
|
|
||||||
serviceUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
return userService.resendInviteCode({ userId }, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendEmailCode({
|
export async function sendEmailCode({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId,
|
userId,
|
||||||
@@ -929,6 +925,45 @@ export async function getAuthRequest({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDeviceAuthorizationRequest({
|
||||||
|
serviceUrl,
|
||||||
|
userCode,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string;
|
||||||
|
userCode: string;
|
||||||
|
}) {
|
||||||
|
const oidcService = await createServiceForHost(OIDCService, serviceUrl);
|
||||||
|
|
||||||
|
return oidcService.getDeviceAuthorizationRequest({
|
||||||
|
userCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authorizeOrDenyDeviceAuthorization({
|
||||||
|
serviceUrl,
|
||||||
|
deviceAuthorizationId,
|
||||||
|
session,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string;
|
||||||
|
deviceAuthorizationId: string;
|
||||||
|
session?: { sessionId: string; sessionToken: string };
|
||||||
|
}) {
|
||||||
|
const oidcService = await createServiceForHost(OIDCService, serviceUrl);
|
||||||
|
|
||||||
|
return oidcService.authorizeOrDenyDeviceAuthorization({
|
||||||
|
deviceAuthorizationId,
|
||||||
|
decision: session
|
||||||
|
? {
|
||||||
|
case: "session",
|
||||||
|
value: session,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
case: "deny",
|
||||||
|
value: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function createCallback({
|
export async function createCallback({
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
req,
|
req,
|
||||||
@@ -1116,13 +1151,11 @@ export async function setUserPassword({
|
|||||||
serviceUrl,
|
serviceUrl,
|
||||||
userId,
|
userId,
|
||||||
password,
|
password,
|
||||||
user,
|
|
||||||
code,
|
code,
|
||||||
}: {
|
}: {
|
||||||
serviceUrl: string;
|
serviceUrl: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
password: string;
|
password: string;
|
||||||
user: User;
|
|
||||||
code?: string;
|
code?: string;
|
||||||
}) {
|
}) {
|
||||||
let payload = create(SetPasswordRequestSchema, {
|
let payload = create(SetPasswordRequestSchema, {
|
||||||
@@ -1132,22 +1165,6 @@ export async function setUserPassword({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// check if the user has no password set in order to set a password
|
|
||||||
if (!code) {
|
|
||||||
const authmethods = await listAuthenticationMethodTypes({
|
|
||||||
serviceUrl,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// if the user has no authmethods set, we can set a password otherwise we need a code
|
|
||||||
if (
|
|
||||||
!(authmethods.authMethodTypes.length === 0) &&
|
|
||||||
user.state !== UserState.INITIAL
|
|
||||||
) {
|
|
||||||
return { error: "Provide a code to set a password" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
payload = {
|
payload = {
|
||||||
...payload,
|
...payload,
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { getServiceUrlFromHeaders } from "./lib/service";
|
import { DEFAULT_CSP } from "../constants/csp";
|
||||||
|
import { getServiceUrlFromHeaders } from "./lib/service-url";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
@@ -22,6 +23,20 @@ export async function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||||
|
|
||||||
|
// Call the /security route handler
|
||||||
|
// TODO check this on cloud run deployment
|
||||||
|
const securityResponse = await fetch(`${request.nextUrl.origin}/security`);
|
||||||
|
|
||||||
|
if (!securityResponse.ok) {
|
||||||
|
console.error(
|
||||||
|
"Failed to fetch security settings:",
|
||||||
|
securityResponse.statusText,
|
||||||
|
);
|
||||||
|
return NextResponse.next(); // Fallback if the request fails
|
||||||
|
}
|
||||||
|
|
||||||
|
const { settings: securitySettings } = await securityResponse.json();
|
||||||
|
|
||||||
const instanceHost = `${serviceUrl}`
|
const instanceHost = `${serviceUrl}`
|
||||||
.replace("https://", "")
|
.replace("https://", "")
|
||||||
.replace("http://", "");
|
.replace("http://", "");
|
||||||
@@ -39,7 +54,17 @@ export async function middleware(request: NextRequest) {
|
|||||||
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
||||||
responseHeaders.set("Access-Control-Allow-Headers", "*");
|
responseHeaders.set("Access-Control-Allow-Headers", "*");
|
||||||
|
|
||||||
|
if (securitySettings?.embeddedIframe?.enabled) {
|
||||||
|
securitySettings.embeddedIframe.allowedOrigins;
|
||||||
|
responseHeaders.set(
|
||||||
|
"Content-Security-Policy",
|
||||||
|
`${DEFAULT_CSP} frame-ancestors ${securitySettings.embeddedIframe.allowedOrigins.join(" ")};`,
|
||||||
|
);
|
||||||
|
responseHeaders.delete("X-Frame-Options");
|
||||||
|
}
|
||||||
|
|
||||||
request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`;
|
request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`;
|
||||||
|
|
||||||
return NextResponse.rewrite(request.nextUrl, {
|
return NextResponse.rewrite(request.nextUrl, {
|
||||||
request: {
|
request: {
|
||||||
headers: requestHeaders,
|
headers: requestHeaders,
|
||||||
|
@@ -5,6 +5,10 @@
|
|||||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
|
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
|
||||||
"dependsOn": ["^build"]
|
"dependsOn": ["^build"]
|
||||||
},
|
},
|
||||||
|
"build:standalone": {
|
||||||
|
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
|
||||||
|
"dependsOn": ["^build"]
|
||||||
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"dependsOn": ["@zitadel/client#build"]
|
"dependsOn": ["@zitadel/client#build"]
|
||||||
},
|
},
|
||||||
|
@@ -1,5 +1,17 @@
|
|||||||
# @zitadel/client
|
# @zitadel/client
|
||||||
|
|
||||||
|
## 1.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 62ad388: revert CJS support
|
||||||
|
|
||||||
|
## 1.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 9692297: add CJS and ESM support
|
||||||
|
|
||||||
## 1.0.7
|
## 1.0.7
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@zitadel/client",
|
"name": "@zitadel/client",
|
||||||
"version": "1.0.7",
|
"version": "1.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
@@ -1,5 +1,17 @@
|
|||||||
# @zitadel/proto
|
# @zitadel/proto
|
||||||
|
|
||||||
|
## 1.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 62ad388: revert CJS support
|
||||||
|
|
||||||
|
## 1.1.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 9692297: add CJS and ESM support
|
||||||
|
|
||||||
## 1.0.4
|
## 1.0.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@zitadel/proto",
|
"name": "@zitadel/proto",
|
||||||
"version": "1.0.4",
|
"version": "1.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
"tasks": {
|
"tasks": {
|
||||||
"generate": {
|
"generate": {
|
||||||
"outputs": ["zitadel/**"],
|
"outputs": ["zitadel/**"],
|
||||||
"cache": true
|
"cache": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,8 @@
|
|||||||
"ZITADEL_API_URL",
|
"ZITADEL_API_URL",
|
||||||
"ZITADEL_SERVICE_USER_TOKEN",
|
"ZITADEL_SERVICE_USER_TOKEN",
|
||||||
"NEXT_PUBLIC_BASE_PATH",
|
"NEXT_PUBLIC_BASE_PATH",
|
||||||
"CUSTOM_REQUEST_HEADERS"
|
"CUSTOM_REQUEST_HEADERS",
|
||||||
|
"NODE_ENV"
|
||||||
],
|
],
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"generate": {
|
"generate": {
|
||||||
|
Reference in New Issue
Block a user