diff --git a/.env.example b/.env.example index 4cbc466..4fb43a7 100644 --- a/.env.example +++ b/.env.example @@ -29,4 +29,4 @@ LOG_LEVEL=0 APP_TITLE=Tinyauth SSO FORGOT_PASSWORD_MESSAGE=Some message about resetting the password OAUTH_AUTO_REDIRECT=none -BACKGROUND_IMAGE=some_image_url \ No newline at end of file +BACKGROUND_IMAGE=some_image_url diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 76c434e..015a641 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,12 +2,22 @@ version: 2 updates: - package-ecosystem: "bun" directory: "/frontend" + minor-patch: + update-types: + - "patch" + - "minor" schedule: interval: "daily" + - package-ecosystem: "gomod" directory: "/" + minor-patch: + update-types: + - "patch" + - "minor" schedule: interval: "daily" + - package-ecosystem: "docker" directory: "/" schedule: diff --git a/.github/workflows/sponsors.yml b/.github/workflows/sponsors.yml new file mode 100644 index 0000000..68f53b0 --- /dev/null +++ b/.github/workflows/sponsors.yml @@ -0,0 +1,31 @@ +name: Generate Sponsors List +on: + workflow_dispatch: + +jobs: + generate-sponsors: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Generate Sponsors + uses: JamesIves/github-sponsors-readme-action@v1 + with: + token: ${{ secrets.SPONSORS_GENERATOR_PAT }} + active-only: false + file: "README.md" + template: 'User avatar: {{{ login }}}  ' + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: | + docs: regenerate readme sponsors list + committer: GitHub + author: GitHub + branch: docs/update-readme + title: | + docs: regenerate readme sponsors list + labels: bot diff --git a/Dockerfile b/Dockerfile index 833b79d..aab29b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Site builder -FROM oven/bun:1.2.10-alpine AS frontend-builder +FROM oven/bun:1.2.12-alpine AS frontend-builder WORKDIR /frontend diff --git a/cmd/root.go b/cmd/root.go index e5fdbf6..082b77b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -92,6 +92,7 @@ var rootCmd = &cobra.Command{ Domain: domain, ForgotPasswordMessage: config.FogotPasswordMessage, BackgroundImage: config.BackgroundImage, + OAuthAutoRedirect: config.OAuthAutoRedirect, } // Create api config @@ -112,6 +113,11 @@ var rootCmd = &cobra.Command{ LoginMaxRetries: config.LoginMaxRetries, } + // Create hooks config + hooksConfig := types.HooksConfig{ + Domain: domain, + } + // Create docker service docker := docker.NewDocker() @@ -129,7 +135,7 @@ var rootCmd = &cobra.Command{ providers.Init() // Create hooks service - hooks := hooks.NewHooks(auth, providers) + hooks := hooks.NewHooks(hooksConfig, auth, providers) // Create handlers handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) @@ -190,9 +196,10 @@ func init() { rootCmd.Flags().String("generic-auth-url", "", "Generic OAuth auth URL.") rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token URL.") rootCmd.Flags().String("generic-user-url", "", "Generic OAuth user info URL.") - rootCmd.Flags().String("generic-name", "Other", "Generic OAuth provider name.") + rootCmd.Flags().String("generic-name", "Generic", "Generic OAuth provider name.") rootCmd.Flags().Bool("disable-continue", false, "Disable continue screen and redirect to app directly.") rootCmd.Flags().String("oauth-whitelist", "", "Comma separated list of email addresses to whitelist when using OAuth.") + rootCmd.Flags().String("oauth-auto-redirect", "none", "Auto redirect to the specified OAuth provider if configured. (available providers: github, google, generic)") rootCmd.Flags().Int("session-expiry", 86400, "Session (cookie) expiration time in seconds.") rootCmd.Flags().Int("login-timeout", 300, "Login timeout in seconds after max retries reached (0 to disable).") rootCmd.Flags().Int("login-max-retries", 5, "Maximum login attempts before timeout (0 to disable).") @@ -226,6 +233,7 @@ func init() { viper.BindEnv("generic-name", "GENERIC_NAME") viper.BindEnv("disable-continue", "DISABLE_CONTINUE") viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST") + viper.BindEnv("oauth-auto-redirect", "OAUTH_AUTO_REDIRECT") viper.BindEnv("session-expiry", "SESSION_EXPIRY") viper.BindEnv("log-level", "LOG_LEVEL") viper.BindEnv("app-title", "APP_TITLE") diff --git a/frontend/package.json b/frontend/package.json index 527ea9f..be3a7f7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,4 +55,4 @@ "typescript-eslint": "^8.26.1", "vite": "^6.3.1" } -} +} \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/af-ZA.json b/frontend/src/lib/i18n/locales/af-ZA.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/af-ZA.json +++ b/frontend/src/lib/i18n/locales/af-ZA.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ar-SA.json b/frontend/src/lib/i18n/locales/ar-SA.json index 2990e0d..287dc04 100644 --- a/frontend/src/lib/i18n/locales/ar-SA.json +++ b/frontend/src/lib/i18n/locales/ar-SA.json @@ -41,9 +41,11 @@ "totpTitle": "أدخل رمز TOTP الخاص بك", "unauthorizedTitle": "غير مرخص", "unauthorizedResourceSubtitle": "المستخدم الذي يحمل اسم المستخدم {{username}} غير مصرح له بالوصول إلى المورد {{resource}}.", - "unaothorizedLoginSubtitle": "المستخدم الذي يحمل اسم المستخدم {{username}} غير مصرح له بتسجيل الدخول.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "حاول مجددا", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "إلغاء" + "cancelTitle": "إلغاء", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ca-ES.json b/frontend/src/lib/i18n/locales/ca-ES.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/ca-ES.json +++ b/frontend/src/lib/i18n/locales/ca-ES.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/cs-CZ.json b/frontend/src/lib/i18n/locales/cs-CZ.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/cs-CZ.json +++ b/frontend/src/lib/i18n/locales/cs-CZ.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/da-DK.json b/frontend/src/lib/i18n/locales/da-DK.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/da-DK.json +++ b/frontend/src/lib/i18n/locales/da-DK.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/de-DE.json b/frontend/src/lib/i18n/locales/de-DE.json index eed31ce..aac72bc 100644 --- a/frontend/src/lib/i18n/locales/de-DE.json +++ b/frontend/src/lib/i18n/locales/de-DE.json @@ -41,9 +41,11 @@ "totpTitle": "Geben Sie Ihren TOTP Code ein", "unauthorizedTitle": "Unautorisiert", "unauthorizedResourceSubtitle": "Der Benutzer mit Benutzername {{username}} ist nicht berechtigt auf die Ressource {{resource}} zuzugreifen.", - "unaothorizedLoginSubtitle": "Der Benutzer mit dem Benutzernamen {{username}} ist nicht berechtigt, sich einzuloggen.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Erneut versuchen", - "untrustedRedirectTitle": "Untrusted redirect", - "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "untrustedRedirectTitle": "Nicht vertrauenswürdige Weiterleitung", + "untrustedRedirectSubtitle": "Sie versuchen auf eine Domain umzuleiten, die nicht mit Ihrer konfigurierten Domain übereinstimmt ({{domain}}). Sind Sie sicher, dass Sie fortfahren möchten?", + "cancelTitle": "Abbrechen", + "forgotPasswordTitle": "Passwort vergessen?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/el-GR.json b/frontend/src/lib/i18n/locales/el-GR.json index 7730bcf..baa71be 100644 --- a/frontend/src/lib/i18n/locales/el-GR.json +++ b/frontend/src/lib/i18n/locales/el-GR.json @@ -41,9 +41,11 @@ "totpTitle": "Εισάγετε τον κωδικό TOTP", "unauthorizedTitle": "Μη εξουσιοδοτημένο", "unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν έχει άδεια πρόσβασης στον πόρο {{resource}}.", - "unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να συνδεθεί.", + "unauthorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να συνδεθεί.", + "unauthorizedGroupsSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι στις ομάδες που απαιτούνται από τον πόρο {{resource}}.", "unauthorizedButton": "Προσπαθήστε ξανά", "untrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση", "untrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε έναν τομέα που δεν ταιριάζει με τον ρυθμισμένο τομέα σας ({{domain}}). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;", - "cancelTitle": "Ακύρωση" + "cancelTitle": "Ακύρωση", + "forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/es-ES.json b/frontend/src/lib/i18n/locales/es-ES.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/es-ES.json +++ b/frontend/src/lib/i18n/locales/es-ES.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/fi-FI.json b/frontend/src/lib/i18n/locales/fi-FI.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/fi-FI.json +++ b/frontend/src/lib/i18n/locales/fi-FI.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/fr-FR.json b/frontend/src/lib/i18n/locales/fr-FR.json index 2bc537e..7260d95 100644 --- a/frontend/src/lib/i18n/locales/fr-FR.json +++ b/frontend/src/lib/i18n/locales/fr-FR.json @@ -41,9 +41,11 @@ "totpTitle": "Saisissez votre code TOTP", "unauthorizedTitle": "Non autorisé", "unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à accéder à la ressource {{resource}}.", - "unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à se connecter.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Réessayer", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/he-IL.json b/frontend/src/lib/i18n/locales/he-IL.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/he-IL.json +++ b/frontend/src/lib/i18n/locales/he-IL.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/hu-HU.json b/frontend/src/lib/i18n/locales/hu-HU.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/hu-HU.json +++ b/frontend/src/lib/i18n/locales/hu-HU.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/it-IT.json b/frontend/src/lib/i18n/locales/it-IT.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/it-IT.json +++ b/frontend/src/lib/i18n/locales/it-IT.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ja-JP.json b/frontend/src/lib/i18n/locales/ja-JP.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/ja-JP.json +++ b/frontend/src/lib/i18n/locales/ja-JP.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ko-KR.json b/frontend/src/lib/i18n/locales/ko-KR.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/ko-KR.json +++ b/frontend/src/lib/i18n/locales/ko-KR.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/nl-NL.json b/frontend/src/lib/i18n/locales/nl-NL.json index e8b9134..2685002 100644 --- a/frontend/src/lib/i18n/locales/nl-NL.json +++ b/frontend/src/lib/i18n/locales/nl-NL.json @@ -41,9 +41,11 @@ "totpTitle": "Voer je TOTP-code in", "unauthorizedTitle": "Ongeautoriseerd", "unauthorizedResourceSubtitle": "De gebruiker met gebruikersnaam {{username}} heeft geen toegang tot de bron {{resource}}.", - "unaothorizedLoginSubtitle": "De gebruiker met gebruikersnaam {{username}} is niet gemachtigd om in te loggen.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Opnieuw proberen", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/no-NO.json b/frontend/src/lib/i18n/locales/no-NO.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/no-NO.json +++ b/frontend/src/lib/i18n/locales/no-NO.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/pl-PL.json b/frontend/src/lib/i18n/locales/pl-PL.json index d20b48e..454b488 100644 --- a/frontend/src/lib/i18n/locales/pl-PL.json +++ b/frontend/src/lib/i18n/locales/pl-PL.json @@ -41,9 +41,11 @@ "totpTitle": "Wprowadź kod TOTP", "unauthorizedTitle": "Nieautoryzowany", "unauthorizedResourceSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do uzyskania dostępu do zasobu {{resource}}.", - "unaothorizedLoginSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do logowania.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Spróbuj ponownie", "untrustedRedirectTitle": "Niezaufane przekierowanie", "untrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do skonfigurowanej przez Ciebie domeny ({{domain}}). Czy na pewno chcesz kontynuować?", - "cancelTitle": "Anuluj" + "cancelTitle": "Anuluj", + "forgotPasswordTitle": "Nie pamiętasz hasła?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/pt-BR.json b/frontend/src/lib/i18n/locales/pt-BR.json index 43609e0..6a86fee 100644 --- a/frontend/src/lib/i18n/locales/pt-BR.json +++ b/frontend/src/lib/i18n/locales/pt-BR.json @@ -1,5 +1,5 @@ { - "loginTitle": "Bem-vindo de volta, faça o login com", + "loginTitle": "Bem-vindo de volta, acesse com", "loginDivider": "Ou continuar com uma senha", "loginUsername": "Nome de usuário", "loginPassword": "Senha", @@ -41,9 +41,11 @@ "totpTitle": "Insira o seu código TOTP", "unauthorizedTitle": "Não autorizado", "unauthorizedResourceSubtitle": "O usuário com nome de usuário {{username}} não está autorizado a acessar o recurso {{resource}}.", - "unaothorizedLoginSubtitle": "O usuário com o nome {{username}} não está autorizado a acessar.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Tentar novamente", "untrustedRedirectTitle": "Redirecionamento não confiável", "untrustedRedirectSubtitle": "Você está tentando redirecionar para um domínio que não corresponde ao seu domínio configurado ({{domain}}). Tem certeza que deseja continuar?", - "cancelTitle": "Cancelar" + "cancelTitle": "Cancelar", + "forgotPasswordTitle": "Esqueceu sua senha?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/pt-PT.json b/frontend/src/lib/i18n/locales/pt-PT.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/pt-PT.json +++ b/frontend/src/lib/i18n/locales/pt-PT.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ro-RO.json b/frontend/src/lib/i18n/locales/ro-RO.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/ro-RO.json +++ b/frontend/src/lib/i18n/locales/ro-RO.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/ru-RU.json b/frontend/src/lib/i18n/locales/ru-RU.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/ru-RU.json +++ b/frontend/src/lib/i18n/locales/ru-RU.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/sr-SP.json b/frontend/src/lib/i18n/locales/sr-SP.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/sr-SP.json +++ b/frontend/src/lib/i18n/locales/sr-SP.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/sv-SE.json b/frontend/src/lib/i18n/locales/sv-SE.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/sv-SE.json +++ b/frontend/src/lib/i18n/locales/sv-SE.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/tr-TR.json b/frontend/src/lib/i18n/locales/tr-TR.json index 317b4ad..379c8e7 100644 --- a/frontend/src/lib/i18n/locales/tr-TR.json +++ b/frontend/src/lib/i18n/locales/tr-TR.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "İptal" + "cancelTitle": "İptal", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/uk-UA.json b/frontend/src/lib/i18n/locales/uk-UA.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/uk-UA.json +++ b/frontend/src/lib/i18n/locales/uk-UA.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/vi-VN.json b/frontend/src/lib/i18n/locales/vi-VN.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/vi-VN.json +++ b/frontend/src/lib/i18n/locales/vi-VN.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/zh-CN.json b/frontend/src/lib/i18n/locales/zh-CN.json index 5a64207..64d06db 100644 --- a/frontend/src/lib/i18n/locales/zh-CN.json +++ b/frontend/src/lib/i18n/locales/zh-CN.json @@ -41,9 +41,11 @@ "totpTitle": "输入您的 TOTP 代码", "unauthorizedTitle": "未授权", "unauthorizedResourceSubtitle": "用户 {{username}} 无权访问资源 {{resource}}。", - "unaothorizedLoginSubtitle": "用户名 {{username}} 无登录权限。", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "重试", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/lib/i18n/locales/zh-TW.json b/frontend/src/lib/i18n/locales/zh-TW.json index b31e2ee..6d1af3c 100644 --- a/frontend/src/lib/i18n/locales/zh-TW.json +++ b/frontend/src/lib/i18n/locales/zh-TW.json @@ -41,9 +41,11 @@ "totpTitle": "Enter your TOTP code", "unauthorizedTitle": "Unauthorized", "unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource {{resource}}.", - "unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.", + "unauthorizedGroupsSubtitle": "The user with username {{username}} is not in the groups required by the resource {{resource}}.", "unauthorizedButton": "Try again", "untrustedRedirectTitle": "Untrusted redirect", "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain ({{domain}}). Are you sure you want to continue?", - "cancelTitle": "Cancel" + "cancelTitle": "Cancel", + "forgotPasswordTitle": "Forgot your password?" } \ No newline at end of file diff --git a/frontend/src/pages/login-page.tsx b/frontend/src/pages/login-page.tsx index c670e06..3a99aa9 100644 --- a/frontend/src/pages/login-page.tsx +++ b/frontend/src/pages/login-page.tsx @@ -94,6 +94,14 @@ export const LoginPage = () => { } }); + useEffect(() => { + if (isMounted()) { + if (oauthConfigured && configuredProviders.includes(oauthAutoRedirect)) { + oauthMutation.mutate(oauthAutoRedirect); + } + } + }, []); + return ( diff --git a/frontend/src/schemas/app-context-schema.ts b/frontend/src/schemas/app-context-schema.ts index 386143a..7ccd0ba 100644 --- a/frontend/src/schemas/app-context-schema.ts +++ b/frontend/src/schemas/app-context-schema.ts @@ -7,7 +7,7 @@ export const appContextSchema = z.object({ genericName: z.string(), domain: z.string(), forgotPasswordMessage: z.string(), - oauthAutoRedirect: z.string(), + oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), backgroundImage: z.string(), }) diff --git a/go.mod b/go.mod index ff1ad0e..d33cd0d 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 - golang.org/x/crypto v0.37.0 + golang.org/x/crypto v0.38.0 ) require ( @@ -25,7 +25,7 @@ require ( go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect go.opentelemetry.io/otel/sdk v1.34.0 // indirect - golang.org/x/term v0.31.0 // indirect + golang.org/x/term v0.32.0 // indirect gotest.tools/v3 v3.5.2 // indirect rsc.io/qr v0.2.0 // indirect ) @@ -103,10 +103,10 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.13.0 // indirect golang.org/x/net v0.38.0 // indirect - golang.org/x/oauth2 v0.29.0 - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/oauth2 v0.30.0 + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect google.golang.org/protobuf v1.36.3 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 89bb36e..b4344e4 100644 --- a/go.sum +++ b/go.sum @@ -269,8 +269,8 @@ golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -281,13 +281,13 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -297,14 +297,14 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/api/api_test.go b/internal/api/api_test.go index e0cb6e5..23c1baf 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -45,6 +45,11 @@ var authConfig = types.AuthConfig{ LoginMaxRetries: 0, } +// Simple hooks config for tests +var hooksConfig = types.HooksConfig{ + Domain: "localhost", +} + // Cookie var cookie string @@ -83,7 +88,7 @@ func getAPI(t *testing.T) *api.API { providers.Init() // Create hooks service - hooks := hooks.NewHooks(auth, providers) + hooks := hooks.NewHooks(hooksConfig, auth, providers) // Create handlers service handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index d6ed5f3..d593f2f 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -160,9 +160,12 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) // Set data session.Values["username"] = data.Username + session.Values["name"] = data.Name + session.Values["email"] = data.Email session.Values["provider"] = data.Provider session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix() session.Values["totpPending"] = data.TotpPending + session.Values["oauthGroups"] = data.OAuthGroups // Save session err = session.Save(c.Request, c.Writer) @@ -211,14 +214,24 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) return types.SessionCookie{}, err } + log.Debug().Msg("Got session") + // Get data from session username, usernameOk := session.Values["username"].(string) + email, emailOk := session.Values["email"].(string) + name, nameOk := session.Values["name"].(string) provider, providerOK := session.Values["provider"].(string) expiry, expiryOk := session.Values["expiry"].(int64) totpPending, totpPendingOk := session.Values["totpPending"].(bool) + oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string) - if !usernameOk || !providerOK || !expiryOk || !totpPendingOk { - log.Warn().Msg("Session cookie is missing data") + if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk { + log.Warn().Msg("Session cookie is invalid") + + // If any data is missing, delete the session cookie + auth.DeleteSessionCookie(c) + + // Return empty cookie return types.SessionCookie{}, nil } @@ -233,13 +246,16 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) return types.SessionCookie{}, nil } - log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Msg("Parsed cookie") + log.Debug().Str("username", username).Str("provider", provider).Int64("expiry", expiry).Bool("totpPending", totpPending).Str("name", name).Str("email", email).Str("oauthGroups", oauthGroups).Msg("Parsed cookie") // Return the cookie return types.SessionCookie{ Username: username, + Name: name, + Email: email, Provider: provider, TotpPending: totpPending, + OAuthGroups: oauthGroups, }, nil } @@ -248,48 +264,52 @@ func (auth *Auth) UserAuthConfigured() bool { return len(auth.Config.Users) > 0 } -func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) { - // Get headers - host := c.Request.Header.Get("X-Forwarded-Host") - - // Get app id - appId := strings.Split(host, ".")[0] - - // Get the container labels - labels, err := auth.Docker.GetLabels(appId) - - // If there is an error, return false - if err != nil { - return false, err - } - +func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool { // Check if oauth is allowed if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") - return utils.CheckWhitelist(labels.OAuthWhitelist, context.Username), nil + return utils.CheckWhitelist(labels.OAuthWhitelist, context.Email) } // Check users log.Debug().Msg("Checking users") - return utils.CheckWhitelist(labels.Users, context.Username), nil + return utils.CheckWhitelist(labels.Users, context.Username) } -func (auth *Auth) AuthEnabled(c *gin.Context) (bool, error) { +func (auth *Auth) OAuthGroup(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool { + // Check if groups are required + if labels.OAuthGroups == "" { + return true + } + + // Check if we are using the generic oauth provider + if context.Provider != "generic" { + log.Debug().Msg("Not using generic provider, skipping group check") + return true + } + + // Split the groups by comma (no need to parse since they are from the API response) + oauthGroups := strings.Split(context.OAuthGroups, ",") + + // For every group check if it is in the required groups + for _, group := range oauthGroups { + if utils.CheckWhitelist(labels.OAuthGroups, group) { + log.Debug().Str("group", group).Msg("Group is in required groups") + return true + } + } + + // No groups matched + log.Debug().Msg("No groups matched") + + // Return false + return false +} + +func (auth *Auth) AuthEnabled(c *gin.Context, labels types.TinyauthLabels) (bool, error) { // Get headers uri := c.Request.Header.Get("X-Forwarded-Uri") - host := c.Request.Header.Get("X-Forwarded-Host") - - // Get app id - appId := strings.Split(host, ".")[0] - - // Get the container labels - labels, err := auth.Docker.GetLabels(appId) - - // If there is an error, auth enabled - if err != nil { - return true, err - } // Check if the allowed label is empty if labels.Allowed == "" { diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 37aa55d..72480b6 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -6,4 +6,13 @@ var TinyauthLabels = []string{ "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", + "tinyauth.oauth.groups", +} + +// Claims are the OIDC supported claims (including preferd username for some reason) +type Claims struct { + Name string `json:"name"` + Email string `json:"email"` + PreferredUsername string `json:"preferred_username"` + Groups []string `json:"groups"` } diff --git a/internal/docker/docker.go b/internal/docker/docker.go index 07962e0..170de23 100644 --- a/internal/docker/docker.go +++ b/internal/docker/docker.go @@ -6,8 +6,7 @@ import ( "tinyauth/internal/types" "tinyauth/internal/utils" - apiTypes "github.com/docker/docker/api/types" - containerTypes "github.com/docker/docker/api/types/container" + container "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/rs/zerolog/log" ) @@ -30,17 +29,22 @@ func (docker *Docker) Init() error { return err } - // Set the context and api client + // Create the context docker.Context = context.Background() + + // Negotiate API version + client.NegotiateAPIVersion(docker.Context) + + // Set client docker.Client = client // Done return nil } -func (docker *Docker) GetContainers() ([]apiTypes.Container, error) { +func (docker *Docker) GetContainers() ([]container.Summary, error) { // Get the list of containers - containers, err := docker.Client.ContainerList(docker.Context, containerTypes.ListOptions{}) + containers, err := docker.Client.ContainerList(docker.Context, container.ListOptions{}) // Check if there was an error if err != nil { @@ -51,13 +55,13 @@ func (docker *Docker) GetContainers() ([]apiTypes.Container, error) { return containers, nil } -func (docker *Docker) InspectContainer(containerId string) (apiTypes.ContainerJSON, error) { +func (docker *Docker) InspectContainer(containerId string) (container.InspectResponse, error) { // Inspect the container inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) // Check if there was an error if err != nil { - return apiTypes.ContainerJSON{}, err + return container.InspectResponse{}, err } // Return the inspect diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 6b360fa..3d10710 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -10,6 +10,7 @@ import ( "tinyauth/internal/hooks" "tinyauth/internal/providers" "tinyauth/internal/types" + "tinyauth/internal/utils" "github.com/gin-gonic/gin" "github.com/google/go-querystring/query" @@ -68,12 +69,17 @@ func (h *Handlers) AuthHandler(c *gin.Context) { proto := c.Request.Header.Get("X-Forwarded-Proto") host := c.Request.Header.Get("X-Forwarded-Host") - // Check if auth is enabled - authEnabled, err := h.Auth.AuthEnabled(c) + // Get the app id + appId := strings.Split(host, ".")[0] + + // Get the container labels + labels, err := h.Docker.GetLabels(appId) + + log.Debug().Interface("labels", labels).Msg("Got labels") // Check if there was an error if err != nil { - log.Error().Err(err).Msg("Failed to check if app is allowed") + log.Error().Err(err).Msg("Failed to get container labels") if proxy.Proxy == "nginx" || !isBrowser { c.JSON(500, gin.H{ @@ -87,11 +93,8 @@ func (h *Handlers) AuthHandler(c *gin.Context) { return } - // Get the app id - appId := strings.Split(host, ".")[0] - - // Get the container labels - labels, err := h.Docker.GetLabels(appId) + // Check if auth is enabled + authEnabled, err := h.Auth.AuthEnabled(c, labels) // Check if there was an error if err != nil { @@ -113,7 +116,7 @@ func (h *Handlers) AuthHandler(c *gin.Context) { if !authEnabled { for key, value := range labels.Headers { log.Debug().Str("key", key).Str("value", value).Msg("Setting header") - c.Header(key, value) + c.Header(key, utils.SanitizeHeader(value)) } c.JSON(200, gin.H{ "status": 200, @@ -125,28 +128,18 @@ func (h *Handlers) AuthHandler(c *gin.Context) { // Get user context userContext := h.Hooks.UseUserContext(c) + // If we are using basic auth, we need to check if the user has totp and if it does then disable basic auth + if userContext.Provider == "basic" && userContext.TotpEnabled { + log.Warn().Str("username", userContext.Username).Msg("User has totp enabled, disabling basic auth") + userContext.IsLoggedIn = false + } + // Check if user is logged in if userContext.IsLoggedIn { log.Debug().Msg("Authenticated") // Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx - appAllowed, err := h.Auth.ResourceAllowed(c, userContext) - - // Check if there was an error - if err != nil { - log.Error().Err(err).Msg("Failed to check if app is allowed") - - if proxy.Proxy == "nginx" || !isBrowser { - c.JSON(500, gin.H{ - "status": 500, - "message": "Internal Server Error", - }) - return - } - - c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) - return - } + appAllowed := h.Auth.ResourceAllowed(c, userContext, labels) log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed") @@ -154,9 +147,6 @@ func (h *Handlers) AuthHandler(c *gin.Context) { if !appAllowed { log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User not allowed") - // Set WWW-Authenticate header - c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") - if proxy.Proxy == "nginx" || !isBrowser { c.JSON(401, gin.H{ "status": 401, @@ -165,11 +155,20 @@ func (h *Handlers) AuthHandler(c *gin.Context) { return } - // Build query - queries, err := query.Values(types.UnauthorizedQuery{ - Username: userContext.Username, + // Values + values := types.UnauthorizedQuery{ Resource: strings.Split(host, ".")[0], - }) + } + + // Use either username or email + if userContext.OAuth { + values.Username = userContext.Email + } else { + values.Username = userContext.Username + } + + // Build query + queries, err := query.Values(values) // Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik) if err != nil { @@ -183,13 +182,63 @@ func (h *Handlers) AuthHandler(c *gin.Context) { return } - // Set the user header - c.Header("Remote-User", userContext.Username) + // Check groups if using OAuth + if userContext.OAuth { + // Check if user is in required groups + groupOk := h.Auth.OAuthGroup(c, userContext, labels) + + log.Debug().Bool("groupOk", groupOk).Msg("Checking if user is in required groups") + + // The user is not allowed to access the app + if !groupOk { + log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User is not in required groups") + + if proxy.Proxy == "nginx" || !isBrowser { + c.JSON(401, gin.H{ + "status": 401, + "message": "Unauthorized", + }) + return + } + + // Values + values := types.UnauthorizedQuery{ + Resource: strings.Split(host, ".")[0], + GroupErr: true, + } + + // Use either username or email + if userContext.OAuth { + values.Username = userContext.Email + } else { + values.Username = userContext.Username + } + + // Build query + queries, err := query.Values(values) + + // Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik) + if err != nil { + log.Error().Err(err).Msg("Failed to build queries") + c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) + return + } + + // We are using caddy/traefik so redirect + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode())) + return + } + } + + c.Header("Remote-User", utils.SanitizeHeader(userContext.Username)) + c.Header("Remote-Name", utils.SanitizeHeader(userContext.Name)) + c.Header("Remote-Email", utils.SanitizeHeader(userContext.Email)) + c.Header("Remote-Groups", utils.SanitizeHeader(userContext.OAuthGroups)) // Set the rest of the headers for key, value := range labels.Headers { log.Debug().Str("key", key).Str("value", value).Msg("Setting header") - c.Header(key, value) + c.Header(key, utils.SanitizeHeader(value)) } // The user is allowed to access the app @@ -203,9 +252,6 @@ func (h *Handlers) AuthHandler(c *gin.Context) { // The user is not logged in log.Debug().Msg("Unauthorized") - // Set www-authenticate header - c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") - if proxy.Proxy == "nginx" || !isBrowser { c.JSON(401, gin.H{ "status": 401, @@ -310,6 +356,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) { // Set totp pending cookie h.Auth.CreateSessionCookie(c, &types.SessionCookie{ Username: login.Username, + Name: utils.Capitalize(login.Username), + Email: fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain), Provider: "username", TotpPending: true, }) @@ -328,6 +376,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) { // Create session cookie with username as provider h.Auth.CreateSessionCookie(c, &types.SessionCookie{ Username: login.Username, + Name: utils.Capitalize(login.Username), + Email: fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain), Provider: "username", }) @@ -402,6 +452,8 @@ func (h *Handlers) TotpHandler(c *gin.Context) { // Create session cookie with username as provider h.Auth.CreateSessionCookie(c, &types.SessionCookie{ Username: user.Username, + Name: utils.Capitalize(user.Username), + Email: fmt.Sprintf("%s@%s", strings.ToLower(user.Username), h.Config.Domain), Provider: "username", }) @@ -449,6 +501,7 @@ func (h *Handlers) AppHandler(c *gin.Context) { Domain: h.Config.Domain, ForgotPasswordMessage: h.Config.ForgotPasswordMessage, BackgroundImage: h.Config.BackgroundImage, + OAuthAutoRedirect: h.Config.OAuthAutoRedirect, } // Return app context @@ -466,15 +519,16 @@ func (h *Handlers) UserHandler(c *gin.Context) { Status: 200, IsLoggedIn: userContext.IsLoggedIn, Username: userContext.Username, + Name: userContext.Name, + Email: userContext.Email, Provider: userContext.Provider, Oauth: userContext.OAuth, TotpPending: userContext.TotpPending, } - // If we are not logged in we set the status to 401 and add the WWW-Authenticate header else we set it to 200 + // If we are not logged in we set the status to 401 else we set it to 200 if !userContext.IsLoggedIn { log.Debug().Msg("Unauthorized") - c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") userContextResponse.Message = "Unauthorized" } else { log.Debug().Interface("userContext", userContext).Msg("Authenticated") @@ -614,25 +668,32 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) { return } - // Get email - email, err := h.Providers.GetUser(providerName.Provider) - - log.Debug().Str("email", email).Msg("Got email") + // Get user + user, err := h.Providers.GetUser(providerName.Provider) // Handle error if err != nil { - log.Error().Err(err).Msg("Failed to get email") + log.Error().Msg("Failed to get user") + c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) + return + } + + log.Debug().Msg("Got user") + + // Check that email is not empty + if user.Email == "" { + log.Error().Msg("Email is empty") c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) return } // Email is not whitelisted - if !h.Auth.EmailWhitelisted(email) { - log.Warn().Str("email", email).Msg("Email not whitelisted") + if !h.Auth.EmailWhitelisted(user.Email) { + log.Warn().Str("email", user.Email).Msg("Email not whitelisted") // Build query queries, err := query.Values(types.UnauthorizedQuery{ - Username: email, + Username: user.Email, }) // Handle error @@ -648,10 +709,31 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) { log.Debug().Msg("Email whitelisted") + // Get username + var username string + + if user.PreferredUsername != "" { + username = user.PreferredUsername + } else { + username = fmt.Sprintf("%s_%s", strings.Split(user.Email, "@")[0], strings.Split(user.Email, "@")[1]) + } + + // Get name + var name string + + if user.Name != "" { + name = user.Name + } else { + name = fmt.Sprintf("%s (%s)", utils.Capitalize(strings.Split(user.Email, "@")[0]), strings.Split(user.Email, "@")[1]) + } + // Create session cookie (also cleans up redirect cookie) h.Auth.CreateSessionCookie(c, &types.SessionCookie{ - Username: email, - Provider: providerName.Provider, + Username: username, + Name: name, + Email: user.Email, + Provider: providerName.Provider, + OAuthGroups: strings.Join(user.Groups, ","), }) // Check if we have a redirect URI diff --git a/internal/hooks/hooks.go b/internal/hooks/hooks.go index 5e9a689..15947be 100644 --- a/internal/hooks/hooks.go +++ b/internal/hooks/hooks.go @@ -1,22 +1,27 @@ package hooks import ( + "fmt" + "strings" "tinyauth/internal/auth" "tinyauth/internal/providers" "tinyauth/internal/types" + "tinyauth/internal/utils" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" ) -func NewHooks(auth *auth.Auth, providers *providers.Providers) *Hooks { +func NewHooks(config types.HooksConfig, auth *auth.Auth, providers *providers.Providers) *Hooks { return &Hooks{ + Config: config, Auth: auth, Providers: providers, } } type Hooks struct { + Config types.HooksConfig Auth *auth.Auth Providers *providers.Providers } @@ -30,17 +35,27 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { if basic != nil { log.Debug().Msg("Got basic auth") - // Check if user exists and password is correct + // Get user user := hooks.Auth.GetUser(basic.Username) - if user != nil && hooks.Auth.CheckPassword(*user, basic.Password) { + // Check we have a user + if user == nil { + log.Error().Str("username", basic.Username).Msg("User does not exist") + + // Return empty context + return types.UserContext{} + } + + // Check if the user has a correct password + if hooks.Auth.CheckPassword(*user, basic.Password) { // Return user context since we are logged in with basic auth return types.UserContext{ Username: basic.Username, + Name: utils.Capitalize(basic.Username), + Email: fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), hooks.Config.Domain), IsLoggedIn: true, - OAuth: false, Provider: "basic", - TotpPending: false, + TotpEnabled: user.TotpSecret != "", } } @@ -50,13 +65,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { if err != nil { log.Error().Err(err).Msg("Failed to get session cookie") // Return empty context - return types.UserContext{ - Username: "", - IsLoggedIn: false, - OAuth: false, - Provider: "", - TotpPending: false, - } + return types.UserContext{} } // Check if session cookie has totp pending @@ -65,8 +74,8 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { // Return empty context since we are pending totp return types.UserContext{ Username: cookie.Username, - IsLoggedIn: false, - OAuth: false, + Name: cookie.Name, + Email: cookie.Email, Provider: cookie.Provider, TotpPending: true, } @@ -82,11 +91,11 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { // It exists so we are logged in return types.UserContext{ - Username: cookie.Username, - IsLoggedIn: true, - OAuth: false, - Provider: "username", - TotpPending: false, + Username: cookie.Username, + Name: cookie.Name, + Email: cookie.Email, + IsLoggedIn: true, + Provider: "username", } } } @@ -101,20 +110,14 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { log.Debug().Msg("Provider exists") // Check if the oauth email is whitelisted - if !hooks.Auth.EmailWhitelisted(cookie.Username) { - log.Error().Str("email", cookie.Username).Msg("Email is not whitelisted") + if !hooks.Auth.EmailWhitelisted(cookie.Email) { + log.Error().Str("email", cookie.Email).Msg("Email is not whitelisted") // It isn't so we delete the cookie and return an empty context hooks.Auth.DeleteSessionCookie(c) // Return empty context - return types.UserContext{ - Username: "", - IsLoggedIn: false, - OAuth: false, - Provider: "", - TotpPending: false, - } + return types.UserContext{} } log.Debug().Msg("Email is whitelisted") @@ -122,19 +125,15 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { // Return user context since we are logged in with oauth return types.UserContext{ Username: cookie.Username, + Name: cookie.Name, + Email: cookie.Email, IsLoggedIn: true, OAuth: true, Provider: cookie.Provider, - TotpPending: false, + OAuthGroups: cookie.OAuthGroups, } } // Neither basic auth or oauth is set so we return an empty context - return types.UserContext{ - Username: "", - IsLoggedIn: false, - OAuth: false, - Provider: "", - TotpPending: false, - } + return types.UserContext{} } diff --git a/internal/providers/generic.go b/internal/providers/generic.go index 798b039..5e6a0e0 100644 --- a/internal/providers/generic.go +++ b/internal/providers/generic.go @@ -4,24 +4,25 @@ import ( "encoding/json" "io" "net/http" + "tinyauth/internal/constants" "github.com/rs/zerolog/log" ) -// We are assuming that the generic provider will return a JSON object with an email field -type GenericUserInfoResponse struct { - Email string `json:"email"` -} +func GetGenericUser(client *http.Client, url string) (constants.Claims, error) { + // Create user struct + var user constants.Claims -func GetGenericEmail(client *http.Client, url string) (string, error) { // Using the oauth client get the user info url res, err := client.Get(url) // Check if there was an error if err != nil { - return "", err + return user, err } + defer res.Body.Close() + log.Debug().Msg("Got response from generic provider") // Read the body of the response @@ -29,24 +30,21 @@ func GetGenericEmail(client *http.Client, url string) (string, error) { // Check if there was an error if err != nil { - return "", err + return user, err } log.Debug().Msg("Read body from generic provider") - // Parse the body into a user struct - var user GenericUserInfoResponse - // Unmarshal the body into the user struct err = json.Unmarshal(body, &user) // Check if there was an error if err != nil { - return "", err + return user, err } log.Debug().Msg("Parsed user from generic provider") - // Return the email - return user.Email, nil + // Return the user + return user, nil } diff --git a/internal/providers/github.go b/internal/providers/github.go index 010e799..a67b4e8 100644 --- a/internal/providers/github.go +++ b/internal/providers/github.go @@ -5,51 +5,96 @@ import ( "errors" "io" "net/http" + "tinyauth/internal/constants" "github.com/rs/zerolog/log" ) -// Github has a different response than the generic provider -type GithubUserInfoResponse []struct { +// Response for the github email endpoint +type GithubEmailResponse []struct { Email string `json:"email"` Primary bool `json:"primary"` } -// The scopes required for the github provider -func GithubScopes() []string { - return []string{"user:email"} +// Response for the github user endpoint +type GithubUserInfoResponse struct { + Login string `json:"login"` + Name string `json:"name"` } -func GetGithubEmail(client *http.Client) (string, error) { - // Get the user emails from github using the oauth http client - res, err := client.Get("https://api.github.com/user/emails") +// The scopes required for the github provider +func GithubScopes() []string { + return []string{"user:email", "read:user"} +} + +func GetGithubUser(client *http.Client) (constants.Claims, error) { + // Create user struct + var user constants.Claims + + // Get the user info from github using the oauth http client + res, err := client.Get("https://api.github.com/user") // Check if there was an error if err != nil { - return "", err + return user, err } - log.Debug().Msg("Got response from github") + defer res.Body.Close() + + log.Debug().Msg("Got user response from github") // Read the body of the response body, err := io.ReadAll(res.Body) // Check if there was an error if err != nil { - return "", err + return user, err } - log.Debug().Msg("Read body from github") + log.Debug().Msg("Read user body from github") // Parse the body into a user struct - var emails GithubUserInfoResponse + var userInfo GithubUserInfoResponse + + // Unmarshal the body into the user struct + err = json.Unmarshal(body, &userInfo) + + // Check if there was an error + if err != nil { + return user, err + } + + // Get the user emails from github using the oauth http client + res, err = client.Get("https://api.github.com/user/emails") + + // Check if there was an error + if err != nil { + return user, err + } + + defer res.Body.Close() + + log.Debug().Msg("Got email response from github") + + // Read the body of the response + body, err = io.ReadAll(res.Body) + + // Check if there was an error + if err != nil { + return user, err + } + + log.Debug().Msg("Read email body from github") + + // Parse the body into a user struct + var emails GithubEmailResponse // Unmarshal the body into the user struct err = json.Unmarshal(body, &emails) // Check if there was an error if err != nil { - return "", err + return user, err } log.Debug().Msg("Parsed emails from github") @@ -57,10 +102,28 @@ func GetGithubEmail(client *http.Client) (string, error) { // Find and return the primary email for _, email := range emails { if email.Primary { - return email.Email, nil + // Set the email then exit + log.Debug().Str("email", email.Email).Msg("Found primary email") + user.Email = email.Email + break } } - // User does not have a primary email? - return "", errors.New("no primary email found") + // If no primary email was found, use the first available email + if len(emails) == 0 { + return user, errors.New("no emails found") + } + + // Set the email if it is not set picking the first one + if user.Email == "" { + log.Warn().Str("email", emails[0].Email).Msg("No primary email found, using first email") + user.Email = emails[0].Email + } + + // Set the username and name + user.PreferredUsername = userInfo.Login + user.Name = userInfo.Name + + // Return + return user, nil } diff --git a/internal/providers/google.go b/internal/providers/google.go index ba5c8b4..2023aec 100644 --- a/internal/providers/google.go +++ b/internal/providers/google.go @@ -4,29 +4,37 @@ import ( "encoding/json" "io" "net/http" + "strings" + "tinyauth/internal/constants" "github.com/rs/zerolog/log" ) -// Google works the same as the generic provider +// Response for the google user endpoint type GoogleUserInfoResponse struct { Email string `json:"email"` + Name string `json:"name"` } // The scopes required for the google provider func GoogleScopes() []string { - return []string{"https://www.googleapis.com/auth/userinfo.email"} + return []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"} } -func GetGoogleEmail(client *http.Client) (string, error) { +func GetGoogleUser(client *http.Client) (constants.Claims, error) { + // Create user struct + var user constants.Claims + // Get the user info from google using the oauth http client res, err := client.Get("https://www.googleapis.com/userinfo/v2/me") // Check if there was an error if err != nil { - return "", err + return user, err } + defer res.Body.Close() + log.Debug().Msg("Got response from google") // Read the body of the response @@ -34,24 +42,29 @@ func GetGoogleEmail(client *http.Client) (string, error) { // Check if there was an error if err != nil { - return "", err + return user, err } log.Debug().Msg("Read body from google") - // Parse the body into a user struct - var user GoogleUserInfoResponse + // Create a new user info struct + var userInfo GoogleUserInfoResponse // Unmarshal the body into the user struct - err = json.Unmarshal(body, &user) + err = json.Unmarshal(body, &userInfo) // Check if there was an error if err != nil { - return "", err + return user, err } log.Debug().Msg("Parsed user from google") - // Return the email - return user.Email, nil + // Map the user info to the user struct + user.PreferredUsername = strings.Split(userInfo.Email, "@")[0] + user.Name = userInfo.Name + user.Email = userInfo.Email + + // Return the user + return user, nil } diff --git a/internal/providers/providers.go b/internal/providers/providers.go index c1bad5e..a22e83e 100644 --- a/internal/providers/providers.go +++ b/internal/providers/providers.go @@ -2,6 +2,7 @@ package providers import ( "fmt" + "tinyauth/internal/constants" "tinyauth/internal/oauth" "tinyauth/internal/types" @@ -93,14 +94,17 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth { } } -func (providers *Providers) GetUser(provider string) (string, error) { - // Get the email from the provider +func (providers *Providers) GetUser(provider string) (constants.Claims, error) { + // Create user struct + var user constants.Claims + + // Get the user from the provider switch provider { case "github": // If the github provider is not configured, return an error if providers.Github == nil { log.Debug().Msg("Github provider not configured") - return "", nil + return user, nil } // Get the client from the github provider @@ -108,23 +112,23 @@ func (providers *Providers) GetUser(provider string) (string, error) { log.Debug().Msg("Got client from github") - // Get the email from the github provider - email, err := GetGithubEmail(client) + // Get the user from the github provider + user, err := GetGithubUser(client) // Check if there was an error if err != nil { - return "", err + return user, err } - log.Debug().Msg("Got email from github") + log.Debug().Msg("Got user from github") - // Return the email - return email, nil + // Return the user + return user, nil case "google": // If the google provider is not configured, return an error if providers.Google == nil { log.Debug().Msg("Google provider not configured") - return "", nil + return user, nil } // Get the client from the google provider @@ -132,23 +136,23 @@ func (providers *Providers) GetUser(provider string) (string, error) { log.Debug().Msg("Got client from google") - // Get the email from the google provider - email, err := GetGoogleEmail(client) + // Get the user from the google provider + user, err := GetGoogleUser(client) // Check if there was an error if err != nil { - return "", err + return user, err } - log.Debug().Msg("Got email from google") + log.Debug().Msg("Got user from google") - // Return the email - return email, nil + // Return the user + return user, nil case "generic": // If the generic provider is not configured, return an error if providers.Generic == nil { log.Debug().Msg("Generic provider not configured") - return "", nil + return user, nil } // Get the client from the generic provider @@ -156,20 +160,20 @@ func (providers *Providers) GetUser(provider string) (string, error) { log.Debug().Msg("Got client from generic") - // Get the email from the generic provider - email, err := GetGenericEmail(client, providers.Config.GenericUserURL) + // Get the user from the generic provider + user, err := GetGenericUser(client, providers.Config.GenericUserURL) // Check if there was an error if err != nil { - return "", err + return user, err } - log.Debug().Msg("Got email from generic") + log.Debug().Msg("Got user from generic") // Return the email - return email, nil + return user, nil default: - return "", nil + return user, nil } } diff --git a/internal/types/api.go b/internal/types/api.go index 6798956..8e5d73a 100644 --- a/internal/types/api.go +++ b/internal/types/api.go @@ -20,6 +20,7 @@ type OAuthRequest struct { type UnauthorizedQuery struct { Username string `url:"username"` Resource string `url:"resource"` + GroupErr bool `url:"groupErr"` } // Proxy is the uri parameters for the proxy endpoint @@ -33,6 +34,8 @@ type UserContextResponse struct { Message string `json:"message"` IsLoggedIn bool `json:"isLoggedIn"` Username string `json:"username"` + Name string `json:"name"` + Email string `json:"email"` Provider string `json:"provider"` Oauth bool `json:"oauth"` TotpPending bool `json:"totpPending"` @@ -49,6 +52,7 @@ type AppContext struct { Domain string `json:"domain"` ForgotPasswordMessage string `json:"forgotPasswordMessage"` BackgroundImage string `json:"backgroundImage"` + OAuthAutoRedirect string `json:"oauthAutoRedirect"` } // Totp request is the request for the totp endpoint diff --git a/internal/types/config.go b/internal/types/config.go index ad7e327..fdf7bf8 100644 --- a/internal/types/config.go +++ b/internal/types/config.go @@ -26,6 +26,7 @@ type Config struct { GenericName string `mapstructure:"generic-name"` DisableContinue bool `mapstructure:"disable-continue"` OAuthWhitelist string `mapstructure:"oauth-whitelist"` + OAuthAutoRedirect string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` SessionExpiry int `mapstructure:"session-expiry"` LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"` Title string `mapstructure:"app-title"` @@ -46,6 +47,7 @@ type HandlersConfig struct { Title string ForgotPasswordMessage string BackgroundImage string + OAuthAutoRedirect string } // OAuthConfig is the configuration for the providers @@ -80,3 +82,8 @@ type AuthConfig struct { LoginTimeout int LoginMaxRetries int } + +// HooksConfig is the configuration for the hooks service +type HooksConfig struct { + Domain string +} diff --git a/internal/types/types.go b/internal/types/types.go index dc6f9c1..d51e0d0 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -25,8 +25,11 @@ type OAuthProviders struct { // SessionCookie is the cookie for the session (exculding the expiry) type SessionCookie struct { Username string + Name string + Email string Provider string TotpPending bool + OAuthGroups string } // TinyauthLabels is the labels for the tinyauth container @@ -35,15 +38,20 @@ type TinyauthLabels struct { Users string Allowed string Headers map[string]string + OAuthGroups string } // UserContext is the context for the user type UserContext struct { Username string + Name string + Email string IsLoggedIn bool OAuth bool Provider string TotpPending bool + OAuthGroups string + TotpEnabled bool } // LoginAttempt tracks information about login attempts for rate limiting diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 2583015..e49075f 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -204,6 +204,8 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels { } tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1] } + case "tinyauth.oauth.groups": + tinyauthLabels.OAuthGroups = value } } } @@ -323,3 +325,22 @@ func CheckWhitelist(whitelist string, str string) bool { // Return false if no match was found return false } + +// Capitalize just the first letter of a string +func Capitalize(str string) string { + if len(str) == 0 { + return "" + } + return strings.ToUpper(string([]rune(str)[0])) + string([]rune(str)[1:]) +} + +// Sanitize header removes all control characters from a string +func SanitizeHeader(header string) string { + return strings.Map(func(r rune) rune { + // Allow only printable ASCII characters (32-126) and safe whitespace (space, tab) + if r == ' ' || r == '\t' || (r >= 32 && r <= 126) { + return r + } + return -1 + }, header) +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index 041da98..42ae900 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -467,3 +467,65 @@ func TestCheckWhitelist(t *testing.T) { t.Fatalf("Expected %v, got %v", expected, result) } } + +// Test capitalize +func TestCapitalize(t *testing.T) { + t.Log("Testing capitalize with a valid string") + + // Create variables + str := "test" + expected := "Test" + + // Test the capitalize function + result := utils.Capitalize(str) + + // Check if the result is equal to the expected + if result != expected { + t.Fatalf("Expected %v, got %v", expected, result) + } + + t.Log("Testing capitalize with an empty string") + + // Create variables + str = "" + expected = "" + + // Test the capitalize function + result = utils.Capitalize(str) + + // Check if the result is equal to the expected + if result != expected { + t.Fatalf("Expected %v, got %v", expected, result) + } +} + +// Test the header sanitizer +func TestSanitizeHeader(t *testing.T) { + t.Log("Testing sanitize header with a valid string") + + // Create variables + str := "X-Header=value" + expected := "X-Header=value" + + // Test the sanitize header function + result := utils.SanitizeHeader(str) + + // Check if the result is equal to the expected + if result != expected { + t.Fatalf("Expected %v, got %v", expected, result) + } + + t.Log("Testing sanitize header with an invalid string") + + // Create variables + str = "X-Header=val\nue" + expected = "X-Header=value" + + // Test the sanitize header function + result = utils.SanitizeHeader(str) + + // Check if the result is equal to the expected + if result != expected { + t.Fatalf("Expected %v, got %v", expected, result) + } +}