mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-24 16:32:20 -04:00
Refactor SRP logic to shared auth service (#1404)
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -14,13 +12,12 @@ import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useHeaderButtons } from '@/entrypoints/popup/context/HeaderButtonsContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import ConversionUtility from '@/entrypoints/popup/utils/ConversionUtility';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
|
||||
import { AppInfo } from '@/utils/AppInfo';
|
||||
import { SrpAuthService } from '@/utils/auth/SrpAuthService';
|
||||
import type { VaultResponse, LoginResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
|
||||
import type { MobileLoginResult } from '@/utils/types/messaging/MobileLoginResult';
|
||||
|
||||
@@ -152,32 +149,27 @@ const Login: React.FC = () => {
|
||||
// Clear global message if set with every login attempt.
|
||||
app.clearGlobalMessage();
|
||||
|
||||
// Use the srpUtil instance instead of the imported singleton
|
||||
const loginResponse = await srpUtil.initiateLogin(ConversionUtility.normalizeUsername(credentials.username));
|
||||
// Initiate login with server
|
||||
const normalizedUsername = SrpAuthService.normalizeUsername(credentials.username);
|
||||
const loginResponse = await srpUtil.initiateLogin(normalizedUsername);
|
||||
|
||||
// 1. Derive key from password using Argon2id
|
||||
const passwordHash = await EncryptionUtility.deriveKeyFromPassword(
|
||||
// Derive key from password using Argon2id and prepare credentials
|
||||
const { passwordHashString, passwordHashBase64 } = await SrpAuthService.prepareCredentials(
|
||||
credentials.password,
|
||||
loginResponse.salt,
|
||||
loginResponse.encryptionType,
|
||||
loginResponse.encryptionSettings
|
||||
);
|
||||
|
||||
// Convert uint8 array to uppercase hex string which is expected by the server.
|
||||
const passwordHashString = Buffer.from(passwordHash).toString('hex').toUpperCase();
|
||||
|
||||
// Get the derived key as base64 string required for decryption.
|
||||
const passwordHashBase64 = Buffer.from(passwordHash).toString('base64');
|
||||
|
||||
// 2. Validate login with SRP protocol
|
||||
// Validate login with SRP protocol
|
||||
const validationResponse = await srpUtil.validateLogin(
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
normalizedUsername,
|
||||
passwordHashString,
|
||||
rememberMe,
|
||||
loginResponse
|
||||
);
|
||||
|
||||
// 3. Handle 2FA if required
|
||||
// Handle 2FA if required
|
||||
if (validationResponse.requiresTwoFactor) {
|
||||
// Store login response as we need it for 2FA validation
|
||||
setLoginResponse(loginResponse);
|
||||
@@ -198,7 +190,7 @@ const Login: React.FC = () => {
|
||||
|
||||
// Handle successful authentication
|
||||
await handleSuccessfulAuth(
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
normalizedUsername,
|
||||
validationResponse.token.token,
|
||||
validationResponse.token.refreshToken,
|
||||
passwordHashBase64,
|
||||
@@ -235,8 +227,9 @@ const Login: React.FC = () => {
|
||||
throw new Error(t('auth.errors.invalidCode'));
|
||||
}
|
||||
|
||||
const twoFaUsername = SrpAuthService.normalizeUsername(credentials.username);
|
||||
const validationResponse = await srpUtil.validateLogin2Fa(
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
twoFaUsername,
|
||||
passwordHashString,
|
||||
rememberMe,
|
||||
loginResponse,
|
||||
@@ -250,7 +243,7 @@ const Login: React.FC = () => {
|
||||
|
||||
// Handle successful authentication
|
||||
await handleSuccessfulAuth(
|
||||
ConversionUtility.normalizeUsername(credentials.username),
|
||||
twoFaUsername,
|
||||
validationResponse.token.token,
|
||||
validationResponse.token.refreshToken,
|
||||
passwordHashBase64,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -20,9 +18,9 @@ import { useWebApi } from '@/entrypoints/popup/context/WebApiContext';
|
||||
import { PopoutUtility } from '@/entrypoints/popup/utils/PopoutUtility';
|
||||
import SrpUtility from '@/entrypoints/popup/utils/SrpUtility';
|
||||
|
||||
import { SrpAuthService } from '@/utils/auth/SrpAuthService';
|
||||
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
|
||||
import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata';
|
||||
import EncryptionUtility from '@/utils/EncryptionUtility';
|
||||
import {
|
||||
getPinLength,
|
||||
isPinEnabled,
|
||||
@@ -222,15 +220,13 @@ const Unlock: React.FC = () => {
|
||||
const loginResponse = await srpUtil.initiateLogin(authContext.username!);
|
||||
|
||||
// Derive key from password using user's encryption settings
|
||||
const passwordHash = await EncryptionUtility.deriveKeyFromPassword(
|
||||
const credentials = await SrpAuthService.prepareCredentials(
|
||||
password,
|
||||
loginResponse.salt,
|
||||
loginResponse.encryptionType,
|
||||
loginResponse.encryptionSettings
|
||||
);
|
||||
|
||||
// Get the derived key as base64 string required for decryption.
|
||||
passwordHashBase64 = Buffer.from(passwordHash).toString('base64');
|
||||
passwordHashBase64 = credentials.passwordHashBase64;
|
||||
|
||||
// Store encryption params for future offline unlock
|
||||
await dbContext.storeEncryptionKeyDerivationParams({
|
||||
@@ -250,14 +246,13 @@ const Unlock: React.FC = () => {
|
||||
}
|
||||
|
||||
// Derive key from password using stored encryption settings
|
||||
const passwordHash = await EncryptionUtility.deriveKeyFromPassword(
|
||||
const credentials = await SrpAuthService.prepareCredentials(
|
||||
password,
|
||||
storedParams.salt,
|
||||
storedParams.encryptionType,
|
||||
storedParams.encryptionSettings
|
||||
);
|
||||
|
||||
passwordHashBase64 = Buffer.from(passwordHash).toString('base64');
|
||||
passwordHashBase64 = credentials.passwordHashBase64;
|
||||
|
||||
// Set offline mode
|
||||
await dbContext.setIsOffline(true);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import srp from 'secure-remote-password/client'
|
||||
|
||||
import type { LoginRequest, LoginResponse, ValidateLoginRequest, ValidateLoginRequest2Fa, ValidateLoginResponse, BadRequestResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import type { LoginResponse, ValidateLoginResponse, ValidateLoginRequest, ValidateLoginRequest2Fa, BadRequestResponse } from '@/utils/dist/shared/models/webapi';
|
||||
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError';
|
||||
import { WebApiService } from '@/utils/WebApiService';
|
||||
import { SrpAuthService } from '@/utils/auth/SrpAuthService';
|
||||
|
||||
/**
|
||||
* Utility class for SRP authentication operations.
|
||||
*
|
||||
* This class wraps the SrpAuthService to provide WebApiService-aware
|
||||
* authentication methods for the browser extension popup.
|
||||
*/
|
||||
class SrpUtility {
|
||||
private readonly webApiService: WebApiService;
|
||||
@@ -13,7 +15,7 @@ class SrpUtility {
|
||||
/**
|
||||
* Constructor for the SrpUtility class.
|
||||
*
|
||||
* @param {WebApiService} webApiService - The WebApiService instance.
|
||||
* @param webApiService - The WebApiService instance.
|
||||
*/
|
||||
public constructor(webApiService: WebApiService) {
|
||||
this.webApiService = webApiService;
|
||||
@@ -23,16 +25,14 @@ class SrpUtility {
|
||||
* Initiate login with server.
|
||||
*/
|
||||
public async initiateLogin(username: string): Promise<LoginResponse> {
|
||||
const model: LoginRequest = {
|
||||
username: username.toLowerCase().trim(),
|
||||
};
|
||||
const normalizedUsername = SrpAuthService.normalizeUsername(username);
|
||||
|
||||
const response = await this.webApiService.rawFetch('Auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(model),
|
||||
body: JSON.stringify({ username: normalizedUsername }),
|
||||
});
|
||||
|
||||
// Check if response is a bad request (400)
|
||||
@@ -55,26 +55,32 @@ class SrpUtility {
|
||||
rememberMe: boolean,
|
||||
loginResponse: LoginResponse
|
||||
): Promise<ValidateLoginResponse> {
|
||||
const normalizedUsername = SrpAuthService.normalizeUsername(username);
|
||||
|
||||
// Generate client ephemeral
|
||||
const clientEphemeral = srp.generateEphemeral()
|
||||
const clientEphemeral = SrpAuthService.generateEphemeral();
|
||||
|
||||
// Derive private key
|
||||
const privateKey = srp.derivePrivateKey(loginResponse.salt, username, passwordHashString);
|
||||
const privateKey = SrpAuthService.derivePrivateKey(
|
||||
loginResponse.salt,
|
||||
normalizedUsername,
|
||||
passwordHashString
|
||||
);
|
||||
|
||||
// Derive session
|
||||
const sessionProof = srp.deriveSession(
|
||||
const session = SrpAuthService.deriveSession(
|
||||
clientEphemeral.secret,
|
||||
loginResponse.serverEphemeral,
|
||||
loginResponse.salt,
|
||||
username,
|
||||
normalizedUsername,
|
||||
privateKey
|
||||
);
|
||||
|
||||
const model: ValidateLoginRequest = {
|
||||
username: username.toLowerCase().trim(),
|
||||
username: normalizedUsername,
|
||||
rememberMe: rememberMe,
|
||||
clientPublicEphemeral: clientEphemeral.public,
|
||||
clientSessionProof: sessionProof.proof,
|
||||
clientSessionProof: session.proof,
|
||||
};
|
||||
|
||||
const response = await this.webApiService.rawFetch('Auth/validate', {
|
||||
@@ -106,25 +112,32 @@ class SrpUtility {
|
||||
loginResponse: LoginResponse,
|
||||
code2Fa: number
|
||||
): Promise<ValidateLoginResponse> {
|
||||
const normalizedUsername = SrpAuthService.normalizeUsername(username);
|
||||
|
||||
// Generate client ephemeral
|
||||
const clientEphemeral = srp.generateEphemeral()
|
||||
const clientEphemeral = SrpAuthService.generateEphemeral();
|
||||
|
||||
// Derive private key
|
||||
const privateKey = srp.derivePrivateKey(loginResponse.salt, username, passwordHashString);
|
||||
const privateKey = SrpAuthService.derivePrivateKey(
|
||||
loginResponse.salt,
|
||||
normalizedUsername,
|
||||
passwordHashString
|
||||
);
|
||||
|
||||
// Derive session
|
||||
const sessionProof = srp.deriveSession(
|
||||
const session = SrpAuthService.deriveSession(
|
||||
clientEphemeral.secret,
|
||||
loginResponse.serverEphemeral,
|
||||
loginResponse.salt,
|
||||
username,
|
||||
normalizedUsername,
|
||||
privateKey
|
||||
);
|
||||
|
||||
const model: ValidateLoginRequest2Fa = {
|
||||
username: username.toLowerCase().trim(),
|
||||
username: normalizedUsername,
|
||||
rememberMe,
|
||||
clientPublicEphemeral: clientEphemeral.public,
|
||||
clientSessionProof: sessionProof.proof,
|
||||
clientSessionProof: session.proof,
|
||||
code2Fa,
|
||||
};
|
||||
|
||||
|
||||
403
apps/browser-extension/src/utils/auth/SrpAuthService.ts
Normal file
403
apps/browser-extension/src/utils/auth/SrpAuthService.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import srp from 'secure-remote-password/client';
|
||||
|
||||
import type { TokenModel, LoginResponse, BadRequestResponse } from '@/utils/dist/shared/models/webapi';
|
||||
|
||||
import { EncryptionUtility } from '../EncryptionUtility';
|
||||
|
||||
/**
|
||||
* Register request type for creating a new user.
|
||||
*/
|
||||
export type RegisterRequest = {
|
||||
username: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
encryptionType: string;
|
||||
encryptionSettings: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Registration result type.
|
||||
*/
|
||||
export type RegistrationResult = {
|
||||
success: boolean;
|
||||
token?: TokenModel;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Login credentials prepared from password derivation.
|
||||
*/
|
||||
export type PreparedCredentials = {
|
||||
/** Password hash as uppercase hex string for SRP */
|
||||
passwordHashString: string;
|
||||
/** Password hash as base64 string for encryption/decryption */
|
||||
passwordHashBase64: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Default encryption settings for Argon2Id.
|
||||
* These match the server defaults in AliasVault.Cryptography.Client/Defaults.cs
|
||||
*/
|
||||
export const DEFAULT_ENCRYPTION = {
|
||||
type: 'Argon2Id',
|
||||
settings: JSON.stringify({
|
||||
DegreeOfParallelism: 1,
|
||||
MemorySize: 19456,
|
||||
Iterations: 2,
|
||||
}),
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* SrpAuthService provides SRP-based authentication utilities.
|
||||
*
|
||||
* This service handles:
|
||||
* - User registration with SRP protocol
|
||||
* - Password hashing and key derivation
|
||||
* - SRP verifier generation
|
||||
*
|
||||
* It is designed to be used by both the browser extension UI and E2E tests.
|
||||
*/
|
||||
export class SrpAuthService {
|
||||
/**
|
||||
* Normalizes a username by converting to lowercase and trimming whitespace.
|
||||
*
|
||||
* @param username - The username to normalize
|
||||
* @returns The normalized username
|
||||
*/
|
||||
public static normalizeUsername(username: string): string {
|
||||
return username.toLowerCase().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cryptographically secure SRP salt.
|
||||
*
|
||||
* @returns A random salt string
|
||||
*/
|
||||
public static generateSalt(): string {
|
||||
return srp.generateSalt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Uint8Array to an uppercase hex string.
|
||||
*
|
||||
* @param bytes - The byte array to convert
|
||||
* @returns Uppercase hex string
|
||||
*/
|
||||
public static bytesToHexString(bytes: Uint8Array): string {
|
||||
return Array.from(bytes)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Uint8Array to a base64 string.
|
||||
*
|
||||
* @param bytes - The byte array to convert
|
||||
* @returns Base64 string
|
||||
*/
|
||||
public static bytesToBase64(bytes: Uint8Array): string {
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives an SRP private key from credentials.
|
||||
*
|
||||
* @param salt - The SRP salt
|
||||
* @param username - The normalized username
|
||||
* @param passwordHashString - The password hash as uppercase hex string
|
||||
* @returns The SRP private key
|
||||
*/
|
||||
public static derivePrivateKey(
|
||||
salt: string,
|
||||
username: string,
|
||||
passwordHashString: string
|
||||
): string {
|
||||
return srp.derivePrivateKey(salt, SrpAuthService.normalizeUsername(username), passwordHashString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives an SRP verifier from a private key.
|
||||
*
|
||||
* @param privateKey - The SRP private key
|
||||
* @returns The SRP verifier
|
||||
*/
|
||||
public static deriveVerifier(privateKey: string): string {
|
||||
return srp.deriveVerifier(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SRP ephemeral key pair for client-side authentication.
|
||||
*
|
||||
* @returns Object containing public and secret ephemeral values
|
||||
*/
|
||||
public static generateEphemeral(): { public: string; secret: string } {
|
||||
return srp.generateEphemeral();
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives an SRP session from the authentication exchange.
|
||||
*
|
||||
* @param clientSecretEphemeral - Client's secret ephemeral value
|
||||
* @param serverPublicEphemeral - Server's public ephemeral value
|
||||
* @param salt - The SRP salt
|
||||
* @param username - The normalized username
|
||||
* @param privateKey - The SRP private key
|
||||
* @returns The SRP session containing proof and key
|
||||
*/
|
||||
public static deriveSession(
|
||||
clientSecretEphemeral: string,
|
||||
serverPublicEphemeral: string,
|
||||
salt: string,
|
||||
username: string,
|
||||
privateKey: string
|
||||
): { proof: string; key: string } {
|
||||
return srp.deriveSession(
|
||||
clientSecretEphemeral,
|
||||
serverPublicEphemeral,
|
||||
salt,
|
||||
SrpAuthService.normalizeUsername(username),
|
||||
privateKey
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares login credentials by deriving the password hash.
|
||||
*
|
||||
* This method derives the encryption key from the password using the
|
||||
* encryption parameters from the login initiate response.
|
||||
*
|
||||
* @param password - The user's password
|
||||
* @param salt - The salt from login initiate response
|
||||
* @param encryptionType - The encryption type (e.g., 'Argon2Id')
|
||||
* @param encryptionSettings - The encryption settings JSON string
|
||||
* @returns Prepared credentials with hash in both hex and base64 formats
|
||||
*/
|
||||
public static async prepareCredentials(
|
||||
password: string,
|
||||
salt: string,
|
||||
encryptionType: string,
|
||||
encryptionSettings: string
|
||||
): Promise<PreparedCredentials> {
|
||||
// Derive key from password using Argon2Id
|
||||
const passwordHash = await EncryptionUtility.deriveKeyFromPassword(
|
||||
password,
|
||||
salt,
|
||||
encryptionType,
|
||||
encryptionSettings
|
||||
);
|
||||
|
||||
return {
|
||||
passwordHashString: SrpAuthService.bytesToHexString(passwordHash),
|
||||
passwordHashBase64: SrpAuthService.bytesToBase64(passwordHash),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param username - The username for registration
|
||||
* @param password - The password for registration
|
||||
* @returns Registration request data ready to send to the API
|
||||
*/
|
||||
public static async prepareRegistration(
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<RegisterRequest> {
|
||||
const normalizedUsername = SrpAuthService.normalizeUsername(username);
|
||||
const salt = SrpAuthService.generateSalt();
|
||||
|
||||
// Derive key from password using default Argon2Id settings
|
||||
const credentials = await SrpAuthService.prepareCredentials(
|
||||
password,
|
||||
salt,
|
||||
DEFAULT_ENCRYPTION.type,
|
||||
DEFAULT_ENCRYPTION.settings
|
||||
);
|
||||
|
||||
// Generate SRP private key and verifier
|
||||
const privateKey = SrpAuthService.derivePrivateKey(salt, normalizedUsername, credentials.passwordHashString);
|
||||
const verifier = SrpAuthService.deriveVerifier(privateKey);
|
||||
|
||||
return {
|
||||
username: normalizedUsername,
|
||||
salt,
|
||||
verifier,
|
||||
encryptionType: DEFAULT_ENCRYPTION.type,
|
||||
encryptionSettings: DEFAULT_ENCRYPTION.settings,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new user via the API.
|
||||
*
|
||||
* @param apiBaseUrl - The base URL of the API (e.g., 'http://localhost:5092')
|
||||
* @param username - The username for the new account
|
||||
* @param password - The password for the new account
|
||||
* @returns Registration result with token on success
|
||||
*/
|
||||
public static async registerUser(
|
||||
apiBaseUrl: string,
|
||||
username: string,
|
||||
password: string
|
||||
): Promise<RegistrationResult> {
|
||||
try {
|
||||
// Prepare registration data
|
||||
const registerRequest = await SrpAuthService.prepareRegistration(username, password);
|
||||
|
||||
// Normalize the API URL
|
||||
const baseUrl = apiBaseUrl.replace(/\/$/, '') + '/v1/';
|
||||
|
||||
// Send registration request to API
|
||||
const response = await fetch(`${baseUrl}Auth/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(registerRequest),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
let errorMessage = `Registration failed with status ${response.status}`;
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText) as BadRequestResponse;
|
||||
errorMessage = errorJson.title || errorMessage;
|
||||
} catch {
|
||||
errorMessage = errorText || errorMessage;
|
||||
}
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
|
||||
const tokenModel = (await response.json()) as TokenModel;
|
||||
return { success: true, token: tokenModel };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the full SRP login flow.
|
||||
*
|
||||
* @param apiBaseUrl - The base URL of the API
|
||||
* @param username - The username
|
||||
* @param password - The password
|
||||
* @param rememberMe - Whether to request extended token lifetime
|
||||
* @returns Login result with tokens and encryption key
|
||||
*/
|
||||
public static async login(
|
||||
apiBaseUrl: string,
|
||||
username: string,
|
||||
password: string,
|
||||
rememberMe: boolean = false
|
||||
): Promise<{
|
||||
success: boolean;
|
||||
token?: TokenModel;
|
||||
passwordHashBase64?: string;
|
||||
loginResponse?: LoginResponse;
|
||||
requiresTwoFactor?: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const baseUrl = apiBaseUrl.replace(/\/$/, '') + '/v1/';
|
||||
const normalizedUsername = SrpAuthService.normalizeUsername(username);
|
||||
|
||||
// Step 1: Initiate login
|
||||
const initiateResponse = await fetch(`${baseUrl}Auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: normalizedUsername }),
|
||||
});
|
||||
|
||||
if (!initiateResponse.ok) {
|
||||
const errorText = await initiateResponse.text();
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText) as BadRequestResponse;
|
||||
return { success: false, error: errorJson.title };
|
||||
} catch {
|
||||
return { success: false, error: errorText || 'Login initiation failed' };
|
||||
}
|
||||
}
|
||||
|
||||
const loginResponse = (await initiateResponse.json()) as LoginResponse;
|
||||
|
||||
// Step 2: Prepare credentials
|
||||
const credentials = await SrpAuthService.prepareCredentials(
|
||||
password,
|
||||
loginResponse.salt,
|
||||
loginResponse.encryptionType,
|
||||
loginResponse.encryptionSettings
|
||||
);
|
||||
|
||||
// Step 3: Generate SRP session
|
||||
const clientEphemeral = SrpAuthService.generateEphemeral();
|
||||
const privateKey = SrpAuthService.derivePrivateKey(
|
||||
loginResponse.salt,
|
||||
normalizedUsername,
|
||||
credentials.passwordHashString
|
||||
);
|
||||
const session = SrpAuthService.deriveSession(
|
||||
clientEphemeral.secret,
|
||||
loginResponse.serverEphemeral,
|
||||
loginResponse.salt,
|
||||
normalizedUsername,
|
||||
privateKey
|
||||
);
|
||||
|
||||
// Step 4: Validate login
|
||||
const validateResponse = await fetch(`${baseUrl}Auth/validate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
username: normalizedUsername,
|
||||
rememberMe,
|
||||
clientPublicEphemeral: clientEphemeral.public,
|
||||
clientSessionProof: session.proof,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!validateResponse.ok) {
|
||||
const errorText = await validateResponse.text();
|
||||
try {
|
||||
const errorJson = JSON.parse(errorText) as BadRequestResponse;
|
||||
return { success: false, error: errorJson.title };
|
||||
} catch {
|
||||
return { success: false, error: errorText || 'Login validation failed' };
|
||||
}
|
||||
}
|
||||
|
||||
const validateResult = await validateResponse.json();
|
||||
|
||||
// Check for 2FA requirement
|
||||
if (validateResult.requiresTwoFactor) {
|
||||
return {
|
||||
success: false,
|
||||
requiresTwoFactor: true,
|
||||
loginResponse,
|
||||
passwordHashBase64: credentials.passwordHashBase64,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
token: validateResult.token,
|
||||
passwordHashBase64: credentials.passwordHashBase64,
|
||||
loginResponse,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SrpAuthService;
|
||||
Reference in New Issue
Block a user