Skip to content

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:

{
  "id": "user-123",
  "email": "user@example.com",
  "fullName": "John Doe",
  "isActive": false
}

Login

POST /auth/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "SecurePassword123!"
}

Response:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

Refresh Token

POST /auth/refresh
Content-Type: application/json

{
  "refresh": "eyJhbGciOiJIUzI1NiIs..."
}

Response:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs..."
}

Request Password Reset

POST /auth/password-reset/ask
Content-Type: application/json

{
  "email": "user@example.com"
}

Response:

{
  "email": "user@example.com"
}

Reset Password

POST /auth/password-reset
Content-Type: application/json

{
  "token": "reset-token-from-email",
  "password": "NewSecurePassword123!"
}

Response:

{
  "email": "user@example.com"
}

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.