Skip to content

Commit 80a2e39

Browse files
author
Ketbome
committed
fix: update cookie handling and token expiration settings for improved security
1 parent 950b3f8 commit 80a2e39

13 files changed

+24
-13
lines changed

backend/src/auth/auth.controller.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ describe('AuthController', () => {
138138
);
139139
});
140140

141-
it('should set insecure cookies in development mode', async () => {
141+
it('should set secure cookies in development mode by default', async () => {
142142
process.env.NODE_ENV = 'development';
143143
delete process.env.ALLOW_INSECURE_AUTH_COOKIES;
144144

@@ -156,12 +156,12 @@ describe('AuthController', () => {
156156
expect(mockResponse.cookie).toHaveBeenCalledWith(
157157
'access_token',
158158
'jwt.access.token',
159-
expect.objectContaining({ secure: false, httpOnly: true, sameSite: 'lax' }),
159+
expect.objectContaining({ secure: true, httpOnly: true, sameSite: 'lax' }),
160160
);
161161
expect(mockResponse.cookie).toHaveBeenCalledWith(
162162
'refresh_token',
163163
'jwt.refresh.token',
164-
expect.objectContaining({ secure: false, httpOnly: true, sameSite: 'lax' }),
164+
expect.objectContaining({ secure: true, httpOnly: true, sameSite: 'lax' }),
165165
);
166166
});
167167

backend/src/auth/auth.controller.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { Response, Request, CookieOptions } from 'express';
66

77
@Controller('auth')
88
export class AuthController {
9-
private static readonly ACCESS_TOKEN_MAX_AGE = 15 * 60 * 1000; // 15 minutes
109
private static readonly REFRESH_TOKEN_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
1110

1211
constructor(private readonly authService: AuthService) {}
@@ -23,7 +22,7 @@ export class AuthController {
2322
const tokens = await this.authService.generateJwt(user);
2423

2524
// Set httpOnly cookies
26-
res.cookie('access_token', tokens.access_token, this.getAuthCookieOptions(AuthController.ACCESS_TOKEN_MAX_AGE));
25+
res.cookie('access_token', tokens.access_token, this.getAuthCookieOptions(tokens.expires_in * 1000));
2726
res.cookie('refresh_token', tokens.refresh_token, this.getAuthCookieOptions(AuthController.REFRESH_TOKEN_MAX_AGE));
2827

2928
return {
@@ -64,7 +63,7 @@ export class AuthController {
6463
const tokens = await this.authService.generateJwt(user);
6564

6665
// Update cookies with new tokens
67-
res.cookie('access_token', tokens.access_token, this.getAuthCookieOptions(AuthController.ACCESS_TOKEN_MAX_AGE));
66+
res.cookie('access_token', tokens.access_token, this.getAuthCookieOptions(tokens.expires_in * 1000));
6867
res.cookie('refresh_token', tokens.refresh_token, this.getAuthCookieOptions(AuthController.REFRESH_TOKEN_MAX_AGE));
6968

7069
return {
@@ -100,9 +99,6 @@ export class AuthController {
10099
}
101100

102101
private shouldUseSecureCookies(): boolean {
103-
if (process.env.ALLOW_INSECURE_AUTH_COOKIES === 'true') {
104-
return false;
105-
}
106-
return process.env.NODE_ENV === 'production';
102+
return process.env.ALLOW_INSECURE_AUTH_COOKIES !== 'true';
107103
}
108104
}

backend/src/auth/auth.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { RefreshToken } from './entities/refresh-token.entity';
2121
return {
2222
secret: configService.get('jwtSecret'),
2323
signOptions: {
24-
expiresIn: '15m',
24+
expiresIn: configService.get('jwtExpiresIn') || '2d',
2525
issuer: configService.get('jwtIssuer'),
2626
audience: configService.get('jwtAudience'),
2727
},

backend/src/auth/auth.service.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('AuthService', () => {
3030
beforeEach(async () => {
3131
const mockJwtService = {
3232
sign: jest.fn(),
33+
decode: jest.fn(),
3334
};
3435

3536
const mockUsersService = {
@@ -121,6 +122,7 @@ describe('AuthService', () => {
121122
const mockHashedToken = 'hashed.refresh.token';
122123

123124
jwtService.sign.mockReturnValue(mockAccessToken);
125+
jwtService.decode.mockReturnValue({ iat: 100, exp: 120 } as any);
124126
(bcrypt.hash as jest.Mock).mockResolvedValue(mockHashedToken);
125127
refreshTokenRepo.save.mockResolvedValue({} as any);
126128

@@ -136,7 +138,7 @@ describe('AuthService', () => {
136138
access_token: mockAccessToken,
137139
refresh_token: 'mock-random-token',
138140
username: 'testuser',
139-
expires_in: 900,
141+
expires_in: 20,
140142
});
141143
expect(jwtService.sign).toHaveBeenCalledWith(payload);
142144
expect(refreshTokenRepo.save).toHaveBeenCalledWith(

backend/src/auth/auth.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,14 @@ export class AuthService {
5151

5252
const accessToken = this.jwtService.sign(payload);
5353
const refreshToken = await this.createRefreshToken(user.userId);
54+
const decodedToken = this.jwtService.decode(accessToken) as { exp?: number; iat?: number } | null;
55+
const expiresIn = decodedToken?.exp && decodedToken?.iat ? decodedToken.exp - decodedToken.iat : 900;
5456

5557
return {
5658
access_token: accessToken,
5759
refresh_token: refreshToken,
5860
username: user.username,
59-
expires_in: 900, // 15 minutes in seconds
61+
expires_in: expiresIn,
6062
};
6163
}
6264

backend/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export default () => ({
22
jwtSecret: process.env.JWT_SECRET,
3+
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '2d',
34
jwtIssuer: process.env.JWT_ISSUER || 'minepanel',
45
jwtAudience: process.env.JWT_AUDIENCE || 'minepanel-users',
56
clientUsername: process.env.CLIENT_USERNAME,

doc/configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ All variables can be set in `.env` or `docker-compose.yml`.
3535
| ----------------- | ------- | -------------- |
3636
| `CLIENT_USERNAME` | `admin` | Login username |
3737
| `CLIENT_PASSWORD` | `admin` | Login password |
38+
| `JWT_EXPIRES_IN` | `2d` | Access token expiration (`20s`, `15m`, `1h`, `2d`) |
3839
| `ALLOW_INSECURE_AUTH_COOKIES` | `false` | Set `true` only for HTTP/LAN access when browsers block auth cookies |
3940

4041
### URLs
@@ -68,13 +69,15 @@ Use this only for trusted LAN/development environments. Prefer HTTPS whenever po
6869
```bash
6970
# .env
7071
JWT_SECRET=your_secret
72+
JWT_EXPIRES_IN=2d
7173
```
7274

7375
### Remote Access
7476

7577
```bash
7678
# .env
7779
JWT_SECRET=your_secret
80+
JWT_EXPIRES_IN=2d
7881
FRONTEND_URL=http://your-ip:3000
7982
NEXT_PUBLIC_BACKEND_URL=http://your-ip:8091
8083
```
@@ -84,6 +87,7 @@ NEXT_PUBLIC_BACKEND_URL=http://your-ip:8091
8487
```bash
8588
# .env
8689
JWT_SECRET=your_secret
90+
JWT_EXPIRES_IN=2d
8791
FRONTEND_URL=https://minepanel.yourdomain.com
8892
NEXT_PUBLIC_BACKEND_URL=https://api.yourdomain.com
8993
```

doc/development.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Runs on `http://localhost:3000`
8181
FRONTEND_URL= 'http://localhost:3000' # URL of the frontend application
8282
# Generate a strong random secret: openssl rand -base64 32
8383
JWT_SECRET= # Example: your-super-secret-jwt-key-change-this-in-production
84+
JWT_EXPIRES_IN=2d # Access token expiration (use 20s to test refresh flow)
8485
CLIENT_PASSWORD= # Password for client
8586
CLIENT_USERNAME= # Username for the client
8687
DB_PATH=./data/minepanel.db

docker-compose.development.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ services:
88
environment:
99
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:3000}
1010
- JWT_SECRET=${JWT_SECRET}
11+
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-2d}
1112
- CLIENT_PASSWORD=${CLIENT_PASSWORD:-admin}
1213
- CLIENT_USERNAME=${CLIENT_USERNAME:-admin}
1314
- ALLOW_INSECURE_AUTH_COOKIES=${ALLOW_INSECURE_AUTH_COOKIES:-false}

docker-compose.single.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ services:
88
# Backend Configuration
99
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:3000}
1010
- JWT_SECRET=${JWT_SECRET} # JWT_SECRET environment variable is required. Generate one with: openssl rand -base64 32
11+
- JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-2d}
1112
- CLIENT_PASSWORD=${CLIENT_PASSWORD:-admin}
1213
- CLIENT_USERNAME=${CLIENT_USERNAME:-admin}
1314
- ALLOW_INSECURE_AUTH_COOKIES=${ALLOW_INSECURE_AUTH_COOKIES:-false}

0 commit comments

Comments
 (0)