mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-30 05:35:44 +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