Auth Module
Complete authentication and authorization system for CodeNotify.
Overview
The Auth Module handles user authentication, token management, and authorization using JWT (JSON Web Tokens) with refresh token rotation.
Module Structure
graph TB
AuthRoot[auth/]
Module[auth.module.ts<br/>Module configuration]
Controller[auth.controller.ts<br/>REST endpoints]
Service[auth.service.ts<br/>Business logic]
Decorators[decorators/]
RolesDecorator[roles.decorator.ts<br/>@Roles decorator]
Guards[guards/]
JwtGuard[jwt-auth.guard.ts<br/>JWT authentication guard]
RolesGuard[roles.guard.ts<br/>Role-based access guard]
Strategies[strategies/]
JwtStrategy[jwt.strategy.ts<br/>Passport JWT strategy]
AuthRoot --> Module
AuthRoot --> Controller
AuthRoot --> Service
AuthRoot --> Decorators
AuthRoot --> Guards
AuthRoot --> Strategies
Decorators --> RolesDecorator
Guards --> JwtGuard
Guards --> RolesGuard
Strategies --> JwtStrategyDependencies
Imports
- UsersModule: User CRUD operations
- PassportModule: Authentication middleware
- JwtModule: Token generation and validation
- ConfigModule: Environment configuration
Providers
- AuthService
- JwtStrategy
- JwtAuthGuard
- RolesGuard
Exports
- AuthService (used by other modules)
Configuration
JWT Module Setup
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET', AUTH.JWT_SECRET),
signOptions: { expiresIn: '15m' },
}),
inject: [ConfigService],
})Token Configuration
| Token Type | Secret | Expiry | Purpose |
|---|---|---|---|
| Access Token | JWT_SECRET | 15 minutes | API authentication |
| Refresh Token | JWT_REFRESH_SECRET | 7 days | Token renewal |
API Endpoints
1. Sign Up
Endpoint: POST /auth/signup
Access: Public
Status Code: 201 Created
Request:
{
name: string; // Min 2 characters
email: string; // Valid email, unique
password: string; // Min 6 characters
phoneNumber?: string; // Optional
}Response:
{
user: {
id: string;
email: string;
name: string;
phoneNumber?: string;
role: string;
};
accessToken: string;
refreshToken: string;
}Process:
- Validate input with Zod schema
- Check if email already exists (409 Conflict)
- Hash password with bcrypt (12 salt rounds)
- Create user in database
- Generate email verification OTP
- Send verification email
- Return user data (isEmailVerified: false)
2. Sign In
Endpoint: POST /auth/signin
Access: Public
Status Code: 200 OK
Request:
{
email: string;
password: string;
}Response: Same as Sign Up
Process:
- Find user by email (401 if not found)
- Check if account is active (401 if deactivated)
- Verify password with bcrypt (401 if invalid)
- Generate new access + refresh tokens
- Hash and store new refresh token
- Update lastLogin timestamp
- Return user data and tokens
3. Verify Email
Endpoint: POST /auth/verify-email
Access: Public
Status Code: 200 OK
Request:
{
email: string;
otp: string;
}Response:
{
message: "Email verified successfully",
user: UserProfile
}Process:
- Find user by email (404 if not found)
- Verify OTP code against stored session/redis
- Check if OTP is expired
- Update user
isEmailVerifiedto true - Return success message
4. Sign Out
Endpoint: POST /auth/signout
Access: Protected (JWT required)
Status Code: 200 OK
Request: No body (user from JWT)
Response:
{
message: "Successfully signed out"
}Process:
- Extract user from JWT token
- Validate user exists (401 if not found)
- Clear refresh token from database (set to null)
- Return success message
Note: Access token remains valid until expiry (15 minutes). For immediate invalidation, implement token blacklist.
4. Refresh Token
Endpoint: POST /auth/refresh
Access: Public
Status Code: 200 OK
Request:
{
userId: string;
refreshToken: string;
}Response:
{
accessToken: string; // NEW token
refreshToken: string; // SAME token
}Process:
- Find user by userId (401 if not found)
- Check if user has refresh token (401 if null)
- Compare provided token with stored hash (401 if mismatch)
- Generate NEW access token (15 min expiry)
- Return new access token + SAME refresh token
Important: Refresh token is NOT regenerated. This prevents rotation issues and maintains 7-day session duration.
Authentication Flow
Initial Authentication (Sign Up/Sign In)
sequenceDiagram
participant Client
participant AuthService as Auth Service
participant Database
Client->>AuthService: POST /auth/signup
AuthService->>Database: Check email exists
Database-->>AuthService: Email available
AuthService->>AuthService: Hash password (bcrypt)
AuthService->>Database: Create user
Database-->>AuthService: User created
AuthService->>AuthService: Generate tokens (JWT)
AuthService->>Database: Store hashed refresh token
Database-->>AuthService: Token stored
AuthService-->>Client: { user, tokens }Token Refresh Flow
sequenceDiagram
participant Client
participant AuthService as Auth Service
participant Database
Client->>AuthService: POST /auth/refresh<br/>{ userId, refreshToken }
AuthService->>Database: Get user
Database-->>AuthService: User data
AuthService->>AuthService: Verify refresh token<br/>(bcrypt compare)
AuthService->>AuthService: Generate NEW access token<br/>(15 min)
AuthService-->>Client: { accessToken (NEW),<br/>refreshToken (SAME) }Security Implementation
Password Hashing
// Sign Up / Password Change
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Sign In / Verification
const isValid = await bcrypt.compare(password, hashedPassword);Configuration:
- Algorithm: bcrypt
- Salt Rounds: 12
- Automatic salt generation
- Constant-time comparison
Token Generation
const payload: JwtPayload = {
sub: userId, // Subject (user ID)
email: email, // User email
role: role // User role (user/admin)
};
// Access Token
const accessToken = await jwtService.signAsync(payload, {
secret: JWT_SECRET,
expiresIn: '15m'
});
// Refresh Token
const refreshToken = await jwtService.signAsync(payload, {
secret: JWT_REFRESH_SECRET,
expiresIn: '7d'
});Refresh Token Storage
// Hash before storage
const hashedRefreshToken = await bcrypt.hash(refreshToken, 12);
await usersService.updateRefreshToken(userId, hashedRefreshToken);
// Verify on refresh
const matches = await bcrypt.compare(providedToken, storedHash);Why hash refresh tokens?
- Prevents token theft if database is compromised
- Even with database access, attacker can't use tokens
- Follows security best practices
Guards & Strategies
JWT Strategy
File: strategies/jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: JwtPayload) {
return {
id: payload.sub,
email: payload.email,
role: payload.role
};
}
}Purpose: Validates JWT tokens and extracts user information
JWT Auth Guard
File: guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
// Check if route is marked as @Public()
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_KEY,
[context.getHandler(), context.getClass()]
);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}Purpose: Protects routes, allows @Public() bypass
Roles Guard
File: guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()]
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.role === role);
}
}Purpose: Enforces role-based access control
Decorators
@Public()
File: common/decorators/public.decorator.ts
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);Usage:
@Public()
@Post('signup')
async signup(@Body() dto: CreateUserDto) {
// No JWT required
}@Roles()
File: auth/decorators/roles.decorator.ts
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);Usage:
@Roles('admin')
@Delete('users/:id')
async deleteUser(@Param('id') id: string) {
// Only admin can access
}@CurrentUser()
File: common/decorators/current-user.decorator.ts
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);Usage:
@Post('signout')
async signout(@CurrentUser() user: UserDocument) {
// User extracted from JWT
}Error Handling
Common Errors
| Error | Status | Cause | Solution |
|---|---|---|---|
| Invalid credentials | 401 | Wrong email/password | Check credentials |
| Account deactivated | 401 | User.isActive = false | Contact admin |
| Email already exists | 409 | Duplicate email | Use different email |
| Access denied | 401 | Invalid/expired token | Refresh or re-login |
| User not found | 401 | Invalid userId | Check user exists |
Error Responses
// 401 Unauthorized
{
statusCode: 401,
message: "Invalid credentials",
error: "Unauthorized"
}
// 409 Conflict
{
statusCode: 409,
message: "User with this email already exists",
error: "Conflict"
}Testing
Unit Tests
describe('AuthService', () => {
it('should hash password on signup', async () => {
const result = await authService.signup(createUserDto);
expect(result.user.password).not.toBe(createUserDto.password);
});
it('should generate tokens on signin', async () => {
const result = await authService.signin(signinDto);
expect(result.accessToken).toBeDefined();
expect(result.refreshToken).toBeDefined();
});
it('should return same refresh token on refresh', async () => {
const result = await authService.refreshAccessToken(userId, token);
expect(result.refreshToken).toBe(token);
});
});E2E Tests
describe('Auth (e2e)', () => {
it('/auth/signup (POST)', () => {
return request(app.getHttpServer())
.post('/auth/signup')
.send(createUserDto)
.expect(201)
.expect((res) => {
expect(res.body.accessToken).toBeDefined();
});
});
});Best Practices
✅ Do
- Always hash passwords before storage
- Use environment variables for secrets
- Implement rate limiting on auth endpoints
- Log authentication events for security monitoring
- Validate all inputs with Zod schemas
- Use HTTPS in production
- Implement token refresh before expiry
- Clear tokens on logout
❌ Don't
- Don't store plain text passwords
- Don't expose sensitive data in responses
- Don't use weak secrets (use strong random strings)
- Don't skip validation
- Don't log passwords or tokens
- Don't share JWT secrets between environments
- Don't implement custom crypto (use bcrypt/JWT libraries)
Production Checklist
- [ ] Strong JWT secrets (32+ random characters)
- [ ] HTTPS enabled
- [ ] Rate limiting configured
- [ ] Password requirements enforced (8+ chars, complexity)
- [ ] Email verification implemented
- [ ] Account lockout after failed attempts
- [ ] Security headers configured (Helmet)
- [ ] CORS properly configured
- [ ] Logging and monitoring enabled
- [ ] Token blacklist for immediate logout (optional)
- [ ] 2FA support (optional)
Related Documentation
- Sign Up Endpoint - User registration
- Sign In Endpoint - User authentication
- Refresh Token Endpoint - Token renewal
- Sign Out Endpoint - Session termination
- JWT Strategy - Token validation
- Users Module - User management
- JWT Authentication - Security implementation

