ADR 0001 — Authentication model#
Status: Accepted (Phase 0, 2026-07-03) · Amended for real password model (Phase 1, 2026-07-14)
Context#
The legacy Cube 1.x uses ASP.NET Forms cookie authentication backed by AspnetUsers / AspnetMembership (classic ASP.NET Membership schema). We need a token-based scheme that works for browser SPAs, desktop clients, and service-to-service calls, and that can coexist with the legacy system during the migration.
Decision#
- Issue JWT access tokens signed with HS256 for development and lower environments; RS256 upgrade path via
Jwt:SigningKeyswap when a KMS-backed key is available. - One central Auth API endpoint set under
/api/v1/auth/(login,me,permissions). - Passwords are verified through
IPasswordHasher, which dispatches on the stored format:$2*prefix → BCrypt (new users / rehashed users, cost factor 11).- Otherwise → the row’s
PasswordFormatcolumn decides:2(Encrypted, the current legacy default) → verified via the portedSqlMembershipPasswordVerifier.1(Hashed) → SHA1(salt+password), for any historical rows using it.0(Clear) → rejected outright (should not exist in production).
- Legacy verification is gated by
FeatureFlags:UseLegacyPasswordHashso we can force a full cutover. - Claims carried on the token (see ADR 0005 for the full mapping):
sub,unique_name,jti(standard),role— permission strings fromAspNetRoleswhereIsPermission=1,grp— group GUIDs fromaspmnx_GroupUsers,mod_admin— module keys fromaspnet_Profileadmin flags,itar—aspnet_Profile.IsItarAuthorized.
Confirmed password model (Phase 1)#
The legacy system uses SqlMembershipProvider with passwordFormat="Encrypted". Passwords are not hashed — they are AES-encrypted using the ASP.NET machineKey (validationKey + decryptionKey from Web.config / machine.config), with a per-user salt from AspnetMembership.PasswordSalt.
Verification flow in Cube 2.0:
- Look up
aspnet_UsersbyLoweredUserName. - Load
AspnetMembership.Password,PasswordSalt,PasswordFormat. - If
Passwordstarts with$2→ BCrypt verify. - Else if
PasswordFormat == 2→ port ASP.NET’s algorithm:HMACSHA1(validationKey, saltBytes || passwordBytes) → AES-encrypt(decryptionKey, hash) → Base64→ compare to stored value. - Else if
PasswordFormat == 1→ SHA1 comparison (rare). - On success and stored format is not BCrypt: rehash-on-login — BCrypt the plaintext (which we already have from the submitted password), update
AspnetMembership.Password = bcryptHash+PasswordFormat = <sentinel>inside the same transaction asLastLoginDate.
The machine-key values live as secrets in Machinekey:ValidationKey and Machinekey:DecryptionKey (env vars Machinekey__ValidationKey / Machinekey__DecryptionKey). Production values are held by ops — dev values match manexcloud/newSite/Web.config for local testing against seeded rows.
Consequences#
- Refresh tokens are not in scope for Phase 0 or Phase 1. Access token lifetime defaults to 60 min.
- Legacy password verification requires the machine key as a secret. Ops rotates it out-of-band; the app reads it via configuration.
- Rehash-on-login means the
AspnetMembership.Passwordcolumn ends up mixed-format (BCrypt for rehashed rows, AES-encrypted for cold rows) during migration. ThePasswordFormatcolumn tells verification which path to take. - Once every user has logged in at least once post-cutover, we can flip
FeatureFlags:UseLegacyPasswordHash = falseand stop reading the machine key at runtime. - Group / role / permission population is finalized in ADR 0005. Phase 0’s empty-array stubs are replaced in Phase 1.
- The Web.config machine key in
manexcloud/newSite/Web.configis a dev/staging placeholder (confirmed with the team). Production machineKey lives on prod IIS and is fetched separately.