Add fixed SrpIdentity column to allow username changes (#1429)

This commit is contained in:
Leendert de Borst
2025-12-11 13:35:16 +01:00
parent f42828ebd1
commit 4fc2ce20cf
20 changed files with 1193 additions and 42 deletions

View File

@@ -13,6 +13,8 @@ export type RegisterRequest = {
verifier: string;
encryptionType: string;
encryptionSettings: string;
/** The SRP identity used for authentication (a random GUID generated at registration). */
srpIdentity: string;
};
/**
@@ -193,12 +195,22 @@ export class SrpAuthService {
};
}
/**
* Generates a random UUID v4 for use as SRP identity.
*
* @returns A random UUID string
*/
public static generateSrpIdentity(): string {
return crypto.randomUUID();
}
/**
* Prepares SRP registration data for a new user.
*
* This generates all the cryptographic values needed to register a user:
* - Salt for key derivation
* - Verifier for SRP authentication
* - SRP identity (random GUID) for immutable authentication identity
*
* @param username - The username for registration
* @param password - The password for registration
@@ -211,6 +223,12 @@ export class SrpAuthService {
const normalizedUsername = SrpAuthService.normalizeUsername(username);
const salt = SrpAuthService.generateSalt();
/**
* Generate a random GUID for SRP identity. This is used for all SRP operations,
* is set during registration, and never changes.
*/
const srpIdentity = SrpAuthService.generateSrpIdentity();
// Derive key from password using default Argon2Id settings
const credentials = await SrpAuthService.prepareCredentials(
password,
@@ -219,8 +237,8 @@ export class SrpAuthService {
DEFAULT_ENCRYPTION.settings
);
// Generate SRP private key and verifier
const privateKey = SrpAuthService.derivePrivateKey(salt, normalizedUsername, credentials.passwordHashString);
// Generate SRP private key and verifier using srpIdentity (not username)
const privateKey = SrpAuthService.derivePrivateKey(salt, srpIdentity, credentials.passwordHashString);
const verifier = SrpAuthService.deriveVerifier(privateKey);
return {
@@ -229,6 +247,7 @@ export class SrpAuthService {
verifier,
encryptionType: DEFAULT_ENCRYPTION.type,
encryptionSettings: DEFAULT_ENCRYPTION.settings,
srpIdentity,
};
}
@@ -328,6 +347,12 @@ export class SrpAuthService {
const loginResponse = (await initiateResponse.json()) as LoginResponse;
/**
* Use srpIdentity from server response if available, otherwise fall back to normalized username.
* Note: the fallback can be removed in the future after 0.26.0+ is deployed.
*/
const srpIdentity = loginResponse.srpIdentity ?? normalizedUsername;
// Step 2: Prepare credentials
const credentials = await SrpAuthService.prepareCredentials(
password,
@@ -336,18 +361,18 @@ export class SrpAuthService {
loginResponse.encryptionSettings
);
// Step 3: Generate SRP session
// Step 3: Generate SRP session using srpIdentity (not the typed username)
const clientEphemeral = SrpAuthService.generateEphemeral();
const privateKey = SrpAuthService.derivePrivateKey(
loginResponse.salt,
normalizedUsername,
srpIdentity,
credentials.passwordHashString
);
const session = SrpAuthService.deriveSession(
clientEphemeral.secret,
loginResponse.serverEphemeral,
loginResponse.salt,
normalizedUsername,
srpIdentity,
privateKey
);

View File

@@ -82,6 +82,7 @@ type LoginResponse = {
serverEphemeral: string;
encryptionType: string;
encryptionSettings: string;
srpIdentity?: string;
};
/**
@@ -318,6 +319,7 @@ type PasswordChangeInitiateResponse = {
serverEphemeral: string;
encryptionType: string;
encryptionSettings: string;
srpIdentity?: string;
};
/**

View File

@@ -49,18 +49,19 @@ export class SrpUtility {
rememberMe: boolean,
loginResponse: LoginResponse
): Promise<ValidateLoginResponse> {
// Generate client ephemeral
/**
* Use srpIdentity from server response if available, otherwise fall back to username.
* Note: the fallback can be removed in the future after 0.26.0+ is deployed.
*/
const srpIdentity = loginResponse.srpIdentity ?? username;
const clientEphemeral = srp.generateEphemeral();
// Derive private key
const privateKey = srp.derivePrivateKey(loginResponse.salt, username, passwordHash);
// Derive session
const privateKey = srp.derivePrivateKey(loginResponse.salt, srpIdentity, passwordHash);
const sessionProof = srp.deriveSession(
clientEphemeral.secret,
loginResponse.serverEphemeral,
loginResponse.salt,
username,
srpIdentity,
privateKey
);
@@ -98,18 +99,19 @@ export class SrpUtility {
loginResponse: LoginResponse,
twoFactorCode: number
): Promise<ValidateLoginResponse> {
// Generate client ephemeral
/**
* Use srpIdentity from server response if available, otherwise fall back to username.
* Note: the fallback can be removed in the future after 0.26.0+ is deployed.
*/
const srpIdentity = loginResponse.srpIdentity ?? username;
const clientEphemeral = srp.generateEphemeral();
// Derive private key
const privateKey = srp.derivePrivateKey(loginResponse.salt, username, passwordHash);
// Derive session
const privateKey = srp.derivePrivateKey(loginResponse.salt, srpIdentity, passwordHash);
const sessionProof = srp.deriveSession(
clientEphemeral.secret,
loginResponse.serverEphemeral,
loginResponse.salt,
username,
srpIdentity,
privateKey
);

View File

@@ -82,6 +82,7 @@ type LoginResponse = {
serverEphemeral: string;
encryptionType: string;
encryptionSettings: string;
srpIdentity?: string;
};
/**
@@ -318,6 +319,7 @@ type PasswordChangeInitiateResponse = {
serverEphemeral: string;
encryptionType: string;
encryptionSettings: string;
srpIdentity?: string;
};
/**

View File

@@ -23,6 +23,10 @@
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -166,13 +166,17 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM
// Retrieve latest vault of user which contains the current salt and verifier.
var latestVaultEncryptionSettings = AuthHelper.GetUserLatestVaultEncryptionSettings(user);
// Get or create SRP identity. For existing users without SrpIdentity, fall back to username (lowercase).
var srpIdentity = user.SrpIdentity ?? user.UserName!.ToLowerInvariant();
// Server creates ephemeral and sends to client
var ephemeral = Srp.GenerateEphemeralServer(latestVaultEncryptionSettings.Verifier);
// Store the server ephemeral in memory cache for Validate() endpoint to use.
cache.Set(AuthHelper.CachePrefixEphemeral + model.Username, ephemeral.Secret, TimeSpan.FromMinutes(5));
// Use SrpIdentity as the cache key to ensure consistency.
cache.Set(AuthHelper.CachePrefixEphemeral + srpIdentity, ephemeral.Secret, TimeSpan.FromMinutes(5));
return Ok(new LoginInitiateResponse(latestVaultEncryptionSettings.Salt, ephemeral.Public, latestVaultEncryptionSettings.EncryptionType, latestVaultEncryptionSettings.EncryptionSettings));
return Ok(new LoginInitiateResponse(latestVaultEncryptionSettings.Salt, ephemeral.Public, latestVaultEncryptionSettings.EncryptionType, latestVaultEncryptionSettings.EncryptionSettings, srpIdentity));
}
/// <summary>
@@ -416,9 +420,14 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM
return BadRequest(ApiErrorCodeHelper.CreateValidationErrorResponse(apiErrorCode, 400));
}
// Use the SrpIdentity from the request if provided (typically a GUID generated by the client),
// otherwise fall back to lowercase username for backward compatibility.
var srpIdentity = model.SrpIdentity ?? model.Username.ToLowerInvariant();
var user = new AliasVaultUser
{
UserName = model.Username,
SrpIdentity = srpIdentity,
CreatedAt = timeProvider.UtcNow,
UpdatedAt = timeProvider.UtcNow,
PasswordChangedAt = timeProvider.UtcNow,
@@ -476,13 +485,17 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM
// Retrieve latest vault of user which contains the current salt and verifier.
var latestVaultEncryptionSettings = AuthHelper.GetUserLatestVaultEncryptionSettings(user);
// Get or create SRP identity. For existing users without SrpIdentity, fall back to username (lowercase).
var srpIdentity = user.SrpIdentity ?? user.UserName!.ToLowerInvariant();
// Server creates ephemeral and sends to client
var ephemeral = Srp.GenerateEphemeralServer(latestVaultEncryptionSettings.Verifier);
// Store the server ephemeral in memory cache for the Vault update (and set new password) endpoint to use.
cache.Set(AuthHelper.CachePrefixEphemeral + user.UserName!, ephemeral.Secret, TimeSpan.FromMinutes(5));
// Use SrpIdentity as the cache key to ensure consistency.
cache.Set(AuthHelper.CachePrefixEphemeral + srpIdentity, ephemeral.Secret, TimeSpan.FromMinutes(5));
return Ok(new PasswordChangeInitiateResponse(latestVaultEncryptionSettings.Salt, ephemeral.Public, latestVaultEncryptionSettings.EncryptionType, latestVaultEncryptionSettings.EncryptionSettings));
return Ok(new PasswordChangeInitiateResponse(latestVaultEncryptionSettings.Salt, ephemeral.Public, latestVaultEncryptionSettings.EncryptionType, latestVaultEncryptionSettings.EncryptionSettings, srpIdentity));
}
/// <summary>
@@ -542,17 +555,22 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM
// Retrieve latest vault of user which contains the current salt and verifier.
var latestVaultEncryptionSettings = AuthHelper.GetUserLatestVaultEncryptionSettings(user);
// Get or create SRP identity. For existing users without SrpIdentity, fall back to username (lowercase).
var srpIdentity = user.SrpIdentity ?? user.UserName!.ToLowerInvariant();
// Server creates ephemeral and sends to client
var ephemeral = Srp.GenerateEphemeralServer(latestVaultEncryptionSettings.Verifier);
// Store the server ephemeral in memory cache for confirmation endpoint.
cache.Set(AuthHelper.CachePrefixEphemeral + model.Username, ephemeral.Secret, TimeSpan.FromMinutes(5));
// Use SrpIdentity as the cache key to ensure consistency.
cache.Set(AuthHelper.CachePrefixEphemeral + srpIdentity, ephemeral.Secret, TimeSpan.FromMinutes(5));
return Ok(new LoginInitiateResponse(
latestVaultEncryptionSettings.Salt,
ephemeral.Public,
latestVaultEncryptionSettings.EncryptionType,
latestVaultEncryptionSettings.EncryptionSettings));
latestVaultEncryptionSettings.EncryptionSettings,
srpIdentity));
}
/// <summary>

View File

@@ -28,16 +28,19 @@ public static class AuthHelper
public static readonly string CachePrefixFakeData = "FakeData_";
/// <summary>
/// Helper method that validates the SRP session based on provided username, ephemeral and proof.
/// Helper method that validates the SRP session based on provided SRP identity, ephemeral and proof.
/// </summary>
/// <param name="cache">IMemoryCache instance.</param>
/// <param name="user">The user object.</param>
/// <param name="clientEphemeral">The client ephemeral value.</param>
/// <param name="clientSessionProof">The client session proof.</param>
/// <returns>Tuple.</returns>
/// <returns>SrpSession if validation succeeds, null otherwise.</returns>
public static SrpSession? ValidateSrpSession(IMemoryCache cache, AliasVaultUser user, string clientEphemeral, string clientSessionProof)
{
if (!cache.TryGetValue(CachePrefixEphemeral + user.UserName, out var serverSecretEphemeral) || serverSecretEphemeral is not string)
// Get or create SRP identity. For existing users without SrpIdentity, fall back to username (lowercase).
var srpIdentity = user.SrpIdentity ?? user.UserName!.ToLowerInvariant();
if (!cache.TryGetValue(CachePrefixEphemeral + srpIdentity, out var serverSecretEphemeral) || serverSecretEphemeral is not string)
{
return null;
}
@@ -45,11 +48,13 @@ public static class AuthHelper
// Retrieve latest vault of user which contains the current salt and verifier.
var latestVaultEncryptionSettings = GetUserLatestVaultEncryptionSettings(user);
// Use SrpIdentity for the SRP session derivation. This is the fixed identity that was used
// when the verifier was originally created, ensuring username changes don't break authentication.
var serverSession = Srp.DeriveSessionServer(
serverSecretEphemeral.ToString() ?? string.Empty,
clientEphemeral,
latestVaultEncryptionSettings.Salt,
user.UserName ?? string.Empty,
srpIdentity,
latestVaultEncryptionSettings.Verifier,
clientSessionProof);

View File

@@ -263,18 +263,22 @@ else
];
}
// Use srpIdentity from server response if available, otherwise fall back to username.
// Note: the fallback can be removed in the future after 0.26.0+ is deployed.
var srpIdentity = loginResponse.SrpIdentity ?? username;
// 3. Client derives shared session key.
_passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_loginModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings);
var passwordHashString = BitConverter.ToString(_passwordHash).Replace("-", string.Empty);
_clientEphemeral = Srp.GenerateEphemeralClient();
var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, username, passwordHashString);
var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, srpIdentity, passwordHashString);
_clientSession = Srp.DeriveSessionClient(
privateKey,
_clientEphemeral.Secret,
loginResponse.ServerEphemeral,
loginResponse.Salt,
username);
srpIdentity);
// 4. Client sends proof of session key to server.
result = await Http.PostAsJsonAsync("v1/Auth/validate", new ValidateLoginRequest(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof));

View File

@@ -93,6 +93,12 @@ else
/// </summary>
private string CurrentEncryptionSettings { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the SRP identity for authentication. This is a fixed value that doesn't change
/// even if the display username is updated.
/// </summary>
private string? CurrentSrpIdentity { get; set; }
private SrpEphemeral ClientEphemeral = new();
private SrpSession ClientSession = new();
@@ -141,6 +147,7 @@ else
CurrentSalt = response.Salt;
CurrentEncryptionType = response.EncryptionType;
CurrentEncryptionSettings = response.EncryptionSettings;
CurrentSrpIdentity = response.SrpIdentity;
}
/// <summary>
@@ -158,13 +165,18 @@ else
ClientEphemeral = Srp.GenerateEphemeralClient();
var username = await GetUsernameAsync();
var privateKey = Srp.DerivePrivateKey(CurrentSalt, username, currentPasswordHashString);
// Use srpIdentity from server response if available, otherwise fall back to username.
// Note: the fallback can be removed in the future after 0.26.0+ is deployed.
var srpIdentity = CurrentSrpIdentity ?? username.ToLowerInvariant();
var privateKey = Srp.DerivePrivateKey(CurrentSalt, srpIdentity, currentPasswordHashString);
ClientSession = Srp.DeriveSessionClient(
privateKey,
ClientEphemeral.Secret,
CurrentServerEphemeral,
CurrentSalt,
username);
srpIdentity);
// Generate salt and verifier for new password.
var client = new SrpClient();
@@ -180,7 +192,8 @@ else
// it is encrypted with the new password hash.
await AuthService.StoreEncryptionKeyAsync(newPasswordHash);
var srpPasswordChange = Srp.PasswordChangeAsync(client, newSalt, username, newPasswordHashString);
// Use srpIdentity for generating the new verifier to maintain consistency.
var srpPasswordChange = Srp.PasswordChangeAsync(client, newSalt, srpIdentity, newPasswordHashString);
// Prepare new vault model to update to.
var encryptedBase64String = await DbService.GetEncryptedDatabaseBase64String();

View File

@@ -171,18 +171,22 @@
return;
}
// Use srpIdentity from server response if available, otherwise fall back to username.
// Note: the fallback can be removed in the future after 0.27.0+ is deployed.
var srpIdentity = loginResponse.SrpIdentity ?? username;
// Verify password using SRP
var passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_passwordModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings);
var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty);
ClientEphemeral = Srp.GenerateEphemeralClient();
var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, username, passwordHashString);
var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, srpIdentity, passwordHashString);
ClientSession = Srp.DeriveSessionClient(
privateKey,
ClientEphemeral.Secret,
loginResponse.ServerEphemeral,
loginResponse.Salt,
username);
srpIdentity);
// Send final delete request with SRP proof.
result = await Http.PostAsJsonAsync("v1/Auth/delete-account/confirm", new DeleteAccountRequest(username, ClientEphemeral.Public, ClientSession.Proof));

View File

@@ -41,6 +41,10 @@ public class UserRegistrationService(HttpClient httpClient, AuthenticationStateP
var client = new SrpClient();
var salt = client.GenerateSalt();
// Generate a random GUID for SRP identity. This is used for all SRP operations,
// is set during registration, and never changes.
var srpIdentity = Guid.NewGuid().ToString();
string encryptionType = Defaults.EncryptionType;
string encryptionSettings = Defaults.EncryptionSettings;
if (config.CryptographyOverrideType is not null && config.CryptographyOverrideSettings is not null)
@@ -51,9 +55,9 @@ public class UserRegistrationService(HttpClient httpClient, AuthenticationStateP
var passwordHash = await Encryption.DeriveKeyFromPasswordAsync(password, salt, encryptionType, encryptionSettings);
var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty);
var srpSignup = Srp.PasswordChangeAsync(client, salt, username, passwordHashString);
var srpSignup = Srp.PasswordChangeAsync(client, salt, srpIdentity, passwordHashString);
var registerRequest = new RegisterRequest(srpSignup.Username, srpSignup.Salt, srpSignup.Verifier, encryptionType, encryptionSettings);
var registerRequest = new RegisterRequest(username, srpSignup.Salt, srpSignup.Verifier, encryptionType, encryptionSettings, srpIdentity);
var result = await httpClient.PostAsJsonAsync("v1/Auth/register", registerRequest);
var responseContent = await result.Content.ReadAsStringAsync();

View File

@@ -14,6 +14,13 @@ using Microsoft.AspNetCore.Identity;
/// </summary>
public class AliasVaultUser : IdentityUser
{
/// <summary>
/// Gets or sets the SRP identity used for authentication. This is a fixed value (typically a random GUID)
/// that is used for all SRP operations, is set during registration, and never changes.
/// </summary>
[System.ComponentModel.DataAnnotations.StringLength(255)]
public string? SrpIdentity { get; set; }
/// <summary>
/// Gets or sets created timestamp.
/// </summary>

View File

@@ -0,0 +1,989 @@
// <auto-generated />
using System;
using AliasServerDb;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace AliasServerDb.Migrations
{
[DbContext(typeof(AliasServerDbContext))]
[Migration("20251211120421_AddSrpIdentityToUser")]
partial class AddSrpIdentityToUser
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.4")
.HasAnnotation("Proxies:ChangeTracking", false)
.HasAnnotation("Proxies:CheckEquality", false)
.HasAnnotation("Proxies:LazyLoading", true)
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("AliasServerDb.AdminRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("NormalizedName")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("AdminRoles");
});
modelBuilder.Entity("AliasServerDb.AdminUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<DateTime?>("LastPasswordChanged")
.HasColumnType("timestamp with time zone");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasColumnType("text");
b.Property<string>("NormalizedUserName")
.HasColumnType("text");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("AdminUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("NormalizedName")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("AliasVaultRoles");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<bool>("Blocked")
.HasColumnType("boolean");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<int>("EmailsReceived")
.HasColumnType("integer");
b.Property<DateTime?>("LastActivityDate")
.HasColumnType("timestamp with time zone");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<int>("MaxEmailAgeDays")
.HasColumnType("integer");
b.Property<int>("MaxEmails")
.HasColumnType("integer");
b.Property<string>("NormalizedEmail")
.HasColumnType("text");
b.Property<string>("NormalizedUserName")
.HasColumnType("text");
b.Property<DateTime>("PasswordChangedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<string>("SrpIdentity")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserName")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("AliasVaultUsers");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DeviceIdentifier")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("ExpireDate")
.HasMaxLength(255)
.HasColumnType("timestamp with time zone");
b.Property<string>("IpAddress")
.HasMaxLength(45)
.HasColumnType("character varying(45)");
b.Property<string>("PreviousTokenValue")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AliasVaultUserRefreshTokens");
});
modelBuilder.Entity("AliasServerDb.AuthLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("AdditionalInfo")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Browser")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Client")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Country")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("DeviceType")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<int>("EventType")
.HasColumnType("integer");
b.Property<int?>("FailureReason")
.HasColumnType("integer");
b.Property<string>("IpAddress")
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<bool>("IsSuccess")
.HasColumnType("boolean");
b.Property<bool>("IsSuspiciousActivity")
.HasColumnType("boolean");
b.Property<string>("OperatingSystem")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("RequestPath")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("Timestamp")
.HasColumnType("timestamp with time zone");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex(new[] { "EventType" }, "IX_EventType");
b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress");
b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp");
b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp")
.IsDescending(false, false, true);
b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp")
.IsDescending(false, true);
b.ToTable("AuthLogs");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("DateSystem")
.HasColumnType("timestamp with time zone");
b.Property<string>("EncryptedSymmetricKey")
.IsRequired()
.HasColumnType("text");
b.Property<string>("From")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FromDomain")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FromLocal")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MessageHtml")
.HasColumnType("text");
b.Property<string>("MessagePlain")
.HasColumnType("text");
b.Property<string>("MessagePreview")
.HasColumnType("text");
b.Property<string>("MessageSource")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("PushNotificationSent")
.HasColumnType("boolean");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<string>("To")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ToDomain")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ToLocal")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserEncryptionKeyId")
.HasMaxLength(255)
.HasColumnType("uuid");
b.Property<bool>("Visible")
.HasColumnType("boolean");
b.HasKey("Id");
b.HasIndex("Date");
b.HasIndex("DateSystem");
b.HasIndex("PushNotificationSent");
b.HasIndex("ToLocal");
b.HasIndex("UserEncryptionKeyId");
b.HasIndex("Visible");
b.ToTable("Emails");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<byte[]>("Bytes")
.IsRequired()
.HasColumnType("bytea");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int>("EmailId")
.HasColumnType("integer");
b.Property<string>("Filename")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Filesize")
.HasColumnType("integer");
b.Property<string>("MimeType")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("EmailId");
b.ToTable("EmailAttachments");
});
modelBuilder.Entity("AliasServerDb.Log", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Application")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("Exception")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Level")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)");
b.Property<string>("LogEvent")
.IsRequired()
.HasColumnType("text")
.HasColumnName("LogEvent");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MessageTemplate")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Properties")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SourceContext")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("TimeStamp")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("Application");
b.HasIndex("TimeStamp");
b.ToTable("Logs", (string)null);
});
modelBuilder.Entity("AliasServerDb.MobileLoginRequest", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<DateTime?>("ClearedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("ClientIpAddress")
.HasColumnType("text");
b.Property<string>("ClientPublicKey")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EncryptedDecryptionKey")
.HasColumnType("text");
b.Property<DateTime?>("FulfilledAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("MobileIpAddress")
.HasColumnType("text");
b.Property<DateTime?>("RetrievedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex(new[] { "ClientIpAddress" }, "IX_ClientIpAddress");
b.HasIndex(new[] { "CreatedAt" }, "IX_CreatedAt");
b.HasIndex(new[] { "MobileIpAddress" }, "IX_MobileIpAddress");
b.HasIndex(new[] { "RetrievedAt", "ClearedAt", "FulfilledAt" }, "IX_RetrievedAt_ClearedAt_FulfilledAt");
b.HasIndex(new[] { "UserId" }, "IX_UserId");
b.ToTable("MobileLoginRequests");
});
modelBuilder.Entity("AliasServerDb.ServerSetting", b =>
{
b.Property<string>("Key")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("Key");
b.ToTable("ServerSettings");
});
modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<TimeOnly?>("EndTime")
.HasColumnType("time without time zone");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
b.Property<bool>("IsOnDemand")
.HasColumnType("boolean");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("RunDate")
.HasColumnType("timestamp with time zone");
b.Property<TimeOnly>("StartTime")
.HasColumnType("time without time zone");
b.Property<int>("Status")
.HasColumnType("integer");
b.HasKey("Id");
b.ToTable("TaskRunnerJobs");
});
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Address")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("AddressDomain")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("AddressLocal")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("Disabled")
.HasColumnType("boolean");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex("Address")
.IsUnique();
b.HasIndex("UserId", "Disabled");
b.ToTable("UserEmailClaims");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("IsPrimary")
.HasColumnType("boolean");
b.Property<string>("PublicKey")
.IsRequired()
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserEncryptionKeys");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Client")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("CredentialsCount")
.HasColumnType("integer");
b.Property<int>("EmailClaimsCount")
.HasColumnType("integer");
b.Property<string>("EncryptionSettings")
.IsRequired()
.HasColumnType("text");
b.Property<string>("EncryptionType")
.IsRequired()
.HasColumnType("text");
b.Property<int>("FileSize")
.HasColumnType("integer");
b.Property<long>("RevisionNumber")
.HasColumnType("bigint");
b.Property<string>("Salt")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("UserId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("VaultBlob")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Verifier")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<string>("Version")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Vaults");
});
modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("CurrentStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("DesiredStatus")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<DateTime>("Heartbeat")
.HasColumnType("timestamp with time zone");
b.Property<string>("ServiceName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.HasKey("Id");
b.ToTable("WorkerServiceStatuses");
});
modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("FriendlyName")
.HasColumnType("text");
b.Property<string>("Xml")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("DataProtectionKeys");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("RoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("text");
b.Property<string>("ClaimValue")
.HasColumnType("text");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("UserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("ProviderKey")
.HasColumnType("text");
b.Property<string>("ProviderDisplayName")
.HasColumnType("text");
b.Property<string>("UserId")
.HasColumnType("text");
b.HasKey("LoginProvider", "ProviderKey");
b.ToTable("UserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("RoleId")
.HasColumnType("text");
b.HasKey("UserId", "RoleId");
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("text");
b.Property<string>("LoginProvider")
.HasColumnType("text");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Value")
.HasColumnType("text");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserTokens", (string)null);
});
modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey")
.WithMany("Emails")
.HasForeignKey("UserEncryptionKeyId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("EncryptionKey");
});
modelBuilder.Entity("AliasServerDb.EmailAttachment", b =>
{
b.HasOne("AliasServerDb.Email", "Email")
.WithMany("Attachments")
.HasForeignKey("EmailId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Email");
});
modelBuilder.Entity("AliasServerDb.MobileLoginRequest", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.UserEmailClaim", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("EmailClaims")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("EncryptionKeys")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.Vault", b =>
{
b.HasOne("AliasServerDb.AliasVaultUser", "User")
.WithMany("Vaults")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("AliasServerDb.AliasVaultUser", b =>
{
b.Navigation("EmailClaims");
b.Navigation("EncryptionKeys");
b.Navigation("Vaults");
});
modelBuilder.Entity("AliasServerDb.Email", b =>
{
b.Navigation("Attachments");
});
modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b =>
{
b.Navigation("Emails");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,38 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AliasServerDb.Migrations
{
/// <inheritdoc />
public partial class AddSrpIdentityToUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SrpIdentity",
table: "AliasVaultUsers",
type: "character varying(255)",
maxLength: 255,
nullable: true);
// Populate SrpIdentity for existing users using their current username which was
// used before this migration was applied. All newly registered users will have
// this field populated with a random GUID generated by the client upon registration.
migrationBuilder.Sql(@"
UPDATE ""AliasVaultUsers""
SET ""SrpIdentity"" = LOWER(""UserName"")
WHERE ""SrpIdentity"" IS NULL;
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SrpIdentity",
table: "AliasVaultUsers");
}
}
}

View File

@@ -180,6 +180,10 @@ namespace AliasServerDb.Migrations
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<string>("SrpIdentity")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");

View File

@@ -21,12 +21,14 @@ public class LoginInitiateResponse
/// <param name="serverEphemeral">Server ephemeral.</param>
/// <param name="encryptionType">Encryption type.</param>
/// <param name="encryptionSettings">Encryption settings.</param>
public LoginInitiateResponse(string salt, string serverEphemeral, string encryptionType, string encryptionSettings)
/// <param name="srpIdentity">The SRP identity to use for authentication (optional for backward compatibility).</param>
public LoginInitiateResponse(string salt, string serverEphemeral, string encryptionType, string encryptionSettings, string? srpIdentity = null)
{
Salt = salt;
ServerEphemeral = serverEphemeral;
EncryptionType = encryptionType;
EncryptionSettings = encryptionSettings;
SrpIdentity = srpIdentity;
}
/// <summary>
@@ -52,4 +54,12 @@ public class LoginInitiateResponse
/// </summary>
[JsonPropertyName("encryptionSettings")]
public string EncryptionSettings { get; set; }
/// <summary>
/// Gets or sets the SRP identity to use for authentication. This is a fixed value that doesn't change
/// even if the display username is updated. Clients should use this value (instead of the typed username)
/// for all SRP operations.
/// </summary>
[JsonPropertyName("srpIdentity")]
public string? SrpIdentity { get; set; }
}

View File

@@ -20,13 +20,15 @@ public class RegisterRequest
/// <param name="verifier">The verifier value.</param>
/// <param name="encryptionType">The encryption type.</param>
/// <param name="encryptionSettings">The encryption settings.</param>
public RegisterRequest(string username, string salt, string verifier, string encryptionType, string encryptionSettings)
/// <param name="srpIdentity">The SRP identity.</param>
public RegisterRequest(string username, string salt, string verifier, string encryptionType, string encryptionSettings, string? srpIdentity = null)
{
Username = username.ToLowerInvariant().Trim();
Salt = salt;
Verifier = verifier;
EncryptionType = encryptionType;
EncryptionSettings = encryptionSettings;
SrpIdentity = srpIdentity;
}
/// <summary>
@@ -53,4 +55,11 @@ public class RegisterRequest
/// Gets the encryption settings.
/// </summary>
public string EncryptionSettings { get; }
/// <summary>
/// Gets the SRP identity used for authentication. This is a fixed value (typically a GUID) that
/// is used for all SRP operations. If not provided, defaults to the lowercase username for
/// backward compatibility.
/// </summary>
public string? SrpIdentity { get; }
}

View File

@@ -10,7 +10,7 @@ namespace AliasVault.Shared.Models.WebApi.PasswordChange;
using System.Text.Json.Serialization;
/// <summary>
/// Represents a login response.
/// Represents a password change initiate response.
/// </summary>
public class PasswordChangeInitiateResponse
{
@@ -21,12 +21,14 @@ public class PasswordChangeInitiateResponse
/// <param name="serverEphemeral">Server ephemeral.</param>
/// <param name="encryptionType">Encryption type.</param>
/// <param name="encryptionSettings">Encryption settings.</param>
public PasswordChangeInitiateResponse(string salt, string serverEphemeral, string encryptionType, string encryptionSettings)
/// <param name="srpIdentity">The SRP identity.</param>
public PasswordChangeInitiateResponse(string salt, string serverEphemeral, string encryptionType, string encryptionSettings, string? srpIdentity = null)
{
Salt = salt;
ServerEphemeral = serverEphemeral;
EncryptionType = encryptionType;
EncryptionSettings = encryptionSettings;
SrpIdentity = srpIdentity;
}
/// <summary>
@@ -52,4 +54,11 @@ public class PasswordChangeInitiateResponse
/// </summary>
[JsonPropertyName("encryptionSettings")]
public string EncryptionSettings { get; set; }
/// <summary>
/// Gets or sets the SRP identity to use for authentication. This is a fixed value that doesn't change
/// even if the display username is updated. Clients should use this value for all SRP operations.
/// </summary>
[JsonPropertyName("srpIdentity")]
public string? SrpIdentity { get; set; }
}

View File

@@ -13,4 +13,5 @@ export type LoginResponse = {
serverEphemeral: string;
encryptionType: string;
encryptionSettings: string;
srpIdentity?: string;
}

View File

@@ -6,4 +6,5 @@ export type PasswordChangeInitiateResponse = {
serverEphemeral: string;
encryptionType: string;
encryptionSettings: string;
srpIdentity?: string;
};