Imagine you're at a party, and someone claims to be your friend Sarah's boyfriend. You've never met him before. To verify his identity, you have two options:
Scenario 1: The Wrong Way
You call Sarah and ask, "Hey, what's your Facebook password?"
Sarah reluctantly tells you, then you log into her account to check her relationship status.
This approach is problematic because:
- Sarah had to reveal sensitive information
- You now have access to information you shouldn't
- The process required an uncomfortable breach of privacy
Scenario 2: The Right Way
You text Sarah: "There's a guy here claiming to be your boyfriend. Can you confirm?"
Sarah simply replies with a yes or no.
This is clearly better because:
- Sarah's password remains private
- You got the information you needed
- The responsibility stayed with the right person
Back to Code
This everyday example perfectly illustrates what's happening in our API code:
The Wrong Approach
// Service layer peeks at private data
async login(credentials: LoginRequestDto): Promise<LoginResponseDto> {
const user = await this.repository.findUserByEmail(email);
// Breaking abstraction to get the password
const rawUserData = await this.repository['prisma'].users.findUnique({
where: { email }
});
// Checking the password ourselves
if (!CryptoUtil.verifyPassword(
credentials.password,
rawUserData.password,
rawUserData.salt
)) {
throw new UnauthorizedError();
}
}
The Better Approach
// User entity class
class UserEntity {
private password: string;
private salt: string;
// The user keeps their own secrets
verifyPassword(attemptedPassword: string): boolean {
return CryptoUtil.verifyPassword(
attemptedPassword,
this.password,
this.salt
);
}
}
// Service simply asks the user
async login(credentials: LoginRequestDto): Promise<LoginResponseDto> {
const user = await this.repository.findUserByEmail(email);
// Just like texting Sarah - we ask without seeing the secrets
if (!user.verifyPassword(credentials.password)) {
throw new UnauthorizedError();
}
}
Real-World Benefits
This approach isn't just academically satisfying - it has practical benefits:
Easier to maintain: When password hashing algorithms need updating, you change code in one place, not everywhere passwords are checked.
Fewer bugs: The service layer doesn't need to know how passwords are stored or verified.
Better security: Sensitive data stays encapsulated and isn't passed around unnecessarily.
Cleaner code: Your authentication logic lives where it belongs.
Just like in real life, good boundaries make for better relationships. When each part of your system maintains responsibility for its own data, your code becomes more intuitive, maintainable, and secure.
The next time you're writing authentication logic, ask yourself: am I asking for someone's password, or am I simply asking them to verify something only they should know?
Top comments (0)