Compare commits

..

6 Commits

Author SHA1 Message Date
Stavros
7ae16d6bdc fix: prevent ddos attacks in oauth rate limit 2026-03-22 20:43:33 +02:00
Stavros
db73c56dfe fix: review comments 2026-03-21 20:52:01 +02:00
Stavros
4a85a9d010 tests: fix tests 2026-03-21 20:35:59 +02:00
Stavros
7bead41ae9 feat: move oauth logic into auth service and handle multiple sessions 2026-03-21 16:37:04 +02:00
Stavros
2491d453cf feat: add oauth session impl in auth service 2026-03-21 12:55:22 +02:00
Stavros
1a1712eaeb wip 2026-03-21 12:42:05 +02:00
4 changed files with 5 additions and 53 deletions

View File

@@ -15,6 +15,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: nightly
- name: Generate metadata - name: Generate metadata
id: metadata id: metadata

View File

@@ -37,7 +37,7 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@tanstack/eslint-plugin-query": "^5.91.4", "@tanstack/eslint-plugin-query": "^5.91.4",
"@types/node": "^25.5.0", "@types/node": "^25.4.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4", "@vitejs/plugin-react": "^5.1.4",
@@ -417,7 +417,7 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], "@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="],
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],

View File

@@ -43,7 +43,7 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@tanstack/eslint-plugin-query": "^5.91.4", "@tanstack/eslint-plugin-query": "^5.91.4",
"@types/node": "^25.5.0", "@types/node": "^25.4.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4", "@vitejs/plugin-react": "^5.1.4",

View File

@@ -21,11 +21,8 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
// hard-defaults, may make configurable in the future if needed,
// but for now these are just safety limits to prevent unbounded memory usage
const MaxOAuthPendingSessions = 256 const MaxOAuthPendingSessions = 256
const OAuthCleanupCount = 16 const OAuthCleanupCount = 16
const MaxLoginAttemptRecords = 256
type OAuthPendingSession struct { type OAuthPendingSession struct {
State string State string
@@ -46,11 +43,6 @@ type LoginAttempt struct {
LockedUntil time.Time LockedUntil time.Time
} }
type Lockdown struct {
Active bool
ActiveUntil time.Time
}
type AuthServiceConfig struct { type AuthServiceConfig struct {
Users []config.User Users []config.User
OauthWhitelist []string OauthWhitelist []string
@@ -77,7 +69,6 @@ type AuthService struct {
ldap *LdapService ldap *LdapService
queries *repository.Queries queries *repository.Queries
oauthBroker *OAuthBrokerService oauthBroker *OAuthBrokerService
lockdown *Lockdown
} }
func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService { func NewAuthService(config AuthServiceConfig, docker *DockerService, ldap *LdapService, queries *repository.Queries, oauthBroker *OAuthBrokerService) *AuthService {
@@ -211,11 +202,6 @@ func (auth *AuthService) IsAccountLocked(identifier string) (bool, int) {
auth.loginMutex.RLock() auth.loginMutex.RLock()
defer auth.loginMutex.RUnlock() defer auth.loginMutex.RUnlock()
if auth.lockdown != nil && auth.lockdown.Active {
remaining := int(time.Until(auth.lockdown.ActiveUntil).Seconds())
return true, remaining
}
if auth.config.LoginMaxRetries <= 0 || auth.config.LoginTimeout <= 0 { if auth.config.LoginMaxRetries <= 0 || auth.config.LoginTimeout <= 0 {
return false, 0 return false, 0
} }
@@ -241,14 +227,6 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) {
auth.loginMutex.Lock() auth.loginMutex.Lock()
defer auth.loginMutex.Unlock() defer auth.loginMutex.Unlock()
if len(auth.loginAttempts) >= MaxLoginAttemptRecords {
if auth.lockdown != nil && auth.lockdown.Active {
return
}
go auth.lockdownMode()
return
}
attempt, exists := auth.loginAttempts[identifier] attempt, exists := auth.loginAttempts[identifier]
if !exists { if !exists {
attempt = &LoginAttempt{} attempt = &LoginAttempt{}
@@ -768,31 +746,3 @@ func (auth *AuthService) ensureOAuthSessionLimit() {
} }
} }
} }
func (auth *AuthService) lockdownMode() {
auth.loginMutex.Lock()
tlog.App.Warn().Msg("Multiple login attempts detected, possibly DDOS attack. Activating temporary lockdown.")
auth.lockdown = &Lockdown{
Active: true,
ActiveUntil: time.Now().Add(time.Duration(auth.config.LoginTimeout) * time.Second),
}
// At this point all login attemps will also expire so,
// we might as well clear them to free up memory
auth.loginAttempts = make(map[string]*LoginAttempt)
timer := time.NewTimer(time.Until(auth.lockdown.ActiveUntil))
defer timer.Stop()
auth.loginMutex.Unlock()
<-timer.C
auth.loginMutex.Lock()
tlog.App.Info().Msg("Lockdown period ended, resuming normal operation")
auth.lockdown = nil
auth.loginMutex.Unlock()
}