mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-31 06:05:43 +00:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			v3.2.0-alp
			...
			v3.2.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | bafcb9a867 | ||
|   | d322c13791 | ||
|   | 8e84e59c2f | ||
|   | bd7e160e10 | ||
|   | df849d5a5c | ||
|   | 5cf4e208c6 | ||
|   | 07ddd4f917 | ||
|   | 98abe514e1 | 
| @@ -26,5 +26,7 @@ DISABLE_CONTINUE=false | |||||||
| OAUTH_WHITELIST= | OAUTH_WHITELIST= | ||||||
| GENERIC_NAME=My OAuth | GENERIC_NAME=My OAuth | ||||||
| SESSION_EXPIRY=7200 | SESSION_EXPIRY=7200 | ||||||
|  | LOGIN_TIMEOUT=300 | ||||||
|  | LOGIN_MAX_RETRIES=5 | ||||||
| LOG_LEVEL=0 | LOG_LEVEL=0 | ||||||
| APP_TITLE=Tinyauth SSO | APP_TITLE=Tinyauth SSO | ||||||
							
								
								
									
										58
									
								
								.github/workflows/translations.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										58
									
								
								.github/workflows/translations.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,7 +3,7 @@ name: Publish translations | |||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - i18n_v* | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|  |  | ||||||
| permissions: | permissions: | ||||||
| @@ -16,7 +16,53 @@ concurrency: | |||||||
|   cancel-in-progress: false |   cancel-in-progress: false | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|  |   get-branches: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     outputs: | ||||||
|  |       i18n-branches: ${{ steps.get-branches.outputs.result }} | ||||||
|  |     steps: | ||||||
|  |       - name: Get branches | ||||||
|  |         id: get-branches | ||||||
|  |         uses: actions/github-script@v7 | ||||||
|  |         with: | ||||||
|  |           script: | | ||||||
|  |             const { data: repos } = await github.rest.repos.listBranches({ | ||||||
|  |               owner: context.repo.owner, | ||||||
|  |               repo: context.repo.repo, | ||||||
|  |             }) | ||||||
|  |  | ||||||
|  |             const i18nBranches = repos.filter((branch) => branch.name.startsWith("i18n_v")) | ||||||
|  |             const i18nBranchNames = i18nBranches.map((branch) => branch.name) | ||||||
|  |  | ||||||
|  |             return i18nBranchNames | ||||||
|  |  | ||||||
|  |   get-translations: | ||||||
|  |     needs: get-branches | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     strategy: | ||||||
|  |       matrix: | ||||||
|  |         branch: ${{ fromJson(needs.get-branches.outputs.i18n-branches) }} | ||||||
|  |     steps: | ||||||
|  |       - name: Checkout | ||||||
|  |         uses: actions/checkout@v4 | ||||||
|  |         with: | ||||||
|  |           ref: ${{ matrix.branch }} | ||||||
|  |  | ||||||
|  |       - name: Get translation version | ||||||
|  |         id: get-version | ||||||
|  |         run: | | ||||||
|  |           branch=${{ matrix.branch }} | ||||||
|  |           version=${branch#i18n_} | ||||||
|  |           echo "version=$version" >> $GITHUB_OUTPUT | ||||||
|  |  | ||||||
|  |       - name: Upload translations | ||||||
|  |         uses: actions/upload-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           name: ${{ steps.get-version.outputs.version }} | ||||||
|  |           path: frontend/src/lib/i18n/locales | ||||||
|  |  | ||||||
|   build: |   build: | ||||||
|  |     needs: get-translations | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
| @@ -25,10 +71,14 @@ jobs: | |||||||
|       - name: Setup Pages |       - name: Setup Pages | ||||||
|         uses: actions/configure-pages@v4 |         uses: actions/configure-pages@v4 | ||||||
|  |  | ||||||
|       - name: Move translations |       - name: Prepare output directory | ||||||
|         run: | |         run: | | ||||||
|           mkdir -p dist |           mkdir -p dist/i18n/ | ||||||
|           mv frontend/src/lib/i18n/locales dist/i18n |  | ||||||
|  |       - name: Download translations | ||||||
|  |         uses: actions/download-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           path: dist/i18n/ | ||||||
|  |  | ||||||
|       - name: Upload artifact |       - name: Upload artifact | ||||||
|         uses: actions/upload-pages-artifact@v3 |         uses: actions/upload-pages-artifact@v3 | ||||||
|   | |||||||
| @@ -61,3 +61,7 @@ Credits for the logo of this app go to: | |||||||
|  |  | ||||||
| - **Freepik** for providing the police hat and badge. | - **Freepik** for providing the police hat and badge. | ||||||
| - **Renee French** for the original gopher logo. | - **Renee French** for the original gopher logo. | ||||||
|  |  | ||||||
|  | ## Star History | ||||||
|  |  | ||||||
|  | [](https://www.star-history.com/#steveiliop56/tinyauth&Date) | ||||||
							
								
								
									
										33
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -2,7 +2,6 @@ package cmd | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -94,10 +93,8 @@ var rootCmd = &cobra.Command{ | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create handlers config | 		// Create handlers config | ||||||
| 		serverConfig := types.HandlersConfig{ | 		handlersConfig := types.HandlersConfig{ | ||||||
| 			AppURL:          config.AppURL, | 			AppURL:          config.AppURL, | ||||||
| 			Domain:          fmt.Sprintf(".%s", domain), |  | ||||||
| 			CookieSecure:    config.CookieSecure, |  | ||||||
| 			DisableContinue: config.DisableContinue, | 			DisableContinue: config.DisableContinue, | ||||||
| 			Title:           config.Title, | 			Title:           config.Title, | ||||||
| 			GenericName:     config.GenericName, | 			GenericName:     config.GenericName, | ||||||
| @@ -105,12 +102,20 @@ var rootCmd = &cobra.Command{ | |||||||
|  |  | ||||||
| 		// Create api config | 		// Create api config | ||||||
| 		apiConfig := types.APIConfig{ | 		apiConfig := types.APIConfig{ | ||||||
| 			Port:          config.Port, | 			Port:    config.Port, | ||||||
| 			Address:       config.Address, | 			Address: config.Address, | ||||||
| 			Secret:        config.Secret, | 		} | ||||||
| 			CookieSecure:  config.CookieSecure, |  | ||||||
| 			SessionExpiry: config.SessionExpiry, | 		// Create auth config | ||||||
| 			Domain:        domain, | 		authConfig := types.AuthConfig{ | ||||||
|  | 			Users:           users, | ||||||
|  | 			OauthWhitelist:  oauthWhitelist, | ||||||
|  | 			Secret:          config.Secret, | ||||||
|  | 			CookieSecure:    config.CookieSecure, | ||||||
|  | 			SessionExpiry:   config.SessionExpiry, | ||||||
|  | 			Domain:          domain, | ||||||
|  | 			LoginTimeout:    config.LoginTimeout, | ||||||
|  | 			LoginMaxRetries: config.LoginMaxRetries, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Create docker service | 		// Create docker service | ||||||
| @@ -121,7 +126,7 @@ var rootCmd = &cobra.Command{ | |||||||
| 		HandleError(err, "Failed to initialize docker") | 		HandleError(err, "Failed to initialize docker") | ||||||
|  |  | ||||||
| 		// Create auth service | 		// Create auth service | ||||||
| 		auth := auth.NewAuth(docker, users, oauthWhitelist, config.SessionExpiry) | 		auth := auth.NewAuth(authConfig, docker) | ||||||
|  |  | ||||||
| 		// Create OAuth providers service | 		// Create OAuth providers service | ||||||
| 		providers := providers.NewProviders(oauthConfig) | 		providers := providers.NewProviders(oauthConfig) | ||||||
| @@ -133,7 +138,7 @@ var rootCmd = &cobra.Command{ | |||||||
| 		hooks := hooks.NewHooks(auth, providers) | 		hooks := hooks.NewHooks(auth, providers) | ||||||
|  |  | ||||||
| 		// Create handlers | 		// Create handlers | ||||||
| 		handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers, docker) | 		handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker) | ||||||
|  |  | ||||||
| 		// Create API | 		// Create API | ||||||
| 		api := api.NewAPI(apiConfig, handlers) | 		api := api.NewAPI(apiConfig, handlers) | ||||||
| @@ -198,6 +203,8 @@ func init() { | |||||||
| 	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().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-max-retries", 5, "Maximum login attempts before timeout (0 to disable).") | ||||||
| 	rootCmd.Flags().Int("log-level", 1, "Log level.") | 	rootCmd.Flags().Int("log-level", 1, "Log level.") | ||||||
| 	rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.") | 	rootCmd.Flags().String("app-title", "Tinyauth", "Title of the app.") | ||||||
|  |  | ||||||
| @@ -232,6 +239,8 @@ func init() { | |||||||
| 	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") | ||||||
|  | 	viper.BindEnv("login-timeout", "LOGIN_TIMEOUT") | ||||||
|  | 	viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES") | ||||||
|  |  | ||||||
| 	// Bind flags to viper | 	// Bind flags to viper | ||||||
| 	viper.BindPFlags(rootCmd.Flags()) | 	viper.BindPFlags(rootCmd.Flags()) | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ i18n | |||||||
|       ], |       ], | ||||||
|       backendOptions: [ |       backendOptions: [ | ||||||
|         { |         { | ||||||
|           loadPath: "https://cdn.tinyauth.app/i18n/{{lng}}.json", |           loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json", | ||||||
|         }, |         }, | ||||||
|       ], |       ], | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ | |||||||
|     "loginSubmit": "Login", |     "loginSubmit": "Login", | ||||||
|     "loginFailTitle": "Failed to log in", |     "loginFailTitle": "Failed to log in", | ||||||
|     "loginFailSubtitle": "Please check your username and password", |     "loginFailSubtitle": "Please check your username and password", | ||||||
|  |     "loginFailRateLimit": "You failed to login too many times, please try again later", | ||||||
|     "loginSuccessTitle": "Logged in", |     "loginSuccessTitle": "Logged in", | ||||||
|     "loginSuccessSubtitle": "Welcome back!", |     "loginSuccessSubtitle": "Welcome back!", | ||||||
|     "loginOauthFailTitle": "Internal error", |     "loginOauthFailTitle": "Internal error", | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ | |||||||
|     "loginSubmit": "Login", |     "loginSubmit": "Login", | ||||||
|     "loginFailTitle": "Failed to log in", |     "loginFailTitle": "Failed to log in", | ||||||
|     "loginFailSubtitle": "Please check your username and password", |     "loginFailSubtitle": "Please check your username and password", | ||||||
|  |     "loginFailRateLimit": "You failed to login too many times, please try again later", | ||||||
|     "loginSuccessTitle": "Logged in", |     "loginSuccessTitle": "Logged in", | ||||||
|     "loginSuccessSubtitle": "Welcome back!", |     "loginSuccessSubtitle": "Welcome back!", | ||||||
|     "loginOauthFailTitle": "Internal error", |     "loginOauthFailTitle": "Internal error", | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { Paper, Title, Text, Divider } from "@mantine/core"; | import { Paper, Title, Text, Divider } from "@mantine/core"; | ||||||
| import { notifications } from "@mantine/notifications"; | import { notifications } from "@mantine/notifications"; | ||||||
| import { useMutation } from "@tanstack/react-query"; | import { useMutation } from "@tanstack/react-query"; | ||||||
| import axios from "axios"; | import axios, { type AxiosError } from "axios"; | ||||||
| import { useUserContext } from "../context/user-context"; | import { useUserContext } from "../context/user-context"; | ||||||
| import { Navigate } from "react-router"; | import { Navigate } from "react-router"; | ||||||
| import { Layout } from "../components/layouts/layout"; | import { Layout } from "../components/layouts/layout"; | ||||||
| @@ -33,7 +33,17 @@ export const LoginPage = () => { | |||||||
|     mutationFn: (login: LoginFormValues) => { |     mutationFn: (login: LoginFormValues) => { | ||||||
|       return axios.post("/api/login", login); |       return axios.post("/api/login", login); | ||||||
|     }, |     }, | ||||||
|     onError: () => { |     onError: (data: AxiosError) => { | ||||||
|  |       if (data.response) { | ||||||
|  |         if (data.response.status === 429) { | ||||||
|  |           notifications.show({ | ||||||
|  |             title: t("loginFailTitle"), | ||||||
|  |             message: t("loginFailRateLimit"), | ||||||
|  |             color: "red", | ||||||
|  |           }); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|       notifications.show({ |       notifications.show({ | ||||||
|         title: t("loginFailTitle"), |         title: t("loginFailTitle"), | ||||||
|         message: t("loginFailSubtitle"), |         message: t("loginFailSubtitle"), | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,7 +3,6 @@ module tinyauth | |||||||
| go 1.23.2 | go 1.23.2 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	github.com/gin-contrib/sessions v1.0.2 |  | ||||||
| 	github.com/gin-gonic/gin v1.10.0 | 	github.com/gin-gonic/gin v1.10.0 | ||||||
| 	github.com/go-playground/validator/v10 v10.24.0 | 	github.com/go-playground/validator/v10 v10.24.0 | ||||||
| 	github.com/google/go-querystring v1.1.0 | 	github.com/google/go-querystring v1.1.0 | ||||||
| @@ -58,9 +57,8 @@ require ( | |||||||
| 	github.com/go-playground/universal-translator v0.18.1 // indirect | 	github.com/go-playground/universal-translator v0.18.1 // indirect | ||||||
| 	github.com/goccy/go-json v0.10.4 // indirect | 	github.com/goccy/go-json v0.10.4 // indirect | ||||||
| 	github.com/gogo/protobuf v1.3.2 // indirect | 	github.com/gogo/protobuf v1.3.2 // indirect | ||||||
| 	github.com/gorilla/context v1.1.2 // indirect |  | ||||||
| 	github.com/gorilla/securecookie v1.1.2 // indirect | 	github.com/gorilla/securecookie v1.1.2 // indirect | ||||||
| 	github.com/gorilla/sessions v1.2.2 // indirect | 	github.com/gorilla/sessions v1.2.2 | ||||||
| 	github.com/hashicorp/hcl v1.0.0 // indirect | 	github.com/hashicorp/hcl v1.0.0 // indirect | ||||||
| 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||||||
| 	github.com/json-iterator/go v1.1.12 // indirect | 	github.com/json-iterator/go v1.1.12 // indirect | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @@ -65,8 +65,6 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos | |||||||
| github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= | ||||||
| github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= | ||||||
| github.com/gin-contrib/sessions v1.0.2 h1:UaIjUvTH1cMeOdj3in6dl+Xb6It8RiKRF9Z1anbUyCA= |  | ||||||
| github.com/gin-contrib/sessions v1.0.2/go.mod h1:KxKxWqWP5LJVDCInulOl4WbLzK2KSPlLesfZ66wRvMs= |  | ||||||
| github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= | github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= | ||||||
| github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= | github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= | ||||||
| github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= | ||||||
| @@ -99,8 +97,6 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= | |||||||
| github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
| github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
| github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= |  | ||||||
| github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= |  | ||||||
| github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= | ||||||
| github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= | ||||||
| github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= | github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= | ||||||
|   | |||||||
| @@ -11,8 +11,6 @@ import ( | |||||||
| 	"tinyauth/internal/handlers" | 	"tinyauth/internal/handlers" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
|  |  | ||||||
| 	"github.com/gin-contrib/sessions" |  | ||||||
| 	"github.com/gin-contrib/sessions/cookie" |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| ) | ) | ||||||
| @@ -51,21 +49,6 @@ func (api *API) Init() { | |||||||
| 	log.Debug().Msg("Setting up file server") | 	log.Debug().Msg("Setting up file server") | ||||||
| 	fileServer := http.FileServer(http.FS(dist)) | 	fileServer := http.FileServer(http.FS(dist)) | ||||||
|  |  | ||||||
| 	// Setup cookie store |  | ||||||
| 	log.Debug().Msg("Setting up cookie store") |  | ||||||
| 	store := cookie.NewStore([]byte(api.Config.Secret)) |  | ||||||
|  |  | ||||||
| 	// Use session middleware |  | ||||||
| 	store.Options(sessions.Options{ |  | ||||||
| 		Domain:   api.Config.Domain, |  | ||||||
| 		Path:     "/", |  | ||||||
| 		HttpOnly: true, |  | ||||||
| 		Secure:   api.Config.CookieSecure, |  | ||||||
| 		MaxAge:   api.Config.SessionExpiry, |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	router.Use(sessions.Sessions("tinyauth", store)) |  | ||||||
|  |  | ||||||
| 	// UI middleware | 	// UI middleware | ||||||
| 	router.Use(func(c *gin.Context) { | 	router.Use(func(c *gin.Context) { | ||||||
| 		// If not an API request, serve the UI | 		// If not an API request, serve the UI | ||||||
|   | |||||||
| @@ -21,23 +21,29 @@ import ( | |||||||
|  |  | ||||||
| // Simple API config for tests | // Simple API config for tests | ||||||
| var apiConfig = types.APIConfig{ | var apiConfig = types.APIConfig{ | ||||||
| 	Port:          8080, | 	Port:    8080, | ||||||
| 	Address:       "0.0.0.0", | 	Address: "0.0.0.0", | ||||||
| 	Secret:        "super-secret-api-thing-for-tests", // It is 32 chars long |  | ||||||
| 	CookieSecure:  false, |  | ||||||
| 	SessionExpiry: 3600, |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Simple handlers config for tests | // Simple handlers config for tests | ||||||
| var handlersConfig = types.HandlersConfig{ | var handlersConfig = types.HandlersConfig{ | ||||||
| 	AppURL:          "http://localhost:8080", | 	AppURL:          "http://localhost:8080", | ||||||
| 	Domain:          ".localhost", |  | ||||||
| 	CookieSecure:    false, |  | ||||||
| 	DisableContinue: false, | 	DisableContinue: false, | ||||||
| 	Title:           "Tinyauth", | 	Title:           "Tinyauth", | ||||||
| 	GenericName:     "Generic", | 	GenericName:     "Generic", | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // Simple auth config for tests | ||||||
|  | var authConfig = types.AuthConfig{ | ||||||
|  | 	Users:           types.Users{}, | ||||||
|  | 	OauthWhitelist:  []string{}, | ||||||
|  | 	Secret:          "super-secret-api-thing-for-tests", // It is 32 chars long | ||||||
|  | 	CookieSecure:    false, | ||||||
|  | 	SessionExpiry:   3600, | ||||||
|  | 	LoginTimeout:    0, | ||||||
|  | 	LoginMaxRetries: 0, | ||||||
|  | } | ||||||
|  |  | ||||||
| // Cookie | // Cookie | ||||||
| var cookie string | var cookie string | ||||||
|  |  | ||||||
| @@ -61,12 +67,13 @@ func getAPI(t *testing.T) *api.API { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Create auth service | 	// Create auth service | ||||||
| 	auth := auth.NewAuth(docker, types.Users{ | 	authConfig.Users = types.Users{ | ||||||
| 		{ | 		{ | ||||||
| 			Username: user.Username, | 			Username: user.Username, | ||||||
| 			Password: user.Password, | 			Password: user.Password, | ||||||
| 		}, | 		}, | ||||||
| 	}, nil, apiConfig.SessionExpiry) | 	} | ||||||
|  | 	auth := auth.NewAuth(authConfig, docker) | ||||||
|  |  | ||||||
| 	// Create providers service | 	// Create providers service | ||||||
| 	providers := providers.NewProviders(types.OAuthConfig{}) | 	providers := providers.NewProviders(types.OAuthConfig{}) | ||||||
|   | |||||||
| @@ -1,38 +1,64 @@ | |||||||
| package auth | package auth | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"net/http" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"slices" | 	"slices" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
| 	"tinyauth/internal/docker" | 	"tinyauth/internal/docker" | ||||||
| 	"tinyauth/internal/types" | 	"tinyauth/internal/types" | ||||||
|  |  | ||||||
| 	"github.com/gin-contrib/sessions" |  | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
|  | 	"github.com/gorilla/sessions" | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func NewAuth(docker *docker.Docker, userList types.Users, oauthWhitelist []string, sessionExpiry int) *Auth { | func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth { | ||||||
| 	return &Auth{ | 	return &Auth{ | ||||||
| 		Docker:         docker, | 		Config:        config, | ||||||
| 		Users:          userList, | 		Docker:        docker, | ||||||
| 		OAuthWhitelist: oauthWhitelist, | 		LoginAttempts: make(map[string]*types.LoginAttempt), | ||||||
| 		SessionExpiry:  sessionExpiry, |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| type Auth struct { | type Auth struct { | ||||||
| 	Users          types.Users | 	Config        types.AuthConfig | ||||||
| 	Docker         *docker.Docker | 	Docker        *docker.Docker | ||||||
| 	OAuthWhitelist []string | 	LoginAttempts map[string]*types.LoginAttempt | ||||||
| 	SessionExpiry  int | 	LoginMutex    sync.RWMutex | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (auth *Auth) GetSession(c *gin.Context) (*sessions.Session, error) { | ||||||
|  | 	// Create cookie store | ||||||
|  | 	store := sessions.NewCookieStore([]byte(auth.Config.Secret)) | ||||||
|  |  | ||||||
|  | 	// Configure cookie store | ||||||
|  | 	store.Options = &sessions.Options{ | ||||||
|  | 		Path:     "/", | ||||||
|  | 		MaxAge:   auth.Config.SessionExpiry, | ||||||
|  | 		Secure:   auth.Config.CookieSecure, | ||||||
|  | 		HttpOnly: true, | ||||||
|  | 		SameSite: http.SameSiteDefaultMode, | ||||||
|  | 		Domain:   fmt.Sprintf(".%s", auth.Config.Domain), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get session | ||||||
|  | 	session, err := store.Get(c.Request, "tinyauth") | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error().Err(err).Msg("Failed to get session") | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return session, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) GetUser(username string) *types.User { | func (auth *Auth) GetUser(username string) *types.User { | ||||||
| 	// Loop through users and return the user if the username matches | 	// Loop through users and return the user if the username matches | ||||||
| 	for _, user := range auth.Users { | 	for _, user := range auth.Config.Users { | ||||||
| 		if user.Username == username { | 		if user.Username == username { | ||||||
| 			return &user | 			return &user | ||||||
| 		} | 		} | ||||||
| @@ -45,14 +71,78 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool { | |||||||
| 	return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil | 	return bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // IsAccountLocked checks if a username or IP is locked due to too many failed login attempts | ||||||
|  | func (auth *Auth) IsAccountLocked(identifier string) (bool, int) { | ||||||
|  | 	auth.LoginMutex.RLock() | ||||||
|  | 	defer auth.LoginMutex.RUnlock() | ||||||
|  |  | ||||||
|  | 	// Return false if rate limiting is not configured | ||||||
|  | 	if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { | ||||||
|  | 		return false, 0 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if the identifier exists in the map | ||||||
|  | 	attempt, exists := auth.LoginAttempts[identifier] | ||||||
|  | 	if !exists { | ||||||
|  | 		return false, 0 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If account is locked, check if lock time has expired | ||||||
|  | 	if attempt.LockedUntil.After(time.Now()) { | ||||||
|  | 		// Calculate remaining lockout time in seconds | ||||||
|  | 		remaining := int(time.Until(attempt.LockedUntil).Seconds()) | ||||||
|  | 		return true, remaining | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Lock has expired | ||||||
|  | 	return false, 0 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RecordLoginAttempt records a login attempt for rate limiting | ||||||
|  | func (auth *Auth) RecordLoginAttempt(identifier string, success bool) { | ||||||
|  | 	// Skip if rate limiting is not configured | ||||||
|  | 	if auth.Config.LoginMaxRetries <= 0 || auth.Config.LoginTimeout <= 0 { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	auth.LoginMutex.Lock() | ||||||
|  | 	defer auth.LoginMutex.Unlock() | ||||||
|  |  | ||||||
|  | 	// Get current attempt record or create a new one | ||||||
|  | 	attempt, exists := auth.LoginAttempts[identifier] | ||||||
|  | 	if !exists { | ||||||
|  | 		attempt = &types.LoginAttempt{} | ||||||
|  | 		auth.LoginAttempts[identifier] = attempt | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Update last attempt time | ||||||
|  | 	attempt.LastAttempt = time.Now() | ||||||
|  |  | ||||||
|  | 	// If successful login, reset failed attempts | ||||||
|  | 	if success { | ||||||
|  | 		attempt.FailedAttempts = 0 | ||||||
|  | 		attempt.LockedUntil = time.Time{} // Reset lock time | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Increment failed attempts | ||||||
|  | 	attempt.FailedAttempts++ | ||||||
|  |  | ||||||
|  | 	// If max retries reached, lock the account | ||||||
|  | 	if attempt.FailedAttempts >= auth.Config.LoginMaxRetries { | ||||||
|  | 		attempt.LockedUntil = time.Now().Add(time.Duration(auth.Config.LoginTimeout) * time.Second) | ||||||
|  | 		log.Warn().Str("identifier", identifier).Int("timeout", auth.Config.LoginTimeout).Msg("Account locked due to too many failed login attempts") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func (auth *Auth) EmailWhitelisted(emailSrc string) bool { | func (auth *Auth) EmailWhitelisted(emailSrc string) bool { | ||||||
| 	// If the whitelist is empty, allow all emails | 	// If the whitelist is empty, allow all emails | ||||||
| 	if len(auth.OAuthWhitelist) == 0 { | 	if len(auth.Config.OauthWhitelist) == 0 { | ||||||
| 		return true | 		return true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Loop through the whitelist and return true if the email matches | 	// Loop through the whitelist and return true if the email matches | ||||||
| 	for _, email := range auth.OAuthWhitelist { | 	for _, email := range auth.Config.OauthWhitelist { | ||||||
| 		if email == emailSrc { | 		if email == emailSrc { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| @@ -62,11 +152,15 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool { | |||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) { | func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error { | ||||||
| 	log.Debug().Msg("Creating session cookie") | 	log.Debug().Msg("Creating session cookie") | ||||||
|  |  | ||||||
| 	// Get session | 	// Get session | ||||||
| 	sessions := sessions.Default(c) | 	session, err := auth.GetSession(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error().Err(err).Msg("Failed to get session") | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Setting session cookie") | 	log.Debug().Msg("Setting session cookie") | ||||||
|  |  | ||||||
| @@ -76,54 +170,73 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) | |||||||
| 	if data.TotpPending { | 	if data.TotpPending { | ||||||
| 		sessionExpiry = 3600 | 		sessionExpiry = 3600 | ||||||
| 	} else { | 	} else { | ||||||
| 		sessionExpiry = auth.SessionExpiry | 		sessionExpiry = auth.Config.SessionExpiry | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Set data | 	// Set data | ||||||
| 	sessions.Set("username", data.Username) | 	session.Values["username"] = data.Username | ||||||
| 	sessions.Set("provider", data.Provider) | 	session.Values["provider"] = data.Provider | ||||||
| 	sessions.Set("expiry", time.Now().Add(time.Duration(sessionExpiry)*time.Second).Unix()) | 	session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix() | ||||||
| 	sessions.Set("totpPending", data.TotpPending) | 	session.Values["totpPending"] = data.TotpPending | ||||||
|  | 	session.Values["redirectURI"] = data.RedirectURI | ||||||
|  |  | ||||||
| 	// Save session | 	// Save session | ||||||
| 	sessions.Save() | 	err = session.Save(c.Request, c.Writer) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error().Err(err).Msg("Failed to save session") | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Return nil | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) DeleteSessionCookie(c *gin.Context) { | func (auth *Auth) DeleteSessionCookie(c *gin.Context) error { | ||||||
| 	log.Debug().Msg("Deleting session cookie") | 	log.Debug().Msg("Deleting session cookie") | ||||||
|  |  | ||||||
| 	// Get session | 	// Get session | ||||||
| 	sessions := sessions.Default(c) | 	session, err := auth.GetSession(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error().Err(err).Msg("Failed to get session") | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Clear session | 	// Delete all values in the session | ||||||
| 	sessions.Clear() | 	for key := range session.Values { | ||||||
|  | 		delete(session.Values, key) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Save session | 	// Save session | ||||||
| 	sessions.Save() | 	err = session.Save(c.Request, c.Writer) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error().Err(err).Msg("Failed to save session") | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Return nil | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie { | func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) { | ||||||
| 	log.Debug().Msg("Getting session cookie") | 	log.Debug().Msg("Getting session cookie") | ||||||
|  |  | ||||||
| 	// Get session | 	// Get session | ||||||
| 	sessions := sessions.Default(c) | 	session, err := auth.GetSession(c) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error().Err(err).Msg("Failed to get session") | ||||||
|  | 		return types.SessionCookie{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Get data | 	// Get data from session | ||||||
| 	cookieUsername := sessions.Get("username") | 	username, usernameOk := session.Values["username"].(string) | ||||||
| 	cookieProvider := sessions.Get("provider") | 	provider, providerOK := session.Values["provider"].(string) | ||||||
| 	cookieExpiry := sessions.Get("expiry") | 	redirectURI, redirectOK := session.Values["redirectURI"].(string) | ||||||
| 	cookieTotpPending := sessions.Get("totpPending") | 	expiry, expiryOk := session.Values["expiry"].(int64) | ||||||
|  | 	totpPending, totpPendingOk := session.Values["totpPending"].(bool) | ||||||
|  |  | ||||||
| 	// Convert interfaces to correct types | 	if !usernameOk || !providerOK || !expiryOk || !redirectOK || !totpPendingOk { | ||||||
| 	username, usernameOk := cookieUsername.(string) | 		log.Warn().Msg("Session cookie is missing data") | ||||||
| 	provider, providerOk := cookieProvider.(string) | 		return types.SessionCookie{}, nil | ||||||
| 	expiry, expiryOk := cookieExpiry.(int64) |  | ||||||
| 	totpPending, totpPendingOk := cookieTotpPending.(bool) |  | ||||||
|  |  | ||||||
| 	// Check if the cookie is invalid |  | ||||||
| 	if !usernameOk || !providerOk || !expiryOk || !totpPendingOk { |  | ||||||
| 		log.Warn().Msg("Session cookie invalid") |  | ||||||
| 		return types.SessionCookie{} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if the cookie has expired | 	// Check if the cookie has expired | ||||||
| @@ -134,7 +247,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie { | |||||||
| 		auth.DeleteSessionCookie(c) | 		auth.DeleteSessionCookie(c) | ||||||
|  |  | ||||||
| 		// Return empty cookie | 		// Return empty cookie | ||||||
| 		return types.SessionCookie{} | 		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).Msg("Parsed cookie") | ||||||
| @@ -144,12 +257,13 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) types.SessionCookie { | |||||||
| 		Username:    username, | 		Username:    username, | ||||||
| 		Provider:    provider, | 		Provider:    provider, | ||||||
| 		TotpPending: totpPending, | 		TotpPending: totpPending, | ||||||
| 	} | 		RedirectURI: redirectURI, | ||||||
|  | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (auth *Auth) UserAuthConfigured() bool { | func (auth *Auth) UserAuthConfigured() bool { | ||||||
| 	// If there are users, return true | 	// If there are users, return true | ||||||
| 	return len(auth.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) (bool, error) { | ||||||
|   | |||||||
							
								
								
									
										147
									
								
								internal/auth/auth_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								internal/auth/auth_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | |||||||
|  | package auth_test | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  | 	"tinyauth/internal/auth" | ||||||
|  | 	"tinyauth/internal/docker" | ||||||
|  | 	"tinyauth/internal/types" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var config = types.AuthConfig{ | ||||||
|  | 	Users:          types.Users{}, | ||||||
|  | 	OauthWhitelist: []string{}, | ||||||
|  | 	SessionExpiry:  3600, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestLoginRateLimiting(t *testing.T) { | ||||||
|  | 	// Initialize a new auth service with 3 max retries and 5 seconds timeout | ||||||
|  | 	config.LoginMaxRetries = 3 | ||||||
|  | 	config.LoginTimeout = 5 | ||||||
|  | 	authService := auth.NewAuth(config, &docker.Docker{}) | ||||||
|  |  | ||||||
|  | 	// Test identifier | ||||||
|  | 	identifier := "test_user" | ||||||
|  |  | ||||||
|  | 	// Test successful login - should not lock account | ||||||
|  | 	t.Log("Testing successful login") | ||||||
|  |  | ||||||
|  | 	authService.RecordLoginAttempt(identifier, true) | ||||||
|  | 	locked, _ := authService.IsAccountLocked(identifier) | ||||||
|  |  | ||||||
|  | 	if locked { | ||||||
|  | 		t.Fatalf("Account should not be locked after successful login") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test 2 failed attempts - should not lock account yet | ||||||
|  | 	t.Log("Testing 2 failed login attempts") | ||||||
|  |  | ||||||
|  | 	authService.RecordLoginAttempt(identifier, false) | ||||||
|  | 	authService.RecordLoginAttempt(identifier, false) | ||||||
|  | 	locked, _ = authService.IsAccountLocked(identifier) | ||||||
|  |  | ||||||
|  | 	if locked { | ||||||
|  | 		t.Fatalf("Account should not be locked after only 2 failed attempts") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Add one more failed attempt (total 3) - should lock account with maxRetries=3 | ||||||
|  | 	t.Log("Testing 3 failed login attempts") | ||||||
|  | 	authService.RecordLoginAttempt(identifier, false) | ||||||
|  | 	locked, remainingTime := authService.IsAccountLocked(identifier) | ||||||
|  |  | ||||||
|  | 	if !locked { | ||||||
|  | 		t.Fatalf("Account should be locked after reaching max retries") | ||||||
|  | 	} | ||||||
|  | 	if remainingTime <= 0 || remainingTime > 5 { | ||||||
|  | 		t.Fatalf("Expected remaining time between 1-5 seconds, got %d", remainingTime) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test reset after waiting for timeout - use 1 second timeout for fast testing | ||||||
|  | 	t.Log("Testing unlocking after timeout") | ||||||
|  |  | ||||||
|  | 	// Reinitialize auth service with a shorter timeout for testing | ||||||
|  | 	config.LoginTimeout = 1 | ||||||
|  | 	config.LoginMaxRetries = 3 | ||||||
|  | 	authService = auth.NewAuth(config, &docker.Docker{}) | ||||||
|  |  | ||||||
|  | 	// Add enough failed attempts to lock the account | ||||||
|  | 	for i := 0; i < 3; i++ { | ||||||
|  | 		authService.RecordLoginAttempt(identifier, false) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Verify it's locked | ||||||
|  | 	locked, _ = authService.IsAccountLocked(identifier) | ||||||
|  | 	if !locked { | ||||||
|  | 		t.Fatalf("Account should be locked initially") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Wait a bit and verify it gets unlocked after timeout | ||||||
|  | 	time.Sleep(1500 * time.Millisecond) // Wait longer than the timeout | ||||||
|  | 	locked, _ = authService.IsAccountLocked(identifier) | ||||||
|  |  | ||||||
|  | 	if locked { | ||||||
|  | 		t.Fatalf("Account should be unlocked after timeout period") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test disabled rate limiting | ||||||
|  | 	t.Log("Testing disabled rate limiting") | ||||||
|  | 	config.LoginMaxRetries = 0 | ||||||
|  | 	config.LoginTimeout = 0 | ||||||
|  | 	authService = auth.NewAuth(config, &docker.Docker{}) | ||||||
|  |  | ||||||
|  | 	for i := 0; i < 10; i++ { | ||||||
|  | 		authService.RecordLoginAttempt(identifier, false) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	locked, _ = authService.IsAccountLocked(identifier) | ||||||
|  | 	if locked { | ||||||
|  | 		t.Fatalf("Account should not be locked when rate limiting is disabled") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestConcurrentLoginAttempts(t *testing.T) { | ||||||
|  | 	// Initialize a new auth service with 2 max retries and 5 seconds timeout | ||||||
|  | 	config.LoginMaxRetries = 2 | ||||||
|  | 	config.LoginTimeout = 5 | ||||||
|  | 	authService := auth.NewAuth(config, &docker.Docker{}) | ||||||
|  |  | ||||||
|  | 	// Test multiple identifiers | ||||||
|  | 	identifiers := []string{"user1", "user2", "user3"} | ||||||
|  |  | ||||||
|  | 	// Test that locking one identifier doesn't affect others | ||||||
|  | 	t.Log("Testing multiple identifiers") | ||||||
|  |  | ||||||
|  | 	// Add enough failed attempts to lock first user (2 attempts with maxRetries=2) | ||||||
|  | 	authService.RecordLoginAttempt(identifiers[0], false) | ||||||
|  | 	authService.RecordLoginAttempt(identifiers[0], false) | ||||||
|  |  | ||||||
|  | 	// Check if first user is locked | ||||||
|  | 	locked, _ := authService.IsAccountLocked(identifiers[0]) | ||||||
|  | 	if !locked { | ||||||
|  | 		t.Fatalf("User1 should be locked after reaching max retries") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check that other users are not affected | ||||||
|  | 	for i := 1; i < len(identifiers); i++ { | ||||||
|  | 		locked, _ := authService.IsAccountLocked(identifiers[i]) | ||||||
|  | 		if locked { | ||||||
|  | 			t.Fatalf("User%d should not be locked", i+1) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Test successful login after failed attempts (but before lock) | ||||||
|  | 	t.Log("Testing successful login after failed attempts but before lock") | ||||||
|  |  | ||||||
|  | 	// One failed attempt for user2 | ||||||
|  | 	authService.RecordLoginAttempt(identifiers[1], false) | ||||||
|  |  | ||||||
|  | 	// Successful login should reset the counter | ||||||
|  | 	authService.RecordLoginAttempt(identifiers[1], true) | ||||||
|  |  | ||||||
|  | 	// Now try a failed login again - should not be locked as counter was reset | ||||||
|  | 	authService.RecordLoginAttempt(identifiers[1], false) | ||||||
|  | 	locked, _ = authService.IsAccountLocked(identifiers[1]) | ||||||
|  | 	if locked { | ||||||
|  | 		t.Fatalf("User2 should not be locked after successful login reset") | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -249,12 +249,34 @@ func (h *Handlers) LoginHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	log.Debug().Msg("Got login request") | 	log.Debug().Msg("Got login request") | ||||||
|  |  | ||||||
|  | 	// Get client IP for rate limiting | ||||||
|  | 	clientIP := c.ClientIP() | ||||||
|  |  | ||||||
|  | 	// Create an identifier for rate limiting (username or IP if username doesn't exist yet) | ||||||
|  | 	rateIdentifier := login.Username | ||||||
|  | 	if rateIdentifier == "" { | ||||||
|  | 		rateIdentifier = clientIP | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Check if the account is locked due to too many failed attempts | ||||||
|  | 	locked, remainingTime := h.Auth.IsAccountLocked(rateIdentifier) | ||||||
|  | 	if locked { | ||||||
|  | 		log.Warn().Str("identifier", rateIdentifier).Int("remaining_seconds", remainingTime).Msg("Account is locked due to too many failed login attempts") | ||||||
|  | 		c.JSON(429, gin.H{ | ||||||
|  | 			"status":  429, | ||||||
|  | 			"message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime), | ||||||
|  | 		}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Get user based on username | 	// Get user based on username | ||||||
| 	user := h.Auth.GetUser(login.Username) | 	user := h.Auth.GetUser(login.Username) | ||||||
|  |  | ||||||
| 	// User does not exist | 	// User does not exist | ||||||
| 	if user == nil { | 	if user == nil { | ||||||
| 		log.Debug().Str("username", login.Username).Msg("User not found") | 		log.Debug().Str("username", login.Username).Msg("User not found") | ||||||
|  | 		// Record failed login attempt | ||||||
|  | 		h.Auth.RecordLoginAttempt(rateIdentifier, false) | ||||||
| 		c.JSON(401, gin.H{ | 		c.JSON(401, gin.H{ | ||||||
| 			"status":  401, | 			"status":  401, | ||||||
| 			"message": "Unauthorized", | 			"message": "Unauthorized", | ||||||
| @@ -267,6 +289,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) { | |||||||
| 	// Check if password is correct | 	// Check if password is correct | ||||||
| 	if !h.Auth.CheckPassword(*user, login.Password) { | 	if !h.Auth.CheckPassword(*user, login.Password) { | ||||||
| 		log.Debug().Str("username", login.Username).Msg("Password incorrect") | 		log.Debug().Str("username", login.Username).Msg("Password incorrect") | ||||||
|  | 		// Record failed login attempt | ||||||
|  | 		h.Auth.RecordLoginAttempt(rateIdentifier, false) | ||||||
| 		c.JSON(401, gin.H{ | 		c.JSON(401, gin.H{ | ||||||
| 			"status":  401, | 			"status":  401, | ||||||
| 			"message": "Unauthorized", | 			"message": "Unauthorized", | ||||||
| @@ -276,6 +300,9 @@ func (h *Handlers) LoginHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	log.Debug().Msg("Password correct, checking totp") | 	log.Debug().Msg("Password correct, checking totp") | ||||||
|  |  | ||||||
|  | 	// Record successful login attempt (will reset failed attempt counter) | ||||||
|  | 	h.Auth.RecordLoginAttempt(rateIdentifier, true) | ||||||
|  |  | ||||||
| 	// Check if user has totp enabled | 	// Check if user has totp enabled | ||||||
| 	if user.TotpSecret != "" { | 	if user.TotpSecret != "" { | ||||||
| 		log.Debug().Msg("Totp enabled") | 		log.Debug().Msg("Totp enabled") | ||||||
| @@ -393,9 +420,6 @@ func (h *Handlers) LogoutHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	log.Debug().Msg("Cleaning up redirect cookie") | 	log.Debug().Msg("Cleaning up redirect cookie") | ||||||
|  |  | ||||||
| 	// Clean up redirect cookie if it exists |  | ||||||
| 	c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true) |  | ||||||
|  |  | ||||||
| 	// Return logged out | 	// Return logged out | ||||||
| 	c.JSON(200, gin.H{ | 	c.JSON(200, gin.H{ | ||||||
| 		"status":  200, | 		"status":  200, | ||||||
| @@ -502,7 +526,9 @@ func (h *Handlers) OauthUrlHandler(c *gin.Context) { | |||||||
| 	// Set redirect cookie if redirect URI is provided | 	// Set redirect cookie if redirect URI is provided | ||||||
| 	if redirectURI != "" { | 	if redirectURI != "" { | ||||||
| 		log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie") | 		log.Debug().Str("redirectURI", redirectURI).Msg("Setting redirect cookie") | ||||||
| 		c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", h.Config.Domain, h.Config.CookieSecure, true) | 		h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||||
|  | 			RedirectURI: redirectURI, | ||||||
|  | 		}) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it | 	// Tailscale does not have an auth url so we create a random code (does not need to be secure) to avoid caching and send it | ||||||
| @@ -624,28 +650,25 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) { | |||||||
|  |  | ||||||
| 	log.Debug().Msg("Email whitelisted") | 	log.Debug().Msg("Email whitelisted") | ||||||
|  |  | ||||||
| 	// Create session cookie | 	// Get redirect URI | ||||||
|  | 	cookie, err := h.Auth.GetSessionCookie(c) | ||||||
|  |  | ||||||
|  | 	// Create session cookie (also cleans up redirect cookie) | ||||||
| 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | 	h.Auth.CreateSessionCookie(c, &types.SessionCookie{ | ||||||
| 		Username: email, | 		Username: email, | ||||||
| 		Provider: providerName.Provider, | 		Provider: providerName.Provider, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	// Get redirect URI |  | ||||||
| 	redirectURI, err := c.Cookie("tinyauth_redirect_uri") |  | ||||||
|  |  | ||||||
| 	// If it is empty it means that no redirect_uri was provided to the login screen so we just log in | 	// If it is empty it means that no redirect_uri was provided to the login screen so we just log in | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL) | 		c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	log.Debug().Str("redirectURI", redirectURI).Msg("Got redirect URI") | 	log.Debug().Str("redirectURI", cookie.RedirectURI).Msg("Got redirect URI") | ||||||
|  |  | ||||||
| 	// Clean up redirect cookie since we already have the value |  | ||||||
| 	c.SetCookie("tinyauth_redirect_uri", "", -1, "/", h.Config.Domain, h.Config.CookieSecure, true) |  | ||||||
|  |  | ||||||
| 	// Build query | 	// Build query | ||||||
| 	queries, err := query.Values(types.LoginQuery{ | 	queries, err := query.Values(types.LoginQuery{ | ||||||
| 		RedirectURI: redirectURI, | 		RedirectURI: cookie.RedirectURI, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	log.Debug().Msg("Got redirect query") | 	log.Debug().Msg("Got redirect query") | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ type Hooks struct { | |||||||
|  |  | ||||||
| func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | ||||||
| 	// Get session cookie and basic auth | 	// Get session cookie and basic auth | ||||||
| 	cookie := hooks.Auth.GetSessionCookie(c) | 	cookie, err := hooks.Auth.GetSessionCookie(c) | ||||||
| 	basic := hooks.Auth.GetBasicAuth(c) | 	basic := hooks.Auth.GetBasicAuth(c) | ||||||
|  |  | ||||||
| 	// Check if basic auth is set | 	// Check if basic auth is set | ||||||
| @@ -46,6 +46,19 @@ func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext { | |||||||
|  |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Check cookie error after basic auth | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error().Err(err).Msg("Failed to get session cookie") | ||||||
|  | 		// Return empty context | ||||||
|  | 		return types.UserContext{ | ||||||
|  | 			Username:    "", | ||||||
|  | 			IsLoggedIn:  false, | ||||||
|  | 			OAuth:       false, | ||||||
|  | 			Provider:    "", | ||||||
|  | 			TotpPending: false, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Check if session cookie has totp pending | 	// Check if session cookie has totp pending | ||||||
| 	if cookie.TotpPending { | 	if cookie.TotpPending { | ||||||
| 		log.Debug().Msg("Totp pending") | 		log.Debug().Msg("Totp pending") | ||||||
|   | |||||||
							
								
								
									
										59
									
								
								internal/types/api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								internal/types/api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | package types | ||||||
|  |  | ||||||
|  | // LoginQuery is the query parameters for the login endpoint | ||||||
|  | type LoginQuery struct { | ||||||
|  | 	RedirectURI string `url:"redirect_uri"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LoginRequest is the request body for the login endpoint | ||||||
|  | type LoginRequest struct { | ||||||
|  | 	Username string `json:"username"` | ||||||
|  | 	Password string `json:"password"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // OAuthRequest is the request for the OAuth endpoint | ||||||
|  | type OAuthRequest struct { | ||||||
|  | 	Provider string `uri:"provider" binding:"required"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // UnauthorizedQuery is the query parameters for the unauthorized endpoint | ||||||
|  | type UnauthorizedQuery struct { | ||||||
|  | 	Username string `url:"username"` | ||||||
|  | 	Resource string `url:"resource"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TailscaleQuery is the query parameters for the tailscale endpoint | ||||||
|  | type TailscaleQuery struct { | ||||||
|  | 	Code int `url:"code"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Proxy is the uri parameters for the proxy endpoint | ||||||
|  | type Proxy struct { | ||||||
|  | 	Proxy string `uri:"proxy" binding:"required"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // User Context response is the response for the user context endpoint | ||||||
|  | type UserContextResponse struct { | ||||||
|  | 	Status      int    `json:"status"` | ||||||
|  | 	Message     string `json:"message"` | ||||||
|  | 	IsLoggedIn  bool   `json:"isLoggedIn"` | ||||||
|  | 	Username    string `json:"username"` | ||||||
|  | 	Provider    string `json:"provider"` | ||||||
|  | 	Oauth       bool   `json:"oauth"` | ||||||
|  | 	TotpPending bool   `json:"totpPending"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // App Context is the response for the app context endpoint | ||||||
|  | type AppContext struct { | ||||||
|  | 	Status              int      `json:"status"` | ||||||
|  | 	Message             string   `json:"message"` | ||||||
|  | 	ConfiguredProviders []string `json:"configuredProviders"` | ||||||
|  | 	DisableContinue     bool     `json:"disableContinue"` | ||||||
|  | 	Title               string   `json:"title"` | ||||||
|  | 	GenericName         string   `json:"genericName"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Totp request is the request for the totp endpoint | ||||||
|  | type TotpRequest struct { | ||||||
|  | 	Code string `json:"code"` | ||||||
|  | } | ||||||
							
								
								
									
										81
									
								
								internal/types/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								internal/types/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | package types | ||||||
|  |  | ||||||
|  | // Config is the configuration for the tinyauth server | ||||||
|  | type Config struct { | ||||||
|  | 	Port                      int    `mapstructure:"port" validate:"required"` | ||||||
|  | 	Address                   string `validate:"required,ip4_addr" mapstructure:"address"` | ||||||
|  | 	Secret                    string `validate:"required,len=32" mapstructure:"secret"` | ||||||
|  | 	SecretFile                string `mapstructure:"secret-file"` | ||||||
|  | 	AppURL                    string `validate:"required,url" mapstructure:"app-url"` | ||||||
|  | 	Users                     string `mapstructure:"users"` | ||||||
|  | 	UsersFile                 string `mapstructure:"users-file"` | ||||||
|  | 	CookieSecure              bool   `mapstructure:"cookie-secure"` | ||||||
|  | 	GithubClientId            string `mapstructure:"github-client-id"` | ||||||
|  | 	GithubClientSecret        string `mapstructure:"github-client-secret"` | ||||||
|  | 	GithubClientSecretFile    string `mapstructure:"github-client-secret-file"` | ||||||
|  | 	GoogleClientId            string `mapstructure:"google-client-id"` | ||||||
|  | 	GoogleClientSecret        string `mapstructure:"google-client-secret"` | ||||||
|  | 	GoogleClientSecretFile    string `mapstructure:"google-client-secret-file"` | ||||||
|  | 	TailscaleClientId         string `mapstructure:"tailscale-client-id"` | ||||||
|  | 	TailscaleClientSecret     string `mapstructure:"tailscale-client-secret"` | ||||||
|  | 	TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"` | ||||||
|  | 	GenericClientId           string `mapstructure:"generic-client-id"` | ||||||
|  | 	GenericClientSecret       string `mapstructure:"generic-client-secret"` | ||||||
|  | 	GenericClientSecretFile   string `mapstructure:"generic-client-secret-file"` | ||||||
|  | 	GenericScopes             string `mapstructure:"generic-scopes"` | ||||||
|  | 	GenericAuthURL            string `mapstructure:"generic-auth-url"` | ||||||
|  | 	GenericTokenURL           string `mapstructure:"generic-token-url"` | ||||||
|  | 	GenericUserURL            string `mapstructure:"generic-user-url"` | ||||||
|  | 	GenericName               string `mapstructure:"generic-name"` | ||||||
|  | 	DisableContinue           bool   `mapstructure:"disable-continue"` | ||||||
|  | 	OAuthWhitelist            string `mapstructure:"oauth-whitelist"` | ||||||
|  | 	SessionExpiry             int    `mapstructure:"session-expiry"` | ||||||
|  | 	LogLevel                  int8   `mapstructure:"log-level" validate:"min=-1,max=5"` | ||||||
|  | 	Title                     string `mapstructure:"app-title"` | ||||||
|  | 	EnvFile                   string `mapstructure:"env-file"` | ||||||
|  | 	LoginTimeout              int    `mapstructure:"login-timeout"` | ||||||
|  | 	LoginMaxRetries           int    `mapstructure:"login-max-retries"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Server configuration | ||||||
|  | type HandlersConfig struct { | ||||||
|  | 	AppURL          string | ||||||
|  | 	DisableContinue bool | ||||||
|  | 	GenericName     string | ||||||
|  | 	Title           string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // OAuthConfig is the configuration for the providers | ||||||
|  | type OAuthConfig struct { | ||||||
|  | 	GithubClientId        string | ||||||
|  | 	GithubClientSecret    string | ||||||
|  | 	GoogleClientId        string | ||||||
|  | 	GoogleClientSecret    string | ||||||
|  | 	TailscaleClientId     string | ||||||
|  | 	TailscaleClientSecret string | ||||||
|  | 	GenericClientId       string | ||||||
|  | 	GenericClientSecret   string | ||||||
|  | 	GenericScopes         []string | ||||||
|  | 	GenericAuthURL        string | ||||||
|  | 	GenericTokenURL       string | ||||||
|  | 	GenericUserURL        string | ||||||
|  | 	AppURL                string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // APIConfig is the configuration for the API | ||||||
|  | type APIConfig struct { | ||||||
|  | 	Port    int | ||||||
|  | 	Address string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AuthConfig is the configuration for the auth service | ||||||
|  | type AuthConfig struct { | ||||||
|  | 	Users           Users | ||||||
|  | 	OauthWhitelist  []string | ||||||
|  | 	SessionExpiry   int | ||||||
|  | 	Secret          string | ||||||
|  | 	CookieSecure    bool | ||||||
|  | 	Domain          string | ||||||
|  | 	LoginTimeout    int | ||||||
|  | 	LoginMaxRetries int | ||||||
|  | } | ||||||
| @@ -1,17 +1,9 @@ | |||||||
| package types | package types | ||||||
|  |  | ||||||
| import "tinyauth/internal/oauth" | import ( | ||||||
|  | 	"time" | ||||||
| // LoginQuery is the query parameters for the login endpoint | 	"tinyauth/internal/oauth" | ||||||
| type LoginQuery struct { | ) | ||||||
| 	RedirectURI string `url:"redirect_uri"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // LoginRequest is the request body for the login endpoint |  | ||||||
| type LoginRequest struct { |  | ||||||
| 	Username string `json:"username"` |  | ||||||
| 	Password string `json:"password"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // User is the struct for a user | // User is the struct for a user | ||||||
| type User struct { | type User struct { | ||||||
| @@ -23,39 +15,27 @@ type User struct { | |||||||
| // Users is a list of users | // Users is a list of users | ||||||
| type Users []User | type Users []User | ||||||
|  |  | ||||||
| // Config is the configuration for the tinyauth server | // OAuthProviders is the struct for the OAuth providers | ||||||
| type Config struct { | type OAuthProviders struct { | ||||||
| 	Port                      int    `mapstructure:"port" validate:"required"` | 	Github    *oauth.OAuth | ||||||
| 	Address                   string `validate:"required,ip4_addr" mapstructure:"address"` | 	Google    *oauth.OAuth | ||||||
| 	Secret                    string `validate:"required,len=32" mapstructure:"secret"` | 	Microsoft *oauth.OAuth | ||||||
| 	SecretFile                string `mapstructure:"secret-file"` | } | ||||||
| 	AppURL                    string `validate:"required,url" mapstructure:"app-url"` |  | ||||||
| 	Users                     string `mapstructure:"users"` | // SessionCookie is the cookie for the session (exculding the expiry) | ||||||
| 	UsersFile                 string `mapstructure:"users-file"` | type SessionCookie struct { | ||||||
| 	CookieSecure              bool   `mapstructure:"cookie-secure"` | 	Username    string | ||||||
| 	GithubClientId            string `mapstructure:"github-client-id"` | 	Provider    string | ||||||
| 	GithubClientSecret        string `mapstructure:"github-client-secret"` | 	TotpPending bool | ||||||
| 	GithubClientSecretFile    string `mapstructure:"github-client-secret-file"` | 	RedirectURI string | ||||||
| 	GoogleClientId            string `mapstructure:"google-client-id"` | } | ||||||
| 	GoogleClientSecret        string `mapstructure:"google-client-secret"` |  | ||||||
| 	GoogleClientSecretFile    string `mapstructure:"google-client-secret-file"` | // TinyauthLabels is the labels for the tinyauth container | ||||||
| 	TailscaleClientId         string `mapstructure:"tailscale-client-id"` | type TinyauthLabels struct { | ||||||
| 	TailscaleClientSecret     string `mapstructure:"tailscale-client-secret"` | 	OAuthWhitelist []string | ||||||
| 	TailscaleClientSecretFile string `mapstructure:"tailscale-client-secret-file"` | 	Users          []string | ||||||
| 	GenericClientId           string `mapstructure:"generic-client-id"` | 	Allowed        string | ||||||
| 	GenericClientSecret       string `mapstructure:"generic-client-secret"` | 	Headers        map[string]string | ||||||
| 	GenericClientSecretFile   string `mapstructure:"generic-client-secret-file"` |  | ||||||
| 	GenericScopes             string `mapstructure:"generic-scopes"` |  | ||||||
| 	GenericAuthURL            string `mapstructure:"generic-auth-url"` |  | ||||||
| 	GenericTokenURL           string `mapstructure:"generic-token-url"` |  | ||||||
| 	GenericUserURL            string `mapstructure:"generic-user-url"` |  | ||||||
| 	GenericName               string `mapstructure:"generic-name"` |  | ||||||
| 	DisableContinue           bool   `mapstructure:"disable-continue"` |  | ||||||
| 	OAuthWhitelist            string `mapstructure:"oauth-whitelist"` |  | ||||||
| 	SessionExpiry             int    `mapstructure:"session-expiry"` |  | ||||||
| 	LogLevel                  int8   `mapstructure:"log-level" validate:"min=-1,max=5"` |  | ||||||
| 	Title                     string `mapstructure:"app-title"` |  | ||||||
| 	EnvFile                   string `mapstructure:"env-file"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // UserContext is the context for the user | // UserContext is the context for the user | ||||||
| @@ -67,108 +47,9 @@ type UserContext struct { | |||||||
| 	TotpPending bool | 	TotpPending bool | ||||||
| } | } | ||||||
|  |  | ||||||
| // APIConfig is the configuration for the API | // LoginAttempt tracks information about login attempts for rate limiting | ||||||
| type APIConfig struct { | type LoginAttempt struct { | ||||||
| 	Port          int | 	FailedAttempts int | ||||||
| 	Address       string | 	LastAttempt    time.Time | ||||||
| 	Secret        string | 	LockedUntil    time.Time | ||||||
| 	CookieSecure  bool |  | ||||||
| 	SessionExpiry int |  | ||||||
| 	Domain        string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // OAuthConfig is the configuration for the providers |  | ||||||
| type OAuthConfig struct { |  | ||||||
| 	GithubClientId        string |  | ||||||
| 	GithubClientSecret    string |  | ||||||
| 	GoogleClientId        string |  | ||||||
| 	GoogleClientSecret    string |  | ||||||
| 	TailscaleClientId     string |  | ||||||
| 	TailscaleClientSecret string |  | ||||||
| 	GenericClientId       string |  | ||||||
| 	GenericClientSecret   string |  | ||||||
| 	GenericScopes         []string |  | ||||||
| 	GenericAuthURL        string |  | ||||||
| 	GenericTokenURL       string |  | ||||||
| 	GenericUserURL        string |  | ||||||
| 	AppURL                string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // OAuthRequest is the request for the OAuth endpoint |  | ||||||
| type OAuthRequest struct { |  | ||||||
| 	Provider string `uri:"provider" binding:"required"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // OAuthProviders is the struct for the OAuth providers |  | ||||||
| type OAuthProviders struct { |  | ||||||
| 	Github    *oauth.OAuth |  | ||||||
| 	Google    *oauth.OAuth |  | ||||||
| 	Microsoft *oauth.OAuth |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // UnauthorizedQuery is the query parameters for the unauthorized endpoint |  | ||||||
| type UnauthorizedQuery struct { |  | ||||||
| 	Username string `url:"username"` |  | ||||||
| 	Resource string `url:"resource"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SessionCookie is the cookie for the session (exculding the expiry) |  | ||||||
| type SessionCookie struct { |  | ||||||
| 	Username    string |  | ||||||
| 	Provider    string |  | ||||||
| 	TotpPending bool |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // TinyauthLabels is the labels for the tinyauth container |  | ||||||
| type TinyauthLabels struct { |  | ||||||
| 	OAuthWhitelist []string |  | ||||||
| 	Users          []string |  | ||||||
| 	Allowed        string |  | ||||||
| 	Headers        map[string]string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // TailscaleQuery is the query parameters for the tailscale endpoint |  | ||||||
| type TailscaleQuery struct { |  | ||||||
| 	Code int `url:"code"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Proxy is the uri parameters for the proxy endpoint |  | ||||||
| type Proxy struct { |  | ||||||
| 	Proxy string `uri:"proxy" binding:"required"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // User Context response is the response for the user context endpoint |  | ||||||
| type UserContextResponse struct { |  | ||||||
| 	Status      int    `json:"status"` |  | ||||||
| 	Message     string `json:"message"` |  | ||||||
| 	IsLoggedIn  bool   `json:"isLoggedIn"` |  | ||||||
| 	Username    string `json:"username"` |  | ||||||
| 	Provider    string `json:"provider"` |  | ||||||
| 	Oauth       bool   `json:"oauth"` |  | ||||||
| 	TotpPending bool   `json:"totpPending"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // App Context is the response for the app context endpoint |  | ||||||
| type AppContext struct { |  | ||||||
| 	Status              int      `json:"status"` |  | ||||||
| 	Message             string   `json:"message"` |  | ||||||
| 	ConfiguredProviders []string `json:"configuredProviders"` |  | ||||||
| 	DisableContinue     bool     `json:"disableContinue"` |  | ||||||
| 	Title               string   `json:"title"` |  | ||||||
| 	GenericName         string   `json:"genericName"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Totp request is the request for the totp endpoint |  | ||||||
| type TotpRequest struct { |  | ||||||
| 	Code string `json:"code"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Server configuration |  | ||||||
| type HandlersConfig struct { |  | ||||||
| 	AppURL          string |  | ||||||
| 	Domain          string |  | ||||||
| 	CookieSecure    bool |  | ||||||
| 	DisableContinue bool |  | ||||||
| 	GenericName     string |  | ||||||
| 	Title           string |  | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user