ソースを参照

Web: allow to require password change and two-factor for admins

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 1 年間 前
コミット
de089e51fd

+ 25 - 25
go.mod

@@ -9,15 +9,15 @@ require (
 	github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
 	github.com/alexedwards/argon2id v1.0.0
 	github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
-	github.com/aws/aws-sdk-go-v2 v1.25.0
-	github.com/aws/aws-sdk-go-v2/config v1.27.0
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.0
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0
-	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2
-	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.1
-	github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1
-	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.1
-	github.com/aws/aws-sdk-go-v2/service/sts v1.27.0
+	github.com/aws/aws-sdk-go-v2 v1.25.1
+	github.com/aws/aws-sdk-go-v2/config v1.27.2
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.2
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1
+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.4
+	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.3
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3
+	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.3
+	github.com/aws/aws-sdk-go-v2/service/sts v1.27.2
 	github.com/bmatcuk/doublestar/v4 v4.6.1
 	github.com/cockroachdb/cockroach-go/v2 v2.3.6
 	github.com/coreos/go-oidc/v3 v3.9.0
@@ -38,7 +38,7 @@ require (
 	github.com/hashicorp/go-retryablehttp v0.7.5
 	github.com/jackc/pgx/v5 v5.5.3
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
-	github.com/klauspost/compress v1.17.6
+	github.com/klauspost/compress v1.17.7
 	github.com/lestrrat-go/jwx/v2 v2.0.20
 	github.com/lithammer/shortuuid/v3 v3.0.7
 	github.com/mattn/go-sqlite3 v1.14.22
@@ -74,7 +74,7 @@ require (
 	golang.org/x/sys v0.17.0
 	golang.org/x/term v0.17.0
 	golang.org/x/time v0.5.0
-	google.golang.org/api v0.165.0
+	google.golang.org/api v0.166.0
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1
 )
 
@@ -85,18 +85,18 @@ require (
 	cloud.google.com/go/iam v1.1.6 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
 	github.com/ajg/form v1.5.1 // indirect
-	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0 // indirect
+	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 // indirect
-	github.com/aws/smithy-go v1.20.0 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.19.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2 // indirect
+	github.com/aws/smithy-go v1.20.1 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/boombuler/barcode v1.0.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
@@ -173,9 +173,9 @@ require (
 	golang.org/x/tools v0.18.0 // indirect
 	golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
-	google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 // indirect
+	google.golang.org/genproto v0.0.0-20240221002015-b0ce06bbee7c // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
 	google.golang.org/grpc v1.61.1 // indirect
 	google.golang.org/protobuf v1.32.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect

+ 50 - 50
go.sum

@@ -33,48 +33,48 @@ github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHc
 github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
 github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964 h1:I9YN9WMo3SUh7p/4wKeNvD/IQla3U3SUa61U7ul+xM4=
 github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964/go.mod h1:eFiR01PwTcpbzXtdMces7zxg6utvFM5puiWHpWB8D/k=
-github.com/aws/aws-sdk-go-v2 v1.25.0 h1:sv7+1JVJxOu/dD/sz/csHX7jFqmP001TIY7aytBWDSQ=
-github.com/aws/aws-sdk-go-v2 v1.25.0/go.mod h1:G104G1Aho5WqF+SR3mDIobTABQzpYV0WxMsKxlMggOA=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0 h1:2UO6/nT1lCZq1LqM67Oa4tdgP1CvL1sLSxvuD+VrOeE=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.0/go.mod h1:5zGj2eA85ClyedTDK+Whsu+w9yimnVIZvhvBKrDquM8=
-github.com/aws/aws-sdk-go-v2/config v1.27.0 h1:J5sdGCAHuWKIXLeXiqr8II/adSvetkx0qdZwdbXXpb0=
-github.com/aws/aws-sdk-go-v2/config v1.27.0/go.mod h1:cfh8v69nuSUohNFMbIISP2fhmblGmYEOKs5V53HiHnk=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.0 h1:lMW2x6sKBsiAJrpi1doOXqWFyEPoE886DTb1X0wb7So=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.0/go.mod h1:uT41FIH8cCIxOdUYIL0PYyHlL1NoneDuDSCwg5VE/5o=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 h1:xWCwjjvVz2ojYTP4kBKUuUh9ZrXfcAXpflhOUUeXg1k=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0/go.mod h1:j3fACuqXg4oMTQOR2yY7m0NmJY0yBK4L4sLsRXq1Ins=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2 h1:VEekE/fJWqAWYozxFQ07B+h8NdvTPAYhV13xIBenuO0=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.2/go.mod h1:8vozqAHmDNmoD4YbuDKIfpnLbByzngczL4My1RELLVo=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0 h1:NPs/EqVO+ajwOoq56EfcGKa3L3ruWuazkIw1BqxwOPw=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.0/go.mod h1:D+duLy2ylgatV+yTlQ8JTuLfDD0BnFvnQRc+o6tbZ4M=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0 h1:ks7KGMVUMoDzcxNWUlEdI+/lokMFD136EL6DWmUOV80=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.0/go.mod h1:hL6BWM/d/qz113fVitZjbXR0E+RCTU1+x+1Idyn5NgE=
+github.com/aws/aws-sdk-go-v2 v1.25.1 h1:P7hU6A5qEdmajGwvae/zDkOq+ULLC9tQBTwqqiwFGpI=
+github.com/aws/aws-sdk-go-v2 v1.25.1/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1/go.mod h1:sxpLb+nZk7tIfCWChfd+h4QwHNUR57d8hA1cleTkjJo=
+github.com/aws/aws-sdk-go-v2/config v1.27.2 h1:XnMKB9JRjfnxg9ZkUic4MiapnWJISWRo8HVM+7nx9qQ=
+github.com/aws/aws-sdk-go-v2/config v1.27.2/go.mod h1:z/XIktFoVIKNEqX/811vx4eHetrC3tAkgJKL1ZY/KM4=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.2 h1:tCZXWtH0HiIEZ50NJ7/QEaXmuzEd36L+2JUiZkp2nsc=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.2/go.mod h1:7Zo+D6q4auSIo3p4EItuTKTk7J+RqjASISZqLvmUgpc=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1 h1:lk1ZZFbdb24qpOwVC1AwYNrswUjAxeyey6kFBVANudQ=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1/go.mod h1:/xJ6x1NehNGCX4tvGzzj2bq5TBOT/Yxq+qbL9Jpx2Vk=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.4 h1:yuhSpqtahkrC8kRCU5v4gEaTDy/ccTIPIkufIRF7YTk=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.4/go.mod h1:q3SxgP2WD9YRLCybtyse8EgO3vKKWVmxlTmBNeRXPyk=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 h1:evvi7FbTAoFxdP/mixmP7LIYzQWAmzBcwNB/es9XPNc=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1/go.mod h1:rH61DT6FDdikhPghymripNUCsf+uVF4Cnk4c4DBKH64=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 h1:RAnaIrbxPtlXNVI/OIlh1sidTQ3e1qM6LRjs7N0bE0I=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1/go.mod h1:nbgAGkH5lk0RZRMh6A4K/oG6Xj11eC/1CyDow+DUAFI=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.0 h1:TkbRExyKSVHELwG9gz2+gql37jjec2R5vus9faTomwE=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.0/go.mod h1:T3/9xMKudHhnj8it5EqIrhvv11tVZqWYkKcot+BFStc=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0 h1:a33HuFlO0KsveiP90IUJh8Xr/cx9US2PqkSroaLc+o8=
-github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.0/go.mod h1:SxIkWpByiGbhbHYTo9CMTUnx2G4p4ZQMrDPcRRy//1c=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.0 h1:UiSyK6ent6OKpkMJN3+k5HZ4sk4UfchEaaW5wv7SblQ=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.0/go.mod h1:l7kzl8n8DXoRyFz5cIMG70HnPauWa649TUhgw8Rq6lo=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0 h1:SHN/umDLTmFTmYfI+gkanz6da3vK8Kvj/5wkqnTHbuA=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.0/go.mod h1:l8gPU5RYGOFHJqWEpPMoRTP0VoaWQSkJdKo+hwWnnDA=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0 h1:l5puwOHr7IxECuPMIuZG7UKOzAnF24v6t4l+Z5Moay4=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.0/go.mod h1:Oov79flWa/n7Ni+lQC3z+VM7PoRM47omRqbJU9B5Y7E=
-github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.1 h1:eHNChn4Sp+g1hdz4rkx96n1l/LpJEQLDuFB0V+fA/yg=
-github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.1/go.mod h1:9ev55pJx9xNX3UAOKzZmbmaTbwwuLTCemOJPsd7rUz8=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1 h1:bjpWJEXch7moIt3PX2r5XpGROsletl7enqG1Q3Te1Dc=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.50.1/go.mod h1:1o/W6JFUuREj2ExoQ21vHJgO7wakvjhol91M9eknFgs=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.1 h1:ss/HbHbONu0uscM549++4YanT6MnjNN0BGhE5pZRfG4=
-github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.1/go.mod h1:JsJDZFHwLGZu6dxhV9EV1gJrMnCeE4GEXubSZA59xdA=
-github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 h1:u6OkVDxtBPnxPkZ9/63ynEe+8kHbtS5IfaC4PzVxzWM=
-github.com/aws/aws-sdk-go-v2/service/sso v1.19.0/go.mod h1:YqbU3RS/pkDVu+v+Nwxvn0i1WB0HkNWEePWbmODEbbs=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 h1:6DL0qu5+315wbsAEEmzK+P9leRwNbkp+lGjPC+CEvb8=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0/go.mod h1:olUAyg+FaoFaL/zFaeQQONjOZ9HXoxgvI/c7mQTYz7M=
-github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 h1:cjTRjh700H36MQ8M0LnDn33W3JmwC77mdxIIyPWCdpM=
-github.com/aws/aws-sdk-go-v2/service/sts v1.27.0/go.mod h1:nXfOBMWPokIbOY+Gi7a1psWMSvskUCemZzI+SMB7Akc=
-github.com/aws/smithy-go v1.20.0 h1:6+kZsCXZwKxZS9RfISnPc4EXlHoyAkm2hPuM8X2BrrQ=
-github.com/aws/smithy-go v1.20.0/go.mod h1:uo5RKksAl4PzhqaAbjd4rLgFoq5koTsQKYuGe7dklGc=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1 h1:rtYJd3w6IWCTVS8vmMaiXjW198noh2PBm5CiXyJea9o=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1/go.mod h1:zvXu+CTlib30LUy4LTNFc6HTZ/K6zCae5YIHTdX9wIo=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 h1:5Wxh862HkXL9CbQ83BIkWKLIgQapGeuh5zG2G9OZtQk=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1/go.mod h1:V7GLA01pNUxMCYSQsibdVrqUrNIYIT/9lCOyR8ExNvQ=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 h1:cVP8mng1RjDyI3JN/AXFCn5FHNlsBaBH0/MBtG1bg0o=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1/go.mod h1:C8sQjoyAsdfjC7hpy4+S6B92hnFzx0d0UAyHicaOTIE=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 h1:OYmmIcyw19f7x0qLBLQ3XsrCZSSyLhxd9GXng5evsN4=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1/go.mod h1:s5rqdn74Vdg10k61Pwf4ZHEApOSD6CKRe6qpeHDq32I=
+github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.3 h1:uOHZ8HCjUHrRUi+sezA1yCeJVwa4Yy91tZDrWn1sT8w=
+github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.20.3/go.mod h1:TzgisXFoXgCssgP11SzC6KrvcyCErz5c3w++m3xFOfo=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3 h1:Cv/HH7sLzEdJMYQi4MCNHxZeyubQNOOIdVc0VU0lo3Q=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3/go.mod h1:lTW7O4iMAnO2o7H3XJTvqaWFZCH6zIPs+eP7RdG/yp0=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.3 h1:LP38dk6XSNKyWAr3ZNEVECBPjEnoP+/SGvOfX0tRy+U=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.27.3/go.mod h1:RA3ERghFSivbTf0Sbsxv/grUuLMcyAjm0F/PylJMmEs=
+github.com/aws/aws-sdk-go-v2/service/sso v1.19.2 h1:pnj8llQoBAHD4UmbM8UM5GdfycFJKMhgPSeaOyRaZ34=
+github.com/aws/aws-sdk-go-v2/service/sso v1.19.2/go.mod h1:x6/tCd1o/AOKQR+iYnjrzhJxD+w0xRN34asGPaSV7ew=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2 h1:L4yhKxW6HbTSQ08OsvPJuaspaLE40qMgprgXUNFUiMg=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2/go.mod h1:lZB123q0SVQ3dfIbEOcGzhQHrwVBcHVReNS9tm20oU4=
+github.com/aws/aws-sdk-go-v2/service/sts v1.27.2 h1:Dr+7r/p20XpN+1U5tVNZfA2bLq0kQ9IjVBM0iAyMMLg=
+github.com/aws/aws-sdk-go-v2/service/sts v1.27.2/go.mod h1:ozhhG9/NB5c9jcmhGq6tX9dpp21LYdmRWRQVppASim4=
+github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
+github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
@@ -252,8 +252,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
-github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
-github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
+github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -529,8 +529,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
-google.golang.org/api v0.165.0 h1:zd5d4JIIIaYYsfVy1HzoXYZ9rWCSBxxAglbczzo7Bgc=
-google.golang.org/api v0.165.0/go.mod h1:2OatzO7ZDQsoS7IFf3rvsE17/TldiU3F/zxFHeqUB5o=
+google.golang.org/api v0.166.0 h1:6m4NUwrZYhAaVIHZWxaKjw1L1vNAjtMwORmKRyEEo24=
+google.golang.org/api v0.166.0/go.mod h1:4FcBc686KFi7QI/U51/2GKKevfZMpM17sCdibqe/bSA=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
@@ -538,12 +538,12 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
-google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
-google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9 h1:4++qSzdWBUy9/2x8L5KZgwZw+mjJZ2yDSCGMVM0YzRs=
-google.golang.org/genproto/googleapis/api v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:PVreiBMirk8ypES6aw9d4p6iiBNSIfZEBqr3UGoAi2E=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk=
+google.golang.org/genproto v0.0.0-20240221002015-b0ce06bbee7c h1:Zmyn5CV/jxzKnF+3d+xzbomACPwLQqVpLTpyXN5uTaQ=
+google.golang.org/genproto v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo=
+google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c h1:9g7erC9qu44ks7UK4gDNlnk4kOxZG707xKm4jVniy6o=
+google.golang.org/genproto/googleapis/api v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=

+ 6 - 0
internal/dataprovider/admin.go

@@ -199,6 +199,10 @@ type AdminFilters struct {
 	AllowList []string `json:"allow_list,omitempty"`
 	// API key auth allows to impersonate this administrator with an API key
 	AllowAPIKeyAuth bool `json:"allow_api_key_auth,omitempty"`
+	// A password change is required at the next login
+	RequirePasswordChange bool `json:"require_password_change,omitempty"`
+	// Require two factor authentication
+	RequireTwoFactor bool `json:"require_two_factor"`
 	// Time-based one time passwords configuration
 	TOTPConfig AdminTOTPConfig `json:"totp_config,omitempty"`
 	// Recovery codes to use if the user loses access to their second factor auth device.
@@ -615,6 +619,8 @@ func (a *Admin) getACopy() Admin {
 	filters := AdminFilters{}
 	filters.AllowList = make([]string, len(a.Filters.AllowList))
 	filters.AllowAPIKeyAuth = a.Filters.AllowAPIKeyAuth
+	filters.RequirePasswordChange = a.Filters.RequirePasswordChange
+	filters.RequireTwoFactor = a.Filters.RequireTwoFactor
 	filters.TOTPConfig.Enabled = a.Filters.TOTPConfig.Enabled
 	filters.TOTPConfig.ConfigName = a.Filters.TOTPConfig.ConfigName
 	filters.TOTPConfig.Secret = a.Filters.TOTPConfig.Secret.Clone()

+ 3 - 0
internal/httpd/api_admin.go

@@ -150,6 +150,8 @@ func updateAdmin(w http.ResponseWriter, r *http.Request) {
 			sendAPIResponse(w, r, errors.New("you cannot add/change your role"), "", http.StatusBadRequest)
 			return
 		}
+		updatedAdmin.Filters.RequirePasswordChange = admin.Filters.RequirePasswordChange
+		updatedAdmin.Filters.RequireTwoFactor = admin.Filters.RequireTwoFactor
 	}
 	updatedAdmin.ID = admin.ID
 	updatedAdmin.Username = admin.Username
@@ -317,6 +319,7 @@ func doChangeAdminPassword(r *http.Request, currentPassword, newPassword, confir
 	}
 
 	admin.Password = newPassword
+	admin.Filters.RequirePasswordChange = false
 
 	return dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr), claims.Role)
 }

+ 12 - 7
internal/httpd/api_mfa.go

@@ -122,23 +122,25 @@ func saveTOTPConfig(w http.ResponseWriter, r *http.Request) {
 		code := getNewRecoveryCode()
 		recoveryCodes = append(recoveryCodes, dataprovider.RecoveryCode{Secret: kms.NewPlainSecret(code)})
 	}
+	baseURL := webBaseClientPath
 	if claims.hasUserAudience() {
 		if err := saveUserTOTPConfig(claims.Username, r, recoveryCodes); err != nil {
 			sendAPIResponse(w, r, err, "", getRespStatus(err))
 			return
 		}
-		if claims.MustSetTwoFactorAuth {
-			// force logout
-			defer func() {
-				c := jwtTokenClaims{}
-				c.removeCookie(w, r, webBaseClientPath)
-			}()
-		}
 	} else {
 		if err := saveAdminTOTPConfig(claims.Username, r, recoveryCodes); err != nil {
 			sendAPIResponse(w, r, err, "", getRespStatus(err))
 			return
 		}
+		baseURL = webBasePath
+	}
+	if claims.MustSetTwoFactorAuth {
+		// force logout
+		defer func() {
+			c := jwtTokenClaims{}
+			c.removeCookie(w, r, baseURL)
+		}()
 	}
 
 	sendAPIResponse(w, r, nil, "TOTP configuration saved", http.StatusOK)
@@ -303,6 +305,9 @@ func saveAdminTOTPConfig(username string, r *http.Request, recoveryCodes []datap
 	if err != nil {
 		return util.NewValidationError(fmt.Sprintf("unable to decode JSON body: %v", err))
 	}
+	if !admin.Filters.TOTPConfig.Enabled && admin.Filters.RequireTwoFactor {
+		return util.NewValidationError("two-factor authentication must be enabled")
+	}
 	if admin.Filters.TOTPConfig.Enabled {
 		if admin.CountUnusedRecoveryCodes() < 5 && admin.Filters.TOTPConfig.Enabled {
 			admin.Filters.RecoveryCodes = recoveryCodes

+ 1 - 0
internal/httpd/api_utils.go

@@ -830,6 +830,7 @@ func handleResetPassword(r *http.Request, code, newPassword, confirmPassword str
 			return &admin, &user, util.NewValidationError("unable to associate the confirmation code with an existing admin")
 		}
 		admin.Password = newPassword
+		admin.Filters.RequirePasswordChange = false
 		err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr, admin.Role)
 		if err != nil {
 			return &admin, &user, util.NewGenericError(fmt.Sprintf("unable to set the new password: %v", err))

+ 207 - 0
internal/httpd/httpd_test.go

@@ -3501,6 +3501,194 @@ func TestTwoFactorRequirementsGroupLevel(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func TestAdminMustChangePasswordRequirement(t *testing.T) {
+	admin := getTestAdmin()
+	admin.Username = altAdminUsername
+	admin.Password = altAdminPassword
+	admin.Filters.RequirePasswordChange = true
+	admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)
+	assert.NoError(t, err)
+
+	token, _, err := httpdtest.GetToken(altAdminUsername, altAdminPassword)
+	assert.NoError(t, err)
+	httpdtest.SetJWTToken(token)
+
+	_, _, err = httpdtest.GetUsers(0, 0, http.StatusForbidden)
+	assert.NoError(t, err)
+	_, _, err = httpdtest.GetStatus(http.StatusForbidden)
+	assert.NoError(t, err)
+
+	_, err = httpdtest.ChangeAdminPassword(altAdminPassword, defaultTokenAuthPass, http.StatusOK)
+	assert.NoError(t, err)
+
+	httpdtest.SetJWTToken("")
+
+	admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
+	assert.NoError(t, err)
+	assert.False(t, admin.Filters.RequirePasswordChange)
+
+	// get a new token
+	token, _, err = httpdtest.GetToken(altAdminUsername, defaultTokenAuthPass)
+	assert.NoError(t, err)
+	httpdtest.SetJWTToken(token)
+
+	_, _, err = httpdtest.GetUsers(0, 0, http.StatusOK)
+	assert.NoError(t, err)
+
+	desc := xid.New().String()
+	admin.Filters.RequirePasswordChange = true
+	admin.Filters.RequireTwoFactor = true
+	admin.Description = desc
+	_, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)
+	if assert.Error(t, err) {
+		assert.ErrorContains(t, err, "require password change mismatch")
+	}
+	admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
+	assert.NoError(t, err)
+	assert.False(t, admin.Filters.RequirePasswordChange)
+	assert.False(t, admin.Filters.RequireTwoFactor)
+	assert.Equal(t, desc, admin.Description)
+
+	httpdtest.SetJWTToken("")
+
+	admin.Filters.RequirePasswordChange = true
+	_, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)
+	assert.NoError(t, err)
+	// test the same for the WebAdmin
+	webToken, err := getJWTWebTokenFromTestServer(altAdminUsername, defaultTokenAuthPass)
+	assert.NoError(t, err)
+	req, err := http.NewRequest(http.MethodGet, webUsersPath, nil)
+	assert.NoError(t, err)
+	req.RequestURI = webUsersPath
+	setJWTCookieForReq(req, webToken)
+	rr := executeRequest(req)
+	checkResponseCode(t, http.StatusForbidden, rr)
+
+	csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
+	assert.NoError(t, err)
+	form := make(url.Values)
+	form.Set(csrfFormToken, csrfToken)
+	form.Set("current_password", defaultTokenAuthPass)
+	form.Set("new_password1", altAdminPassword)
+	form.Set("new_password2", altAdminPassword)
+	req, err = http.NewRequest(http.MethodPost, webChangeAdminPwdPath, bytes.NewBuffer([]byte(form.Encode())))
+	assert.NoError(t, err)
+	req.RemoteAddr = defaultRemoteAddr
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	setJWTCookieForReq(req, webToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusFound, rr)
+	assert.Equal(t, webLoginPath, rr.Header().Get("Location"))
+
+	admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
+	assert.NoError(t, err)
+	assert.False(t, admin.Filters.RequirePasswordChange)
+
+	webToken, err = getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)
+	assert.NoError(t, err)
+	req, err = http.NewRequest(http.MethodGet, webUsersPath, nil)
+	assert.NoError(t, err)
+	req.RequestURI = webUsersPath
+	setJWTCookieForReq(req, webToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
+
+	_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
+	assert.NoError(t, err)
+}
+
+func TestAdminTwoFactorRequirements(t *testing.T) {
+	admin := getTestAdmin()
+	admin.Username = altAdminUsername
+	admin.Password = altAdminPassword
+	admin.Filters.RequireTwoFactor = true
+	admin, _, err := httpdtest.AddAdmin(admin, http.StatusCreated)
+	assert.NoError(t, err)
+
+	token, err := getJWTAPITokenFromTestServer(altAdminUsername, altAdminPassword)
+	assert.NoError(t, err)
+	req, err := http.NewRequest(http.MethodGet, serverStatusPath, nil)
+	assert.NoError(t, err)
+	setBearerForReq(req, token)
+	rr := executeRequest(req)
+	checkResponseCode(t, http.StatusForbidden, rr)
+	assert.Contains(t, rr.Body.String(), "Two-factor authentication requirements not met")
+
+	webToken, err := getJWTWebTokenFromTestServer(altAdminUsername, altAdminPassword)
+	assert.NoError(t, err)
+	req, err = http.NewRequest(http.MethodGet, webFoldersPath, nil)
+	assert.NoError(t, err)
+	req.RequestURI = webFoldersPath
+	setJWTCookieForReq(req, webToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusForbidden, rr)
+	assert.Contains(t, rr.Body.String(), util.I18nError2FARequiredGeneric)
+	// add TOTP config
+	configName, key, _, err := mfa.GenerateTOTPSecret(mfa.GetAvailableTOTPConfigNames()[0], altAdminUsername)
+	assert.NoError(t, err)
+	adminTOTPConfig := dataprovider.AdminTOTPConfig{
+		Enabled:    true,
+		ConfigName: configName,
+		Secret:     kms.NewPlainSecret(key.Secret()),
+	}
+	asJSON, err := json.Marshal(adminTOTPConfig)
+	assert.NoError(t, err)
+	req, err = http.NewRequest(http.MethodPost, adminTOTPSavePath, bytes.NewBuffer(asJSON))
+	assert.NoError(t, err)
+	setBearerForReq(req, token)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
+	admin, _, err = httpdtest.GetAdminByUsername(altAdminUsername, http.StatusOK)
+	assert.NoError(t, err)
+	assert.True(t, admin.Filters.TOTPConfig.Enabled)
+
+	passcode, err := generateTOTPPasscode(key.Secret())
+	assert.NoError(t, err)
+	req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, tokenPath), nil)
+	assert.NoError(t, err)
+	req.Header.Set("X-SFTPGO-OTP", passcode)
+	req.SetBasicAuth(altAdminUsername, altAdminPassword)
+	resp, err := httpclient.GetHTTPClient().Do(req)
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusOK, resp.StatusCode)
+	responseHolder := make(map[string]any)
+	err = render.DecodeJSON(resp.Body, &responseHolder)
+	assert.NoError(t, err)
+	token = responseHolder["access_token"].(string)
+	assert.NotEmpty(t, token)
+	err = resp.Body.Close()
+	assert.NoError(t, err)
+
+	req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%v%v", httpBaseURL, serverStatusPath), nil)
+	assert.NoError(t, err)
+	setBearerForReq(req, token)
+	resp, err = httpclient.GetHTTPClient().Do(req)
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusOK, resp.StatusCode)
+	err = resp.Body.Close()
+	assert.NoError(t, err)
+	// try to disable 2FA
+	disableReq := map[string]any{
+		"enabled": false,
+	}
+	asJSON, err = json.Marshal(disableReq)
+	assert.NoError(t, err)
+	req, err = http.NewRequest(http.MethodPost, fmt.Sprintf("%v%v", httpBaseURL, adminTOTPSavePath), bytes.NewBuffer(asJSON))
+	assert.NoError(t, err)
+	setBearerForReq(req, token)
+	resp, err = httpclient.GetHTTPClient().Do(req)
+	assert.NoError(t, err)
+	assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
+	bodyResp, err := io.ReadAll(resp.Body)
+	assert.NoError(t, err)
+	assert.Contains(t, string(bodyResp), "two-factor authentication must be enabled")
+	err = resp.Body.Close()
+	assert.NoError(t, err)
+
+	_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
+	assert.NoError(t, err)
+}
+
 func TestLoginUserAPITOTP(t *testing.T) {
 	user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
 	assert.NoError(t, err)
@@ -19835,6 +20023,20 @@ func TestAdminUpdateSelfMock(t *testing.T) {
 	assert.Contains(t, rr.Body.String(), util.I18nErrorAdminSelfDisable)
 
 	form.Set("status", "1")
+	form.Set("require_two_factor", "1")
+	form.Set("require_password_change", "1")
+	req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode())))
+	req.RemoteAddr = defaultRemoteAddr
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	setJWTCookieForReq(req, token)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusSeeOther, rr)
+
+	admin, _, err = httpdtest.GetAdminByUsername(defaultTokenAuthUser, http.StatusOK)
+	assert.NoError(t, err)
+	assert.False(t, admin.Filters.RequirePasswordChange)
+	assert.False(t, admin.Filters.RequireTwoFactor)
+
 	form.Set("role", "my role")
 	req, _ = http.NewRequest(http.MethodPost, path.Join(webAdminPath, defaultTokenAuthUser), bytes.NewBuffer([]byte(form.Encode())))
 	req.RemoteAddr = defaultRemoteAddr
@@ -24375,6 +24577,7 @@ func TestAdminForgotPassword(t *testing.T) {
 	a := getTestAdmin()
 	a.Username = altAdminUsername
 	a.Password = altAdminPassword
+	a.Filters.RequirePasswordChange = true
 	admin, _, err := httpdtest.AddAdmin(a, http.StatusCreated)
 	assert.NoError(t, err)
 
@@ -24514,6 +24717,10 @@ func TestAdminForgotPassword(t *testing.T) {
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusNotFound, rr)
 
+	admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
+	assert.NoError(t, err)
+	assert.False(t, admin.Filters.RequirePasswordChange)
+
 	_, err = httpdtest.RemoveAdmin(admin, http.StatusOK)
 	assert.NoError(t, err)
 }

+ 8 - 0
internal/httpd/internal_test.go

@@ -1554,6 +1554,14 @@ func TestJWTTokenValidation(t *testing.T) {
 	fn.ServeHTTP(rr, req.WithContext(ctx))
 	assert.Equal(t, http.StatusBadRequest, rr.Code)
 
+	fn = server.checkAuthRequirements(r)
+	rr = httptest.NewRecorder()
+	req, _ = http.NewRequest(http.MethodPost, webGroupsPath, nil)
+	req.RequestURI = webGroupsPath
+	ctx = jwtauth.NewContext(req.Context(), token, errTest)
+	fn.ServeHTTP(rr, req.WithContext(ctx))
+	assert.Equal(t, http.StatusBadRequest, rr.Code)
+
 	rr = httptest.NewRecorder()
 	req, _ = http.NewRequest(http.MethodPost, userSharesPath, nil)
 	req.RequestURI = userSharesPath

+ 27 - 12
internal/httpd/middleware.go

@@ -223,7 +223,11 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler {
 		_, claims, err := jwtauth.FromContext(r.Context())
 		if err != nil {
 			if isWebRequest(r) {
-				s.renderClientBadRequestPage(w, r, err)
+				if isWebClientRequest(r) {
+					s.renderClientBadRequestPage(w, r, err)
+				} else {
+					s.renderBadRequestPage(w, r, err)
+				}
 			} else {
 				sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
 			}
@@ -234,16 +238,23 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler {
 		if tokenClaims.MustSetTwoFactorAuth || tokenClaims.MustChangePassword {
 			var err error
 			if tokenClaims.MustSetTwoFactorAuth {
-				protocols := strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", ")
-				err = util.NewI18nError(
-					util.NewGenericError(
-						fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v",
-							protocols)),
-					util.I18nError2FARequired,
-					util.I18nErrorArgs(map[string]any{
-						"val": protocols,
-					}),
-				)
+				if len(tokenClaims.RequiredTwoFactorProtocols) > 0 {
+					protocols := strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", ")
+					err = util.NewI18nError(
+						util.NewGenericError(
+							fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v",
+								protocols)),
+						util.I18nError2FARequired,
+						util.I18nErrorArgs(map[string]any{
+							"val": protocols,
+						}),
+					)
+				} else {
+					err = util.NewI18nError(
+						util.NewGenericError("Two-factor authentication requirements not met, please configure two-factor authentication"),
+						util.I18nError2FARequiredGeneric,
+					)
+				}
 			} else {
 				err = util.NewI18nError(
 					util.NewGenericError("Password change required. Please set a new password to continue to use your account"),
@@ -251,7 +262,11 @@ func (s *httpdServer) checkAuthRequirements(next http.Handler) http.Handler {
 				)
 			}
 			if isWebRequest(r) {
-				s.renderClientForbiddenPage(w, r, err)
+				if isWebClientRequest(r) {
+					s.renderClientForbiddenPage(w, r, err)
+				} else {
+					s.renderForbiddenPage(w, r, err)
+				}
 			} else {
 				sendAPIResponse(w, r, err, "", http.StatusForbidden)
 			}

+ 245 - 231
internal/httpd/server.go

@@ -794,6 +794,8 @@ func (s *httpdServer) loginAdmin(
 		Role:                 admin.Role,
 		Signature:            admin.GetSignature(),
 		HideUserPageSections: admin.Filters.Preferences.HideUserPageSections,
+		MustSetTwoFactorAuth: admin.Filters.RequireTwoFactor && !admin.Filters.TOTPConfig.Enabled,
+		MustChangePassword:   admin.Filters.RequirePasswordChange,
 	}
 
 	audience := tokenAudienceWebAdmin
@@ -982,10 +984,12 @@ func (s *httpdServer) getToken(w http.ResponseWriter, r *http.Request) {
 
 func (s *httpdServer) generateAndSendToken(w http.ResponseWriter, r *http.Request, admin dataprovider.Admin, ip string) {
 	c := jwtTokenClaims{
-		Username:    admin.Username,
-		Permissions: admin.Permissions,
-		Role:        admin.Role,
-		Signature:   admin.GetSignature(),
+		Username:             admin.Username,
+		Permissions:          admin.Permissions,
+		Role:                 admin.Role,
+		Signature:            admin.GetSignature(),
+		MustSetTwoFactorAuth: admin.Filters.RequireTwoFactor && !admin.Filters.TOTPConfig.Enabled,
+		MustChangePassword:   admin.Filters.RequirePasswordChange,
 	}
 
 	resp, err := c.createTokenResponse(s.tokenAuth, tokenAudienceAPI, ip)
@@ -1318,7 +1322,7 @@ func (s *httpdServer) initializeRouter() {
 
 			router.With(forbidAPIKeyAuthentication).Get(logoutPath, s.logout)
 			router.With(forbidAPIKeyAuthentication).Get(adminProfilePath, getAdminProfile)
-			router.With(forbidAPIKeyAuthentication).Put(adminProfilePath, updateAdminProfile)
+			router.With(forbidAPIKeyAuthentication, s.checkAuthRequirements).Put(adminProfilePath, updateAdminProfile)
 			router.With(forbidAPIKeyAuthentication).Put(adminPwdPath, changeAdminPassword)
 			// admin TOTP APIs
 			router.With(forbidAPIKeyAuthentication).Get(adminTOTPConfigsPath, getTOTPConfigs)
@@ -1328,62 +1332,6 @@ func (s *httpdServer) initializeRouter() {
 			router.With(forbidAPIKeyAuthentication).Get(admin2FARecoveryCodesPath, getRecoveryCodes)
 			router.With(forbidAPIKeyAuthentication).Post(admin2FARecoveryCodesPath, generateRecoveryCodes)
 
-			router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus)).
-				Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
-					r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
-					render.JSON(w, r, getServicesStatus())
-				})
-
-			router.With(s.checkPerm(dataprovider.PermAdminViewConnections)).Get(activeConnectionsPath, getActiveConnections)
-			router.With(s.checkPerm(dataprovider.PermAdminCloseConnections)).
-				Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection)
-			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/users/scans", getUsersQuotaScans)
-			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/users/{username}/scan", startUserQuotaScan)
-			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/folders/scans", getFoldersQuotaScans)
-			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/folders/{name}/scan", startFolderQuotaScan)
-			router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers)
-			router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser)
-			router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername) //nolint:goconst
-			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser)
-			router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser)
-			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath, getFolders)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath+"/{name}", getFolderByName) //nolint:goconst
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(folderPath, addFolder)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Put(folderPath+"/{name}", updateFolder)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Delete(folderPath+"/{name}", deleteFolder)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath, getGroups)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath+"/{name}", getGroupByName)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(groupPath, addGroup)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Put(groupPath+"/{name}", updateGroup)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Delete(groupPath+"/{name}", deleteGroup)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest)
-			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/usage",
-				updateUserQuotaUsage)
-			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/transfer-usage",
-				updateUserTransferQuotaUsage)
-			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/folders/{name}/usage",
-				updateFolderQuotaUsage)
-			router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts, getDefenderHosts)
-			router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts+"/{id}", getDefenderHostByID)
-			router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(defenderHosts+"/{id}", deleteDefenderHostByID)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath, getAdmins)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(adminPath, addAdmin)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
-			router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks)
-			router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check",
-				startRetentionCheck)
-			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
-				Get(fsEventsPath, searchFsEvents)
-			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
-				Get(providerEventsPath, searchProviderEvents)
-			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
-				Get(logEventsPath, searchLogEvents)
 			router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
 				Get(apiKeysPath, getAPIKeys)
 			router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
@@ -1394,27 +1342,88 @@ func (s *httpdServer) initializeRouter() {
 				Put(apiKeysPath+"/{id}", updateAPIKey)
 			router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
 				Delete(apiKeysPath+"/{id}", deleteAPIKey)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath, getEventActions)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath+"/{name}", getEventActionByName)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventActionsPath, addEventAction)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventActionsPath+"/{name}", updateEventAction)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventActionsPath+"/{name}", deleteEventAction)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath, getEventRules)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath+"/{name}", getEventRuleByName)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath, addEventRule)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventRulesPath+"/{name}", updateEventRule)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventRulesPath+"/{name}", deleteEventRule)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath+"/run/{name}", runOnDemandRule)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Get(rolesPath, getRoles)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(rolesPath, addRole)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Get(rolesPath+"/{name}", getRoleByName)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Put(rolesPath+"/{name}", updateRole)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Delete(rolesPath+"/{name}", deleteRole)
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler).Get(ipListsPath+"/{type}", getIPListEntries) //nolint:goconst
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(ipListsPath+"/{type}", addIPListEntry)
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(ipListsPath+"/{type}/{ipornet}", getIPListEntry) //nolint:goconst
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Put(ipListsPath+"/{type}/{ipornet}", updateIPListEntry)
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Delete(ipListsPath+"/{type}/{ipornet}", deleteIPListEntry)
+
+			router.Group(func(router chi.Router) {
+				router.Use(s.checkAuthRequirements)
+
+				router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus)).
+					Get(serverStatusPath, func(w http.ResponseWriter, r *http.Request) {
+						r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+						render.JSON(w, r, getServicesStatus())
+					})
+
+				router.With(s.checkPerm(dataprovider.PermAdminViewConnections)).Get(activeConnectionsPath, getActiveConnections)
+				router.With(s.checkPerm(dataprovider.PermAdminCloseConnections)).
+					Delete(activeConnectionsPath+"/{connectionID}", handleCloseConnection)
+				router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/users/scans", getUsersQuotaScans)
+				router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/users/{username}/scan", startUserQuotaScan)
+				router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Get(quotasBasePath+"/folders/scans", getFoldersQuotaScans)
+				router.With(s.checkPerm(dataprovider.PermAdminQuotaScans)).Post(quotasBasePath+"/folders/{name}/scan", startFolderQuotaScan)
+				router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath, getUsers)
+				router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(userPath, addUser)
+				router.With(s.checkPerm(dataprovider.PermAdminViewUsers)).Get(userPath+"/{username}", getUserByUsername) //nolint:goconst
+				router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}", updateUser)
+				router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers)).Delete(userPath+"/{username}", deleteUser)
+				router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(userPath+"/{username}/2fa/disable", disableUser2FA)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath, getFolders)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Get(folderPath+"/{name}", getFolderByName) //nolint:goconst
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(folderPath, addFolder)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Put(folderPath+"/{name}", updateFolder)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Delete(folderPath+"/{name}", deleteFolder)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath, getGroups)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Get(groupPath+"/{name}", getGroupByName)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(groupPath, addGroup)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Put(groupPath+"/{name}", updateGroup)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Delete(groupPath+"/{name}", deleteGroup)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(dumpDataPath, dumpData)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(loadDataPath, loadData)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(loadDataPath, loadDataFromRequest)
+				router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/usage",
+					updateUserQuotaUsage)
+				router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/users/{username}/transfer-usage",
+					updateUserTransferQuotaUsage)
+				router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Put(quotasBasePath+"/folders/{name}/usage",
+					updateFolderQuotaUsage)
+				router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts, getDefenderHosts)
+				router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(defenderHosts+"/{id}", getDefenderHostByID)
+				router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(defenderHosts+"/{id}", deleteDefenderHostByID)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath, getAdmins)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(adminPath, addAdmin)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Get(adminPath+"/{username}", getAdminByUsername)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}", updateAdmin)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Delete(adminPath+"/{username}", deleteAdmin)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Put(adminPath+"/{username}/2fa/disable", disableAdmin2FA)
+				router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Get(retentionChecksPath, getRetentionChecks)
+				router.With(s.checkPerm(dataprovider.PermAdminRetentionChecks)).Post(retentionBasePath+"/{username}/check",
+					startRetentionCheck)
+				router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
+					Get(fsEventsPath, searchFsEvents)
+				router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
+					Get(providerEventsPath, searchProviderEvents)
+				router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
+					Get(logEventsPath, searchLogEvents)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath, getEventActions)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventActionsPath+"/{name}", getEventActionByName)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventActionsPath, addEventAction)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventActionsPath+"/{name}", updateEventAction)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventActionsPath+"/{name}", deleteEventAction)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath, getEventRules)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Get(eventRulesPath+"/{name}", getEventRuleByName)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath, addEventRule)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Put(eventRulesPath+"/{name}", updateEventRule)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Delete(eventRulesPath+"/{name}", deleteEventRule)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(eventRulesPath+"/run/{name}", runOnDemandRule)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Get(rolesPath, getRoles)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(rolesPath, addRole)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Get(rolesPath+"/{name}", getRoleByName)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Put(rolesPath+"/{name}", updateRole)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Delete(rolesPath+"/{name}", deleteRole)
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler).Get(ipListsPath+"/{type}", getIPListEntries) //nolint:goconst
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(ipListsPath+"/{type}", addIPListEntry)
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(ipListsPath+"/{type}/{ipornet}", getIPListEntry) //nolint:goconst
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Put(ipListsPath+"/{type}/{ipornet}", updateIPListEntry)
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Delete(ipListsPath+"/{type}/{ipornet}", deleteIPListEntry)
+			})
 		})
 
 		s.router.Get(userTokenPath, s.getUserToken)
@@ -1675,8 +1684,9 @@ func (s *httpdServer) setupWebAdminRoutes() {
 			router.Use(jwtAuthenticatorWebAdmin)
 
 			router.Get(webLogoutPath, s.handleWebAdminLogout)
-			router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webAdminProfilePath, s.handleWebAdminProfile)
-			router.With(s.requireBuiltinLogin).Post(webAdminProfilePath, s.handleWebAdminProfilePost)
+			router.With(s.refreshCookie, s.checkAuthRequirements, s.requireBuiltinLogin).Get(
+				webAdminProfilePath, s.handleWebAdminProfile)
+			router.With(s.checkAuthRequirements, s.requireBuiltinLogin).Post(webAdminProfilePath, s.handleWebAdminProfilePost)
 			router.With(s.refreshCookie, s.requireBuiltinLogin).Get(webChangeAdminPwdPath, s.handleWebAdminChangePwd)
 			router.With(s.requireBuiltinLogin).Post(webChangeAdminPwdPath, s.handleWebAdminChangePwdPost)
 
@@ -1689,153 +1699,157 @@ func (s *httpdServer) setupWebAdminRoutes() {
 				getRecoveryCodes)
 			router.With(verifyCSRFHeader, s.requireBuiltinLogin).Post(webAdminRecoveryCodesPath, generateRecoveryCodes)
 
-			router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
-				Get(webUsersPath, s.handleGetWebUsers)
-			router.With(s.checkPerm(dataprovider.PermAdminViewUsers), compressor.Handler, s.refreshCookie).
-				Get(webUsersPath+jsonAPISuffix, getAllUsers)
-			router.With(s.checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
-				Get(webUserPath, s.handleWebAddUserGet)
-			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
-				Get(webUserPath+"/{username}", s.handleWebUpdateUserGet)
-			router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(webUserPath, s.handleWebAddUserPost)
-			router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Post(webUserPath+"/{username}",
-				s.handleWebUpdateUserPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
-				Get(webGroupsPath, s.handleWebGetGroups)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups), compressor.Handler, s.refreshCookie).
-				Get(webGroupsPath+jsonAPISuffix, getAllGroups)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
-				Get(webGroupPath, s.handleWebAddGroupGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath, s.handleWebAddGroupPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
-				Get(webGroupPath+"/{name}", s.handleWebUpdateGroupGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath+"/{name}",
-				s.handleWebUpdateGroupPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageGroups), verifyCSRFHeader).
-				Delete(webGroupPath+"/{name}", deleteGroup)
-			router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
-				Get(webConnectionsPath, s.handleWebGetConnections)
-			router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
-				Get(webConnectionsPath+jsonAPISuffix, getActiveConnections)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
-				Get(webFoldersPath, s.handleWebGetFolders)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders), compressor.Handler, s.refreshCookie).
-				Get(webFoldersPath+jsonAPISuffix, getAllFolders)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
-				Get(webFolderPath, s.handleWebAddFolderGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath, s.handleWebAddFolderPost)
-			router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus), s.refreshCookie).
-				Get(webStatusPath, s.handleWebGetStatus)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
-				Get(webAdminsPath, s.handleGetWebAdmins)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), compressor.Handler, s.refreshCookie).
-				Get(webAdminsPath+jsonAPISuffix, getAllAdmins)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
-				Get(webAdminPath, s.handleWebAddAdminGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
-				Get(webAdminPath+"/{username}", s.handleWebUpdateAdminGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, s.handleWebAddAdminPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}",
-				s.handleWebUpdateAdminPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader).
-				Delete(webAdminPath+"/{username}", deleteAdmin)
-			router.With(s.checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
-				Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
-				Get(webFolderPath+"/{name}", s.handleWebUpdateFolderGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath+"/{name}",
-				s.handleWebUpdateFolderPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageFolders), verifyCSRFHeader).
-				Delete(webFolderPath+"/{name}", deleteFolder)
-			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
-				Post(webScanVFolderPath+"/{name}", startFolderQuotaScan)
-			router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
-				Delete(webUserPath+"/{username}", deleteUser)
-			router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
-				Post(webQuotaScanPath+"/{username}", startUserQuotaScan)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, s.handleWebMaintenance)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webRestorePath, s.handleWebRestore)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
-				Get(webTemplateUser, s.handleWebTemplateUserGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateUser, s.handleWebTemplateUserPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
-				Get(webTemplateFolder, s.handleWebTemplateFolderGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, s.handleWebTemplateFolderPost)
-			router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderPath, s.handleWebDefenderPage)
-			router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts)
-			router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}",
-				deleteDefenderHostByID)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie).
-				Get(webAdminEventActionsPath+jsonAPISuffix, getAllActions)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
-				Get(webAdminEventActionsPath, s.handleWebGetEventActions)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
-				Get(webAdminEventActionPath, s.handleWebAddEventActionGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath,
-				s.handleWebAddEventActionPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
-				Get(webAdminEventActionPath+"/{name}", s.handleWebUpdateEventActionGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath+"/{name}",
-				s.handleWebUpdateEventActionPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
-				Delete(webAdminEventActionPath+"/{name}", deleteEventAction)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie).
-				Get(webAdminEventRulesPath+jsonAPISuffix, getAllRules)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
-				Get(webAdminEventRulesPath, s.handleWebGetEventRules)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
-				Get(webAdminEventRulePath, s.handleWebAddEventRuleGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath,
-				s.handleWebAddEventRulePost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
-				Get(webAdminEventRulePath+"/{name}", s.handleWebUpdateEventRuleGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath+"/{name}",
-				s.handleWebUpdateEventRulePost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
-				Delete(webAdminEventRulePath+"/{name}", deleteEventRule)
-			router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
-				Post(webAdminEventRulePath+"/run/{name}", runOnDemandRule)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
-				Get(webAdminRolesPath, s.handleWebGetRoles)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles), compressor.Handler, s.refreshCookie).
-				Get(webAdminRolesPath+jsonAPISuffix, getAllRoles)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
-				Get(webAdminRolePath, s.handleWebAddRoleGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath, s.handleWebAddRolePost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
-				Get(webAdminRolePath+"/{name}", s.handleWebUpdateRoleGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath+"/{name}",
-				s.handleWebUpdateRolePost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageRoles), verifyCSRFHeader).
-				Delete(webAdminRolePath+"/{name}", deleteRole)
-			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), s.refreshCookie).Get(webEventsPath,
-				s.handleWebGetEvents)
-			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).
-				Get(webEventsFsSearchPath, searchFsEvents)
-			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).
-				Get(webEventsProviderSearchPath, searchProviderEvents)
-			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).
-				Get(webEventsLogSearchPath, searchLogEvents)
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(webIPListsPath, s.handleWebIPListsPage)
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler, s.refreshCookie).
-				Get(webIPListsPath+"/{type}", getIPListEntries)
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.refreshCookie).Get(webIPListPath+"/{type}",
-				s.handleWebAddIPListEntryGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}",
-				s.handleWebAddIPListEntryPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.refreshCookie).Get(webIPListPath+"/{type}/{ipornet}",
-				s.handleWebUpdateIPListEntryGet)
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}/{ipornet}",
-				s.handleWebUpdateIPListEntryPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), verifyCSRFHeader).
-				Delete(webIPListPath+"/{type}/{ipornet}", deleteIPListEntry)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).Get(webConfigsPath, s.handleWebConfigs)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webConfigsPath, s.handleWebConfigsPost)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie).
-				Post(webConfigsPath+"/smtp/test", testSMTPConfig)
-			router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie).
-				Post(webOAuth2TokenPath, handleSMTPOAuth2TokenRequestPost)
+			router.Group(func(router chi.Router) {
+				router.Use(s.checkAuthRequirements)
+
+				router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
+					Get(webUsersPath, s.handleGetWebUsers)
+				router.With(s.checkPerm(dataprovider.PermAdminViewUsers), compressor.Handler, s.refreshCookie).
+					Get(webUsersPath+jsonAPISuffix, getAllUsers)
+				router.With(s.checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
+					Get(webUserPath, s.handleWebAddUserGet)
+				router.With(s.checkPerm(dataprovider.PermAdminChangeUsers), s.refreshCookie).
+					Get(webUserPath+"/{username}", s.handleWebUpdateUserGet)
+				router.With(s.checkPerm(dataprovider.PermAdminAddUsers)).Post(webUserPath, s.handleWebAddUserPost)
+				router.With(s.checkPerm(dataprovider.PermAdminChangeUsers)).Post(webUserPath+"/{username}",
+					s.handleWebUpdateUserPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
+					Get(webGroupsPath, s.handleWebGetGroups)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups), compressor.Handler, s.refreshCookie).
+					Get(webGroupsPath+jsonAPISuffix, getAllGroups)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
+					Get(webGroupPath, s.handleWebAddGroupGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath, s.handleWebAddGroupPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
+					Get(webGroupPath+"/{name}", s.handleWebUpdateGroupGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups)).Post(webGroupPath+"/{name}",
+					s.handleWebUpdateGroupPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageGroups), verifyCSRFHeader).
+					Delete(webGroupPath+"/{name}", deleteGroup)
+				router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
+					Get(webConnectionsPath, s.handleWebGetConnections)
+				router.With(s.checkPerm(dataprovider.PermAdminViewConnections), s.refreshCookie).
+					Get(webConnectionsPath+jsonAPISuffix, getActiveConnections)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
+					Get(webFoldersPath, s.handleWebGetFolders)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders), compressor.Handler, s.refreshCookie).
+					Get(webFoldersPath+jsonAPISuffix, getAllFolders)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
+					Get(webFolderPath, s.handleWebAddFolderGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath, s.handleWebAddFolderPost)
+				router.With(s.checkPerm(dataprovider.PermAdminViewServerStatus), s.refreshCookie).
+					Get(webStatusPath, s.handleWebGetStatus)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
+					Get(webAdminsPath, s.handleGetWebAdmins)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), compressor.Handler, s.refreshCookie).
+					Get(webAdminsPath+jsonAPISuffix, getAllAdmins)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
+					Get(webAdminPath, s.handleWebAddAdminGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), s.refreshCookie).
+					Get(webAdminPath+"/{username}", s.handleWebUpdateAdminGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath, s.handleWebAddAdminPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins)).Post(webAdminPath+"/{username}",
+					s.handleWebUpdateAdminPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageAdmins), verifyCSRFHeader).
+					Delete(webAdminPath+"/{username}", deleteAdmin)
+				router.With(s.checkPerm(dataprovider.PermAdminCloseConnections), verifyCSRFHeader).
+					Delete(webConnectionsPath+"/{connectionID}", handleCloseConnection)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
+					Get(webFolderPath+"/{name}", s.handleWebUpdateFolderGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath+"/{name}",
+					s.handleWebUpdateFolderPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageFolders), verifyCSRFHeader).
+					Delete(webFolderPath+"/{name}", deleteFolder)
+				router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
+					Post(webScanVFolderPath+"/{name}", startFolderQuotaScan)
+				router.With(s.checkPerm(dataprovider.PermAdminDeleteUsers), verifyCSRFHeader).
+					Delete(webUserPath+"/{username}", deleteUser)
+				router.With(s.checkPerm(dataprovider.PermAdminQuotaScans), verifyCSRFHeader).
+					Post(webQuotaScanPath+"/{username}", startUserQuotaScan)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webMaintenancePath, s.handleWebMaintenance)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Get(webBackupPath, dumpData)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webRestorePath, s.handleWebRestore)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
+					Get(webTemplateUser, s.handleWebTemplateUserGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateUser, s.handleWebTemplateUserPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).
+					Get(webTemplateFolder, s.handleWebTemplateFolderGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webTemplateFolder, s.handleWebTemplateFolderPost)
+				router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderPath, s.handleWebDefenderPage)
+				router.With(s.checkPerm(dataprovider.PermAdminViewDefender)).Get(webDefenderHostsPath, getDefenderHosts)
+				router.With(s.checkPerm(dataprovider.PermAdminManageDefender)).Delete(webDefenderHostsPath+"/{id}",
+					deleteDefenderHostByID)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie).
+					Get(webAdminEventActionsPath+jsonAPISuffix, getAllActions)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
+					Get(webAdminEventActionsPath, s.handleWebGetEventActions)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
+					Get(webAdminEventActionPath, s.handleWebAddEventActionGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath,
+					s.handleWebAddEventActionPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
+					Get(webAdminEventActionPath+"/{name}", s.handleWebUpdateEventActionGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventActionPath+"/{name}",
+					s.handleWebUpdateEventActionPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
+					Delete(webAdminEventActionPath+"/{name}", deleteEventAction)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), compressor.Handler, s.refreshCookie).
+					Get(webAdminEventRulesPath+jsonAPISuffix, getAllRules)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
+					Get(webAdminEventRulesPath, s.handleWebGetEventRules)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
+					Get(webAdminEventRulePath, s.handleWebAddEventRuleGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath,
+					s.handleWebAddEventRulePost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), s.refreshCookie).
+					Get(webAdminEventRulePath+"/{name}", s.handleWebUpdateEventRuleGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules)).Post(webAdminEventRulePath+"/{name}",
+					s.handleWebUpdateEventRulePost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
+					Delete(webAdminEventRulePath+"/{name}", deleteEventRule)
+				router.With(s.checkPerm(dataprovider.PermAdminManageEventRules), verifyCSRFHeader).
+					Post(webAdminEventRulePath+"/run/{name}", runOnDemandRule)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
+					Get(webAdminRolesPath, s.handleWebGetRoles)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles), compressor.Handler, s.refreshCookie).
+					Get(webAdminRolesPath+jsonAPISuffix, getAllRoles)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
+					Get(webAdminRolePath, s.handleWebAddRoleGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath, s.handleWebAddRolePost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles), s.refreshCookie).
+					Get(webAdminRolePath+"/{name}", s.handleWebUpdateRoleGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles)).Post(webAdminRolePath+"/{name}",
+					s.handleWebUpdateRolePost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageRoles), verifyCSRFHeader).
+					Delete(webAdminRolePath+"/{name}", deleteRole)
+				router.With(s.checkPerm(dataprovider.PermAdminViewEvents), s.refreshCookie).Get(webEventsPath,
+					s.handleWebGetEvents)
+				router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).
+					Get(webEventsFsSearchPath, searchFsEvents)
+				router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).
+					Get(webEventsProviderSearchPath, searchProviderEvents)
+				router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).
+					Get(webEventsLogSearchPath, searchLogEvents)
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(webIPListsPath, s.handleWebIPListsPage)
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler, s.refreshCookie).
+					Get(webIPListsPath+"/{type}", getIPListEntries)
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.refreshCookie).Get(webIPListPath+"/{type}",
+					s.handleWebAddIPListEntryGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}",
+					s.handleWebAddIPListEntryPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), s.refreshCookie).Get(webIPListPath+"/{type}/{ipornet}",
+					s.handleWebUpdateIPListEntryGet)
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Post(webIPListPath+"/{type}/{ipornet}",
+					s.handleWebUpdateIPListEntryPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), verifyCSRFHeader).
+					Delete(webIPListPath+"/{type}/{ipornet}", deleteIPListEntry)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem), s.refreshCookie).Get(webConfigsPath, s.handleWebConfigs)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem)).Post(webConfigsPath, s.handleWebConfigsPost)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie).
+					Post(webConfigsPath+"/smtp/test", testSMTPConfig)
+				router.With(s.checkPerm(dataprovider.PermAdminManageSystem), verifyCSRFHeader, s.refreshCookie).
+					Post(webOAuth2TokenPath, handleSMTPOAuth2TokenRequestPost)
+			})
 		})
 	}
 }

+ 12 - 6
internal/httpd/webadmin.go

@@ -210,12 +210,13 @@ type changePasswordPage struct {
 
 type mfaPage struct {
 	basePage
-	TOTPConfigs     []string
-	TOTPConfig      dataprovider.AdminTOTPConfig
-	GenerateTOTPURL string
-	ValidateTOTPURL string
-	SaveTOTPURL     string
-	RecCodesURL     string
+	TOTPConfigs      []string
+	TOTPConfig       dataprovider.AdminTOTPConfig
+	GenerateTOTPURL  string
+	ValidateTOTPURL  string
+	SaveTOTPURL      string
+	RecCodesURL      string
+	RequireTwoFactor bool
 }
 
 type maintenancePage struct {
@@ -786,6 +787,7 @@ func (s *httpdServer) renderMFAPage(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 	data.TOTPConfig = admin.Filters.TOTPConfig
+	data.RequireTwoFactor = admin.Filters.RequireTwoFactor
 	renderAdminTemplate(w, templateMFA, data)
 }
 
@@ -1741,6 +1743,8 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
 	admin.Role = strings.TrimSpace(r.Form.Get("role"))
 	admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
 	admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
+	admin.Filters.RequireTwoFactor = r.Form.Get("require_two_factor") != ""
+	admin.Filters.RequirePasswordChange = r.Form.Get("require_password_change") != ""
 	admin.AdditionalInfo = r.Form.Get("additional_info")
 	admin.Description = r.Form.Get("description")
 	admin.Filters.Preferences.HideUserPageSections = getAdminHiddenUserPageSections(r)
@@ -3016,6 +3020,8 @@ func (s *httpdServer) handleWebUpdateAdminPost(w http.ResponseWriter, r *http.Re
 				), false)
 			return
 		}
+		updatedAdmin.Filters.RequirePasswordChange = admin.Filters.RequirePasswordChange
+		updatedAdmin.Filters.RequireTwoFactor = admin.Filters.RequireTwoFactor
 	}
 	err = dataprovider.UpdateAdmin(&updatedAdmin, claims.Username, ipAddr, claims.Role)
 	if err != nil {

+ 6 - 0
internal/httpdtest/httpdtest.go

@@ -1970,6 +1970,12 @@ func compareAdminFilters(expected, actual dataprovider.AdminFilters) error {
 	if expected.Preferences.DefaultUsersExpiration != actual.Preferences.DefaultUsersExpiration {
 		return errors.New("default users expiration mismatch")
 	}
+	if expected.RequirePasswordChange != actual.RequirePasswordChange {
+		return errors.New("require password change mismatch")
+	}
+	if expected.RequireTwoFactor != actual.RequireTwoFactor {
+		return errors.New("require two factor mismatch")
+	}
 	return nil
 }
 

+ 1 - 0
internal/util/i18n.go

@@ -159,6 +159,7 @@ const (
 	I18nErrorShareExpired              = "share.expired"
 	I18nErrorLoginFromIPDenied         = "login.ip_not_allowed"
 	I18nError2FARequired               = "login.two_factor_required"
+	I18nError2FARequiredGeneric        = "login.two_factor_required_generic"
 	I18nErrorNoOIDCFeature             = "general.no_oidc_feature"
 	I18nErrorNoPermissions             = "general.no_permissions"
 	I18nErrorShareBrowsePaths          = "share.browsable_multiple_paths"

+ 4 - 0
openapi/openapi.yaml

@@ -6003,6 +6003,10 @@ components:
         allow_api_key_auth:
           type: boolean
           description: 'API key auth allows to impersonate this administrator with an API key'
+        require_two_factor:
+          type: boolean
+        require_password_change:
+          type: boolean
         totp_config:
           $ref: '#/components/schemas/AdminTOTPConfig'
         recovery_codes:

+ 4 - 1
static/locales/en/translation.json

@@ -99,6 +99,7 @@
         "reset_ok_login_error": "The password reset completed successfully but an unexpected error occurred while signing in",
         "ip_not_allowed": "Login is not allowed from this IP address",
         "two_factor_required": "Set up two-factor authentication, it is required for the following protocols: {{val}}",
+        "two_factor_required_generic": "Set up two-factor authentication, it is mandatory for your account",
         "link": "Go to {{link}}"
     },
     "theme": {
@@ -386,6 +387,7 @@
         "msg_enabled": "Two-factor authentication is enabled",
         "msg_disabled": "Secure Your Account",
         "msg_info": "Two-factor authentication adds an extra layer of security to your account. To log in you'll need to provide an additional authentication code.",
+        "require": "Two-factor authentication is required for this account",
         "require_for": "Require 2FA for",
         "generate": "Generate new secret key",
         "recovery_codes": "Recovery codes",
@@ -727,7 +729,8 @@
         "user_page_pref_help": "You can hide some sections from the user page. These are not security settings and are not enforced server side in any way. They are only intended to simplify the add/update user page",
         "hide_sections": "Hide sections",
         "default_users_expiration": "Default users expiration",
-        "default_users_expiration_help": "Default expiration for new users as number of days"
+        "default_users_expiration_help": "Default expiration for new users as number of days",
+        "require_pwd_change_help": "A password change is required at the next login"
     },
     "connections": {
         "view_manage": "View and manage connections",

+ 4 - 1
static/locales/it/translation.json

@@ -99,6 +99,7 @@
         "reset_ok_login_error": "La reimpostazione della password è stata completata correttamente ma si è verificato un errore imprevisto durante l'accesso",
         "ip_not_allowed": "L'accesso non è consentito da questo indirizzo IP",
         "two_factor_required": "Configura l'autenticazione a due fattori, è obbligatoria per i seguenti protocolli: {{val}}",
+        "two_factor_required_generic": "Configura l'autenticazione a due fattori, è obbligatoria per il tuo account",
         "link": "Vai a {{link}}"
     },
     "theme": {
@@ -386,6 +387,7 @@
         "msg_enabled": "L'autenticazione a due fattori è abilitata",
         "msg_disabled": "Metti il tuo account in sicurezza",
         "msg_info": "L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza al tuo account. Per accedere dovrai fornire un ulteriore codice di autenticazione.",
+        "require": "L'autenticazione a due fattori è obbligatoria per questo account",
         "require_for": "Richiedi 2FA per",
         "generate": "Genera una nuova chiave segreta",
         "recovery_codes": "Codici di ripristino",
@@ -727,7 +729,8 @@
         "user_page_pref_help": "Puoi nascondere alcune sezioni dalla pagina utente. Queste non sono impostazioni di sicurezza e non vengono verificate lato server. Hanno il solo scopo di semplificare la pagina di creazione/modifica utenti",
         "hide_sections": "Nascondi sezioni",
         "default_users_expiration": "Scadenza predefinita utenti",
-        "default_users_expiration_help": "Scadenza predefinita per i nuovi utenti espressa in numero di giorni"
+        "default_users_expiration_help": "Scadenza predefinita per i nuovi utenti espressa in numero di giorni",
+        "require_pwd_change_help": "Il cambio password è obbligatorio al prossimo accesso"
     },
     "connections": {
         "view_manage": "Visualizza e gestisci connessioni attive",

+ 24 - 0
templates/webadmin/admin.html

@@ -42,6 +42,18 @@ explicit grant from the SFTPGo Team ([email protected]).
                 </div>
             </div>
 
+            <div class="form-group row align-items-center mt-10 {{if eq .LoggedUser.Username .Admin.Username}}d-none{{end}}">
+                <label data-i18n="user.require_pwd_change" class="col-md-3 col-form-label" for="idRequirePasswordChange">Require password change</label>
+                <div class="col-md-9">
+                    <div class="form-check form-switch form-check-custom form-check-solid">
+                        <input class="form-check-input" type="checkbox" id="idRequirePasswordChange" name="require_password_change" {{if .Admin.Filters.RequirePasswordChange}}checked="checked"{{end}}/>
+                        <label data-i18n="admin.require_pwd_change_help" class="form-check-label fw-semibold text-gray-800" for="idRequirePasswordChange">
+                            A password change is required at the next login
+                        </label>
+                    </div>
+                </div>
+            </div>
+
             <div class="form-group row mt-10">
                 <label for="idStatus" data-i18n="general.status" class="col-md-3 col-form-label">Status</label>
                 <div class="col-md-9">
@@ -245,6 +257,18 @@ explicit grant from the SFTPGo Team ([email protected]).
                 </div>
             </div>
 
+            <div class="form-group row align-items-center mt-10 {{if eq .LoggedUser.Username .Admin.Username}}d-none{{end}}">
+                <label data-i18n="title.two_factor_auth_short" class="col-md-3 col-form-label" for="idRequire2FA">2FA</label>
+                <div class="col-md-9">
+                    <div class="form-check form-switch form-check-custom form-check-solid">
+                        <input class="form-check-input" type="checkbox" id="idRequire2FA" name="require_two_factor" {{if .Admin.Filters.RequireTwoFactor}}checked{{end}}/>
+                        <label data-i18n="2fa.require" class="form-check-label fw-semibold text-gray-800" for="idRequire2FA">
+                            Two-factor authentication is required
+                        </label>
+                    </div>
+                </div>
+            </div>
+
             <div class="form-group row mt-10">
                 <label for="idAdditionalInfo" data-i18n="general.additional_info" class="col-md-3 col-form-label">Additional info</label>
                 <div class="col-md-9">

+ 11 - 0
templates/webadmin/mfa.html

@@ -344,6 +344,16 @@ explicit grant from the SFTPGo Team ([email protected]).
     }
 
     function disableConfig() {
+        //{{- if .RequireTwoFactor}}
+        ModalAlert.fire({
+            text: $.t('2fa.require'),
+            icon: "warning",
+            confirmButtonText: $.t('general.ok'),
+            customClass: {
+                confirmButton: "btn btn-primary"
+            }
+        });
+        //{{- else}}
         ModalAlert.fire({
             text: $.t('2fa.disable_question'),
             icon: "warning",
@@ -358,6 +368,7 @@ explicit grant from the SFTPGo Team ([email protected]).
                 doSaveConfig(document.querySelector('#disable_btn'), null, false, true);
             }
         });
+        //{{- end}}
     }
 
     function validatePasscode() {