mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-11-03 15:45:51 +00:00
Compare commits
2 Commits
v3.2.0-bet
...
feat/multi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df10eb65ce | ||
|
|
8bf5a6067e |
@@ -26,7 +26,5 @@ 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:
|
||||||
- i18n_v*
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -16,53 +16,7 @@ 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
|
||||||
@@ -71,14 +25,10 @@ jobs:
|
|||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v4
|
uses: actions/configure-pages@v4
|
||||||
|
|
||||||
- name: Prepare output directory
|
- name: Move translations
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist/i18n/
|
mkdir -p dist
|
||||||
|
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,7 +61,3 @@ 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)
|
|
||||||
32
cmd/root.go
32
cmd/root.go
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -93,8 +94,10 @@ var rootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create handlers config
|
// Create handlers config
|
||||||
handlersConfig := types.HandlersConfig{
|
serverConfig := 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,
|
||||||
@@ -106,18 +109,6 @@ var rootCmd = &cobra.Command{
|
|||||||
Address: config.Address,
|
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
|
// Create docker service
|
||||||
docker := docker.NewDocker()
|
docker := docker.NewDocker()
|
||||||
|
|
||||||
@@ -126,7 +117,14 @@ 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(authConfig, docker)
|
auth := auth.NewAuth(types.AuthConfig{
|
||||||
|
Domain: domain,
|
||||||
|
Secret: config.Secret,
|
||||||
|
SessionExpiry: config.SessionExpiry,
|
||||||
|
CookieSecure: config.CookieSecure,
|
||||||
|
Users: users,
|
||||||
|
OAuthWhitelist: oauthWhitelist,
|
||||||
|
}, docker)
|
||||||
|
|
||||||
// Create OAuth providers service
|
// Create OAuth providers service
|
||||||
providers := providers.NewProviders(oauthConfig)
|
providers := providers.NewProviders(oauthConfig)
|
||||||
@@ -138,7 +136,7 @@ var rootCmd = &cobra.Command{
|
|||||||
hooks := hooks.NewHooks(auth, providers)
|
hooks := hooks.NewHooks(auth, providers)
|
||||||
|
|
||||||
// Create handlers
|
// Create handlers
|
||||||
handlers := handlers.NewHandlers(handlersConfig, auth, hooks, providers, docker)
|
handlers := handlers.NewHandlers(serverConfig, auth, hooks, providers, docker)
|
||||||
|
|
||||||
// Create API
|
// Create API
|
||||||
api := api.NewAPI(apiConfig, handlers)
|
api := api.NewAPI(apiConfig, handlers)
|
||||||
@@ -203,8 +201,6 @@ 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.")
|
||||||
|
|
||||||
@@ -239,8 +235,6 @@ 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/v1/{{lng}}.json",
|
loadPath: "https://cdn.tinyauth.app/i18n/{{lng}}.json",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"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,7 +6,6 @@
|
|||||||
"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, { type AxiosError } from "axios";
|
import axios 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,17 +33,7 @@ export const LoginPage = () => {
|
|||||||
mutationFn: (login: LoginFormValues) => {
|
mutationFn: (login: LoginFormValues) => {
|
||||||
return axios.post("/api/login", login);
|
return axios.post("/api/login", login);
|
||||||
},
|
},
|
||||||
onError: (data: AxiosError) => {
|
onError: () => {
|
||||||
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"),
|
||||||
|
|||||||
@@ -17,15 +17,15 @@ import (
|
|||||||
|
|
||||||
func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
|
func NewAPI(config types.APIConfig, handlers *handlers.Handlers) *API {
|
||||||
return &API{
|
return &API{
|
||||||
Config: config,
|
|
||||||
Handlers: handlers,
|
Handlers: handlers,
|
||||||
|
Config: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
Config types.APIConfig
|
|
||||||
Router *gin.Engine
|
Router *gin.Engine
|
||||||
Handlers *handlers.Handlers
|
Handlers *handlers.Handlers
|
||||||
|
Config types.APIConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) Init() {
|
func (api *API) Init() {
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ import (
|
|||||||
"github.com/magiconair/properties/assert"
|
"github.com/magiconair/properties/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// User
|
||||||
|
var User = types.User{
|
||||||
|
Username: "user",
|
||||||
|
Password: "$2a$10$AvGHLTYv3xiRJ0xV9xs3XeVIlkGTygI9nqIamFYB5Xu.5.0UWF7B6", // pass
|
||||||
|
}
|
||||||
|
|
||||||
// Simple API config for tests
|
// Simple API config for tests
|
||||||
var apiConfig = types.APIConfig{
|
var apiConfig = types.APIConfig{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
@@ -28,6 +34,8 @@ var apiConfig = types.APIConfig{
|
|||||||
// 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",
|
||||||
@@ -35,24 +43,19 @@ var handlersConfig = types.HandlersConfig{
|
|||||||
|
|
||||||
// Simple auth config for tests
|
// Simple auth config for tests
|
||||||
var authConfig = types.AuthConfig{
|
var authConfig = types.AuthConfig{
|
||||||
Users: types.Users{},
|
Domain: "localhost",
|
||||||
OauthWhitelist: []string{},
|
|
||||||
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
Secret: "super-secret-api-thing-for-tests", // It is 32 chars long
|
||||||
CookieSecure: false,
|
CookieSecure: false,
|
||||||
SessionExpiry: 3600,
|
SessionExpiry: 3600,
|
||||||
LoginTimeout: 0,
|
Users: types.Users{
|
||||||
LoginMaxRetries: 0,
|
User,
|
||||||
|
},
|
||||||
|
OAuthWhitelist: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookie
|
// Cookie
|
||||||
var cookie string
|
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
|
// We need all this to be able to test the API
|
||||||
func getAPI(t *testing.T) *api.API {
|
func getAPI(t *testing.T) *api.API {
|
||||||
// Create docker service
|
// Create docker service
|
||||||
@@ -67,12 +70,6 @@ func getAPI(t *testing.T) *api.API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create auth service
|
// Create auth service
|
||||||
authConfig.Users = types.Users{
|
|
||||||
{
|
|
||||||
Username: user.Username,
|
|
||||||
Password: user.Password,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
auth := auth.NewAuth(authConfig, docker)
|
auth := auth.NewAuth(authConfig, docker)
|
||||||
|
|
||||||
// Create providers service
|
// Create providers service
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
"tinyauth/internal/docker"
|
"tinyauth/internal/docker"
|
||||||
"tinyauth/internal/types"
|
"tinyauth/internal/types"
|
||||||
@@ -19,41 +17,14 @@ import (
|
|||||||
|
|
||||||
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
|
func NewAuth(config types.AuthConfig, docker *docker.Docker) *Auth {
|
||||||
return &Auth{
|
return &Auth{
|
||||||
Config: config,
|
|
||||||
Docker: docker,
|
Docker: docker,
|
||||||
LoginAttempts: make(map[string]*types.LoginAttempt),
|
Config: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
Config types.AuthConfig
|
|
||||||
Docker *docker.Docker
|
Docker *docker.Docker
|
||||||
LoginAttempts map[string]*types.LoginAttempt
|
Config types.AuthConfig
|
||||||
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 {
|
||||||
@@ -71,78 +42,14 @@ 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.Config.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.Config.OauthWhitelist {
|
for _, email := range auth.Config.OAuthWhitelist {
|
||||||
if email == emailSrc {
|
if email == emailSrc {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -152,13 +59,33 @@ func (auth *Auth) EmailWhitelisted(emailSrc string) bool {
|
|||||||
return false
|
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 {
|
func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie) error {
|
||||||
log.Debug().Msg("Creating session cookie")
|
log.Debug().Msg("Creating session cookie")
|
||||||
|
|
||||||
|
// Get cookie store
|
||||||
|
store := auth.GetCookieStore()
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
session, err := auth.GetSession(c)
|
sessions, err := store.Get(c.Request, "tinyauth")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to get session")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,16 +101,15 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set data
|
// Set data
|
||||||
session.Values["username"] = data.Username
|
sessions.Values["username"] = data.Username
|
||||||
session.Values["provider"] = data.Provider
|
sessions.Values["provider"] = data.Provider
|
||||||
session.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
|
sessions.Values["expiry"] = time.Now().Add(time.Duration(sessionExpiry) * time.Second).Unix()
|
||||||
session.Values["totpPending"] = data.TotpPending
|
sessions.Values["totpPending"] = data.TotpPending
|
||||||
session.Values["redirectURI"] = data.RedirectURI
|
|
||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
err = session.Save(c.Request, c.Writer)
|
err = sessions.Save(c.Request, c.Writer)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to save session")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,22 +120,25 @@ func (auth *Auth) CreateSessionCookie(c *gin.Context, data *types.SessionCookie)
|
|||||||
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
|
func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
|
||||||
log.Debug().Msg("Deleting session cookie")
|
log.Debug().Msg("Deleting session cookie")
|
||||||
|
|
||||||
|
// Get cookie store
|
||||||
|
store := auth.GetCookieStore()
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
session, err := auth.GetSession(c)
|
sessions, err := store.Get(c.Request, "tinyauth")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to get session")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all values in the session
|
// Clear session
|
||||||
for key := range session.Values {
|
for key := range sessions.Values {
|
||||||
delete(session.Values, key)
|
delete(sessions.Values, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save session
|
// Save session
|
||||||
err = session.Save(c.Request, c.Writer)
|
err = sessions.Save(c.Request, c.Writer)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to save session")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,22 +149,31 @@ func (auth *Auth) DeleteSessionCookie(c *gin.Context) error {
|
|||||||
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
|
func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error) {
|
||||||
log.Debug().Msg("Getting session cookie")
|
log.Debug().Msg("Getting session cookie")
|
||||||
|
|
||||||
|
// Get cookie store
|
||||||
|
store := auth.GetCookieStore()
|
||||||
|
|
||||||
// Get session
|
// Get session
|
||||||
session, err := auth.GetSession(c)
|
sessions, err := store.Get(c.Request, "tinyauth")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Failed to get session")
|
|
||||||
return types.SessionCookie{}, err
|
return types.SessionCookie{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get data from session
|
// Get data
|
||||||
username, usernameOk := session.Values["username"].(string)
|
cookieUsername := sessions.Values["username"]
|
||||||
provider, providerOK := session.Values["provider"].(string)
|
cookieProvider := sessions.Values["provider"]
|
||||||
redirectURI, redirectOK := session.Values["redirectURI"].(string)
|
cookieExpiry := sessions.Values["expiry"]
|
||||||
expiry, expiryOk := session.Values["expiry"].(int64)
|
cookieTotpPending := sessions.Values["totpPending"]
|
||||||
totpPending, totpPendingOk := session.Values["totpPending"].(bool)
|
|
||||||
|
|
||||||
if !usernameOk || !providerOK || !expiryOk || !redirectOK || !totpPendingOk {
|
// Convert interfaces to correct types
|
||||||
log.Warn().Msg("Session cookie is missing data")
|
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")
|
||||||
return types.SessionCookie{}, nil
|
return types.SessionCookie{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +195,6 @@ func (auth *Auth) GetSessionCookie(c *gin.Context) (types.SessionCookie, error)
|
|||||||
Username: username,
|
Username: username,
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
TotpPending: totpPending,
|
TotpPending: totpPending,
|
||||||
RedirectURI: redirectURI,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,20 +19,20 @@ import (
|
|||||||
|
|
||||||
func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers {
|
func NewHandlers(config types.HandlersConfig, auth *auth.Auth, hooks *hooks.Hooks, providers *providers.Providers, docker *docker.Docker) *Handlers {
|
||||||
return &Handlers{
|
return &Handlers{
|
||||||
Config: config,
|
|
||||||
Auth: auth,
|
Auth: auth,
|
||||||
Hooks: hooks,
|
Hooks: hooks,
|
||||||
Providers: providers,
|
Providers: providers,
|
||||||
Docker: docker,
|
Docker: docker,
|
||||||
|
Config: config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
Config types.HandlersConfig
|
|
||||||
Auth *auth.Auth
|
Auth *auth.Auth
|
||||||
Hooks *hooks.Hooks
|
Hooks *hooks.Hooks
|
||||||
Providers *providers.Providers
|
Providers *providers.Providers
|
||||||
Docker *docker.Docker
|
Docker *docker.Docker
|
||||||
|
Config types.HandlersConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) AuthHandler(c *gin.Context) {
|
func (h *Handlers) AuthHandler(c *gin.Context) {
|
||||||
@@ -249,34 +249,12 @@ 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",
|
||||||
@@ -289,8 +267,6 @@ 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",
|
||||||
@@ -300,9 +276,6 @@ 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")
|
||||||
@@ -420,6 +393,9 @@ 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,
|
||||||
@@ -526,9 +502,7 @@ 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")
|
||||||
h.Auth.CreateSessionCookie(c, &types.SessionCookie{
|
c.SetCookie("tinyauth_redirect_uri", redirectURI, 3600, "/", h.Config.Domain, h.Config.CookieSecure, true)
|
||||||
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
|
||||||
@@ -650,25 +624,28 @@ func (h *Handlers) OauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
log.Debug().Msg("Email whitelisted")
|
log.Debug().Msg("Email whitelisted")
|
||||||
|
|
||||||
// Get redirect URI
|
// Create session cookie
|
||||||
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", cookie.RedirectURI).Msg("Got redirect URI")
|
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)
|
||||||
|
|
||||||
// Build query
|
// Build query
|
||||||
queries, err := query.Values(types.LoginQuery{
|
queries, err := query.Values(types.LoginQuery{
|
||||||
RedirectURI: cookie.RedirectURI,
|
RedirectURI: redirectURI,
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Debug().Msg("Got redirect query")
|
log.Debug().Msg("Got redirect query")
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ 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, err := hooks.Auth.GetSessionCookie(c)
|
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)
|
basic := hooks.Auth.GetBasicAuth(c)
|
||||||
|
|
||||||
// Check if basic auth is set
|
// Check if basic auth is set
|
||||||
@@ -46,19 +52,6 @@ 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")
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ func NewOAuth(config oauth2.Config) *OAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OAuth struct {
|
type OAuth struct {
|
||||||
Config oauth2.Config
|
Verifier string
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Token *oauth2.Token
|
Token *oauth2.Token
|
||||||
Verifier string
|
Config oauth2.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (oauth *OAuth) Init() {
|
func (oauth *OAuth) Init() {
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ func NewProviders(config types.OAuthConfig) *Providers {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Providers struct {
|
type Providers struct {
|
||||||
Config types.OAuthConfig
|
|
||||||
Github *oauth.OAuth
|
Github *oauth.OAuth
|
||||||
Google *oauth.OAuth
|
Google *oauth.OAuth
|
||||||
Tailscale *oauth.OAuth
|
Tailscale *oauth.OAuth
|
||||||
Generic *oauth.OAuth
|
Generic *oauth.OAuth
|
||||||
|
Config types.OAuthConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (providers *Providers) Init() {
|
func (providers *Providers) Init() {
|
||||||
|
|||||||
@@ -43,16 +43,6 @@ type UserContextResponse struct {
|
|||||||
TotpPending bool `json:"totpPending"`
|
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
|
// Totp request is the request for the totp endpoint
|
||||||
type TotpRequest struct {
|
type TotpRequest struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
|
|||||||
@@ -33,16 +33,12 @@ type Config struct {
|
|||||||
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
|
LogLevel int8 `mapstructure:"log-level" validate:"min=-1,max=5"`
|
||||||
Title string `mapstructure:"app-title"`
|
Title string `mapstructure:"app-title"`
|
||||||
EnvFile string `mapstructure:"env-file"`
|
EnvFile string `mapstructure:"env-file"`
|
||||||
LoginTimeout int `mapstructure:"login-timeout"`
|
|
||||||
LoginMaxRetries int `mapstructure:"login-max-retries"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server configuration
|
// APIConfig is the configuration for the API
|
||||||
type HandlersConfig struct {
|
type APIConfig struct {
|
||||||
AppURL string
|
Port int
|
||||||
DisableContinue bool
|
Address string
|
||||||
GenericName string
|
|
||||||
Title string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OAuthConfig is the configuration for the providers
|
// OAuthConfig is the configuration for the providers
|
||||||
@@ -62,20 +58,22 @@ type OAuthConfig struct {
|
|||||||
AppURL string
|
AppURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIConfig is the configuration for the API
|
// Server configuration
|
||||||
type APIConfig struct {
|
type HandlersConfig struct {
|
||||||
Port int
|
AppURL string
|
||||||
Address string
|
Domain string
|
||||||
|
CookieSecure bool
|
||||||
|
DisableContinue bool
|
||||||
|
GenericName string
|
||||||
|
Title string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthConfig is the configuration for the auth service
|
// Auth configuration
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
Users Users
|
Domain string
|
||||||
OauthWhitelist []string
|
|
||||||
SessionExpiry int
|
|
||||||
Secret string
|
Secret string
|
||||||
CookieSecure bool
|
CookieSecure bool
|
||||||
Domain string
|
SessionExpiry int
|
||||||
LoginTimeout int
|
Users Users
|
||||||
LoginMaxRetries int
|
OAuthWhitelist []string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import "tinyauth/internal/oauth"
|
||||||
"time"
|
|
||||||
"tinyauth/internal/oauth"
|
|
||||||
)
|
|
||||||
|
|
||||||
// User is the struct for a user
|
// User is the struct for a user
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -27,7 +24,6 @@ type SessionCookie struct {
|
|||||||
Username string
|
Username string
|
||||||
Provider string
|
Provider string
|
||||||
TotpPending bool
|
TotpPending bool
|
||||||
RedirectURI string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TinyauthLabels is the labels for the tinyauth container
|
// TinyauthLabels is the labels for the tinyauth container
|
||||||
@@ -47,9 +43,12 @@ type UserContext struct {
|
|||||||
TotpPending bool
|
TotpPending bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginAttempt tracks information about login attempts for rate limiting
|
// App Context is the response for the app context endpoint
|
||||||
type LoginAttempt struct {
|
type AppContext struct {
|
||||||
FailedAttempts int
|
Status int `json:"status"`
|
||||||
LastAttempt time.Time
|
Message string `json:"message"`
|
||||||
LockedUntil time.Time
|
ConfiguredProviders []string `json:"configuredProviders"`
|
||||||
|
DisableContinue bool `json:"disableContinue"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
GenericName string `json:"genericName"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user