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: '
'
+
+ - 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)
+ }
+}