Compare commits

..

14 Commits

Author SHA1 Message Date
Stavros
496a56676d New Crowdin updates (#63)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)
2025-04-08 16:41:47 +03:00
Stavros
57e25524c7 chore: update screenshot 2025-04-08 16:33:29 +03:00
Stavros
614a9b468a chore: rename username placeholder 2025-04-08 16:32:52 +03:00
Stavros
94a5359080 chore: add screenshot to readme 2025-04-08 16:30:50 +03:00
Stavros
38c5cd7b32 fix: tinyauth should allow the user to access a resource if a whitelist is not setup 2025-04-08 16:24:25 +03:00
Stavros
c664be5cc5 New Crowdin updates (#61)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)
2025-04-08 15:12:25 +03:00
Stavros
bafcb9a867 feat: add rate limit warning to frontend 2025-04-08 14:52:02 +03:00
Stavros
d322c13791 chore: add star history to readme 2025-04-08 09:23:08 +03:00
Stavros
8e84e59c2f refactor: simplify the get cookie data handling 2025-04-06 20:53:24 +03:00
Stavros
bd7e160e10 refactor: store redirect URI in tinyauth session cookie 2025-04-06 20:37:02 +03:00
Stavros
df849d5a5c refactor: remove dependency on gin sessions 2025-04-06 19:13:09 +03:00
Stavros
5cf4e208c6 refactor: use centralized config in auth service 2025-04-06 18:55:24 +03:00
Alexander
07ddd4f917 feat: add brute force protection (#59)
* feat: add brute force protection

* fix: bind flags to env

---------

Co-authored-by: Stavros <steveiliop56@gmail.com>
2025-04-06 18:28:20 +03:00
Stavros
98abe514e1 refactor: add basic versioning to translations 2025-04-06 15:43:30 +03:00
51 changed files with 585 additions and 220 deletions

View File

@@ -26,5 +26,7 @@ DISABLE_CONTINUE=false
OAUTH_WHITELIST=
GENERIC_NAME=My OAuth
SESSION_EXPIRY=7200
LOGIN_TIMEOUT=300
LOGIN_MAX_RETRIES=5
LOG_LEVEL=0
APP_TITLE=Tinyauth SSO

View File

@@ -3,7 +3,7 @@ name: Publish translations
on:
push:
branches:
- main
- i18n_v*
workflow_dispatch:
permissions:
@@ -16,7 +16,53 @@ concurrency:
cancel-in-progress: false
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:
needs: get-translations
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -25,10 +71,14 @@ jobs:
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Move translations
- name: Prepare output directory
run: |
mkdir -p dist
mv frontend/src/lib/i18n/locales dist/i18n
mkdir -p dist/i18n/
- name: Download translations
uses: actions/download-artifact@v4
with:
path: dist/i18n/
- name: Upload artifact
uses: actions/upload-pages-artifact@v3

View File

@@ -17,6 +17,8 @@
Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic OAuth provider to all of your docker apps. It is made for traefik but it can be extended to work with all reverse proxies like caddy and nginx.
![Login](assets/login.png)
> [!WARNING]
> Tinyauth is in active development and configuration may change often. Please make sure to carefully read the release notes before updating.
@@ -61,3 +63,7 @@ Credits for the logo of this app go to:
- **Freepik** for providing the police hat and badge.
- **Renee French** for the original gopher logo.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=steveiliop56/tinyauth&type=Date)](https://www.star-history.com/#steveiliop56/tinyauth&Date)

BIN
assets/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -2,7 +2,6 @@ package cmd
import (
"errors"
"fmt"
"os"
"strings"
"time"
@@ -94,10 +93,8 @@ var rootCmd = &cobra.Command{
}
// Create handlers config
serverConfig := types.HandlersConfig{
handlersConfig := types.HandlersConfig{
AppURL: config.AppURL,
Domain: fmt.Sprintf(".%s", domain),
CookieSecure: config.CookieSecure,
DisableContinue: config.DisableContinue,
Title: config.Title,
GenericName: config.GenericName,
@@ -109,6 +106,18 @@ var rootCmd = &cobra.Command{
Address: config.Address,
}
// Create auth config
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
docker := docker.NewDocker()
@@ -117,14 +126,7 @@ var rootCmd = &cobra.Command{
HandleError(err, "Failed to initialize docker")
// Create auth service
auth := auth.NewAuth(types.AuthConfig{
Domain: domain,
Secret: config.Secret,
SessionExpiry: config.SessionExpiry,
CookieSecure: config.CookieSecure,
Users: users,
OAuthWhitelist: oauthWhitelist,
}, docker)
auth := auth.NewAuth(authConfig, docker)
// Create OAuth providers service
providers := providers.NewProviders(oauthConfig)
@@ -136,7 +138,7 @@ var rootCmd = &cobra.Command{
hooks := hooks.NewHooks(auth, providers)
// Create handlers
handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers, docker)
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
// Create API
api := api.NewAPI(apiConfig, handlers)
@@ -201,6 +203,8 @@ func init() {
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().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().String("app-title", "Tinyauth", "Title of the app.")
@@ -235,6 +239,8 @@ func init() {
viper.BindEnv("session-expiry", "SESSION_EXPIRY")
viper.BindEnv("log-level", "LOG_LEVEL")
viper.BindEnv("app-title", "APP_TITLE")
viper.BindEnv("login-timeout", "LOGIN_TIMEOUT")
viper.BindEnv("login-max-retries", "LOGIN_MAX_RETRIES")
// Bind flags to viper
viper.BindPFlags(rootCmd.Flags())

View File

@@ -25,7 +25,7 @@ export const LoginForm = (props: LoginFormProps) => {
<form onSubmit={form.onSubmit(onSubmit)}>
<TextInput
label={t("loginUsername")}
placeholder="user@example.com"
placeholder="username"
required
disabled={isLoading}
key={form.key("username")}

View File

@@ -28,7 +28,7 @@ i18n
],
backendOptions: [
{
loadPath: "https://cdn.tinyauth.app/i18n/{{lng}}.json",
loadPath: "https://cdn.tinyauth.app/i18n/v1/{{lng}}.json",
},
],
},

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Είσοδος",
"loginFailTitle": "Αποτυχία σύνδεσης",
"loginFailSubtitle": "Παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό πρόσβασης",
"loginFailRateLimit": "Αποτύχατε να συνδεθείτε πάρα πολλές φορές, παρακαλώ προσπαθήστε ξανά αργότερα",
"loginSuccessTitle": "Συνδεδεμένος",
"loginSuccessSubtitle": "Καλώς ήρθατε!",
"loginOauthFailTitle": "Εσωτερικό σφάλμα",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Ανακατεύθυνση στην εφαρμογή σας",
"totpTitle": "Εισάγετε τον κωδικό TOTP",
"unauthorizedTitle": "Μη εξουσιοδοτημένο",
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να έχει πρόσβαση στον πόρο <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη {{username}} δεν είναι εξουσιοδοτημένος να συνδεθεί.",
"unauthorizedResourceSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}<Code/> δεν έχει άδεια πρόσβασης στον πόρο <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "Ο χρήστης με όνομα χρήστη <Code>{{username}}<Code/> δεν είναι εξουσιοδοτημένος να συνδεθεί.",
"unauthorizedButton": "Προσπαθήστε ξανά"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Se connecter",
"loginFailTitle": "Échec de la connexion",
"loginFailSubtitle": "Veuillez vérifier votre nom d'utilisateur et votre mot de passe",
"loginFailRateLimit": "Vous n'avez pas pu vous connecter trop de fois, veuillez réessayer plus tard",
"loginSuccessTitle": "Connecté",
"loginSuccessSubtitle": "Bienvenue!",
"loginOauthFailTitle": "Erreur interne",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirection vers votre application",
"totpTitle": "Saisissez votre code TOTP",
"unauthorizedTitle": "Non autorisé",
"unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur {{username}} n'est pas autorisé à se connecter.",
"unauthorizedResourceSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}<Code/> n'est pas autorisé à accéder à la ressource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "L'utilisateur avec le nom d'utilisateur <Code>{{username}}<Code/> n'est pas autorisé à se connecter.",
"unauthorizedButton": "Réessayer"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -3,9 +3,10 @@
"loginDivider": "Lub kontynuuj z hasłem",
"loginUsername": "Nazwa użytkownika",
"loginPassword": "Hasło",
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginSubmit": "Zaloguj się",
"loginFailTitle": "Nie udało się zalogować",
"loginFailSubtitle": "Sprawdź swoją nazwę użytkownika i hasło",
"loginFailRateLimit": "Nie udało się zalogować zbyt wiele razy, spróbuj ponownie później",
"loginSuccessTitle": "Zalogowano",
"loginSuccessSubtitle": "Witaj ponownie!",
"loginOauthFailTitle": "Wewnętrzny błąd",
@@ -21,9 +22,9 @@
"continueTitle": "Kontynuuj",
"continueSubtitle": "Kliknij przycisk, aby przejść do aplikacji.",
"internalErrorTitle": "Wewnętrzny błąd serwera",
"internalErrorSubtitle": "An error occurred on the server and it currently cannot serve your request.",
"internalErrorSubtitle": "Wystąpił błąd na serwerze i obecnie nie można obsłużyć tego żądania.",
"internalErrorButton": "Spróbuj ponownie",
"logoutFailTitle": "Failed to log out",
"logoutFailTitle": "Nie udało się wylogować",
"logoutFailSubtitle": "Spróbuj ponownie",
"logoutSuccessTitle": "Wylogowano",
"logoutSuccessSubtitle": "Zostałeś wylogowany",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Przekierowywanie do aplikacji",
"totpTitle": "Wprowadź kod TOTP",
"unauthorizedTitle": "Nieautoryzowany",
"unauthorizedResourceSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "Użytkownik o nazwie {{username}} nie jest upoważniony do logowania.",
"unauthorizedResourceSubtitle": "Użytkownik o nazwie <Code>{{username}}<Code/> nie jest upoważniony do uzyskania dostępu do zasobu <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "Użytkownik o nazwie <Code>{{username}}<Code/> nie jest upoważniony do logowania.",
"unauthorizedButton": "Spróbuj ponownie"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -6,6 +6,7 @@
"loginSubmit": "Login",
"loginFailTitle": "Failed to log in",
"loginFailSubtitle": "Please check your username and password",
"loginFailRateLimit": "You failed to login too many times, please try again later",
"loginSuccessTitle": "Logged in",
"loginSuccessSubtitle": "Welcome back!",
"loginOauthFailTitle": "Internal error",
@@ -39,7 +40,7 @@
"totpSuccessSubtitle": "Redirecting to your app",
"totpTitle": "Enter your TOTP code",
"unauthorizedTitle": "Unauthorized",
"unauthorizedResourceSubtitle": "The user with username {{username}} is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username {{username}} is not authorized to login.",
"unauthorizedResourceSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to access the resource <Code>{{resource}}</Code>.",
"unaothorizedLoginSubtitle": "The user with username <Code>{{username}}<Code/> is not authorized to login.",
"unauthorizedButton": "Try again"
}

View File

@@ -1,7 +1,7 @@
import { Paper, Title, Text, Divider } from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import axios, { type AxiosError } from "axios";
import { useUserContext } from "../context/user-context";
import { Navigate } from "react-router";
import { Layout } from "../components/layouts/layout";
@@ -33,7 +33,17 @@ export const LoginPage = () => {
mutationFn: (login: LoginFormValues) => {
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({
title: t("loginFailTitle"),
message: t("loginFailSubtitle"),

View File

@@ -37,6 +37,7 @@ export const UnauthorizedPage = () => {
<Trans
i18nKey="unauthorizedLoginSubtitle"
t={t}
components={{ Code: <Code /> }}
values={{ username }}
/>
</Text>

View File

@@ -17,15 +17,15 @@ import (
func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
return &API{
Handlers: handlers,
Config: config,
Handlers: handlers,
}
}
type API struct {
Config types.APIConfig
Router *gin.Engine
Handlers *handlers.Handlers
Config types.APIConfig
}
func (api *API) Init() {

View File

@@ -19,12 +19,6 @@ import (
"github.com/magiconair/properties/assert"
)
// User
var User = types.User{
Username: "user",
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
}
// Simple API config for tests
var apiConfig = types.APIConfig{
Port: 8080,
@@ -34,8 +28,6 @@ var apiConfig = types.APIConfig{
// Simple handlers config for tests
var handlersConfig = types.HandlersConfig{
AppURL: "http://localhost:8080",
Domain: ".localhost",
CookieSecure: false,
DisableContinue: false,
Title: "Tinyauth",
GenericName: "Generic",
@@ -43,19 +35,24 @@ var handlersConfig = types.HandlersConfig{
// Simple auth config for tests
var authConfig = types.AuthConfig{
Domain: "localhost",
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
CookieSecure: false,
SessionExpiry: 3600,
Users: types.Users{
User,
},
OAuthWhitelist: []string{},
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
var cookie string
// User
var user = types.User{
Username: "user",
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
}
// We need all this to be able to test the API
func getAPI(t *testing.T) *api.API {
// Create docker service
@@ -70,6 +67,12 @@ func getAPI(t *testing.T) *api.API {
}
// Create auth service
authConfig.Users = types.Users{
{
Username: user.Username,
Password: user.Password,
},
}
auth := auth.NewAuth(authConfig, docker)
// Create providers service

View File

@@ -2,9 +2,11 @@ package auth
import (
"fmt"
"net/http"
"regexp"
"slices"
"strings"
"sync"
"time"
"tinyauth/internal/docker"
"tinyauth/internal/types"
@@ -17,14 +19,41 @@ import (
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
return &Auth{
Docker: docker,
Config: config,
Config: config,
Docker: docker,
LoginAttempts: make(map[string]*types.LoginAttempt),
}
}
type Auth struct {
Docker *docker.Docker
Config types.AuthConfig
Config types.AuthConfig
Docker *docker.Docker
LoginAttempts map[string]*types.LoginAttempt
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 {
@@ -42,14 +71,78 @@ func (auth *Auth) CheckPassword(user types.User, password string) bool {
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 {
// If the whitelist is empty, allow all emails
if len(auth.Config.OAuthWhitelist) == 0 {
if len(auth.Config.OauthWhitelist) == 0 {
return true
}
// Loop through the whitelist and return true if the email matches
for _, email := range auth.Config.OAuthWhitelist {
for _, email := range auth.Config.OauthWhitelist {
if email == emailSrc {
return true
}
@@ -59,33 +152,13 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
return false
}
func (auth *Auth) GetCookieStore() *sessions.CookieStore {
// Create a new cookie store
store := sessions.NewCookieStore([]byte(auth.Config.Secret))
// Configure the cookie store
store.Options = &sessions.Options{
Path: "/",
Domain: fmt.Sprintf(".%s", auth.Config.Domain),
Secure: auth.Config.CookieSecure,
MaxAge: auth.Config.SessionExpiry,
HttpOnly: true,
}
// Set the cookie store
return store
}
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
log.Debug().Msg("Creating session cookie")
// Get cookie store
store := auth.GetCookieStore()
// Get session
sessions, err := store.Get(c.Request, "tinyauth")
session, err := auth.GetSession(c)
if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return err
}
@@ -101,15 +174,16 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
}
// Set data
sessions.Values["username"] = data.Username
sessions.Values["provider"] = data.Provider
sessions.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
sessions.Values["totpPending"] = data.TotpPending
session.Values["username"] = data.Username
session.Values["provider"] = data.Provider
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
session.Values["totpPending"] = data.TotpPending
session.Values["redirectURI"] = data.RedirectURI
// Save session
err = sessions.Save(c.Request, c.Writer)
err = session.Save(c.Request, c.Writer)
if err != nil {
log.Error().Err(err).Msg("Failed to save session")
return err
}
@@ -120,25 +194,22 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
log.Debug().Msg("Deleting session cookie")
// Get cookie store
store := auth.GetCookieStore()
// Get session
sessions, err := store.Get(c.Request, "tinyauth")
session, err := auth.GetSession(c)
if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return err
}
// Clear session
for key := range sessions.Values {
delete(sessions.Values, key)
// Delete all values in the session
for key := range session.Values {
delete(session.Values, key)
}
// Save session
err = sessions.Save(c.Request, c.Writer)
err = session.Save(c.Request, c.Writer)
if err != nil {
log.Error().Err(err).Msg("Failed to save session")
return err
}
@@ -149,31 +220,22 @@ func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
log.Debug().Msg("Getting session cookie")
// Get cookie store
store := auth.GetCookieStore()
// Get session
sessions, err := store.Get(c.Request, "tinyauth")
session, err := auth.GetSession(c)
if err != nil {
log.Error().Err(err).Msg("Failed to get session")
return types.SessionCookie{}, err
}
// Get data
cookieUsername := sessions.Values["username"]
cookieProvider := sessions.Values["provider"]
cookieExpiry := sessions.Values["expiry"]
cookieTotpPending := sessions.Values["totpPending"]
// Get data from session
username, usernameOk := session.Values["username"].(string)
provider, providerOK := session.Values["provider"].(string)
redirectURI, redirectOK := session.Values["redirectURI"].(string)
expiry, expiryOk := session.Values["expiry"].(int64)
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
// Convert interfaces to correct types
username, usernameOk := cookieUsername.(string)
provider, providerOk := cookieProvider.(string)
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")
if !usernameOk || !providerOK || !expiryOk || !redirectOK || !totpPendingOk {
log.Warn().Msg("Session cookie is missing data")
return types.SessionCookie{}, nil
}
@@ -195,6 +257,7 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
Username: username,
Provider: provider,
TotpPending: totpPending,
RedirectURI: redirectURI,
}, nil
}
@@ -232,6 +295,9 @@ func (auth *Auth) ResourceAllowed(c *gin.Context, context types.UserContext) (bo
// Check if user is allowed
if len(labels.Users) != 0 {
log.Debug().Msg("Checking users")
if len(labels.Users) == 0 {
return true, nil
}
if slices.Contains(labels.Users, context.Username) {
return true, nil
}

147
internal/auth/auth_test.go Normal file
View 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")
}
}

View File

@@ -19,20 +19,20 @@ import (
func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers {
return &Handlers{
Config: config,
Auth: auth,
Hooks: hooks,
Providers: providers,
Docker: docker,
Config: config,
}
}
type Handlers struct {
Config types.HandlersConfig
Auth *auth.Auth
Hooks *hooks.Hooks
Providers *providers.Providers
Docker *docker.Docker
Config types.HandlersConfig
}
func (h *Handlers) AuthHandler(c *gin.Context) {
@@ -249,12 +249,34 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
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
user := h.Auth.GetUser(login.Username)
// User does not exist
if user == nil {
log.Debug().Str("username", login.Username).Msg("User not found")
// Record failed login attempt
h.Auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
@@ -267,6 +289,8 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
// Check if password is correct
if !h.Auth.CheckPassword(*user, login.Password) {
log.Debug().Str("username", login.Username).Msg("Password incorrect")
// Record failed login attempt
h.Auth.RecordLoginAttempt(rateIdentifier, false)
c.JSON(401, gin.H{
"status": 401,
"message": "Unauthorized",
@@ -276,6 +300,9 @@ func (h *Handlers) LoginHandler(c *gin.Context) {
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
if user.TotpSecret != "" {
log.Debug().Msg("Totp enabled")
@@ -393,9 +420,6 @@ func (h *Handlers) LogoutHandler(c *gin.Context) {
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
c.JSON(200, gin.H{
"status": 200,
@@ -502,7 +526,9 @@ func (h *Handlers) OauthUrlHandler(c *gin.Context) {
// Set redirect cookie if redirect URI is provided
if redirectURI != "" {
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
@@ -624,28 +650,25 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
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{
Username: email,
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 err != nil {
c.Redirect(http.StatusPermanentRedirect, h.Config.AppURL)
}
log.Debug().Str("redirectURI", 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)
log.Debug().Str("redirectURI", cookie.RedirectURI).Msg("Got redirect URI")
// Build query
queries, err := query.Values(types.LoginQuery{
RedirectURI: redirectURI,
RedirectURI: cookie.RedirectURI,
})
log.Debug().Msg("Got redirect query")

View File

@@ -24,12 +24,6 @@ type Hooks struct {
func (hooks *Hooks) UseUserContext(c *gin.Context) types.UserContext {
// Get session cookie and basic auth
cookie, err := hooks.Auth.GetSessionCookie(c)
if err != nil {
log.Error().Err(err).Msg("Failed to get session cookie")
return types.UserContext{}
}
basic := hooks.Auth.GetBasicAuth(c)
// Check if basic auth is set
@@ -52,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
if cookie.TotpPending {
log.Debug().Msg("Totp pending")

View File

@@ -14,10 +14,10 @@ func NewOAuth(config oauth2.Config) *OAuth {
}
type OAuth struct {
Verifier string
Config oauth2.Config
Context context.Context
Token *oauth2.Token
Config oauth2.Config
Verifier string
}
func (oauth *OAuth) Init() {

View File

@@ -17,11 +17,11 @@ func NewProviders(config types.OAuthConfig) *Providers {
}
type Providers struct {
Config types.OAuthConfig
Github *oauth.OAuth
Google *oauth.OAuth
Tailscale *oauth.OAuth
Generic *oauth.OAuth
Config types.OAuthConfig
}
func (providers *Providers) Init() {

View File

@@ -43,6 +43,16 @@ type UserContextResponse struct {
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"`

View File

@@ -33,12 +33,16 @@ type Config struct {
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"`
}
// APIConfig is the configuration for the API
type APIConfig struct {
Port int
Address string
// Server configuration
type HandlersConfig struct {
AppURL string
DisableContinue bool
GenericName string
Title string
}
// OAuthConfig is the configuration for the providers
@@ -58,22 +62,20 @@ type OAuthConfig struct {
AppURL string
}
// Server configuration
type HandlersConfig struct {
AppURL string
Domain string
CookieSecure bool
DisableContinue bool
GenericName string
Title string
// APIConfig is the configuration for the API
type APIConfig struct {
Port int
Address string
}
// Auth configuration
// AuthConfig is the configuration for the auth service
type AuthConfig struct {
Domain string
Secret string
CookieSecure bool
SessionExpiry int
Users Users
OAuthWhitelist []string
Users Users
OauthWhitelist []string
SessionExpiry int
Secret string
CookieSecure bool
Domain string
LoginTimeout int
LoginMaxRetries int
}

View File

@@ -1,6 +1,9 @@
package types
import "tinyauth/internal/oauth"
import (
"time"
"tinyauth/internal/oauth"
)
// User is the struct for a user
type User struct {
@@ -24,6 +27,7 @@ type SessionCookie struct {
Username string
Provider string
TotpPending bool
RedirectURI string
}
// TinyauthLabels is the labels for the tinyauth container
@@ -43,12 +47,9 @@ type UserContext struct {
TotpPending bool
}
// 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"`
// LoginAttempt tracks information about login attempts for rate limiting
type LoginAttempt struct {
FailedAttempts int
LastAttempt time.Time
LockedUntil time.Time
}