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.