mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 06:05:43 +00:00 
			
		
		
		
	Merge branch 'main' into feat/new-ui
This commit is contained in:
		
							
								
								
									
										10
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -2,12 +2,22 @@ version: 2 | |||||||
| updates: | updates: | ||||||
|   - package-ecosystem: "bun" |   - package-ecosystem: "bun" | ||||||
|     directory: "/frontend" |     directory: "/frontend" | ||||||
|  |     minor-patch: | ||||||
|  |       update-types: | ||||||
|  |         - "patch" | ||||||
|  |         - "minor" | ||||||
|     schedule: |     schedule: | ||||||
|       interval: "daily" |       interval: "daily" | ||||||
|  |  | ||||||
|   - package-ecosystem: "gomod" |   - package-ecosystem: "gomod" | ||||||
|     directory: "/" |     directory: "/" | ||||||
|  |     minor-patch: | ||||||
|  |       update-types: | ||||||
|  |         - "patch" | ||||||
|  |         - "minor" | ||||||
|     schedule: |     schedule: | ||||||
|       interval: "daily" |       interval: "daily" | ||||||
|  |  | ||||||
|   - package-ecosystem: "docker" |   - package-ecosystem: "docker" | ||||||
|     directory: "/" |     directory: "/" | ||||||
|     schedule: |     schedule: | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								.github/workflows/sponsors.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								.github/workflows/sponsors.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -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: '<a href="https://github.com/{{{ login }}}"><img src="{{{ avatarUrl }}}" width="64px" alt="User avatar: {{{ login }}}" /></a>  ' | ||||||
|  |  | ||||||
|  |       - 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 <noreply@github.com> | ||||||
|  |           author: GitHub <noreply@github.com> | ||||||
|  |           branch: docs/update-readme | ||||||
|  |           title: | | ||||||
|  |             docs: regenerate readme sponsors list | ||||||
|  |           labels: bot | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| # Site builder | # Site builder | ||||||
| FROM oven/bun:1.2.10-alpine AS frontend-builder | FROM oven/bun:1.2.12-alpine AS frontend-builder | ||||||
|  |  | ||||||
| WORKDIR /frontend | WORKDIR /frontend | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -92,6 +92,7 @@ var rootCmd = &cobra.Command{ | |||||||
| 			Domain:                domain, | 			Domain:                domain, | ||||||
| 			ForgotPasswordMessage: config.FogotPasswordMessage, | 			ForgotPasswordMessage: config.FogotPasswordMessage, | ||||||
| 			BackgroundImage:       config.BackgroundImage, | 			BackgroundImage:       config.BackgroundImage, | ||||||
|  | 			OAuthAutoRedirect:     config.OAuthAutoRedirect, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create api config | 		// Create api config | ||||||
| @@ -112,6 +113,11 @@ var rootCmd = &cobra.Command{ | |||||||
| 			LoginMaxRetries: config.LoginMaxRetries, | 			LoginMaxRetries: config.LoginMaxRetries, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// Create hooks config | ||||||
|  | 		hooksConfig := types.HooksConfig{ | ||||||
|  | 			Domain: domain, | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// Create docker service | 		// Create docker service | ||||||
| 		docker := docker.NewDocker() | 		docker := docker.NewDocker() | ||||||
|  |  | ||||||
| @@ -129,7 +135,7 @@ var rootCmd = &cobra.Command{ | |||||||
| 		providers.Init() | 		providers.Init() | ||||||
|  |  | ||||||
| 		// Create hooks service | 		// Create hooks service | ||||||
| 		hooks := hooks.NewHooks(auth, providers) | 		hooks := hooks.NewHooks(hooksConfig, auth, providers) | ||||||
|  |  | ||||||
| 		// Create handlers | 		// Create handlers | ||||||
| 		handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) | 		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-auth-url", "", "Generic OAuth auth URL.") | ||||||
| 	rootCmd.Flags().String("generic-token-url", "", "Generic OAuth token 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-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().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-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("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-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).") | 	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("generic-name", "GENERIC_NAME") | ||||||
| 	viper.BindEnv("disable-continue", "DISABLE_CONTINUE") | 	viper.BindEnv("disable-continue", "DISABLE_CONTINUE") | ||||||
| 	viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST") | 	viper.BindEnv("oauth-whitelist", "OAUTH_WHITELIST") | ||||||
|  | 	viper.BindEnv("oauth-auto-redirect", "OAUTH_AUTO_REDIRECT") | ||||||
| 	viper.BindEnv("session-expiry", "SESSION_EXPIRY") | 	viper.BindEnv("session-expiry", "SESSION_EXPIRY") | ||||||
| 	viper.BindEnv("log-level", "LOG_LEVEL") | 	viper.BindEnv("log-level", "LOG_LEVEL") | ||||||
| 	viper.BindEnv("app-title", "APP_TITLE") | 	viper.BindEnv("app-title", "APP_TITLE") | ||||||
|   | |||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "أدخل رمز TOTP الخاص بك", |     "totpTitle": "أدخل رمز TOTP الخاص بك", | ||||||
|     "unauthorizedTitle": "غير مرخص", |     "unauthorizedTitle": "غير مرخص", | ||||||
|     "unauthorizedResourceSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بالوصول إلى المورد <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بالوصول إلى المورد <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "المستخدم الذي يحمل اسم المستخدم <Code>{{username}}</Code> غير مصرح له بتسجيل الدخول.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "حاول مجددا", |     "unauthorizedButton": "حاول مجددا", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "إلغاء" |     "cancelTitle": "إلغاء", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Geben Sie Ihren TOTP Code ein", |     "totpTitle": "Geben Sie Ihren TOTP Code ein", | ||||||
|     "unauthorizedTitle": "Unautorisiert", |     "unauthorizedTitle": "Unautorisiert", | ||||||
|     "unauthorizedResourceSubtitle": "Der Benutzer mit Benutzername <Code>{{username}}</Code> ist nicht berechtigt auf die Ressource <Code>{{resource}}</Code> zuzugreifen.", |     "unauthorizedResourceSubtitle": "Der Benutzer mit Benutzername <Code>{{username}}</Code> ist nicht berechtigt auf die Ressource <Code>{{resource}}</Code> zuzugreifen.", | ||||||
|     "unaothorizedLoginSubtitle": "Der Benutzer mit dem Benutzernamen <Code>{{username}}</Code> ist nicht berechtigt, sich einzuloggen.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Erneut versuchen", |     "unauthorizedButton": "Erneut versuchen", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Nicht vertrauenswürdige Weiterleitung", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "Sie versuchen auf eine Domain umzuleiten, die nicht mit Ihrer konfigurierten Domain übereinstimmt (<Code>{{domain}}</Code>). Sind Sie sicher, dass Sie fortfahren möchten?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Abbrechen", | ||||||
|  |     "forgotPasswordTitle": "Passwort vergessen?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Εισάγετε τον κωδικό TOTP", |     "totpTitle": "Εισάγετε τον κωδικό TOTP", | ||||||
|     "unauthorizedTitle": "Μη εξουσιοδοτημένο", |     "unauthorizedTitle": "Μη εξουσιοδοτημένο", | ||||||
|     "unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν έχει άδεια πρόσβασης στον πόρο <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν έχει άδεια πρόσβασης στον πόρο <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι εξουσιοδοτημένος να συνδεθεί.", |     "unauthorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι εξουσιοδοτημένος να συνδεθεί.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}</Code> δεν είναι στις ομάδες που απαιτούνται από τον πόρο <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Προσπαθήστε ξανά", |     "unauthorizedButton": "Προσπαθήστε ξανά", | ||||||
|     "untrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση", |     "untrustedRedirectTitle": "Μη έμπιστη ανακατεύθυνση", | ||||||
|     "untrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε έναν τομέα που δεν ταιριάζει με τον ρυθμισμένο τομέα σας (<Code>{{domain}}</Code>). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;", |     "untrustedRedirectSubtitle": "Προσπαθείτε να ανακατευθύνετε σε έναν τομέα που δεν ταιριάζει με τον ρυθμισμένο τομέα σας (<Code>{{domain}}</Code>). Είστε βέβαιοι ότι θέλετε να συνεχίσετε;", | ||||||
|     "cancelTitle": "Ακύρωση" |     "cancelTitle": "Ακύρωση", | ||||||
|  |     "forgotPasswordTitle": "Ξεχάσατε το συνθηματικό σας;" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Saisissez votre code TOTP", |     "totpTitle": "Saisissez votre code TOTP", | ||||||
|     "unauthorizedTitle": "Non autorisé", |     "unauthorizedTitle": "Non autorisé", | ||||||
|     "unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}</Code> n'est pas autorisé à se connecter.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Réessayer", |     "unauthorizedButton": "Réessayer", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Voer je TOTP-code in", |     "totpTitle": "Voer je TOTP-code in", | ||||||
|     "unauthorizedTitle": "Ongeautoriseerd", |     "unauthorizedTitle": "Ongeautoriseerd", | ||||||
|     "unauthorizedResourceSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> heeft geen toegang tot de bron <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> heeft geen toegang tot de bron <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "De gebruiker met gebruikersnaam <Code>{{username}}</Code> is niet gemachtigd om in te loggen.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Opnieuw proberen", |     "unauthorizedButton": "Opnieuw proberen", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Wprowadź kod TOTP", |     "totpTitle": "Wprowadź kod TOTP", | ||||||
|     "unauthorizedTitle": "Nieautoryzowany", |     "unauthorizedTitle": "Nieautoryzowany", | ||||||
|     "unauthorizedResourceSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "Użytkownik o nazwie <Code>{{username}}</Code> nie jest upoważniony do logowania.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Spróbuj ponownie", |     "unauthorizedButton": "Spróbuj ponownie", | ||||||
|     "untrustedRedirectTitle": "Niezaufane przekierowanie", |     "untrustedRedirectTitle": "Niezaufane przekierowanie", | ||||||
|     "untrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do skonfigurowanej przez Ciebie domeny (<Code>{{domain}}</Code>). Czy na pewno chcesz kontynuować?", |     "untrustedRedirectSubtitle": "Próbujesz przekierować do domeny, która nie pasuje do skonfigurowanej przez Ciebie domeny (<Code>{{domain}}</Code>). Czy na pewno chcesz kontynuować?", | ||||||
|     "cancelTitle": "Anuluj" |     "cancelTitle": "Anuluj", | ||||||
|  |     "forgotPasswordTitle": "Nie pamiętasz hasła?" | ||||||
| } | } | ||||||
| @@ -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", |     "loginDivider": "Ou continuar com uma senha", | ||||||
|     "loginUsername": "Nome de usuário", |     "loginUsername": "Nome de usuário", | ||||||
|     "loginPassword": "Senha", |     "loginPassword": "Senha", | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Insira o seu código TOTP", |     "totpTitle": "Insira o seu código TOTP", | ||||||
|     "unauthorizedTitle": "Não autorizado", |     "unauthorizedTitle": "Não autorizado", | ||||||
|     "unauthorizedResourceSubtitle": "O usuário com nome de usuário <Code>{{username}}</Code> não está autorizado a acessar o recurso <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "O usuário com nome de usuário <Code>{{username}}</Code> não está autorizado a acessar o recurso <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "O usuário com o nome <Code>{{username}}</Code> não está autorizado a acessar.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Tentar novamente", |     "unauthorizedButton": "Tentar novamente", | ||||||
|     "untrustedRedirectTitle": "Redirecionamento não confiável", |     "untrustedRedirectTitle": "Redirecionamento não confiável", | ||||||
|     "untrustedRedirectSubtitle": "Você está tentando redirecionar para um domínio que não corresponde ao seu domínio configurado (<Code>{{domain}}</Code>). Tem certeza que deseja continuar?", |     "untrustedRedirectSubtitle": "Você está tentando redirecionar para um domínio que não corresponde ao seu domínio configurado (<Code>{{domain}}</Code>). Tem certeza que deseja continuar?", | ||||||
|     "cancelTitle": "Cancelar" |     "cancelTitle": "Cancelar", | ||||||
|  |     "forgotPasswordTitle": "Esqueceu sua senha?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "İptal" |     "cancelTitle": "İptal", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "输入您的 TOTP 代码", |     "totpTitle": "输入您的 TOTP 代码", | ||||||
|     "unauthorizedTitle": "未授权", |     "unauthorizedTitle": "未授权", | ||||||
|     "unauthorizedResourceSubtitle": "用户 <Code>{{username}}</Code> 无权访问资源 <Code>{{resource}}</Code>。", |     "unauthorizedResourceSubtitle": "用户 <Code>{{username}}</Code> 无权访问资源 <Code>{{resource}}</Code>。", | ||||||
|     "unaothorizedLoginSubtitle": "用户名 <Code>{{username}}</Code> 无登录权限。", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "重试", |     "unauthorizedButton": "重试", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -41,9 +41,11 @@ | |||||||
|     "totpTitle": "Enter your TOTP code", |     "totpTitle": "Enter your TOTP code", | ||||||
|     "unauthorizedTitle": "Unauthorized", |     "unauthorizedTitle": "Unauthorized", | ||||||
|     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", |     "unauthorizedResourceSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to access the resource <Code>{{resource}}</Code>.", | ||||||
|     "unaothorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", |     "unauthorizedLoginSubtitle": "The user with username <Code>{{username}}</Code> is not authorized to login.", | ||||||
|  |     "unauthorizedGroupsSubtitle": "The user with username <Code>{{username}}</Code> is not in the groups required by the resource <Code>{{resource}}</Code>.", | ||||||
|     "unauthorizedButton": "Try again", |     "unauthorizedButton": "Try again", | ||||||
|     "untrustedRedirectTitle": "Untrusted redirect", |     "untrustedRedirectTitle": "Untrusted redirect", | ||||||
|     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", |     "untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<Code>{{domain}}</Code>). Are you sure you want to continue?", | ||||||
|     "cancelTitle": "Cancel" |     "cancelTitle": "Cancel", | ||||||
|  |     "forgotPasswordTitle": "Forgot your password?" | ||||||
| } | } | ||||||
| @@ -94,6 +94,14 @@ export const LoginPage = () => { | |||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (isMounted()) { | ||||||
|  |       if (oauthConfigured && configuredProviders.includes(oauthAutoRedirect)) { | ||||||
|  |         oauthMutation.mutate(oauthAutoRedirect); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Card className="min-w-xs sm:min-w-sm"> |     <Card className="min-w-xs sm:min-w-sm"> | ||||||
|       <CardHeader> |       <CardHeader> | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ export const appContextSchema = z.object({ | |||||||
|     genericName: z.string(), |     genericName: z.string(), | ||||||
|     domain: z.string(), |     domain: z.string(), | ||||||
|     forgotPasswordMessage: z.string(), |     forgotPasswordMessage: z.string(), | ||||||
|     oauthAutoRedirect: z.string(), |     oauthAutoRedirect: z.enum(["none", "github", "google", "generic"]), | ||||||
|     backgroundImage: z.string(), |     backgroundImage: z.string(), | ||||||
| }) | }) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								go.mod
									
									
									
									
									
								
							| @@ -10,7 +10,7 @@ require ( | |||||||
| 	github.com/rs/zerolog v1.34.0 | 	github.com/rs/zerolog v1.34.0 | ||||||
| 	github.com/spf13/cobra v1.9.1 | 	github.com/spf13/cobra v1.9.1 | ||||||
| 	github.com/spf13/viper v1.20.1 | 	github.com/spf13/viper v1.20.1 | ||||||
| 	golang.org/x/crypto v0.37.0 | 	golang.org/x/crypto v0.38.0 | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| @@ -25,7 +25,7 @@ require ( | |||||||
| 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect | 	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/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect | ||||||
| 	go.opentelemetry.io/otel/sdk 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 | 	gotest.tools/v3 v3.5.2 // indirect | ||||||
| 	rsc.io/qr v0.2.0 // indirect | 	rsc.io/qr v0.2.0 // indirect | ||||||
| ) | ) | ||||||
| @@ -103,10 +103,10 @@ require ( | |||||||
| 	go.uber.org/multierr v1.9.0 // indirect | 	go.uber.org/multierr v1.9.0 // indirect | ||||||
| 	golang.org/x/arch v0.13.0 // indirect | 	golang.org/x/arch v0.13.0 // indirect | ||||||
| 	golang.org/x/net v0.38.0 // indirect | 	golang.org/x/net v0.38.0 // indirect | ||||||
| 	golang.org/x/oauth2 v0.29.0 | 	golang.org/x/oauth2 v0.30.0 | ||||||
| 	golang.org/x/sync v0.13.0 // indirect | 	golang.org/x/sync v0.14.0 // indirect | ||||||
| 	golang.org/x/sys v0.32.0 // indirect | 	golang.org/x/sys v0.33.0 // indirect | ||||||
| 	golang.org/x/text v0.24.0 // indirect | 	golang.org/x/text v0.25.0 // indirect | ||||||
| 	google.golang.org/protobuf v1.36.3 // indirect | 	google.golang.org/protobuf v1.36.3 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | 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.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= | ||||||
| golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= | 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 h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= | ||||||
| golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= | 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= | 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.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 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= | ||||||
| golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= | 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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= | ||||||
| golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= | 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-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-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.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.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= | ||||||
| golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | 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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | 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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.12.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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= | ||||||
| golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | ||||||
| golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= | ||||||
| golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= | 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.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | 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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= | ||||||
| golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= | 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 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= | ||||||
| golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | 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= | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
|   | |||||||
| @@ -45,6 +45,11 @@ var authConfig = types.AuthConfig{ | |||||||
| 	LoginMaxRetries: 0, | 	LoginMaxRetries: 0, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Simple hooks config for tests | ||||||
|  | var hooksConfig = types.HooksConfig{ | ||||||
|  | 	Domain: "localhost", | ||||||
|  | } | ||||||
|  |  | ||||||
| // Cookie | // Cookie | ||||||
| var cookie string | var cookie string | ||||||
|  |  | ||||||
| @@ -83,7 +88,7 @@ func getAPI(t *testing.T) *api.API { | |||||||
| 	providers.Init() | 	providers.Init() | ||||||
|  |  | ||||||
| 	// Create hooks service | 	// Create hooks service | ||||||
| 	hooks := hooks.NewHooks(auth, providers) | 	hooks := hooks.NewHooks(hooksConfig, auth, providers) | ||||||
|  |  | ||||||
| 	// Create handlers service | 	// Create handlers service | ||||||
| 	handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) | 	handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) | ||||||
|   | |||||||
| @@ -160,9 +160,12 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) | |||||||
|  |  | ||||||
| 	// Set data | 	// Set data | ||||||
| 	session.Values["username"] = data.Username | 	session.Values["username"] = data.Username | ||||||
|  | 	session.Values["name"] = data.Name | ||||||
|  | 	session.Values["email"] = data.Email | ||||||
| 	session.Values["provider"] = data.Provider | 	session.Values["provider"] = data.Provider | ||||||
| 	session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix() | 	session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix() | ||||||
| 	session.Values["totpPending"] = data.TotpPending | 	session.Values["totpPending"] = data.TotpPending | ||||||
|  | 	session.Values["oauthGroups"] = data.OAuthGroups | ||||||
|  |  | ||||||
| 	// Save session | 	// Save session | ||||||
| 	err = session.Save(c.Request, c.Writer) | 	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 | 		return types.SessionCookie{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	log.Debug().Msg("Got session") | ||||||
|  |  | ||||||
| 	// Get data from session | 	// Get data from session | ||||||
| 	username, usernameOk := session.Values["username"].(string) | 	username, usernameOk := session.Values["username"].(string) | ||||||
|  | 	email, emailOk := session.Values["email"].(string) | ||||||
|  | 	name, nameOk := session.Values["name"].(string) | ||||||
| 	provider, providerOK := session.Values["provider"].(string) | 	provider, providerOK := session.Values["provider"].(string) | ||||||
| 	expiry, expiryOk := session.Values["expiry"].(int64) | 	expiry, expiryOk := session.Values["expiry"].(int64) | ||||||
| 	totpPending, totpPendingOk := session.Values["totpPending"].(bool) | 	totpPending, totpPendingOk := session.Values["totpPending"].(bool) | ||||||
|  | 	oauthGroups, oauthGroupsOk := session.Values["oauthGroups"].(string) | ||||||
|  |  | ||||||
| 	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk { | 	if !usernameOk || !providerOK || !expiryOk || !totpPendingOk || !emailOk || !nameOk || !oauthGroupsOk { | ||||||
| 		log.Warn().Msg("Session cookie is missing data") | 		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 | 		return types.SessionCookie{}, nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -233,13 +246,16 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) | |||||||
| 		return types.SessionCookie{}, nil | 		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 the cookie | ||||||
| 	return types.SessionCookie{ | 	return types.SessionCookie{ | ||||||
| 		Username:    username, | 		Username:    username, | ||||||
|  | 		Name:        name, | ||||||
|  | 		Email:       email, | ||||||
| 		Provider:    provider, | 		Provider:    provider, | ||||||
| 		TotpPending: totpPending, | 		TotpPending: totpPending, | ||||||
|  | 		OAuthGroups: oauthGroups, | ||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -248,48 +264,52 @@ func (auth *Auth) UserAuthConfigured() bool { | |||||||
| 	return len(auth.Config.Users) > 0 | 	return len(auth.Config.Users) > 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bool, error) { | func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext, labels types.TinyauthLabels) bool { | ||||||
| 	// 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 |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Check if oauth is allowed | 	// Check if oauth is allowed | ||||||
| 	if context.OAuth { | 	if context.OAuth { | ||||||
| 		log.Debug().Msg("Checking OAuth whitelist") | 		log.Debug().Msg("Checking OAuth whitelist") | ||||||
| 		return utils.CheckWhitelist(labels.OAuthWhitelist, context.Username), nil | 		return utils.CheckWhitelist(labels.OAuthWhitelist, context.Email) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check users | 	// Check users | ||||||
| 	log.Debug().Msg("Checking 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 | 	// Get headers | ||||||
| 	uri := c.Request.Header.Get("X-Forwarded-Uri") | 	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 | 	// Check if the allowed label is empty | ||||||
| 	if labels.Allowed == "" { | 	if labels.Allowed == "" { | ||||||
|   | |||||||
| @@ -6,4 +6,13 @@ var TinyauthLabels = []string{ | |||||||
| 	"tinyauth.users", | 	"tinyauth.users", | ||||||
| 	"tinyauth.allowed", | 	"tinyauth.allowed", | ||||||
| 	"tinyauth.headers", | 	"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"` | ||||||
| } | } | ||||||
|   | |||||||
| @@ -6,8 +6,7 @@ import ( | |||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
| 	"tinyauth/internal/utils" | 	"tinyauth/internal/utils" | ||||||
|  |  | ||||||
| 	apiTypes "github.com/docker/docker/api/types" | 	container "github.com/docker/docker/api/types/container" | ||||||
| 	containerTypes "github.com/docker/docker/api/types/container" |  | ||||||
| 	"github.com/docker/docker/client" | 	"github.com/docker/docker/client" | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| ) | ) | ||||||
| @@ -30,17 +29,22 @@ func (docker *Docker) Init() error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Set the context and api client | 	// Create the context | ||||||
| 	docker.Context = context.Background() | 	docker.Context = context.Background() | ||||||
|  |  | ||||||
|  | 	// Negotiate API version | ||||||
|  | 	client.NegotiateAPIVersion(docker.Context) | ||||||
|  |  | ||||||
|  | 	// Set client | ||||||
| 	docker.Client = client | 	docker.Client = client | ||||||
|  |  | ||||||
| 	// Done | 	// Done | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (docker *Docker) GetContainers() ([]apiTypes.Container, error) { | func (docker *Docker) GetContainers() ([]container.Summary, error) { | ||||||
| 	// Get the list of containers | 	// 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 | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -51,13 +55,13 @@ func (docker *Docker) GetContainers() ([]apiTypes.Container, error) { | |||||||
| 	return containers, nil | 	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 the container | ||||||
| 	inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) | 	inspect, err := docker.Client.ContainerInspect(docker.Context, containerId) | ||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return apiTypes.ContainerJSON{}, err | 		return container.InspectResponse{}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return the inspect | 	// Return the inspect | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import ( | |||||||
| 	"tinyauth/internal/hooks" | 	"tinyauth/internal/hooks" | ||||||
| 	"tinyauth/internal/providers" | 	"tinyauth/internal/providers" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
|  | 	"tinyauth/internal/utils" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/google/go-querystring/query" | 	"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") | 	proto := c.Request.Header.Get("X-Forwarded-Proto") | ||||||
| 	host := c.Request.Header.Get("X-Forwarded-Host") | 	host := c.Request.Header.Get("X-Forwarded-Host") | ||||||
|  |  | ||||||
| 	// Check if auth is enabled | 	// Get the app id | ||||||
| 	authEnabled, err := h.Auth.AuthEnabled(c) | 	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 | 	// Check if there was an error | ||||||
| 	if err != nil { | 	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 { | 		if proxy.Proxy == "nginx" || !isBrowser { | ||||||
| 			c.JSON(500, gin.H{ | 			c.JSON(500, gin.H{ | ||||||
| @@ -87,11 +93,8 @@ func (h *Handlers) AuthHandler(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get the app id | 	// Check if auth is enabled | ||||||
| 	appId := strings.Split(host, ".")[0] | 	authEnabled, err := h.Auth.AuthEnabled(c, labels) | ||||||
|  |  | ||||||
| 	// Get the container labels |  | ||||||
| 	labels, err := h.Docker.GetLabels(appId) |  | ||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @@ -113,7 +116,7 @@ func (h *Handlers) AuthHandler(c *gin.Context) { | |||||||
| 	if !authEnabled { | 	if !authEnabled { | ||||||
| 		for key, value := range labels.Headers { | 		for key, value := range labels.Headers { | ||||||
| 			log.Debug().Str("key", key).Str("value", value).Msg("Setting header") | 			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{ | 		c.JSON(200, gin.H{ | ||||||
| 			"status":  200, | 			"status":  200, | ||||||
| @@ -125,28 +128,18 @@ func (h *Handlers) AuthHandler(c *gin.Context) { | |||||||
| 	// Get user context | 	// Get user context | ||||||
| 	userContext := h.Hooks.UseUserContext(c) | 	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 | 	// Check if user is logged in | ||||||
| 	if userContext.IsLoggedIn { | 	if userContext.IsLoggedIn { | ||||||
| 		log.Debug().Msg("Authenticated") | 		log.Debug().Msg("Authenticated") | ||||||
|  |  | ||||||
| 		// Check if user is allowed to access subdomain, if request is nginx.example.com the subdomain (resource) is nginx | 		// 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) | 		appAllowed := h.Auth.ResourceAllowed(c, userContext, labels) | ||||||
|  |  | ||||||
| 		// 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 |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed") | 		log.Debug().Bool("appAllowed", appAllowed).Msg("Checking if app is allowed") | ||||||
|  |  | ||||||
| @@ -154,9 +147,6 @@ func (h *Handlers) AuthHandler(c *gin.Context) { | |||||||
| 		if !appAllowed { | 		if !appAllowed { | ||||||
| 			log.Warn().Str("username", userContext.Username).Str("host", host).Msg("User not allowed") | 			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 { | 			if proxy.Proxy == "nginx" || !isBrowser { | ||||||
| 				c.JSON(401, gin.H{ | 				c.JSON(401, gin.H{ | ||||||
| 					"status":  401, | 					"status":  401, | ||||||
| @@ -165,11 +155,20 @@ func (h *Handlers) AuthHandler(c *gin.Context) { | |||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			// Build query | 			// Values | ||||||
| 			queries, err := query.Values(types.UnauthorizedQuery{ | 			values := types.UnauthorizedQuery{ | ||||||
| 				Username: userContext.Username, |  | ||||||
| 				Resource: strings.Split(host, ".")[0], | 				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) | 			// Handle error (no need to check for nginx/headers since we are sure we are using caddy/traefik) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @@ -183,13 +182,63 @@ func (h *Handlers) AuthHandler(c *gin.Context) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Set the user header | 		// Check groups if using OAuth | ||||||
| 		c.Header("Remote-User", userContext.Username) | 		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 | 		// Set the rest of the headers | ||||||
| 		for key, value := range labels.Headers { | 		for key, value := range labels.Headers { | ||||||
| 			log.Debug().Str("key", key).Str("value", value).Msg("Setting header") | 			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 | 		// 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 | 	// The user is not logged in | ||||||
| 	log.Debug().Msg("Unauthorized") | 	log.Debug().Msg("Unauthorized") | ||||||
|  |  | ||||||
| 	// Set www-authenticate header |  | ||||||
| 	c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") |  | ||||||
|  |  | ||||||
| 	if proxy.Proxy == "nginx" || !isBrowser { | 	if proxy.Proxy == "nginx" || !isBrowser { | ||||||
| 		c.JSON(401, gin.H{ | 		c.JSON(401, gin.H{ | ||||||
| 			"status":  401, | 			"status":  401, | ||||||
| @@ -310,6 +356,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) { | |||||||
| 		// Set totp pending cookie | 		// Set totp pending cookie | ||||||
| 		h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | 		h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||||
| 			Username:    login.Username, | 			Username:    login.Username, | ||||||
|  | 			Name:        utils.Capitalize(login.Username), | ||||||
|  | 			Email:       fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain), | ||||||
| 			Provider:    "username", | 			Provider:    "username", | ||||||
| 			TotpPending: true, | 			TotpPending: true, | ||||||
| 		}) | 		}) | ||||||
| @@ -328,6 +376,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) { | |||||||
| 	// Create session cookie with username as provider | 	// Create session cookie with username as provider | ||||||
| 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||||
| 		Username: login.Username, | 		Username: login.Username, | ||||||
|  | 		Name:     utils.Capitalize(login.Username), | ||||||
|  | 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(login.Username), h.Config.Domain), | ||||||
| 		Provider: "username", | 		Provider: "username", | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| @@ -402,6 +452,8 @@ func (h *Handlers) TotpHandler(c *gin.Context) { | |||||||
| 	// Create session cookie with username as provider | 	// Create session cookie with username as provider | ||||||
| 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||||
| 		Username: user.Username, | 		Username: user.Username, | ||||||
|  | 		Name:     utils.Capitalize(user.Username), | ||||||
|  | 		Email:    fmt.Sprintf("%s@%s", strings.ToLower(user.Username), h.Config.Domain), | ||||||
| 		Provider: "username", | 		Provider: "username", | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| @@ -449,6 +501,7 @@ func (h *Handlers) AppHandler(c *gin.Context) { | |||||||
| 		Domain:                h.Config.Domain, | 		Domain:                h.Config.Domain, | ||||||
| 		ForgotPasswordMessage: h.Config.ForgotPasswordMessage, | 		ForgotPasswordMessage: h.Config.ForgotPasswordMessage, | ||||||
| 		BackgroundImage:       h.Config.BackgroundImage, | 		BackgroundImage:       h.Config.BackgroundImage, | ||||||
|  | 		OAuthAutoRedirect:     h.Config.OAuthAutoRedirect, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return app context | 	// Return app context | ||||||
| @@ -466,15 +519,16 @@ func (h *Handlers) UserHandler(c *gin.Context) { | |||||||
| 		Status:      200, | 		Status:      200, | ||||||
| 		IsLoggedIn:  userContext.IsLoggedIn, | 		IsLoggedIn:  userContext.IsLoggedIn, | ||||||
| 		Username:    userContext.Username, | 		Username:    userContext.Username, | ||||||
|  | 		Name:        userContext.Name, | ||||||
|  | 		Email:       userContext.Email, | ||||||
| 		Provider:    userContext.Provider, | 		Provider:    userContext.Provider, | ||||||
| 		Oauth:       userContext.OAuth, | 		Oauth:       userContext.OAuth, | ||||||
| 		TotpPending: userContext.TotpPending, | 		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 { | 	if !userContext.IsLoggedIn { | ||||||
| 		log.Debug().Msg("Unauthorized") | 		log.Debug().Msg("Unauthorized") | ||||||
| 		c.Header("WWW-Authenticate", "Basic realm=\"tinyauth\"") |  | ||||||
| 		userContextResponse.Message = "Unauthorized" | 		userContextResponse.Message = "Unauthorized" | ||||||
| 	} else { | 	} else { | ||||||
| 		log.Debug().Interface("userContext", userContext).Msg("Authenticated") | 		log.Debug().Interface("userContext", userContext).Msg("Authenticated") | ||||||
| @@ -614,25 +668,32 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get email | 	// Get user | ||||||
| 	email, err := h.Providers.GetUser(providerName.Provider) | 	user, err := h.Providers.GetUser(providerName.Provider) | ||||||
|  |  | ||||||
| 	log.Debug().Str("email", email).Msg("Got email") |  | ||||||
|  |  | ||||||
| 	// Handle error | 	// Handle error | ||||||
| 	if err != nil { | 	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)) | 		c.Redirect(http.StatusPermanentRedirect, fmt.Sprintf("%s/error", h.Config.AppURL)) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Email is not whitelisted | 	// Email is not whitelisted | ||||||
| 	if !h.Auth.EmailWhitelisted(email) { | 	if !h.Auth.EmailWhitelisted(user.Email) { | ||||||
| 		log.Warn().Str("email", email).Msg("Email not whitelisted") | 		log.Warn().Str("email", user.Email).Msg("Email not whitelisted") | ||||||
|  |  | ||||||
| 		// Build query | 		// Build query | ||||||
| 		queries, err := query.Values(types.UnauthorizedQuery{ | 		queries, err := query.Values(types.UnauthorizedQuery{ | ||||||
| 			Username: email, | 			Username: user.Email, | ||||||
| 		}) | 		}) | ||||||
|  |  | ||||||
| 		// Handle error | 		// Handle error | ||||||
| @@ -648,10 +709,31 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	log.Debug().Msg("Email whitelisted") | 	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) | 	// Create session cookie (also cleans up redirect cookie) | ||||||
| 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||||
| 		Username: email, | 		Username:    username, | ||||||
|  | 		Name:        name, | ||||||
|  | 		Email:       user.Email, | ||||||
| 		Provider:    providerName.Provider, | 		Provider:    providerName.Provider, | ||||||
|  | 		OAuthGroups: strings.Join(user.Groups, ","), | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	// Check if we have a redirect URI | 	// Check if we have a redirect URI | ||||||
|   | |||||||
| @@ -1,22 +1,27 @@ | |||||||
| package hooks | package hooks | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
| 	"tinyauth/internal/auth" | 	"tinyauth/internal/auth" | ||||||
| 	"tinyauth/internal/providers" | 	"tinyauth/internal/providers" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
|  | 	"tinyauth/internal/utils" | ||||||
|  |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/rs/zerolog/log" | 	"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{ | 	return &Hooks{ | ||||||
|  | 		Config:    config, | ||||||
| 		Auth:      auth, | 		Auth:      auth, | ||||||
| 		Providers: providers, | 		Providers: providers, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| type Hooks struct { | type Hooks struct { | ||||||
|  | 	Config    types.HooksConfig | ||||||
| 	Auth      *auth.Auth | 	Auth      *auth.Auth | ||||||
| 	Providers *providers.Providers | 	Providers *providers.Providers | ||||||
| } | } | ||||||
| @@ -30,17 +35,27 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 	if basic != nil { | 	if basic != nil { | ||||||
| 		log.Debug().Msg("Got basic auth") | 		log.Debug().Msg("Got basic auth") | ||||||
|  |  | ||||||
| 		// Check if user exists and password is correct | 		// Get user | ||||||
| 		user := hooks.Auth.GetUser(basic.Username) | 		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 user context since we are logged in with basic auth | ||||||
| 			return types.UserContext{ | 			return types.UserContext{ | ||||||
| 				Username:    basic.Username, | 				Username:    basic.Username, | ||||||
|  | 				Name:        utils.Capitalize(basic.Username), | ||||||
|  | 				Email:       fmt.Sprintf("%s@%s", strings.ToLower(basic.Username), hooks.Config.Domain), | ||||||
| 				IsLoggedIn:  true, | 				IsLoggedIn:  true, | ||||||
| 				OAuth:       false, |  | ||||||
| 				Provider:    "basic", | 				Provider:    "basic", | ||||||
| 				TotpPending: false, | 				TotpEnabled: user.TotpSecret != "", | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -50,13 +65,7 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error().Err(err).Msg("Failed to get session cookie") | 		log.Error().Err(err).Msg("Failed to get session cookie") | ||||||
| 		// Return empty context | 		// Return empty context | ||||||
| 		return types.UserContext{ | 		return types.UserContext{} | ||||||
| 			Username:    "", |  | ||||||
| 			IsLoggedIn:  false, |  | ||||||
| 			OAuth:       false, |  | ||||||
| 			Provider:    "", |  | ||||||
| 			TotpPending: false, |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if session cookie has totp pending | 	// 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 empty context since we are pending totp | ||||||
| 		return types.UserContext{ | 		return types.UserContext{ | ||||||
| 			Username:    cookie.Username, | 			Username:    cookie.Username, | ||||||
| 			IsLoggedIn:  false, | 			Name:        cookie.Name, | ||||||
| 			OAuth:       false, | 			Email:       cookie.Email, | ||||||
| 			Provider:    cookie.Provider, | 			Provider:    cookie.Provider, | ||||||
| 			TotpPending: true, | 			TotpPending: true, | ||||||
| 		} | 		} | ||||||
| @@ -83,10 +92,10 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 			// It exists so we are logged in | 			// It exists so we are logged in | ||||||
| 			return types.UserContext{ | 			return types.UserContext{ | ||||||
| 				Username:   cookie.Username, | 				Username:   cookie.Username, | ||||||
|  | 				Name:       cookie.Name, | ||||||
|  | 				Email:      cookie.Email, | ||||||
| 				IsLoggedIn: true, | 				IsLoggedIn: true, | ||||||
| 				OAuth:       false, |  | ||||||
| 				Provider:   "username", | 				Provider:   "username", | ||||||
| 				TotpPending: false, |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -101,20 +110,14 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
| 		log.Debug().Msg("Provider exists") | 		log.Debug().Msg("Provider exists") | ||||||
|  |  | ||||||
| 		// Check if the oauth email is whitelisted | 		// Check if the oauth email is whitelisted | ||||||
| 		if !hooks.Auth.EmailWhitelisted(cookie.Username) { | 		if !hooks.Auth.EmailWhitelisted(cookie.Email) { | ||||||
| 			log.Error().Str("email", cookie.Username).Msg("Email is not whitelisted") | 			log.Error().Str("email", cookie.Email).Msg("Email is not whitelisted") | ||||||
|  |  | ||||||
| 			// It isn't so we delete the cookie and return an empty context | 			// It isn't so we delete the cookie and return an empty context | ||||||
| 			hooks.Auth.DeleteSessionCookie(c) | 			hooks.Auth.DeleteSessionCookie(c) | ||||||
|  |  | ||||||
| 			// Return empty context | 			// Return empty context | ||||||
| 			return types.UserContext{ | 			return types.UserContext{} | ||||||
| 				Username:    "", |  | ||||||
| 				IsLoggedIn:  false, |  | ||||||
| 				OAuth:       false, |  | ||||||
| 				Provider:    "", |  | ||||||
| 				TotpPending: false, |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		log.Debug().Msg("Email is whitelisted") | 		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 user context since we are logged in with oauth | ||||||
| 		return types.UserContext{ | 		return types.UserContext{ | ||||||
| 			Username:    cookie.Username, | 			Username:    cookie.Username, | ||||||
|  | 			Name:        cookie.Name, | ||||||
|  | 			Email:       cookie.Email, | ||||||
| 			IsLoggedIn:  true, | 			IsLoggedIn:  true, | ||||||
| 			OAuth:       true, | 			OAuth:       true, | ||||||
| 			Provider:    cookie.Provider, | 			Provider:    cookie.Provider, | ||||||
| 			TotpPending: false, | 			OAuthGroups: cookie.OAuthGroups, | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Neither basic auth or oauth is set so we return an empty context | 	// Neither basic auth or oauth is set so we return an empty context | ||||||
| 	return types.UserContext{ | 	return types.UserContext{} | ||||||
| 		Username:    "", |  | ||||||
| 		IsLoggedIn:  false, |  | ||||||
| 		OAuth:       false, |  | ||||||
| 		Provider:    "", |  | ||||||
| 		TotpPending: false, |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,24 +4,25 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"tinyauth/internal/constants" | ||||||
|  |  | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // We are assuming that the generic provider will return a JSON object with an email field | func GetGenericUser(client *http.Client, url string) (constants.Claims, error) { | ||||||
| type GenericUserInfoResponse struct { | 	// Create user struct | ||||||
| 	Email string `json:"email"` | 	var user constants.Claims | ||||||
| } |  | ||||||
|  |  | ||||||
| func GetGenericEmail(client *http.Client, url string) (string, error) { |  | ||||||
| 	// Using the oauth client get the user info url | 	// Using the oauth client get the user info url | ||||||
| 	res, err := client.Get(url) | 	res, err := client.Get(url) | ||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	defer res.Body.Close() | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got response from generic provider") | 	log.Debug().Msg("Got response from generic provider") | ||||||
|  |  | ||||||
| 	// Read the body of the response | 	// 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 | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Read body from generic provider") | 	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 | 	// Unmarshal the body into the user struct | ||||||
| 	err = json.Unmarshal(body, &user) | 	err = json.Unmarshal(body, &user) | ||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Parsed user from generic provider") | 	log.Debug().Msg("Parsed user from generic provider") | ||||||
|  |  | ||||||
| 	// Return the email | 	// Return the user | ||||||
| 	return user.Email, nil | 	return user, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,51 +5,96 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"tinyauth/internal/constants" | ||||||
|  |  | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Github has a different response than the generic provider | // Response for the github email endpoint | ||||||
| type GithubUserInfoResponse []struct { | type GithubEmailResponse []struct { | ||||||
| 	Email   string `json:"email"` | 	Email   string `json:"email"` | ||||||
| 	Primary bool   `json:"primary"` | 	Primary bool   `json:"primary"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // The scopes required for the github provider | // Response for the github user endpoint | ||||||
| func GithubScopes() []string { | type GithubUserInfoResponse struct { | ||||||
| 	return []string{"user:email"} | 	Login string `json:"login"` | ||||||
|  | 	Name  string `json:"name"` | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetGithubEmail(client *http.Client) (string, error) { | // The scopes required for the github provider | ||||||
| 	// Get the user emails from github using the oauth http client | func GithubScopes() []string { | ||||||
| 	res, err := client.Get("https://api.github.com/user/emails") | 	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 | 	// Check if there was an error | ||||||
| 	if err != nil { | 	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 | 	// Read the body of the response | ||||||
| 	body, err := io.ReadAll(res.Body) | 	body, err := io.ReadAll(res.Body) | ||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	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 | 	// 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 | 	// Unmarshal the body into the user struct | ||||||
| 	err = json.Unmarshal(body, &emails) | 	err = json.Unmarshal(body, &emails) | ||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Parsed emails from github") | 	log.Debug().Msg("Parsed emails from github") | ||||||
| @@ -57,10 +102,28 @@ func GetGithubEmail(client *http.Client) (string, error) { | |||||||
| 	// Find and return the primary email | 	// Find and return the primary email | ||||||
| 	for _, email := range emails { | 	for _, email := range emails { | ||||||
| 		if email.Primary { | 		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? | 	// If no primary email was found, use the first available email | ||||||
| 	return "", errors.New("no primary email found") | 	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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,29 +4,37 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 	"tinyauth/internal/constants" | ||||||
|  |  | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Google works the same as the generic provider | // Response for the google user endpoint | ||||||
| type GoogleUserInfoResponse struct { | type GoogleUserInfoResponse struct { | ||||||
| 	Email string `json:"email"` | 	Email string `json:"email"` | ||||||
|  | 	Name  string `json:"name"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // The scopes required for the google provider | // The scopes required for the google provider | ||||||
| func GoogleScopes() []string { | 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 | 	// Get the user info from google using the oauth http client | ||||||
| 	res, err := client.Get("https://www.googleapis.com/userinfo/v2/me") | 	res, err := client.Get("https://www.googleapis.com/userinfo/v2/me") | ||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	defer res.Body.Close() | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got response from google") | 	log.Debug().Msg("Got response from google") | ||||||
|  |  | ||||||
| 	// Read the body of the response | 	// Read the body of the response | ||||||
| @@ -34,24 +42,29 @@ func GetGoogleEmail(client *http.Client) (string, error) { | |||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Read body from google") | 	log.Debug().Msg("Read body from google") | ||||||
|  |  | ||||||
| 	// Parse the body into a user struct | 	// Create a new user info struct | ||||||
| 	var user GoogleUserInfoResponse | 	var userInfo GoogleUserInfoResponse | ||||||
|  |  | ||||||
| 	// Unmarshal the body into the user struct | 	// Unmarshal the body into the user struct | ||||||
| 	err = json.Unmarshal(body, &user) | 	err = json.Unmarshal(body, &userInfo) | ||||||
|  |  | ||||||
| 	// Check if there was an error | 	// Check if there was an error | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return user, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Parsed user from google") | 	log.Debug().Msg("Parsed user from google") | ||||||
|  |  | ||||||
| 	// Return the email | 	// Map the user info to the user struct | ||||||
| 	return user.Email, nil | 	user.PreferredUsername = strings.Split(userInfo.Email, "@")[0] | ||||||
|  | 	user.Name = userInfo.Name | ||||||
|  | 	user.Email = userInfo.Email | ||||||
|  |  | ||||||
|  | 	// Return the user | ||||||
|  | 	return user, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ package providers | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"tinyauth/internal/constants" | ||||||
| 	"tinyauth/internal/oauth" | 	"tinyauth/internal/oauth" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
|  |  | ||||||
| @@ -93,14 +94,17 @@ func (providers *Providers) GetProvider(provider string) *oauth.OAuth { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (providers *Providers) GetUser(provider string) (string, error) { | func (providers *Providers) GetUser(provider string) (constants.Claims, error) { | ||||||
| 	// Get the email from the provider | 	// Create user struct | ||||||
|  | 	var user constants.Claims | ||||||
|  |  | ||||||
|  | 	// Get the user from the provider | ||||||
| 	switch provider { | 	switch provider { | ||||||
| 	case "github": | 	case "github": | ||||||
| 		// If the github provider is not configured, return an error | 		// If the github provider is not configured, return an error | ||||||
| 		if providers.Github == nil { | 		if providers.Github == nil { | ||||||
| 			log.Debug().Msg("Github provider not configured") | 			log.Debug().Msg("Github provider not configured") | ||||||
| 			return "", nil | 			return user, nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Get the client from the github provider | 		// 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") | 		log.Debug().Msg("Got client from github") | ||||||
|  |  | ||||||
| 		// Get the email from the github provider | 		// Get the user from the github provider | ||||||
| 		email, err := GetGithubEmail(client) | 		user, err := GetGithubUser(client) | ||||||
|  |  | ||||||
| 		// Check if there was an error | 		// Check if there was an error | ||||||
| 		if err != nil { | 		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 the user | ||||||
| 		return email, nil | 		return user, nil | ||||||
| 	case "google": | 	case "google": | ||||||
| 		// If the google provider is not configured, return an error | 		// If the google provider is not configured, return an error | ||||||
| 		if providers.Google == nil { | 		if providers.Google == nil { | ||||||
| 			log.Debug().Msg("Google provider not configured") | 			log.Debug().Msg("Google provider not configured") | ||||||
| 			return "", nil | 			return user, nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Get the client from the google provider | 		// 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") | 		log.Debug().Msg("Got client from google") | ||||||
|  |  | ||||||
| 		// Get the email from the google provider | 		// Get the user from the google provider | ||||||
| 		email, err := GetGoogleEmail(client) | 		user, err := GetGoogleUser(client) | ||||||
|  |  | ||||||
| 		// Check if there was an error | 		// Check if there was an error | ||||||
| 		if err != nil { | 		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 the user | ||||||
| 		return email, nil | 		return user, nil | ||||||
| 	case "generic": | 	case "generic": | ||||||
| 		// If the generic provider is not configured, return an error | 		// If the generic provider is not configured, return an error | ||||||
| 		if providers.Generic == nil { | 		if providers.Generic == nil { | ||||||
| 			log.Debug().Msg("Generic provider not configured") | 			log.Debug().Msg("Generic provider not configured") | ||||||
| 			return "", nil | 			return user, nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Get the client from the generic provider | 		// 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") | 		log.Debug().Msg("Got client from generic") | ||||||
|  |  | ||||||
| 		// Get the email from the generic provider | 		// Get the user from the generic provider | ||||||
| 		email, err := GetGenericEmail(client, providers.Config.GenericUserURL) | 		user, err := GetGenericUser(client, providers.Config.GenericUserURL) | ||||||
|  |  | ||||||
| 		// Check if there was an error | 		// Check if there was an error | ||||||
| 		if err != nil { | 		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 the email | ||||||
| 		return email, nil | 		return user, nil | ||||||
| 	default: | 	default: | ||||||
| 		return "", nil | 		return user, nil | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ type OAuthRequest struct { | |||||||
| type UnauthorizedQuery struct { | type UnauthorizedQuery struct { | ||||||
| 	Username string `url:"username"` | 	Username string `url:"username"` | ||||||
| 	Resource string `url:"resource"` | 	Resource string `url:"resource"` | ||||||
|  | 	GroupErr bool   `url:"groupErr"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Proxy is the uri parameters for the proxy endpoint | // Proxy is the uri parameters for the proxy endpoint | ||||||
| @@ -33,6 +34,8 @@ type UserContextResponse struct { | |||||||
| 	Message     string `json:"message"` | 	Message     string `json:"message"` | ||||||
| 	IsLoggedIn  bool   `json:"isLoggedIn"` | 	IsLoggedIn  bool   `json:"isLoggedIn"` | ||||||
| 	Username    string `json:"username"` | 	Username    string `json:"username"` | ||||||
|  | 	Name        string `json:"name"` | ||||||
|  | 	Email       string `json:"email"` | ||||||
| 	Provider    string `json:"provider"` | 	Provider    string `json:"provider"` | ||||||
| 	Oauth       bool   `json:"oauth"` | 	Oauth       bool   `json:"oauth"` | ||||||
| 	TotpPending bool   `json:"totpPending"` | 	TotpPending bool   `json:"totpPending"` | ||||||
| @@ -49,6 +52,7 @@ type AppContext struct { | |||||||
| 	Domain                string   `json:"domain"` | 	Domain                string   `json:"domain"` | ||||||
| 	ForgotPasswordMessage string   `json:"forgotPasswordMessage"` | 	ForgotPasswordMessage string   `json:"forgotPasswordMessage"` | ||||||
| 	BackgroundImage       string   `json:"backgroundImage"` | 	BackgroundImage       string   `json:"backgroundImage"` | ||||||
|  | 	OAuthAutoRedirect     string   `json:"oauthAutoRedirect"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Totp request is the request for the totp endpoint | // Totp request is the request for the totp endpoint | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ type Config struct { | |||||||
| 	GenericName             string `mapstructure:"generic-name"` | 	GenericName             string `mapstructure:"generic-name"` | ||||||
| 	DisableContinue         bool   `mapstructure:"disable-continue"` | 	DisableContinue         bool   `mapstructure:"disable-continue"` | ||||||
| 	OAuthWhitelist          string `mapstructure:"oauth-whitelist"` | 	OAuthWhitelist          string `mapstructure:"oauth-whitelist"` | ||||||
|  | 	OAuthAutoRedirect       string `mapstructure:"oauth-auto-redirect" validate:"oneof=none github google generic"` | ||||||
| 	SessionExpiry           int    `mapstructure:"session-expiry"` | 	SessionExpiry           int    `mapstructure:"session-expiry"` | ||||||
| 	LogLevel                int8   `mapstructure:"log-level" validate:"min=-1,max=5"` | 	LogLevel                int8   `mapstructure:"log-level" validate:"min=-1,max=5"` | ||||||
| 	Title                   string `mapstructure:"app-title"` | 	Title                   string `mapstructure:"app-title"` | ||||||
| @@ -46,6 +47,7 @@ type HandlersConfig struct { | |||||||
| 	Title                 string | 	Title                 string | ||||||
| 	ForgotPasswordMessage string | 	ForgotPasswordMessage string | ||||||
| 	BackgroundImage       string | 	BackgroundImage       string | ||||||
|  | 	OAuthAutoRedirect     string | ||||||
| } | } | ||||||
|  |  | ||||||
| // OAuthConfig is the configuration for the providers | // OAuthConfig is the configuration for the providers | ||||||
| @@ -80,3 +82,8 @@ type AuthConfig struct { | |||||||
| 	LoginTimeout    int | 	LoginTimeout    int | ||||||
| 	LoginMaxRetries int | 	LoginMaxRetries int | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // HooksConfig is the configuration for the hooks service | ||||||
|  | type HooksConfig struct { | ||||||
|  | 	Domain string | ||||||
|  | } | ||||||
|   | |||||||
| @@ -25,8 +25,11 @@ type OAuthProviders struct { | |||||||
| // SessionCookie is the cookie for the session (exculding the expiry) | // SessionCookie is the cookie for the session (exculding the expiry) | ||||||
| type SessionCookie struct { | type SessionCookie struct { | ||||||
| 	Username    string | 	Username    string | ||||||
|  | 	Name        string | ||||||
|  | 	Email       string | ||||||
| 	Provider    string | 	Provider    string | ||||||
| 	TotpPending bool | 	TotpPending bool | ||||||
|  | 	OAuthGroups string | ||||||
| } | } | ||||||
|  |  | ||||||
| // TinyauthLabels is the labels for the tinyauth container | // TinyauthLabels is the labels for the tinyauth container | ||||||
| @@ -35,15 +38,20 @@ type TinyauthLabels struct { | |||||||
| 	Users          string | 	Users          string | ||||||
| 	Allowed        string | 	Allowed        string | ||||||
| 	Headers        map[string]string | 	Headers        map[string]string | ||||||
|  | 	OAuthGroups    string | ||||||
| } | } | ||||||
|  |  | ||||||
| // UserContext is the context for the user | // UserContext is the context for the user | ||||||
| type UserContext struct { | type UserContext struct { | ||||||
| 	Username    string | 	Username    string | ||||||
|  | 	Name        string | ||||||
|  | 	Email       string | ||||||
| 	IsLoggedIn  bool | 	IsLoggedIn  bool | ||||||
| 	OAuth       bool | 	OAuth       bool | ||||||
| 	Provider    string | 	Provider    string | ||||||
| 	TotpPending bool | 	TotpPending bool | ||||||
|  | 	OAuthGroups string | ||||||
|  | 	TotpEnabled bool | ||||||
| } | } | ||||||
|  |  | ||||||
| // LoginAttempt tracks information about login attempts for rate limiting | // LoginAttempt tracks information about login attempts for rate limiting | ||||||
|   | |||||||
| @@ -204,6 +204,8 @@ func GetTinyauthLabels(labels map[string]string) types.TinyauthLabels { | |||||||
| 					} | 					} | ||||||
| 					tinyauthLabels.Headers[headerSplit[0]] = headerSplit[1] | 					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 if no match was found | ||||||
| 	return false | 	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) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -467,3 +467,65 @@ func TestCheckWhitelist(t *testing.T) { | |||||||
| 		t.Fatalf("Expected %v, got %v", expected, result) | 		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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Stavros
					Stavros