From d2eb914637d1edaacf887ae6025ebe9a2f0d6790 Mon Sep 17 00:00:00 2001 From: Lyra Bot Date: Tue, 11 Aug 2026 16:33:00 -0400 Subject: [PATCH 1/6] fix: must-fix items for v1.0 release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security: - Add SecurityHeadersListener (X-Content-Type-Options, X-Frame-Options, CSP, Referrer-Policy, HSTS) - Replace document.write() with document.documentElement.innerHTML in login JS to avoid CSP violations - Add CSS escaping (|e('css')) to env color values in _style.html.twig - Document CSRF protection model: nonce serves as CSRF token for POST form path (single-use, server-generated, 120s TTL) - Reduce TOTP verification window from 10 periods (±5 min) to 1 (±30s) - Remove hardcoded APP_SECRET from bin/franken.sh (now uses env or generates random) - Remove backup code values from debug log output - Add .env to .gitignore Bug fixes: - Fix ->json access on possibly-null in LoginListener (uses null-safe operator ?->) - Fix validReturn() not checking false from parse_url (could cause TypeError on malformed URLs) - Add isHit() race condition check in AcceptListener and AllowListener - Add try/finally in Kernel::terminate() so parent::terminate() always runs even if persist() throws - Add input validation to GenerateBackupCodesCommand (reject count < 1) - Use Response::HTTP_INTERNAL_SERVER_ERROR constant in GetTotpTrait instead of literal 500 Docker/CI: - Explicitly install curl in Docker final image (needed for healthcheck) - Update workflow tag pattern to v*.*.* (standardize on v-prefix) - Extract version without v-prefix for Docker image tag - Remove stale develop branch from CI triggers - Fix publish.yaml git remote add to use set-url on re-runs Code quality: - Add declare(strict_types=1) to all interface files - Add #[AsCommand] attribute to GenerateBackupCodesCommand - Fix BackupCodeInterface default count to match implementation (10) - Lowercase host before TLD lookup in DomainManager - Expand TLD list with many missing multi-part TLDs (.com.au, .co.jp, .com.br, .co.kr, .com.tw, .co.za, etc.) to prevent open redirect vulnerabilities - Disable unused Symfony sessions in framework.yaml Tests: - Update DomainManagerTest for corrected TLD parsing (.com.au, .co.jp, .com.br now correctly recognized as multi-part) - Update GetTotpTraitTest for corrected error message - Update GenerateBackupCodesCommandTest: zero count now throws exception --- .gitea/workflows/develop.yaml | 2 - .gitea/workflows/docker.yaml | 9 +- .gitea/workflows/publish.yaml | 2 +- .gitea/workflows/tests.yaml | 2 - .gitignore | 1 + Dockerfile | 5 +- bin/franken.sh | 5 +- config/packages/framework.yaml | 5 +- src/Command/GenerateBackupCodesCommand.php | 9 +- src/Kernel.php | 8 +- src/Listener/AcceptListener.php | 7 +- src/Listener/AllowListener.php | 7 +- src/Listener/LoginListener.php | 13 ++- src/Listener/SecurityHeadersListener.php | 48 +++++++++ src/Service/BackupCodeInterface.php | 4 +- src/Service/BackupCodeManager.php | 2 +- src/Service/DomainInterface.php | 2 + src/Service/DomainManager.php | 101 ++++++++++++++++-- src/Service/LoginInterface.php | 2 + src/Service/LoginManager.php | 2 +- src/Trait/GetTotpTrait.php | 3 +- templates/_script.html.twig | 4 +- templates/_style.html.twig | 4 +- templates/login.html.twig | 3 + .../GenerateBackupCodesCommandTest.php | 12 +-- tests/Unit/Service/DomainManagerTest.php | 15 ++- tests/Unit/Trait/GetTotpTraitTest.php | 2 +- 27 files changed, 221 insertions(+), 58 deletions(-) create mode 100644 src/Listener/SecurityHeadersListener.php diff --git a/.gitea/workflows/develop.yaml b/.gitea/workflows/develop.yaml index f0df39c..142cef3 100644 --- a/.gitea/workflows/develop.yaml +++ b/.gitea/workflows/develop.yaml @@ -4,7 +4,6 @@ on: push: branches: - 'main' - - 'develop' jobs: docker: @@ -31,4 +30,3 @@ jobs: platforms: linux/amd64,linux/arm64 tags: | ${{ vars.DOCKERHUB_TARGET }}:develop - diff --git a/.gitea/workflows/docker.yaml b/.gitea/workflows/docker.yaml index 5a227ca..ea293f3 100644 --- a/.gitea/workflows/docker.yaml +++ b/.gitea/workflows/docker.yaml @@ -3,7 +3,7 @@ name: Push Docker on: push: tags: - - '*.*.*' + - 'v*.*.*' jobs: docker: @@ -22,6 +22,10 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Extract version + id: version + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - name: Build image uses: docker/build-push-action@v5 with: @@ -30,5 +34,4 @@ jobs: platforms: linux/amd64,linux/arm64 tags: | ${{ vars.DOCKERHUB_TARGET }}:latest - ${{ vars.DOCKERHUB_TARGET }}:${{ github.ref_name }} - + ${{ vars.DOCKERHUB_TARGET }}:${{ steps.version.outputs.VERSION }} diff --git a/.gitea/workflows/publish.yaml b/.gitea/workflows/publish.yaml index eba581a..d433b74 100644 --- a/.gitea/workflows/publish.yaml +++ b/.gitea/workflows/publish.yaml @@ -25,7 +25,7 @@ jobs: SYNC_TOKEN: ${{ secrets.SYNC_GITHUB_TOKEN }} SYNC_TARGET: ${{ vars.SYNC_GITHUB_TARGET }} run: | - git remote add github "https://digitaladapt:${SYNC_TOKEN}@github.com/$SYNC_TARGET" + git remote add github "https://digitaladapt:${SYNC_TOKEN}@github.com/$SYNC_TARGET" 2>/dev/null || git remote set-url github "https://digitaladapt:${SYNC_TOKEN}@github.com/$SYNC_TARGET" - name: Push Current Branch run: | diff --git a/.gitea/workflows/tests.yaml b/.gitea/workflows/tests.yaml index 3eaea61..8b1bb2c 100644 --- a/.gitea/workflows/tests.yaml +++ b/.gitea/workflows/tests.yaml @@ -4,11 +4,9 @@ on: push: branches: - 'main' - - 'develop' pull_request: branches: - 'main' - - 'develop' jobs: test: diff --git a/.gitignore b/.gitignore index afa4fc1..de08fc9 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ /.php-cs-fixer.php /.php-cs-fixer.cache ###< friendsofphp/php-cs-fixer ### +.env diff --git a/Dockerfile b/Dockerfile index ce4355c..b83dbf3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,9 +33,12 @@ RUN composer dump-env prod --empty # start creating final image FROM dunglas/frankenphp:php8.5-trixie -# install APCu +# install APCu and curl (needed for healthcheck) RUN pecl install apcu && \ docker-php-ext-enable apcu +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl && \ + rm -rf /var/lib/apt/lists/* # symfony required environment variables ENV APP_DEBUG=0 diff --git a/bin/franken.sh b/bin/franken.sh index d4b6b52..d71f3ce 100755 --- a/bin/franken.sh +++ b/bin/franken.sh @@ -1,13 +1,14 @@ #!/bin/sh # Dev utility — builds and runs the preauth container locally. # Not for production use. +# APP_SECRET should be set in your environment or .env file. -docker container rm preauth +docker container rm preauth 2>/dev/null docker build . -t digitaladapt/preauth:dev docker run --name preauth \ -e APP_ENV=dev \ -e APP_DEBUG=true \ - -e APP_SECRET=f88a1074691c40415be4439345b79f69 \ + -e APP_SECRET="${APP_SECRET:-$(openssl rand -hex 16)}" \ -e APP_SHARE_DIR=var/share \ -e DEFAULT_URI=http://localhost \ -v ./var/share:/app/var/share \ diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index ecfd1d5..fd1c2f0 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -5,5 +5,6 @@ framework: trusted_proxies: 'private_ranges' trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto'] - # Note that the session will be started ONLY if you read or write from it. - session: true + # Sessions are disabled — preauth implements its own cookie/cache-based + # session management and does not use Symfony's session subsystem. + session: false diff --git a/src/Command/GenerateBackupCodesCommand.php b/src/Command/GenerateBackupCodesCommand.php index ddf86bd..8330d1a 100644 --- a/src/Command/GenerateBackupCodesCommand.php +++ b/src/Command/GenerateBackupCodesCommand.php @@ -7,13 +7,16 @@ namespace App\Command; use App\PersistCache; use App\Service\BackupCodeInterface; use Psr\Cache\InvalidArgumentException; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Exception\InvalidArgumentException as ConsoleInvalidArgumentException; /** simple console command to generate backup codes * usage: php bin/console app:generate-backup-codes [count] */ +#[AsCommand(name: 'app:generate-backup-codes')] final class GenerateBackupCodesCommand extends Command { public function __construct( @@ -25,8 +28,7 @@ final class GenerateBackupCodesCommand extends Command protected function configure(): void { - $this->setName('app:generate-backup-codes'); - $this->setDescription('Generate single‑use backup codes') + $this->setDescription('Generate single-use backup codes') ->addArgument('count', InputArgument::OPTIONAL, 'Number of codes to generate', 10); } @@ -36,6 +38,9 @@ final class GenerateBackupCodesCommand extends Command /* since Kernel::terminate() does not get called, we must boot and persist explicitly */ $this->persistCache->boot(); $count = (int) $input->getArgument('count'); + if ($count < 1) { + throw new ConsoleInvalidArgumentException('Count must be a positive integer.'); + } $codes = $this->manager->generate($count); foreach ($codes as $code) { $output->writeln($code); diff --git a/src/Kernel.php b/src/Kernel.php index df07f35..fe29f52 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -28,8 +28,10 @@ class Kernel extends BaseKernel /** @throws InvalidArgumentException */ public function terminate(Request $request, Response $response): void { - $this->persistCache->persist(); - - parent::terminate($request, $response); + try { + $this->persistCache->persist(); + } finally { + parent::terminate($request, $response); + } } } diff --git a/src/Listener/AcceptListener.php b/src/Listener/AcceptListener.php index e9ec54a..1bf20aa 100644 --- a/src/Listener/AcceptListener.php +++ b/src/Listener/AcceptListener.php @@ -37,7 +37,12 @@ final readonly class AcceptListener $cookieKey = $this->makeCacheKey("cookie_$cookie"); if ($cookie && $this->sessionCache->hasItem($cookieKey)) { /* cookie sent corresponds to valid existing session */ - $id = $this->sessionCache->getItem($cookieKey)->get(); + $item = $this->sessionCache->getItem($cookieKey); + if (! $item->isHit()) { + /* race condition: item was removed between hasItem and getItem */ + return; + } + $id = $item->get(); $this->logger->debug("has valid cookie-session: $id"); $event->setResponse(new Response("hi $id", headers: [ 'Content-Type' => 'text/plain', diff --git a/src/Listener/AllowListener.php b/src/Listener/AllowListener.php index 2785018..cc8d781 100644 --- a/src/Listener/AllowListener.php +++ b/src/Listener/AllowListener.php @@ -32,7 +32,12 @@ final readonly class AllowListener $ipKey = $this->makeCacheKey("ip_{$event->getRequest()->getClientIp()}"); if ($this->sessionCache->hasItem($ipKey)) { /* ip address corresponds to valid existing session */ - $id = $this->sessionCache->getItem($ipKey)->get(); + $item = $this->sessionCache->getItem($ipKey); + if (! $item->isHit()) { + /* race condition: item was removed between hasItem and getItem */ + return; + } + $id = $item->get(); $this->logger->debug("has valid ip-session: $id"); $event->setResponse(new Response("hi $id", headers: [ 'Content-Type' => 'text/plain', diff --git a/src/Listener/LoginListener.php b/src/Listener/LoginListener.php index 4e9d4a8..e2157b5 100644 --- a/src/Listener/LoginListener.php +++ b/src/Listener/LoginListener.php @@ -24,6 +24,15 @@ use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; +/** + * Handles login attempts via X-Preauth header (AJAX) or POST form submission. + * + * CSRF Protection: The nonce field serves as CSRF protection for the POST form + * path. Nonces are server-generated, single-use, and have a 120-second TTL. + * An attacker cannot forge a POST request without first loading the login page + * to obtain a valid nonce, which requires being on the auth subdomain. + * For the AJAX (header) path, the nonce is embedded in the base64url payload. + */ final readonly class LoginListener { use CookieNameTrait; @@ -81,9 +90,9 @@ final readonly class LoginListener $this->logger->debug("logging failure for: {$event->getRequest()->getClientIp()}"); $event->setResponse($this->makeFailedResponse( $limitReached, - $payload->json ?? true, + $payload?->json ?? true, $event->getRequest()->getHost(), - $this->makeCacheKey($payload ? $payload->id : '') + $this->makeCacheKey($payload?->id ?? '') )); } diff --git a/src/Listener/SecurityHeadersListener.php b/src/Listener/SecurityHeadersListener.php new file mode 100644 index 0000000..9c05013 --- /dev/null +++ b/src/Listener/SecurityHeadersListener.php @@ -0,0 +1,48 @@ +isMainRequest()) { + return; + } + + $response = $event->getResponse(); + $headers = $response->headers; + + /* prevent MIME-type sniffing */ + $headers->set('X-Content-Type-Options', 'nosniff'); + + /* prevent clickjacking — this app is never framed */ + $headers->set('X-Frame-Options', 'DENY'); + + /* control referrer information sent to other sites */ + $headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); + + /* Content-Security-Policy — the login page uses inline styles + * and scripts (via Twig includes), so we allow 'unsafe-inline' + * for those. No external resources are loaded. */ + $headers->set( + 'Content-Security-Policy', + "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline';" + ); + + /* HSTS — enforce HTTPS for one year (app is designed for HTTPS behind a proxy) */ + $headers->set('Strict-Transport-Security', 'max-age=31536000'); + } +} diff --git a/src/Service/BackupCodeInterface.php b/src/Service/BackupCodeInterface.php index ac60e91..b18dbe7 100644 --- a/src/Service/BackupCodeInterface.php +++ b/src/Service/BackupCodeInterface.php @@ -1,5 +1,7 @@ sessionCache->getItem($this->makeCacheKey($backupKey)); - $this->logger->debug("checking backup code '{$backupKey}': " . ($backupItem->isHit() ? 'HIT & ' : 'miss & ') . ($backupItem->get() ? 'VALID' : 'invalid')); + $this->logger->debug('checking backup code: ' . ($backupItem->isHit() ? 'HIT & ' : 'miss & ') . ($backupItem->get() ? 'VALID' : 'invalid')); if ($backupItem->isHit() && $backupItem->get()) { $this->logger->debug("valid backup code"); /* mark backup code as spent */ diff --git a/src/Service/DomainInterface.php b/src/Service/DomainInterface.php index 4b17f15..6f73b46 100644 --- a/src/Service/DomainInterface.php +++ b/src/Service/DomainInterface.php @@ -1,5 +1,7 @@ ['com','net','off','org'], 'am' => ['radio'], - 'com' => ['br','cn','co','de','eu','gr','it','jpn','mex','ru','sa','uk','us','za'], + 'at' => ['ac','co','gv','or'], + 'au' => ['com','net','org','edu','gov','asn','id'], + 'az' => ['com','net','org'], + 'bd' => ['com','net','org','gov','mil','ac'], + 'br' => ['com','net','org','gov','mil','eco','emp','g12','ind','inf','rec','tur','tv','edu','far','gov','gru','jor','leg','lec','med','nom','not','ppg','pro','psi','pub','slg','srv','tec','tmp','vip','vlog','wiki','zlg'], + 'by' => ['com','net','org','gov','mil','of'], + 'ca' => ['ab','bc','mb','nb','nf','nl','ns','nt','nu','on','pe','qc','sk','yk'], + 'cc' => [], + 'cn' => ['com','net','org','gov','edu','ac','bj','sh','tj','cq','he','sx','nm','ln','jl','hl','js','zj','ah','fj','jx','sd','ha','hb','hn','gd','gx','hi','sc','gz','yn','sn','gs','qh','nx','xj','tw','hk','mo'], + 'co' => ['com','net','org','gov','mil','edu','arts','firm','info','int','nom','rec','web'], + 'com' => ['br','cn','co','de','eu','gr','it','jpn','mex','ru','sa','uk','us','za','au','bh','bo','cn','ec','eg','gt','hk','hn','il','in','jp','kr','kw','lb','lv','my','mx','ng','ni','np','pe','pf','pg','ph','pk','pl','pr','py','sa','sg','sv','tr','tw','ua','uy','ve','vn','ye'], 'de' => ['com'], + 'dk' => ['co'], + 'ec' => ['com','net','org','gov','mil','edu','fin','med','pro'], + 'ee' => ['com','org','pri'], + 'eg' => ['com','net','org','gov','edu','mil'], + 'es' => ['com','nom','org','edu','gob'], + 'eu' => [], + 'fi' => ['aland'], 'fm' => ['radio'], + 'fr' => ['com','nom','tm','asso','gouv','pol'], + 'ge' => ['com','net','org','edu','gov','mil'], 'gg' => ['co','net','org'], - 'in' => ['co','firm','gen','ind','net','org'], + 'gr' => ['com','net','org','gov','edu','mil'], + 'hk' => ['com','net','org','gov','edu','idv'], + 'hu' => ['co','2000','privat','sport','tm','erotica','sex','video','info','org','net','gov','edu','mil','press','biz'], + 'id' => ['ac','biz','co','desa','go','mil','my','net','or','sch','web'], + 'ie' => ['gov'], + 'il' => ['ac','co','gov','idf','k12','muni','net','org'], + 'in' => ['co','firm','gen','ind','net','org','ac','edu','res','gov','mil'], + 'iq' => ['com','net','org','gov','edu','mil'], + 'ir' => ['ac','co','gov','id','net','org','sch'], + 'is' => ['net','com','org','edu','gov','int'], + 'it' => ['ab','ag','al','an','ao','ap','aq','ar','at','av','ba','bg','bi','bl','bn','bo','br','bs','bt','bz','ca','cb','ce','ch','cl','cn','co','cr','cs','ct','cz','en','fc','fe','fg','fi','fm','fr','ge','go','gr','im','is','kr','lc','le','li','lo','lt','lu','mb','mc','me','mi','mn','mo','ms','mt','na','no','nu','or','pa','pc','pd','pe','pg','pi','pn','po','pr','pt','pu','pv','pz','re','rg','ri','rm','rn','ro','sa','si','so','sp','sr','ss','su','sv','ta','te','tn','to','tp','tr','ts','tv','ud','va','vb','vc','ve','vi','vr','vt','vv','edu','gov','abruzzo','basilicata','calabria','campania','emilia-romagna','friuli-ve-giulia','lazio','liguria','lombardia','marche','molise','piemonte','puglia','sardegna','sicilia','toscana','trentino-a-adige','umbria','valle-aosta','veneto'], 'je' => ['co','net','org'], - 'mx' => ['com','net','org'], - 'net' => ['gb','hu','in','jp','se','uk'], - 'nz' => ['co','net','org'], - 'org' => ['ae','us'], - 'ph' => ['com','net','org'], - 'se' => ['com'], - 'uk' => ['co','me','org'], + 'jo' => ['com','net','org','gov','edu','mil','sch'], + 'jp' => ['ac','ad','co','ed','go','gr','lg','ne','or'], + 'ke' => ['co','ne','or','ac','go','me','mobi','info','sc','pro'], + 'kg' => ['com','net','org','gov','mil','edu'], + 'kr' => ['ac','co','go','hs','kg','mil','ms','ne','or','pe','re','seoul','busan','daegu','incheon','gwangju','daejeon','ulsan','gyeonggi','gangwon','chungbuk','chungnam','jeonbuk','jeonnam','gyeongbuk','gyeongnam','jeju','sejong'], + 'kz' => ['com','net','org','edu','gov','mil'], + 'li' => [], + 'lt' => ['gov'], + 'lv' => ['com','net','org','edu','gov','mil','id','asn','conf'], + 'ly' => ['com','net','org','gov','edu','sch','med','id'], + 'ma' => ['co','net','org','gov','press','ac'], + 'mk' => ['com','net','org','edu','gov','inf','name','pro'], + 'mx' => ['com','net','org','gov','edu','mil'], + 'my' => ['com','net','org','gov','edu','mil','name'], + 'na' => ['com','net','org','alt','edu','gov','mil','pro'], + 'net' => ['gb','hu','in','jp','se','uk','cn','nz'], + 'ng' => ['com','net','org','gov','edu','mil','sch','name','gov'], + 'ni' => ['ac','co','com','edu','gob','mil','net','nom','org'], + 'nl' => ['bv','co'], + 'no' => ['fhs','folkebibl','kommune','mil','stat','priv','vgs','dep','kommune'], + 'nz' => ['co','net','org','ac','geek','gen','maori','school','parliament','govt','health','mil','crii','archie','geek','govt','health','maori','school'], + 'om' => ['com','net','org','gov','edu','med','mil','sch'], + 'org' => ['ae','us','lu'], + 'pe' => ['com','net','org','gob','edu','mil','nom'], + 'ph' => ['com','net','org','gov','edu','mil'], + 'pk' => ['com','net','org','fam','biz','edu','gov','web'], + 'pl' => ['com','net','org','aid','agro','atm','auto','biz','edu','gmina','gsm','info','mail','miasta','media','mil','ngo','nom','pc','powiat','priv','realestate','rel','sex','shop','sklep','sos','szkola','targi','tm','tourism','travel','turystyka','gov','ap','augov','bedzin','bialystok','bielawa','bierun','boleslawiec','bydgoszcz','bytom','cieszyn','czeladz','czest','dlugoleka','elblag','elk','glogow','gniezno','gorlice','gorzow','grodzisk','grudziadz','ilk','jaworzno','jelenia-gora','jgora','kalisz','kazimierz-dolny','karpacz','kartuzy','kaszuby','katowice','kepno','ketrzyn','klodzko','kobierzyce','kolobrzeg','konin','konskowola','krapkowice','krakow','krasnik','krasno','krosniewice','kutno','lapy','lebork','legnica','lezajsk','limanowa','lomza','lowicz','lubin','lukow','malbork','malopolska','mazowsze','mazury','mielec','milicz','mielno','mragowo','naklo','nowaruda','nysa','olawa','olecko','olkusz','olsztyn','opoczno','opole','ostrowiec','ostroleka','ostrowwlkp','pila','pisz','podhale','podlasie','polkowice','pomorze','pomorse','prochowice','pruszkow','przeworsk','pulawy','rabka','rawa-maz','rybnik','rzeszow','sanok','sejny','siedlce','slask','slupsk','sosnowiec','stalowa-wola','skoczow','starachowice','stargard','suwalki','swidnica','swiebodzin','swinoujscie','szczecin','szczytno','tarnobrzeg','tgory','turek','tychy','ustka','walbrzych','warmia','warszawa','waw','wegrow','wielun','wlocl','wloclawek','wodzislaw','wolomin','wroclaw','zachpomor','zagan','zarow','zgora','zgorzelec','plug'], + 'pr' => ['ac','co','edu','gov','info','island','pro','net','org'], + 'pt' => ['com','net','org','gov','edu','int','publ'], + 'py' => ['com','net','org','gov','edu','mil','co'], + 'qa' => ['com','net','org','gov','edu','mil','sch','name'], + 'ro' => ['com','net','org','nom','rec','info','arts','com','firm','tm','www','store','nt','ngo','pro','tm','com','arts','rec','store','info','nom','nt','org','shop','firm','www','rest','travel','transport','tourism','press','media','medical','med','law','jobs','inst','individual','insinfo','guru','fit','engineering','expert','energy','economy','dot','dog','dev','design','dem','dental','craft','corp','consulting','construction','company','com','club','cloud','coach','city','cinema','church','chat','casino','cars','care','cards','broke','blog','bio','bid','band','auto','audio','attorney','apartments','app','art','archi','architects','arena','architects','associates','attorney','auction','auto','baby','band','bank','bar','bargains','beer','berlin','best','bet','bid','bike','bingo','bio','black','blog','blue','boats','bond','boo','book','boutique','build','builders','business','buzz','cab','cafe','call','cam','camp','capital','care','careers','cars','cash','casino','catering','center','ceo','ceramics','cfd','ch','chat','church','city','claims','cleaning','click','clinic','clothing','cloud','club','coach','codes','coffee','college','community','company','computer','condos','construction','consulting','contact','cooking','cool','country','courses','cpa','craft','credit','creditcard','cricket','cruise','cuisinella','cymru','dabur','dance','date','dating','deals','degree','delivery','democrat','dental','design','dev','diamonds','diet','digital','direct','directory','discount','dog','domains','doos','download','ec','edu','education','energy','engineering','enterprises','equipment','estate','events','exchange','expert','exposed','express','fail','faith','family','fan','farm','fashion','film','finance','financial','fish','fit','fitness','flights','florist','flowers','football','forex','forsale','foundation','fun','fund','furniture','futbol','fyi','gal','gallery','game','garden','gift','gifts','gives','glass','global','gold','golf','graphics','gratis','green','gripe','group','guru','health','healthcare','help','helsinki','here','hiphop','hiv','holdings','holiday','homes','horse','host','hosting','house','how','immo','immobilien','in','industries','info','ink','institute','insure','international','investments','irish','jewelry','kaufen','kids','kim','kitchen','kiwi','kred','land','law','lawyer','legal','lgbt','lifestyle','lighting','limited','limo','link','live','loan','loans','lol','london','love','ltd','ltda','luxury','maison','management','market','marketing','markets','media','memorial','men','menu','miami','mobi','moda','moe','mom','money','monster','mortgage','movie','nagoya','name','navy','net','network','news','ngo','ninja','nyc','observer','okinawa','one','ong','onl','online','ooo','org','organic','osaka','paris','partners','parts','party','photo','photography','photos','pics','pictures','pink','pizza','place','plumbing','plus','poker','porn','press','pro','productions','properties','property','pub','qpon','realtor','realty','recipes','red','rehab','reise','reisen','rent','rentals','repair','report','rest','restaurant','review','reviews','rich','rip','rocks','rodeo','run','saarland','sale','salon','sarl','save','saxo','school','schule','science','services','sex','sexy','sg','shop','shopping','show','singles','site','ski','soccer','social','software','solar','solutions','space','store','stream','studio','study','style','supplies','supply','support','surgery','systems','tax','taxi','team','tech','technology','tennis','thai','tips','tires','tirol','today','tokyo','tools','top','tour','tours','town','toys','trade','trading','training','travel','tube','university','uno','vacations','vegas','ventures','vet','viajes','video','villas','vin','vision','vlaanderen','vodka','vote','voting','voto','voyage','wales','watch','webcam','website','wedding','wien','wiki','win','wine','work','works','world','wtf','xxx','xyz','yoga','yokohama','zone'], + 'ru' => ['ac','com','edu','int','net','org','pp','adygeya','altai','amur','arkhangelsk','astrakhan','bashkiria','belgorod','bir','bryansk','buryatia','cbg','chel','chelyabinsk','chita','chukotka','chuvashia','dagestan','dudinka','e-burg','grozny','irkutsk','ivanovo','izhevsk','jar','joshkar-ola','kalmykia','kaluga','kamchatka','karelia','kazan','kchr','kemerovo','khabarovsk','khakassia','khv','kirov','koenigsberg','komi','kostroma','krasnodar','krasnoyarsk','kuban','kurgan','kursk','lipetsk','magadan','mari','mari-el','marine','mil','mordovia','mosreg','msk','murmansk','nalchik','nnov','nov','novosibirsk','nsk','omsk','orenburg','oryol','palana','penza','perm','ptz','rnd','ryazan','sakhalin','samara','saratov','simbirsk','smolensk','spb','stavropol','stv','surgut','tambov','tatarstan','tom','tomsk','tsaritsyn','tsk','tula','tuva','tver','tyumen','udm','udmurtia','ulan-ude','vladikavkaz','vladimir','vladivostok','volgograd','vologda','voronezh','vrn','vyatka','yakutia','yamal','yaroslavl','yevrey'], + 'sa' => ['com','net','org','gov','med','pub','edu','sch'], + 'sb' => ['com','net','org','edu','gov'], + 'sc' => ['com','net','org','gov','edu'], + 'se' => ['a','ac','b','bd','brand','c','d','e','f','fh','fhsk','fhv','g','h','i','k','komforb','kommunal','komvux','kunskapsforb','l','lanbib','m','n','naturbruksgymn','o','org','p','parti','pp','press','r','s','t','tm','u','v','w','x','y','z'], + 'sg' => ['com','net','org','gov','edu','per'], + 'sh' => ['com','net','org','gov','mil','edu'], + 'sk' => ['co','com','edu','gov','mil','net','org','nfo'], + 'st' => ['co','com','consulado','edu','embaixada','gov','mil','net','org','principe','saotome','store'], + 'su' => ['abkhazia','adygeya','ak', 'altai','amur','arkhangelsk','astrakhan','bashkiria','belgorod','bir','bryansk','buryatia','cbg','chel','chelyabinsk','chita','chukotka','chuvashia','dagestan','dudinka','e-burg','grozny','irkutsk','ivanovo','izhevsk','jar','joshkar-ola','kalmykia','kaluga','kamchatka','karelia','kazan','kchr','kemerovo','khabarovsk','khakassia','khv','kirov','koenigsberg','komi','kostroma','krasnodar','krasnoyarsk','kuban','kurgan','kursk','lipetsk','magadan','mari','mari-el','marine','mil','mordovia','mosreg','msk','murmansk','nalchik','nnov','nov','novosibirsk','nsk','omsk','orenburg','oryol','palana','penza','perm','ptz','rnd','ryazan','sakhalin','samara','saratov','simbirsk','smolensk','spb','stavropol','stv','surgut','tambov','tatarstan','tom','tomsk','tsaritsyn','tsk','tula','tuva','tver','tyumen','udm','udmurtia','ulan-ude','vladikavkaz','vladimir','vladivostok','volgograd','vologda','voronezh','vrn','vyatka','yakutia','yamal','yaroslavl','yevrey','com','net','org','gov','pp','edu'], + 'sv' => ['com','edu','gob','org','red'], + 'sy' => ['com','net','org','gov','edu','mil','name'], + 'th' => ['ac','co','go','in','mi','net','or'], + 'tj' => ['ac','biz','co','com','edu','gov','go','info','int','mil','name','net','nic','nom','org','pro','test','web'], + 'tn' => ['agrinet','com','defense','edunet','ens','fin','gov','ind','info','intl','min','nat','net','org','perso','rnrt','rns','rnu','tourism','turen'], + 'tr' => ['com','net','org','gov','biz','info','mil','edu','tv','bbs','k12','pol','bel','dr','gen','av','bbs','k12','name','tel','nc','web','tsk','bel','pol','edu'], + 'tw' => ['com','net','org','edu','gov','mil','idv','game','ebiz','club','gnu'], + 'ua' => ['com','net','org','edu','gov','in','at','cn','crimea','dn','dnepropetrovsk','donetsk','dp','if','ivano-frankivsk','kh','kharkov','kherson','khmelnitskiy','kiev','kirovograd','km','kr','ks','kv','lg','lt','lugansk','lutsk','lv','lviv','mk','mk.ua','mykolaiv','net','nikolaev','od','odessa','pl','poltava','rovno','rv','sebastopol','sm','sumy','te','ternopil','uz','uzhgorod','vinnica','vn','volyn','yalta','zaporizhzhe','zhitomir','zp','zt'], + 'uk' => ['co','me','org','ltd','plc','net','sch','ac','gov','nhs','police','mod','nhs','parliament'], + 'us' => ['ak','al','ar','as','az','ca','co','ct','dc','de','fl','ga','gu','hi','ia','id','il','in','ks','ky','la','ma','md','me','mi','mn','mo','ms','mt','nc','nd','ne','nh','nj','nm','nv','ny','oh','ok','or','pa','pr','ri','sc','sd','tn','tx','ut','vi','vt','va','wa','wi','wv','wy','dni','fed','isa','kids','nsn'], + 'uy' => ['com','net','org','gub','mil','edu'], + 've' => ['co','com','edu','gob','info','net','org','web'], + 'vn' => ['com','net','org','edu','gov','int','ac','biz','info','name','pro','health'], + 'yu' => ['ac','co','edu','gov','org'], + 'za' => ['ac','alt','co','edu','gov','law','mil','net','ngo','nom','org','school','tm','web'], ]; private bool $subdomainRedirect; @@ -61,7 +140,7 @@ final readonly class DomainManager implements DomainInterface if ($this->authBase()) { $host = parse_url($url, PHP_URL_HOST); - if ($host === null) { + if ($host === null || $host === false || $host === '') { return false; } /* do not send the user to another domain */ @@ -104,7 +183,7 @@ final readonly class DomainManager implements DomainInterface return null; } - $parts = explode('.', $host); + $parts = explode('.', strtolower($host)); $keep = $this->baseLength($parts); $parts = array_slice($parts, -$keep); return implode('.', $parts); diff --git a/src/Service/LoginInterface.php b/src/Service/LoginInterface.php index 84f3e44..9ce34eb 100644 --- a/src/Service/LoginInterface.php +++ b/src/Service/LoginInterface.php @@ -1,5 +1,7 @@ scope = Scope::Cookie; } - if ($this->getTotp()->verify($payload->token, null, 10) || + if ($this->getTotp()->verify($payload->token, null, 1) || $this->backupCodeManager->verifyAndConsume($payload->token) ) { /* token is correct (TOTP or Backup) */ diff --git a/src/Trait/GetTotpTrait.php b/src/Trait/GetTotpTrait.php index e4033e4..d6a01f9 100644 --- a/src/Trait/GetTotpTrait.php +++ b/src/Trait/GetTotpTrait.php @@ -7,6 +7,7 @@ namespace App\Trait; use App\ConfigBag; use OTPHP\Factory; use OTPHP\TOTPInterface; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Contracts\Service\Attribute\Required; @@ -29,6 +30,6 @@ trait GetTotpTrait if ($otp instanceof TOTPInterface) { return $otp; } - throw new HttpException(500, 'Internal Server Exception'); + throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'Internal Server Error'); } } diff --git a/templates/_script.html.twig b/templates/_script.html.twig index b88b91e..8c2a4ee 100644 --- a/templates/_script.html.twig +++ b/templates/_script.html.twig @@ -53,9 +53,7 @@ form.addEventListener('submit', (event) => { console.log('got html response'); {% endif -%} response.text().then((html) => { - document.open(); - document.write(html); - document.close(); + document.documentElement.innerHTML = html; }).catch((error) => { console.log('failed to get html from response'); console.log(error); diff --git a/templates/_style.html.twig b/templates/_style.html.twig index f65a197..cac8f83 100644 --- a/templates/_style.html.twig +++ b/templates/_style.html.twig @@ -1,10 +1,10 @@