Authentication
The authentication system provides secure user identity management using JWT (JSON Web Tokens) with access and refresh token patterns, password hashing with bcrypt, and role-based access control.
Overview
2KRIKA uses a token-based authentication system with:
- JWT tokens for stateless authentication
- Dual token strategy (access + refresh tokens)
- Password hashing with bcrypt
- Email verification for account activation
- Password reset via email tokens
- Role-based permissions for authorization
Authentication Flow
Registration
1. User submits registration form
↓
2. Validate email uniqueness
↓
3. Hash password with bcrypt
↓
4. Create User entity (isActive = false)
↓
5. Generate activation token
↓
6. Send welcome email with activation link
↓
7. Return user data (without tokens)
Implementation:
// web/auth/auth.controller.ts
@Post('register')
async register(@Body() data: UserCreateRequest) {
try {
return await userUseCases.create(
data,
this.userRepository,
this.notifier,
this.logger
);
} catch (e: any) {
if (e instanceof ValidationError) {
throw new BadRequestException(e.errors);
}
throw e;
}
}
User creation:
// app/user/usecases.ts
async function create(
data: any,
userRepository: UserRepository,
notifier: Notifier,
logger: AbstractLogger
) {
const validator = new UserCreateValidator(userRepository, logger);
const validatedData = await validator.validate(data);
const passwordHash = await hasher.hash(validatedData.password);
const user = User.create(
userRepository.nextId(),
validatedData.fullName,
validatedData.username || generateUsername(validatedData.fullName),
validatedData.email,
passwordHash,
null, // lastLogin
false // isActive
);
await userRepository.add(user);
const activationToken = getActivationToken(user.id);
notifier.sendWelcomeMessage(activationToken, user);
return toUserDto(user);
}
Account Activation
1. User clicks activation link in email
↓
2. Extract activation token from URL
↓
3. Verify token and decode user ID
↓
4. Set user.isActive = true
↓
5. User can now log in
Activation endpoint:
@Post('activate')
async activate(@Body() data: { token: string }) {
try {
return await userUseCases.activate(
data,
this.userRepository,
this.logger
);
} catch (e: any) {
if (e instanceof ValidationError) {
throw new BadRequestException(e.errors);
}
throw e;
}
}
Login
1. User submits email + password
↓
2. Find user by email (must be active)
↓
3. Compare password with stored hash
↓
4. Generate access token (15min expiry)
↓
5. Generate refresh token (7d expiry)
↓
6. Update user.lastLogin
↓
7. Return both tokens
Login implementation:
// web/auth/auth.service.ts
async login(
email: string,
password: string
): Promise<{ accessToken: string; refreshToken: string }> {
try {
const user = await this.userRepository.getBy({
email: normalizeEmail(email),
isActive: true,
});
const doesPasswordMatch = await hasher.compare(
user.passwordHash,
password
);
if (!doesPasswordMatch) {
throw new UnauthorizedException();
}
const tokens = await this.generateTokens({
email: user.email,
sub: user.id,
fullName: user.fullName,
});
this.userRepository.update(user.id, {
lastLogin: new Date(),
});
return tokens;
} catch (e: any) {
if (e instanceof NotFound) {
throw new UnauthorizedException();
}
throw e;
}
}
Token Generation
async generateTokens(claims: Claims): Promise<Tokens> {
const accessToken = jwt.sign(claims, this.accessSecret, {
expiresIn: "15m",
});
const refreshToken = jwt.sign(claims, this.refreshSecret, {
expiresIn: "7d",
});
return { accessToken, refreshToken };
}
Token payload (Claims):
type Claims = {
sub: string; // User ID
email: string; // User email
fullName: string; // User full name
}
Token Refresh
1. Client sends refresh token
↓
2. Verify refresh token
↓
3. Decode user ID from token
↓
4. Fetch user from database
↓
5. Generate new access token
↓
6. Return new access token
Implementation:
@Post('refresh')
async refresh(@Body() data: RefreshRequest) {
try {
const accessToken = await this.authService.refresh(data.refresh);
return { accessToken };
} catch (e: any) {
throw new UnauthorizedException();
}
}
// AuthService
async refresh(refreshToken: string): Promise<string> {
const payload = jwt.verify(refreshToken, this.refreshSecret) as Claims;
const user = await this.userRepository.get(payload.sub);
return jwt.sign(
{ sub: user.id, email: user.email, fullName: user.fullName },
this.accessSecret,
{ expiresIn: "15m" }
);
}
Authorization (AuthGuard)
The AuthGuard protects routes and enforces authentication.
Guard Implementation
// web/auth/auth.guard.ts
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private userRoleRepository: UserRoleSqlRepository,
private userRepository: UserRepository,
private reflector: Reflector
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Check if route is public
const isPublic = this.reflector.get<boolean>(
'public',
context.getHandler()
);
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
// Allow public routes without token
if (isPublic && !token) {
return true;
}
// Require token for protected routes
if (!token) {
throw new UnauthorizedException();
}
try {
// Verify token
const payload = await this.authService.verifyAccessToken(token);
// Fetch user with permissions
const user = await this.userRepository.get(payload.id);
await this.userRoleRepository.getAllPermissions(user);
// Attach user to request
request['user'] = user;
return true;
} catch (e: any) {
throw new UnauthorizedException();
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
Using AuthGuard
Protected route:
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
@Get('me')
async getMe(@Req() request: any) {
const user = request.user; // User attached by AuthGuard
return toUserDto(user);
}
}
Public route:
@Get('check')
@NoAuth() // Decorator to mark route as public
async checkEmail(@Query('email') email: string) {
const exists = await this.authService.checkByEmail(email);
return { exists, email };
}
Password Reset
Request Password Reset
1. User enters email
↓
2. Find user by email
↓
3. Generate reset token (signed with secret)
↓
4. Send email with reset link
↓
5. Return success (even if email not found)
Implementation:
async askPasswordReset(body: any) {
const { email } = await new AskPasswordResetRequestValidator().validate(body);
try {
const user = await this.userRepository.getBy({
email: email,
isActive: true,
});
const resetToken = hasher.getResetToken(user.email);
await this.notifier.sendPasswordResetEmail(user, resetToken);
} catch (e: any) {
this.logger.error(`askPasswordReset - ${e.stack}`);
} finally {
// Always return success to prevent email enumeration
return { email };
}
}
Reset Password
1. User submits new password + reset token
↓
2. Verify reset token
↓
3. Decode email from token
↓
4. Find user by email
↓
5. Hash new password
↓
6. Update user.passwordHash
↓
7. Return success
Implementation:
async resetPassword(body: any) {
const { token, password } = await new PasswordResetRequestValidator().validate(body);
const email = hasher.verifyResetToken(token);
const user = await this.userRepository.getBy({ email });
const passwordHash = await hasher.hash(password);
await this.userRepository.update(user.id, { passwordHash });
return { email };
}
Password Hashing
Uses bcrypt for secure password hashing.
// app/utils/password.ts
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
const SALT_ROUNDS = 10;
const hasher = {
async hash(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
},
async compare(hash: string, password: string): Promise<boolean> {
return bcrypt.compare(password, hash);
},
getResetToken(email: string): string {
return jwt.sign({ email }, process.env.PASSWORD_RESET_SECRET_KEY!, {
expiresIn: '1h',
});
},
verifyResetToken(token: string): string {
const payload = jwt.verify(token, process.env.PASSWORD_RESET_SECRET_KEY!);
return payload.email;
},
};
export default hasher;
Environment Variables
# JWT Secrets
JWT_ACCESS_SECRET_KEY=your-access-secret-key-min-32-chars
JWT_REFRESH_SECRET_KEY=your-refresh-secret-key-min-32-chars
PASSWORD_RESET_SECRET_KEY=your-reset-secret-key-min-32-chars
# Token Expiry (optional, defaults shown)
JWT_ACCESS_EXPIRY=15m
JWT_REFRESH_EXPIRY=7d
PASSWORD_RESET_EXPIRY=1h
Security Best Practices
✅ Do
- Use strong secrets – Minimum 32 characters, random
- Short access token expiry – 15 minutes recommended
- Longer refresh token expiry – 7 days recommended
- Hash passwords – Always use bcrypt, never store plaintext
- Verify email before activation – Send activation email
- Normalize emails – Lowercase and trim before storage
- Log authentication events – Track login, failed attempts
- Rate limit authentication endpoints – Prevent brute force
❌ Don't
- Don't expose user existence – Password reset should not reveal if email exists
- Don't log passwords – Never log plaintext passwords
- Don't store tokens in database – JWT is stateless
- Don't use predictable secrets – Use random generated secrets
- Don't skip email verification – Require activation
- Don't reuse tokens – Generate new tokens on refresh
API Endpoints
Registration
POST /auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePassword123!",
"fullName": "John Doe",
"username": "johndoe" // Optional
}
Response:
Login
POST /auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "SecurePassword123!"
}
Response:
Refresh Token
Response:
Request Password Reset
Response:
Reset Password
POST /auth/password-reset
Content-Type: application/json
{
"token": "reset-token-from-email",
"password": "NewSecurePassword123!"
}
Response:
Testing
Mock Authentication
// tests/mocks/auth.mock.ts
export class MockAuthService {
async login(email: string, password: string) {
return {
accessToken: 'mock-access-token',
refreshToken: 'mock-refresh-token',
};
}
async verifyAccessToken(token: string) {
return {
id: 'user-123',
email: 'test@example.com',
fullName: 'Test User',
};
}
}
Test Cases
describe('Authentication', () => {
it('should register a new user', async () => {
const response = await request(app)
.post('/auth/register')
.send({
email: 'test@example.com',
password: 'Password123!',
fullName: 'Test User',
});
expect(response.status).toBe(201);
expect(response.body.email).toBe('test@example.com');
expect(response.body.isActive).toBe(false);
});
it('should login with valid credentials', async () => {
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'Password123!',
});
expect(response.status).toBe(200);
expect(response.body.accessToken).toBeDefined();
expect(response.body.refreshToken).toBeDefined();
});
it('should reject invalid credentials', async () => {
const response = await request(app)
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'WrongPassword',
});
expect(response.status).toBe(401);
});
});
Summary
The authentication system provides:
- JWT-based authentication with access and refresh tokens
- Secure password hashing with bcrypt
- Email verification via activation tokens
- Password reset via email tokens
- AuthGuard for route protection
- Role-based permissions for authorization
- Security best practices built-in
This system ensures secure user identity management while maintaining developer-friendly patterns.