First commit
This commit is contained in:
22
packages/feature-auth/domain/package.json
Normal file
22
packages/feature-auth/domain/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@feature/auth/domain",
|
||||
"version": "1.0.0",
|
||||
"description": "Authentication domain logic - pure business rules and use cases",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"build": "nx build",
|
||||
"test": "nx test",
|
||||
"lint": "nx lint",
|
||||
"typecheck": "nx typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.7"
|
||||
},
|
||||
"nx": {
|
||||
"tags": ["scope:feature", "type:domain", "platform:shared"]
|
||||
}
|
||||
}
|
||||
53
packages/feature-auth/domain/src/entities.ts
Normal file
53
packages/feature-auth/domain/src/entities.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
phoneNumber?: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isEmailVerified: boolean;
|
||||
isPhoneVerified: boolean;
|
||||
createdAt: string;
|
||||
lastLoginAt?: string;
|
||||
profilePictureUrl?: string;
|
||||
organizations: UserOrganization[];
|
||||
preferences: UserPreferences;
|
||||
}
|
||||
|
||||
export interface UserOrganization {
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
role: string;
|
||||
permissions: string[];
|
||||
joinedAt: string;
|
||||
status: 'active' | 'suspended' | 'pending';
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
language: string;
|
||||
timezone: string;
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
notifications: NotificationPreferences;
|
||||
}
|
||||
|
||||
export interface NotificationPreferences {
|
||||
email: boolean;
|
||||
push: boolean;
|
||||
sms: boolean;
|
||||
marketing: boolean;
|
||||
}
|
||||
|
||||
export interface AuthResult {
|
||||
success: boolean;
|
||||
session?: any;
|
||||
requiresVerification?: boolean;
|
||||
verificationMethod?: VerificationMethod;
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
nextSteps?: string[];
|
||||
}
|
||||
|
||||
export type VerificationMethod =
|
||||
| { type: 'otp'; target: string }
|
||||
| { type: 'email'; target: string }
|
||||
| { type: 'biometric' }
|
||||
| { type: 'magic_link'; target: string };
|
||||
20
packages/feature-auth/domain/src/index.ts
Normal file
20
packages/feature-auth/domain/src/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export const AuthDomain = {};
|
||||
import { LoginUseCase } from './usecases/LoginUseCase';
|
||||
import { RegistrationUseCase } from './usecases/RegistrationUseCase';
|
||||
import { SessionUseCase } from './usecases/SessionUseCase';
|
||||
import { BiometricUseCase } from './usecases/BiometricUseCase';
|
||||
|
||||
export { LoginUseCase, RegistrationUseCase, SessionUseCase, BiometricUseCase };
|
||||
export type {
|
||||
IAuthRepository,
|
||||
ITrustRepository,
|
||||
Credentials,
|
||||
UserSession,
|
||||
BiometricConfig,
|
||||
AuthConfig
|
||||
} from './interfaces';
|
||||
export type {
|
||||
User,
|
||||
AuthResult,
|
||||
VerificationMethod
|
||||
} from './entities';
|
||||
76
packages/feature-auth/domain/src/interfaces.ts
Normal file
76
packages/feature-auth/domain/src/interfaces.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { User, AuthResult } from './entities';
|
||||
|
||||
/**
|
||||
* Repository interface that the data layer must implement
|
||||
* Following clean architecture principles - domain defines the contract
|
||||
*/
|
||||
export interface IAuthRepository {
|
||||
// F.ID.004: DPoP initialization
|
||||
initializeDPoP(): Promise<string>;
|
||||
|
||||
// F.ID.001: Login flows
|
||||
exchangeCredentialsForToken(creds: Credentials, pubKey: string): Promise<UserSession>;
|
||||
|
||||
// F.ID.002: Verification flows
|
||||
sendOTPVerification(phoneNumber: string): Promise<void>;
|
||||
verifyOTP(phoneNumber: string, code: string): Promise<boolean>;
|
||||
sendEmailVerification(email: string): Promise<void>;
|
||||
verifyEmail(email: string, token: string): Promise<boolean>;
|
||||
|
||||
// Magic Links (F.ID.006 - Enhanced security)
|
||||
generateMagicLink(email: string): Promise<string>;
|
||||
verifyMagicLink(token: string, nonce: string): Promise<UserSession>;
|
||||
|
||||
// F.ID.007: Federated login
|
||||
authenticateWithProvider(provider: 'google' | 'apple', token: string): Promise<UserSession>;
|
||||
|
||||
// Session management
|
||||
storeSession(session: UserSession): Promise<void>;
|
||||
getSession(): Promise<UserSession | null>;
|
||||
refreshSession(refreshToken: string): Promise<UserSession>;
|
||||
revokeSession(): Promise<void>;
|
||||
|
||||
// F.ID.005: Remember me functionality
|
||||
enableRememberMe(duration: number): Promise<void>;
|
||||
disableRememberMe(): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trust/Risk assessment interface
|
||||
*/
|
||||
export interface ITrustRepository {
|
||||
calculateRiskScore(): Promise<{ score: number; signals: Record<string, any> }>;
|
||||
performDeviceAttestation(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface Credentials {
|
||||
identifier: string; // email or phone
|
||||
password?: string;
|
||||
biometricSignature?: string;
|
||||
deviceId: string;
|
||||
}
|
||||
|
||||
export interface UserSession {
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: string;
|
||||
user: User;
|
||||
riskScore?: number;
|
||||
requiresStepUp?: boolean;
|
||||
}
|
||||
|
||||
export interface BiometricConfig {
|
||||
enabled: boolean;
|
||||
fallbackToPassword: boolean;
|
||||
promptMessage: string;
|
||||
}
|
||||
|
||||
export interface AuthConfig {
|
||||
requireEmailVerification: boolean;
|
||||
requirePhoneVerification: boolean;
|
||||
biometrics: BiometricConfig;
|
||||
rememberMeDays: number;
|
||||
maxLoginAttempts: number;
|
||||
lockoutDurationMinutes: number;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class BiometricUseCase {
|
||||
async verify(signature: string) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
177
packages/feature-auth/domain/src/usecases/LoginUseCase.ts
Normal file
177
packages/feature-auth/domain/src/usecases/LoginUseCase.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type { IAuthRepository, ITrustRepository, Credentials, UserSession, AuthConfig } from '../interfaces';
|
||||
import type { AuthResult } from '../entities';
|
||||
|
||||
/**
|
||||
* Login Use Case - Pure business logic
|
||||
* Orchestrates the login flow including risk assessment and DPoP initialization
|
||||
*
|
||||
* Features implemented (per FRD):
|
||||
* - F.ID.001: Login flows
|
||||
* - F.ID.004: DPoP integration
|
||||
* - Risk-adaptive authentication
|
||||
* - Step-up MFA based on risk score
|
||||
*/
|
||||
export class LoginUseCase {
|
||||
constructor(
|
||||
private authRepository: IAuthRepository,
|
||||
private trustRepository: ITrustRepository,
|
||||
private config: AuthConfig
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute login flow with risk assessment
|
||||
*/
|
||||
async execute(credentials: Credentials): Promise<AuthResult> {
|
||||
try {
|
||||
// 1. Risk Assessment (F.TR.004 from Trust SDK)
|
||||
const { score: riskScore, signals } = await this.trustRepository.calculateRiskScore();
|
||||
|
||||
// 2. Initialize DPoP (F.ID.004 / F.SC.004)
|
||||
const publicKey = await this.authRepository.initializeDPoP();
|
||||
|
||||
// 3. Attempt authentication
|
||||
const session = await this.authRepository.exchangeCredentialsForToken(credentials, publicKey);
|
||||
|
||||
// 4. Enhance session with risk information
|
||||
const enhancedSession: UserSession = {
|
||||
...session,
|
||||
riskScore,
|
||||
requiresStepUp: this.shouldRequireStepUp(riskScore, signals)
|
||||
};
|
||||
|
||||
// 5. Store session
|
||||
await this.authRepository.storeSession(enhancedSession);
|
||||
|
||||
// 6. Check if step-up authentication is required
|
||||
if (enhancedSession.requiresStepUp) {
|
||||
return {
|
||||
success: true,
|
||||
session: enhancedSession,
|
||||
requiresVerification: true,
|
||||
verificationMethod: this.determineStepUpMethod(signals),
|
||||
nextSteps: ['Complete additional verification to proceed']
|
||||
};
|
||||
}
|
||||
|
||||
// 7. Enable Remember Me if configured
|
||||
if (this.config.rememberMeDays > 0) {
|
||||
await this.authRepository.enableRememberMe(this.config.rememberMeDays);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
session: enhancedSession
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return this.handleLoginError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Biometric login flow
|
||||
*/
|
||||
async executeWithBiometrics(biometricSignature: string, deviceId: string): Promise<AuthResult> {
|
||||
const credentials: Credentials = {
|
||||
identifier: 'biometric',
|
||||
biometricSignature,
|
||||
deviceId
|
||||
};
|
||||
|
||||
return this.execute(credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic link login flow (F.ID.006)
|
||||
*/
|
||||
async executeWithMagicLink(token: string, nonce: string): Promise<AuthResult> {
|
||||
try {
|
||||
const session = await this.authRepository.verifyMagicLink(token, nonce);
|
||||
await this.authRepository.storeSession(session);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
session
|
||||
};
|
||||
} catch (error) {
|
||||
return this.handleLoginError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Federated login flow (F.ID.007)
|
||||
*/
|
||||
async executeWithProvider(provider: 'google' | 'apple', token: string): Promise<AuthResult> {
|
||||
try {
|
||||
const session = await this.authRepository.authenticateWithProvider(provider, token);
|
||||
await this.authRepository.storeSession(session);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
session
|
||||
};
|
||||
} catch (error) {
|
||||
return this.handleLoginError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldRequireStepUp(riskScore: number, signals: Record<string, any>): boolean {
|
||||
// Risk-based step-up logic
|
||||
if (riskScore > 75) return true;
|
||||
if (signals.isRooted || signals.isEmulator) return true;
|
||||
if (signals.attestationFailed) return true;
|
||||
if (signals.geoDeviation && riskScore > 50) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private determineStepUpMethod(signals: Record<string, any>) {
|
||||
// Determine the most appropriate step-up method based on risk signals
|
||||
if (signals.suspiciousLocation) {
|
||||
return { type: 'otp' as const, target: 'phone' };
|
||||
}
|
||||
|
||||
if (signals.newDevice) {
|
||||
return { type: 'email' as const, target: 'email' };
|
||||
}
|
||||
|
||||
// Default to biometric if available
|
||||
return { type: 'biometric' as const };
|
||||
}
|
||||
|
||||
private handleLoginError(error: any): AuthResult {
|
||||
console.error('Login failed:', error);
|
||||
|
||||
// Map specific errors to user-friendly messages
|
||||
if (error.code === 'INVALID_CREDENTIALS') {
|
||||
return {
|
||||
success: false,
|
||||
errorCode: 'INVALID_CREDENTIALS',
|
||||
errorMessage: 'Invalid email or password'
|
||||
};
|
||||
}
|
||||
|
||||
if (error.code === 'ACCOUNT_LOCKED') {
|
||||
return {
|
||||
success: false,
|
||||
errorCode: 'ACCOUNT_LOCKED',
|
||||
errorMessage: 'Account is temporarily locked due to multiple failed attempts'
|
||||
};
|
||||
}
|
||||
|
||||
if (error.code === 'VERIFICATION_REQUIRED') {
|
||||
return {
|
||||
success: false,
|
||||
requiresVerification: true,
|
||||
verificationMethod: error.verificationMethod,
|
||||
errorMessage: 'Account verification required'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
errorCode: 'UNKNOWN_ERROR',
|
||||
errorMessage: 'An unexpected error occurred'
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class RegistrationUseCase {
|
||||
async execute(payload: any) {
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export class SessionUseCase {
|
||||
async getCurrent(sessionId: string) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user