Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('AuthController', () => {

const mockJwtService = {
sign: jest.fn().mockReturnValue('mock-jwt-token'),
expiresIn: '7d',
};

const mockPrismaService = {
Expand Down Expand Up @@ -88,6 +89,7 @@ describe('AuthController', () => {
expect(result.success).toBe(true);
expect(result.data.token).toBe('mock-jwt-token');
expect(result.data.walletAddress).toBe(walletAddress);
expect(result.data.expiresIn).toBe(mockJwtService.expiresIn);
expect(mockPrismaService.authChallenge.delete).toHaveBeenCalledWith({
where: { walletAddress },
});
Expand Down Expand Up @@ -162,6 +164,8 @@ describe('AuthController', () => {
message: nonce,
}),
).rejects.toThrow(UnauthorizedException);

expect(mockPrismaService.authChallenge.delete).not.toHaveBeenCalled();
});
});
});
12 changes: 6 additions & 6 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,6 @@ export class AuthController {
throw new UnauthorizedException('Invalid challenge message');
}

// Invalidate the nonce immediately after use (one-time use)
await this.prisma.authChallenge.delete({ where: { walletAddress } }).catch((err) => {
this.logger.warn(`Failed to delete challenge for ${walletAddress}: ${err.message}`);
});

try {
const keypair = Keypair.fromPublicKey(walletAddress);
const messageBytes = Buffer.from(message, 'utf8');
Expand All @@ -127,6 +122,11 @@ export class AuthController {
throw new UnauthorizedException('Signature verification failed');
}

// Invalidate the nonce only after successful verification (one-time use).
await this.prisma.authChallenge.delete({ where: { walletAddress } }).catch((err) => {
this.logger.warn(`Failed to delete challenge for ${walletAddress}: ${err.message}`);
});

const token = this.jwtService.sign(walletAddress);
this.logger.log(`Login successful: wallet=${walletAddress}`);

Expand All @@ -135,7 +135,7 @@ export class AuthController {
data: {
token,
walletAddress,
expiresIn: '7d',
expiresIn: this.jwtService.expiresIn,
},
};
}
Expand Down
4 changes: 4 additions & 0 deletions src/auth/jwt.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ describe('JwtService', () => {
expect(decoded.walletAddress).toBe(walletAddress);
});

it('should expose the configured token expiry', () => {
expect(service.expiresIn).toBe('7d');
});

it('should throw UnauthorizedException for an invalid token', () => {
expect(() => {
service.verify('invalid.token.value');
Expand Down
7 changes: 6 additions & 1 deletion src/auth/jwt.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface JwtPayload {
export class JwtService {
private readonly logger = new Logger(JwtService.name);
private readonly secret: string;
private readonly tokenExpiry = '7d';

constructor(private readonly config: ConfigService) {
const secret = config.get<string>('JWT_SECRET');
Expand All @@ -31,13 +32,17 @@ export class JwtService {
this.secret = secret;
}

get expiresIn(): string {
return this.tokenExpiry;
}

/**
* Sign a JWT for the given wallet address.
* Token expires in 7 days.
*/
sign(walletAddress: string): string {
const payload: JwtPayload = { walletAddress };
const token = jwt.sign(payload, this.secret, { expiresIn: '7d' });
const token = jwt.sign(payload, this.secret, { expiresIn: this.expiresIn });
this.logger.log(`JWT issued for wallet: ${walletAddress}`);
return token;
}
Expand Down
Loading