Add authentication with TOTP two-factor to the admin panel - #898
Open
sven-n wants to merge 3 commits into
Open
Conversation
The admin panel had no authentication of its own. It relied on the basic authentication of the reverse proxy against an .htpasswd file, which meant there was no logout, no roles, no audit trail - and no protection at all when the panel was started without a proxy in front of it. The panel now authenticates its users itself, without a page reload: the login component validates the credentials inside the blazor circuit, switches to the second factor input in place, and only then issues a single use ticket which the browser exchanges for the authentication cookie in the background. The new state is pushed into the running circuit, so every AuthorizeView re-renders without navigating. ASP.NET Core Identity Core provides the password hashing (BCrypt, like the rest of the project), the TOTP validation, recovery codes and lockout. Its user store is implemented over a single table instead of pulling in the eight tables of the Identity EF store. Credential storage: - The users live in an own "admin" schema with an own migration history, not in the Account table of the game: a game password travels over the game protocol and is typed into the game client, while an admin panel user can restart servers and edit the whole configuration. The schema is not granted to any of the game server database roles, so a game server can't read or overwrite an admin password hash. It also has to work before the game database exists, because the panel is the tool which creates it. - Passwords are hashed with BCrypt, TOTP secrets are encrypted with data protection and recovery codes are stored as hashes. Two-factor authentication: - Standard TOTP (SHA-1, 6 digits, 30 seconds), so it works with the Microsoft Authenticator app, which ignores deviating parameters in the otpauth uri. - The second factor is only enabled after the user entered a code its app produced, so a failed scan can't lock anybody out. - The time step of the last accepted code is remembered, so an observed code can't be replayed within its time step. - Failed code entries count towards the lockout. Also: - A bootstrap user from the configuration or from OPENMU_ADMIN_USER / OPENMU_ADMIN_PASSWORD works without any database and closes the window in which a fresh installation would be reachable without a login. Until any user exists, the panel runs in an initial setup mode and says so. - Three roles which build up on each other; setup, plugins, updates, log files and user management require the administrator role. - The API controllers and the log file directory are behind authorization now. - The basic authentication and the .htpasswd mounts are removed from the nginx and traefik deployments, which instead persist the data protection key ring. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015MLJ586D4tVVznbF5g7MHp
…uth-no-refresh-1l6ayl # Conflicts: # deploy/all-in-one-traefik/README.md # src/Web/AdminPanel/Readme.md
Master restructured the documentation while this branch was open: the admin panel documentation moved from the repository readmes into the Docusaurus site. The authentication section which this branch had added to the readme is now a page of that site instead, and the pages which still described the old basic authentication are updated. - Add "Signing in", which covers the login, the bootstrap user and the initial setup mode, the second factor with an authenticator app, recovery codes and the roles. - Rewrite the Users page: it manages admin users with roles and a second factor reset now, not entries of an .htpasswd file. - Drop the "default user admin / password openmu" from the deployment and getting-started pages. There is no default user anymore - the panel either has a bootstrap user from the environment, or it runs in its initial setup mode until the first user was created. - Remove the basic authentication middleware from the Traefik example and the note that Traefik has to be restarted after a user changed, which doesn't apply anymore. - Remove the .htpasswd item from the compose projects, since the file is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015MLJ586D4tVVznbF5g7MHp
Deploying openmudocs with
|
| Latest commit: |
45a5704
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://d54a17d8.openmudocs.pages.dev |
| Branch Preview URL: | https://claude-adminpanel-auth-no-re.openmudocs.pages.dev |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The admin panel had no authentication of its own. It relied on the basic authentication of the reverse proxy against an
.htpasswdfile (NginxHtpasswdFileUserService), which meant there was no logout, no roles, no audit trail — and no protection at all when the panel was started without a proxy in front of it (dev.sh, the standaloneStartuphost, and the DaprAdminPanel.Host, which has no nginx).This adds authentication to the panel itself, including a second factor that works with the Microsoft Authenticator app, without requiring a page reload for the login.
How the login works without a page reload
A cookie can only be set on an http response, which an interactive Blazor component doesn't have — its response was sent when the circuit started. That's why the stock Identity template puts the login on a static-SSR page and does a full page load.
Instead:
/auth/completein the background (fetch). That request is a normal http request and can set the cookie.AuthorizeViewre-renders where it stands.A cookie is only ever issued after the second factor was verified, so an authenticated session always implies the second factor was used when the user has one — there is no partial-auth state to defend against.
Where the credentials are stored
A new
adminschema with its ownAdminPanelContext, its own migration history and its own migration — deliberately not theAccounttable of the game:accountdatabase role and could otherwise read and overwrite admin password hashes. Theadminschema is not in the role-grant map ofMyNpgsqlMigrationsSqlGenerator, so none of the game server roles (account,config,guild,friend) can reach it.Passwords are hashed with BCrypt (consistent with the rest of the project), TOTP secrets are encrypted with data protection, and recovery codes are stored as hashes.
Identity Core, not the full Identity stack
AddIdentityCoreprovides the TOTP validation, recovery codes, lockout and the security stamp. ItsSignInManagercookie flow andIdentityDbContextare not used: a customIUserStoreimplementation keeps everything in a singleAdminUsertable instead of the eight tables of the Identity EF store, which avoids reconciling Identity's migrations with this repo's per-schema role grants.Two-factor authentication
algorithm/digitsvalues in the otpauth uri and computes the defaults anyway, so a "stronger" configuration would just produce codes that never validate. This is noted in the code so it doesn't get "improved" later.Bootstrap user and initial setup mode
Until any user exists, the panel stays reachable without a login and says so — otherwise a fresh installation could never reach
/setupto create the database. To close that window, a bootstrap user can be configured which works without any database:It is also the way back in when the last stored user lost its authenticator. Its state is only kept in memory, which is documented.
Also in this change
Viewer,Operator,Administrator). Setup, plugins, updates, log files and user management requireAdministrator.ServerControllerand the other API endpoints are behind authorization now (MapControllers().RequireAuthorization()), with the theme and culture controllers explicitly anonymous so the login page keeps working./logsstatic file mount is served through an authorization check — static files are middleware, not endpoints, so endpoint authorization does not cover them.Userspage now managesAdminUsers (role, password, second-factor reset) instead of editing.htpasswd.IUserService,UserServiceBaseandNginxHtpasswdFileUserServiceare removed..htpasswdmounts are removed from the nginx and Traefik deployments, which instead persist the data protection key ring in anadminpanel-keysvolume. Without that, a container restart would invalidate every session and make every stored TOTP secret unreadable.src/Web/AdminPanel/Readme.mddocuments all of the above.Testing
MUnique.OpenMU.Web.Tests: 25/25 pass (14 new).MUnique.OpenMU.Tests: 769/769 pass.The new tests build a real
UserManagerover an in-memory repository and drive actual TOTP codes through it (the Identity token provider can only validate codes, so the test project has its own RFC 6238 generator). They cover: login with and without a second factor, wrong password, unknown and disabled users, lockout after repeated failures, a valid authenticator code, a wrong code, replay rejection, recovery-code single use, that the second factor stays off until confirmed, that the authenticator key is not stored in plain text, single-use sign-in tickets, and the role/claim expansion.Not verified
There is no database or browser in the environment this was developed in, so:
/auth/completefetch underUseAntiforgery, andAdminUserRepository.EnsureStorageAsyncrunningMigrateAsyncagainst a real server.Known limitation
The replay guard blocks reuse of a code within a time step, not a code replayed in the immediately adjacent step — Identity's token provider accepts a ±1 step window but doesn't report which step matched. This is documented where the guard lives. It is still strictly better than stock Identity, which tracks nothing.
🤖 Generated with Claude Code
https://claude.ai/code/session_015MLJ586D4tVVznbF5g7MHp
Generated by Claude Code