Skip to content
Open
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ PLUGINS_DIR=./data/plugins # Plugin directory
# Master API key (leave empty to disable, or set to secure value)
API_MASTER_KEY=

# First-boot default admin key: always random unless this is set (local dev only)
# ALLOW_DEV_API_KEY=true

# =============================================================================
# DEVELOPER SETTINGS
# =============================================================================
Expand Down
1 change: 1 addition & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ services:
- '127.0.0.1:2785:2785'
environment:
- NODE_ENV=development
- ALLOW_DEV_API_KEY=true
- PORT=2785
- DATABASE_TYPE=sqlite
- DATABASE_NAME=/app/data/openwa.sqlite
Expand Down
4 changes: 2 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ Access:

### API Key

OpenWA seeds a default API key on first run and writes it to:
OpenWA seeds a random default admin API key on first run and writes it to `data/.api-key`. The key is printed in the server startup log.

- `data/.api-key` (development)
For local development only, you may set `ALLOW_DEV_API_KEY=true` to use the predictable key `dev-admin-key` (never enable this in production or shared environments).

You can also create new keys via the API (see [API Specification](./06-api-specification.md)).

Expand Down
10 changes: 10 additions & 0 deletions src/modules/auth/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,16 @@ describe('AuthService', () => {
expect(result.id).toBe(key.id);
});

it('should reject when IP whitelist is set but client IP is unknown', async () => {
const key = createMockApiKey({
allowedIps: ['10.0.0.1'],
Copy link
Copy Markdown

@guerralindao guerralindao May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if we could configure it via the .env file (allowedIps). Have you already thought about this, or is the configuration done via the web interface ?

keyHash: hashKey('ip-no-client'),
});
(repository.findOne as jest.Mock).mockResolvedValue(key);

await expect(service.validateApiKey('ip-no-client')).rejects.toThrow('Client IP could not be determined');
});

it('should throw UnauthorizedException when session not in allowedSessions', async () => {
const key = createMockApiKey({
allowedSessions: ['session-A'],
Expand Down
13 changes: 9 additions & 4 deletions src/modules/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ export class AuthService implements OnModuleInit {
let isNewKey = false;

if (count === 0) {
// Use predictable key in development, random key in production
// Random by default; predictable key only when explicitly opted in for local dev
displayKey =
process.env.NODE_ENV === 'production' ? `owa_k1_${randomBytes(32).toString('hex')}` : 'dev-admin-key';
process.env.ALLOW_DEV_API_KEY === 'true'
? 'dev-admin-key'
: `owa_k1_${randomBytes(32).toString('hex')}`;

await this.seedApiKey(displayKey, 'Default Admin Key', ApiKeyRole.ADMIN);
isNewKey = true;
Expand Down Expand Up @@ -173,8 +175,11 @@ export class AuthService implements OnModuleInit {
throw new UnauthorizedException('API key has expired');
}

// Check IP whitelist
if (apiKey.allowedIps && apiKey.allowedIps.length > 0 && clientIp) {
// Check IP whitelist (fail closed when configured but client IP is unknown)
if (apiKey.allowedIps && apiKey.allowedIps.length > 0) {
if (!clientIp) {
throw new UnauthorizedException('Client IP could not be determined');
}
if (!this.isIpAllowed(clientIp, apiKey.allowedIps)) {
this.logger.warn(`IP not allowed: ${clientIp}`, {
keyId: apiKey.id,
Expand Down
1 change: 1 addition & 0 deletions src/modules/session/session.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export class SessionController {
}

@Get(':id/qr')
@RequireRole(ApiKeyRole.OPERATOR)
@ApiOperation({ summary: 'Get QR code for session authentication' })
@ApiParam({ name: 'id', description: 'Session ID' })
@ApiResponse({
Expand Down