Skip to main content

Overview

CryptoPulse uses JWT (JSON Web Token) bearer authentication to secure all price endpoints. Clients must authenticate via the login endpoint, receive an access token, and include it in the Authorization header for subsequent requests.
The /docs Swagger UI and /docs-json OpenAPI spec are publicly accessible without authentication.

Authentication Flow

Step 1: Login

Request

POST credentials to the login endpoint:
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "admin",
    "password": "secret"
  }'

Response

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE3MDk4MjQ1MDAsImV4cCI6MTcwOTgyODEwMH0.signature",
  "tokenType": "Bearer",
  "expiresIn": "1h"
}
Store the accessToken securely. Never expose it in logs, URLs, or client-side storage vulnerable to XSS.

Login Implementation

The login logic in src/auth/auth.service.ts validates credentials against environment variables:
src/auth/auth.service.ts
async login(credentials: LoginDto): Promise<TokenResponseDto> {
  const expectedUser = this.configService.getOrThrow<string>('ADMIN_USER');
  const expectedPass = this.configService.getOrThrow<string>('ADMIN_PASS');
  
  if (credentials.username !== expectedUser || credentials.password !== expectedPass) {
    throw new UnauthorizedException('Invalid credentials');
  }
  
  const expiresIn = this.configService.get<string>('JWT_EXPIRES_IN') ?? '1h';
  const accessToken = await this.jwtService.signAsync({
    sub: 'admin',
    username: credentials.username,
  });
  
  return {
    accessToken,
    tokenType: 'Bearer',
    expiresIn,
  };
}
The current implementation uses a single admin account configured via environment variables. For production use, consider integrating a user database.

Environment Configuration

.env
ADMIN_USER=admin
ADMIN_PASS=your-secure-password
JWT_SECRET=your-secret-key-min-32-chars
JWT_EXPIRES_IN=1h  # Can be: 30s, 5m, 2h, 7d
Required. Secret key for signing tokens.
  • Must be at least 32 characters
  • Use a cryptographically random string
  • Never commit to version control
Generate with:
openssl rand -base64 32

Rate Limiting on Login

Login endpoint is rate-limited to prevent brute-force attacks:
src/auth/auth.controller.ts
@Post('login')
@Throttle({
  default: {
    ttl: () => toPositiveInt(process.env.THROTTLE_TTL_MS, 60000),
    limit: () => toPositiveInt(process.env.THROTTLE_LOGIN_LIMIT, 5),
  },
})
login(@Body() loginDto: LoginDto): Promise<TokenResponseDto> {
  return this.authService.login(loginDto);
}
Default: 5 login attempts per 60 seconds. Exceeding the limit returns 429 Too Many Requests.

Step 2: Using the Token

Include the token in the Authorization header with the Bearer scheme:
curl http://localhost:3000/v1/price/bitcoin \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Token Validation

All protected endpoints use the JwtAuthGuard:
src/price/price.controller.ts
@ApiTags('price')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('v1/price')
export class PriceController {
  @Get(':coinId')
  getPrice(@Param('coinId') coinId: string): Promise<PriceResponseDto> {
    return this.priceService.getCurrentPrice(coinId);
  }
}

JwtAuthGuard Implementation

The guard extracts and verifies the token:
src/auth/guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}
  
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<AppTypes.AuthenticatedRequest>();
    const token = this.extractTokenFromAuthorizationHeader(request.headers.authorization);
    
    try {
      request.user = await this.jwtService.verifyAsync<AppTypes.JwtPayload>(token);
      return true;
    } catch {
      throw new UnauthorizedException('Invalid or expired token');
    }
  }
  
  private extractTokenFromAuthorizationHeader(
    authorization: string | string[] | undefined,
  ): string {
    if (!authorization) {
      throw new UnauthorizedException('Missing Authorization header');
    }
    
    const rawHeader = Array.isArray(authorization) ? authorization[0] : authorization;
    const trimmedHeader = rawHeader.trim();
    const match = /^Bearer\s+(.+)$/i.exec(trimmedHeader);
    
    if (!match) {
      throw new UnauthorizedException('Authorization header must use Bearer token');
    }
    
    const token = match[1].trim();
    
    if (!token || token.includes(' ')) {
      throw new UnauthorizedException('Invalid Bearer token');
    }
    
    return token;
  }
}

Validation Steps

1

Extract header

Guard reads Authorization header from request
2

Parse Bearer token

Validates format: Bearer <token>Common errors:
  • Missing header → 401 Missing Authorization header
  • Wrong format (e.g., just the token without “Bearer”) → 401 Authorization header must use Bearer token
  • Token contains spaces → 401 Invalid Bearer token
3

Verify JWT signature

Uses JWT_SECRET to verify token hasn’t been tampered with
4

Check expiration

Ensures token hasn’t exceeded JWT_EXPIRES_INIf expired → 401 Invalid or expired token
5

Attach user to request

Decoded payload (e.g., {sub: 'admin', username: 'admin'}) is available in request handlers

JWT Module Configuration

The JWT module is configured in src/auth/auth.module.ts:
src/auth/auth.module.ts
JwtModule.registerAsync({
  inject: [ConfigService],
  useFactory: (configService: ConfigService) => ({
    secret: configService.getOrThrow<string>('JWT_SECRET'),
    signOptions: {
      expiresIn: parseJwtExpirationSeconds(configService.get<string>('JWT_EXPIRES_IN')),
    },
  }),
}),
The helper parseJwtExpirationSeconds converts formats like 1h to seconds:
function parseJwtExpirationSeconds(value: string | undefined): number {
  if (!value) return 3600;
  
  const numeric = Number(value);
  if (Number.isInteger(numeric) && numeric > 0) {
    return numeric;
  }
  
  const match = /^(\d+)([smhd])$/i.exec(value.trim());
  if (!match) return 3600;
  
  const amount = Number(match[1]);
  const unit = match[2].toLowerCase();
  
  if (unit === 's') return amount;
  if (unit === 'm') return amount * 60;
  if (unit === 'h') return amount * 3600;
  return amount * 86400; // days
}

Error Responses

Login Errors

{
  "statusCode": 400,
  "message": ["username should not be empty"],
  "error": "Bad Request"
}
Validation failed (e.g., missing fields).

Protected Endpoint Errors

{
  "statusCode": 401,
  "message": "Missing Authorization header",
  "error": "Unauthorized"
}
No Authorization header provided.

Swagger Integration

The Swagger UI at /docs is configured for bearer auth:
src/main.ts
const swaggerConfig = new DocumentBuilder()
  .setTitle('Crypto Pulse API')
  .setDescription(
    [
      'Crypto asset price inquiry API with bearer authentication...',
      '',
      'Quick start:',
      '1) Call POST /auth/login to get accessToken',
      '2) Click Authorize and use: Bearer <accessToken>',
      '3) Call /v1/price/:coinId and /v1/price/:coinId/history',
    ].join('\n'),
  )
  .addBearerAuth()
  .build();

Using Swagger UI

1

Open Swagger

Navigate to http://localhost:3000/docs
2

Login

Expand POST /auth/login, click Try it out, enter credentials, and execute
3

Copy token

Copy the accessToken from the response
4

Authorize

Click the Authorize button (top right), paste token in format:
Bearer <your-token-here>
Or just paste the token directly - Swagger adds “Bearer” automatically.
5

Make requests

All subsequent requests will include the Authorization header automatically
Swagger UI is the easiest way to test authentication during development.

Security Best Practices

Client-side:
  • Use httpOnly cookies for web apps (prevents XSS)
  • Use secure storage APIs for mobile apps
  • Never store in localStorage if XSS risk exists
Server-side:
  • Never log full tokens
  • Use environment variables for secrets
  • Rotate JWT_SECRET periodically
  • Short-lived tokens (1h) reduce risk if compromised
  • Implement refresh tokens for long-running clients
  • Force re-authentication for sensitive operations
  • Login endpoint is rate-limited by default
  • Consider IP-based limits for additional protection
  • Monitor for brute-force patterns
  • ALWAYS use HTTPS in production
  • Tokens in plain HTTP can be intercepted
  • Configure HSTS headers

Common Issues

Symptom: 401 Invalid or expired token after some time.Solution: Re-authenticate via POST /auth/login to get a fresh token.
Symptom: 401 Authorization header must use Bearer token.Solution: Ensure header is Authorization: Bearer <token>, not just the token.
Symptom: Token from one instance doesn’t work on another.Solution: Ensure all instances use the SAME JWT_SECRET.
Symptom: Token appears expired immediately or works when it shouldn’t.Solution: Synchronize system clocks (NTP) across all instances.

Testing Authentication

Manual Test

# 1. Login
TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"secret"}' \
  | jq -r '.accessToken')

echo "Token: $TOKEN"

# 2. Use token
curl http://localhost:3000/v1/price/bitcoin \
  -H "Authorization: Bearer $TOKEN"

# 3. Test without token (should fail)
curl http://localhost:3000/v1/price/bitcoin

# 4. Test with invalid token (should fail)
curl http://localhost:3000/v1/price/bitcoin \
  -H "Authorization: Bearer invalid-token"

Unit Test Example

From the codebase:
it('should reject missing Authorization header', async () => {
  const request = { headers: {} };
  const context = createMockExecutionContext(request);
  
  await expect(guard.canActivate(context)).rejects.toThrow(
    new UnauthorizedException('Missing Authorization header'),
  );
});

it('should reject invalid Bearer format', async () => {
  const request = { headers: { authorization: 'InvalidFormat token' } };
  const context = createMockExecutionContext(request);
  
  await expect(guard.canActivate(context)).rejects.toThrow(
    new UnauthorizedException('Authorization header must use Bearer token'),
  );
});

Next Steps

API Reference

View detailed login endpoint documentation

Rate Limiting

Learn about throttling and rate limits

Build docs developers (and LLMs) love