mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-04 08:05:42 +00:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			feat/multi
			...
			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