Repository navigation
feat(auth): sign in, sessions and the current user - #28
Merged
Merged
Conversation
USR-34: the Admin replacing the initial password is its own first access, so the system-record guard does not apply. Repeating it keeps the first time.
AUTH-07, AUTH-08: the handler returns invalid_credentials for an unknown login, a login outside the login format and a wrong password, so the answer never reveals which part was wrong. A pending account has no credential and fails like a wrong password (AUTH-02): that test passed without a Red because the previous cycle already covered a login with no stored password.
… change at sign-in AUTH-08, AUTH-09, AUTH-14, AUTH-28: a locked login is refused even with the right password, with the configured minutes and the seconds left; only a correct password reveals a deactivated account; an account with its initial password gets a token flagged for the mandatory change. The deactivated account with a wrong password had no Red: the password check already comes first.
AUTH-17, AUTH-18, AUTH-27, AUTH-14: refresh answers session_expired for an unknown or expired token, checks the account status because the route is anonymous and ends the session of a deactivated account, and keeps the mandatory password change flag.
AUTH-16, AUTH-28, ADR-0019: the password needs the configured minimum length (default 8) and a matching confirmation, with the messages under the fields password and passwordConfirmation. Sign-out ends only the session named by the sid claim. The configured-length test had no Red: the validator already interpolated the setting.
AUTH-24, AUTH-25, USR-34, ADR-0019, ADR-0032: only an account with a mandatory change may use it (403 forbidden otherwise), the new password must differ from the initial one, the Admin's activatedAt is filled through the unit of work, every session ends and a new one starts. The refused save keeps the password unchanged: that test had no Red, since the branch was written with the previous cycle.
PERM-05, PERM-06, PERM-23, USR-21: me lists every screen of the catalog in order, including the denied ones. The no-profiles and Admin tests had no Red, since the effective-access rules are already covered in the domain.
…tial AUTH-13, AUTH-19, ADR-0019, ADR-0022: the Admin credential is created from Admin:InitialPassword when missing, independently of the Admin row, with a mandatory password change. Migration AddAuthentication adds user_credentials (Identity), its unused satellite tables and user_sessions; it has no Red of its own, the startup tests cover it. The restart and invalid-password tests had no Red: the first cycle already brought that behavior.
AUTH-19, AUTH-17, ADR-0019, ADR-0032: sessions keep only the SHA-256 of a 32-byte refresh token. Rotation is a single UPDATE on the old hash while the session is valid, so two refreshes with the same token cannot both succeed. The rotation and expiry behavior is exercised by the API tests.
AUTH-08, D6: the fifth wrong password already locks, the lockout ends after the configured minutes and a success resets the count. Identity reads the real clock for lockout, so CredentialUserManager overrides only the two virtual methods that compare or set the lockout end to use the application's TimeProvider. Tests 34 and 35 had no Red: the first cycle brought the whole store.
AUTH-07, AUTH-08, AUTH-09, AUTH-17, D1, D6, T10: POST /api/v1/auth/sign-in answers 200 with the access token and sets the refresh cookie (HttpOnly, Secure, SameSite=Strict, scoped to the refresh route). The access token carries the user id in sub and the session in sid. A locked login answers 429 with Retry-After and details.retryAfterSeconds, a deactivated account 403. Wrong password, signing-key validation and the JWT claims had no Red: they were covered by the code of the first cycle.
…rer token Operations, D2, T5, T7: /api/v1/me lists the person and their level on every screen. Access tokens are checked with MapInboundClaims off, no clock skew and a lifetime validator that reads TimeProvider, since IdentityModel has no clock seam. A missing, invalid or expired token answers 401 session_expired.
AUTH-17, AUTH-18, ADR-0019, ADR-0032, D3, D10: refresh rotates the cookie, slides the session for 8 hours from the last use and refuses an unknown, expired or missing cookie with session_expired, and a deactivated account with account_inactive (ending its session). Sign-out ends the session named by the sid claim and expires the cookie with the same name and path.
AUTH-14, AUTH-16, AUTH-24, AUTH-25, AUTH-27, USR-34, ADR-0019, ADR-0032, T8: an endpoint filter on the /api/v1 group answers 401 password_change_required while the token carries must_change_password, except for me, sign-out and change-password. Changing the password returns new tokens, ends the other sessions and fills the Admin's activation time. A test-only endpoint mapped through MapApiV1 stands for any other endpoint.
…he logs AUTH-26, AUTH-20, ADR-0023, D14, T9, T10: a sliding window of 60 seconds in 6 segments per client address, 20 sign-ins and 60 refreshes by default. A refused request answers 429 locked_out with Retry-After and details.retryAfterSeconds. A capturing logger at Trace level, during sign-in, refresh and change-password, finds no password, token or cookie value; that test had no Red, since nothing logs them.
A JSON body with null login, password or confirmation crashed the handlers with a NullReferenceException (500). They now fail like empty values do: invalid_credentials at sign-in and validation_failed at change-password (API-12).
AUTH-24 to AUTH-28 and USR-34 are new rules. The Operations table gets the response shapes, the 5th-attempt behavior and the known limitation of AUTH-08 are written down, and the Errors table gains 403 forbidden and details.retryAfterSeconds. The local environment guide lists the new Auth and RateLimiting keys and how to set the secrets; the architecture and testing guides describe Identity, sessions, the JWT and the authentication test factories.
Invoke-RestMethod with a web session does not keep a Secure, SameSite=Strict cookie, so a manual refresh answers 401 although the server is right. curl.exe with a cookie jar is the way to test it by hand.
…s it The Admin is one row in a database shared by the whole test assembly. The change-password tests left activated_at filled, so StartupSeedTests failed on CI, where the classes ran in another order than on the developer machine. The three classes that sign in as the Admin now reset it in DisposeAsync, so the order stops mattering.
…g keys A user secret that was never set binds as null, and both startup validators crashed on it (NullReferenceException, ArgumentNullException) instead of naming Admin:InitialPassword or Auth:SigningKey (T11). That is the most common first-run mistake, so the message matters. The signing key was decoded into a 64-byte buffer, so a valid key longer than 64 bytes was refused as "at least 32 bytes". The buffer now fits the text.
SetMinimumLevel applies only when no filter rule matches, and the appsettings rules (Default: Information, Microsoft.AspNetCore: Warning) always match, so the test never saw a Debug or Trace line although it claimed to. A filter rule of the capturing provider wins over them. With the real capture, no password, token or cookie value is logged either.
Tokens were numbered by the current count of sessions, so after a rotation a new session could get a token already in use, and a later refresh would rotate the wrong session. Test support only: no Red.
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.
Closes #8
What
Sign-in with login and password, sessions, the current user and the mandatory change of the Admin's initial password:
POST /api/v1/auth/sign-in,/auth/refresh,/auth/sign-out,/auth/change-passwordandGET /api/v1/me.user_credentials), sessions as hashed refresh tokens (user_sessions), an HS256 access token withsubandsid, lockout after 5 consecutive failures and a rate limit on sign-in and refresh.AddAuthentication, and the Admin's credential seeded fromAdmin:InitialPassword.Why
AUTH-07 to AUTH-09, AUTH-13 to AUTH-18, AUTH-24 to AUTH-28 (new), USR-34 (new), PERM-05, PERM-06, PERM-23, USR-21; ADR-0009, ADR-0019, ADR-0022, ADR-0023, ADR-0032. The plan and the owner's decisions are in the issue.
How to test
./check.ps1frombackend/ControlService(needs Docker): 243 tests, zero warnings.To try it by hand, set the two secrets (the guide in
docs/agents/guides/local-environment.mdhas a PowerShell command that does not print them), start the AppHost and useControlService.API.http:adminwithAdmin:InitialPassword:mustChangePasswordistrueand every route exceptme, sign-out and change-password answers 401password_change_required.activated_atfilled.GET /api/v1/melists every screen asgerenciador.Notes for the review
CredentialUserManageroverrides the two virtual lockout methods to useTimeProvider, andConfigureJwtBearerjudges the lifetime with aLifetimeValidatoronTimeProvider. Behavior is the planned one (5th failure locks,expexclusive, clock skew zero); only the mechanism differs.Authoptions are split by layer over the same section (AuthOptionsin Infrastructure,JwtOptionsin the API), andAuthSettingscarries the two values that messages state into Application, so the API does not reference Infrastructure outsideProgram.cs.AppDbContextnow inherits Identity's context, whose ownUsersset (credentials) is hidden by the domainUsersset; credentials are reached throughSet<UserCredential>().Checklist
dotnet test --solution ControlService.slnxpasses locallydocs/product/, ADRs, README) when behavior or decisions changed