Skip to content

Add basic authentication endpoint#92

Open
ryo8000 wants to merge 1 commit into
mainfrom
feature/basic-auth
Open

Add basic authentication endpoint#92
ryo8000 wants to merge 1 commit into
mainfrom
feature/basic-auth

Conversation

@ryo8000

@ryo8000 ryo8000 commented Jul 2, 2025

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Feature: Introduced a new endpoint /basic-auth/ for testing HTTP Basic Authentication. This feature allows users to validate their credentials either through query parameters or the Basic Auth header, enhancing the security testing capabilities of our application.
  • Documentation: Updated the API reference table in the README.md file to include details about the new endpoint and its required parameters, improving the comprehensibility of our API documentation.

@github-actions

github-actions Bot commented Jul 2, 2025

Copy link
Copy Markdown

Image description CodeRabbit

Walkthrough

This update introduces a new endpoint /basic-auth/ for testing HTTP Basic Authentication. It validates user credentials either from query parameters or the Basic Auth header, returning appropriate responses. The API reference table in README.md has been updated to reflect this change.

Changes

File Summary
README.md Updated with new endpoint and parameters information
src/app.ts Added route for basic authentication
src/routes/basic-auth.ts Implemented basic authentication logic

🎉🐇

"In the land of code, where logic intertwines,
A new path emerges, through lines and lines.
With user and password, secrets we keep,
In the realm of data, where bytes run deep.
Celebrate, dear friends, for our code takes flight,
Into realms of security, shining bright!" 🌟


Uplevel your code reviews with CodeRabbit Pro

CodeRabbit Pro

If you like this project, please support us by purchasing the Pro version. The Pro version has advanced context, superior noise reduction and several proprietary improvements compared to the open source version. Moreover, CodeRabbit Pro is free for open source projects.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image description CodeRabbit

Commits Files that changed from the base of the PR and between dce2718 and 3ced2bc commits.
Files selected (4)
  • README.md (1)
  • src/app.ts (2)
  • src/routes/basic-auth.ts (1)
  • tests/ut/routes/basic-auth.test.ts (1)
Files ignored due to filter (1)
  • tests/api/api-test-collection.json
Files not summarized due to errors (1)
  • tests/ut/routes/basic-auth.test.ts (diff tokens exceeds limit)
Review comments generated (10)
  • Review: 7
  • LGTM: 3

Tips

Chat with Image description CodeRabbit Bot (@coderabbitai)

  • Reply on review comments left by this bot to ask follow-up questions. A review comment is a comment on a diff or a file.
  • Invite the bot into a review comment chain by tagging @coderabbitai in a reply.

Code suggestions

  • The bot may make code suggestions, but please review them carefully before committing since the line number ranges may be misaligned.
  • You can edit the comment made by the bot and manually tweak the suggestion if it is slightly off.

Pausing incremental reviews

  • Add @coderabbitai: ignore anywhere in the PR description to pause further reviews from the bot.

Comment on lines +1 to +352
import express from 'express';
import request from 'supertest';

describe('basicAuthRouter', () => {
let app: express.Express;

const createApp = async (): Promise<express.Express> => {
const app = express();
app.use(express.json());

const { basicAuthRouter } = await import('../../../src/routes/basic-auth.js');
app.use('/basic-auth', basicAuthRouter);

return app;
};

beforeEach(async () => {
app = await createApp();
});

const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'head'] as const;

describe.each(httpMethods)('%s method', (method) => {
describe('when credentials match', () => {
it('should return 200 with authentication success for matching user/password', async () => {
const response = await request(app)
[method]('/basic-auth?user=testuser&password=testpass')
.auth('testuser', 'testpass');

expect(response.status).toBe(200);
if (method !== 'head') {
expect(response.body).toEqual({
authenticated: true,
user: 'testuser',
password: 'testpass',
message: 'Authentication successful',
});
}
});

it('should authenticate when both parameters are the same complex string', async () => {
const credentials = 'complex-P@ssw0rd!123';
const response = await request(app)
[method](
`/basic-auth?user=${encodeURIComponent(credentials)}&password=${encodeURIComponent(credentials)}`
)
.auth(credentials, credentials);

expect(response.status).toBe(200);
if (method !== 'head') {
expect(response.body).toEqual({
authenticated: true,
user: credentials,
password: credentials,
message: 'Authentication successful',
});
}
});

it('should authenticate with special characters', async () => {
const credentials = 'test@example.com';
const response = await request(app)
[method](
`/basic-auth?user=${encodeURIComponent(credentials)}&password=${encodeURIComponent(credentials)}`
)
.auth(credentials, credentials);

expect(response.status).toBe(200);
if (method !== 'head') {
expect(response.body).toEqual({
authenticated: true,
user: credentials,
password: credentials,
message: 'Authentication successful',
});
}
});
});

describe('when credentials do not match', () => {
it('should return 401 when user and password are different', async () => {
const response = await request(app)
[method]('/basic-auth?user=expecteduser&password=expectedpass')
.auth('provideduser', 'providedpass');

expect(response.status).toBe(401);
if (method !== 'head') {
expect(response.body).toEqual({
authenticated: false,
message: 'Authentication failed',
user: 'provideduser',
password: 'providedpass',
});
}
});

it('should return 401 for case-sensitive mismatch', async () => {
const response = await request(app)
[method]('/basic-auth?user=TestUser&password=TestPass')
.auth('testuser', 'testpass');

expect(response.status).toBe(401);
if (method !== 'head') {
expect(response.body).toEqual({
authenticated: false,
message: 'Authentication failed',
user: 'testuser',
password: 'testpass',
});
}
});
});

describe('when credentials are missing or invalid', () => {
it('should return 400 when user parameter is missing', async () => {
const response = await request(app)[method]('/basic-auth?password=testpass');

expect(response.status).toBe(400);
if (method !== 'head') {
expect(response.body).toEqual({
error: {
message: 'Missing user or password query parameter',
},
});
}
});

it('should return 400 when password parameter is missing', async () => {
const response = await request(app)[method]('/basic-auth?user=testuser');

expect(response.status).toBe(400);
if (method !== 'head') {
expect(response.body).toEqual({
error: {
message: 'Missing user or password query parameter',
},
});
}
});

it('should return 400 when both parameters are missing', async () => {
const response = await request(app)[method]('/basic-auth');

expect(response.status).toBe(400);
if (method !== 'head') {
expect(response.body).toEqual({
error: {
message: 'Missing user or password query parameter',
},
});
}
});

it('should return 400 when user parameter is empty', async () => {
const response = await request(app)[method]('/basic-auth?user=&password=testpass');

expect(response.status).toBe(400);
if (method !== 'head') {
expect(response.body).toEqual({
error: {
message: 'Missing user or password query parameter',
},
});
}
});

it('should return 400 when password parameter is empty', async () => {
const response = await request(app)[method]('/basic-auth?user=testuser&password=');

expect(response.status).toBe(400);
if (method !== 'head') {
expect(response.body).toEqual({
error: {
message: 'Missing user or password query parameter',
},
});
}
});

it('should return 400 when user parameter is only whitespace', async () => {
const response = await request(app)[method]('/basic-auth?user=%20%20&password=testpass');

expect(response.status).toBe(400);
if (method !== 'head') {
expect(response.body).toEqual({
error: {
message: 'Missing user or password query parameter',
},
});
}
});

it('should return 400 when password parameter is only whitespace', async () => {
const response = await request(app)[method]('/basic-auth?user=testuser&password=%20%20');

expect(response.status).toBe(400);
if (method !== 'head') {
expect(response.body).toEqual({
error: {
message: 'Missing user or password query parameter',
},
});
}
});

it('should return 401 when Authorization header is missing', async () => {
const response = await request(app)[method]('/basic-auth?user=testuser&password=testpass');

expect(response.status).toBe(401);
expect(response.headers['www-authenticate']).toBe('Basic realm="Access to /basic-auth"');
if (method !== 'head') {
expect(response.body).toEqual({
authenticated: false,
message: 'Authentication required',
});
}
});

it('should return 401 when Authorization header is malformed', async () => {
const response = await request(app)
[method]('/basic-auth?user=testuser&password=testpass')
.set('Authorization', 'Bearer token123');

expect(response.status).toBe(401);
expect(response.headers['www-authenticate']).toBe('Basic realm="Access to /basic-auth"');
if (method !== 'head') {
expect(response.body).toEqual({
authenticated: false,
message: 'Authentication required',
});
}
});
});

describe('edge cases', () => {
it('should handle numeric credentials when they match', async () => {
const response = await request(app)
[method]('/basic-auth?user=12345&password=67890')
.auth('12345', '67890');

expect(response.status).toBe(200);
if (method !== 'head') {
expect(response.body).toEqual({
authenticated: true,
user: '12345',
password: '67890',
message: 'Authentication successful',
});
}
});

it('should handle single character credentials', async () => {
const response = await request(app)[method]('/basic-auth?user=a&password=a').auth('a', 'a');

expect(response.status).toBe(200);
if (method !== 'head') {
expect(response.body).toEqual({
authenticated: true,
user: 'a',
password: 'a',
message: 'Authentication successful',
});
}
});

it('should handle unicode characters', async () => {
const credentials = '测试用户';
const encoded = encodeURIComponent(credentials);
const response = await request(app)
[method](`/basic-auth?user=${encoded}&password=${encoded}`)
.auth(credentials, credentials);

expect(response.status).toBe(200);
if (method !== 'head') {
expect(response.body).toEqual({
authenticated: true,
user: credentials,
password: credentials,
message: 'Authentication successful',
});
}
});
});
});

describe('URL encoding', () => {
it('should handle URL encoded special characters in credentials', async () => {
const user = 'user+with spaces@domain.com';
const password = 'user+with spaces@domain.com';
const response = await request(app)
.get(
`/basic-auth?user=${encodeURIComponent(user)}&password=${encodeURIComponent(password)}`
)
.auth(user, password);

expect(response.status).toBe(200);
expect(response.body).toEqual({
authenticated: true,
user: user,
password: password,
message: 'Authentication successful',
});
});

it('should fail authentication when encoding differs', async () => {
const response = await request(app)
.get('/basic-auth?user=test%2Buser&password=test+pass')
.auth('test%2Buser', 'test+pass');

expect(response.status).toBe(401);
expect(response.body).toEqual({
authenticated: false,
message: 'Authentication failed',
user: 'test%2Buser',
password: 'test+pass',
});
});
});

describe('with request body (POST method)', () => {
it('should authenticate successfully with POST and body data', async () => {
const response = await request(app)
.post('/basic-auth?user=testuser&password=testpass')
.auth('testuser', 'testpass')
.send({ someData: 'ignored' });

expect(response.status).toBe(200);
expect(response.body).toEqual({
authenticated: true,
user: 'testuser',
password: 'testpass',
message: 'Authentication successful',
});
});
});

describe('query parameter handling', () => {
it('should properly handle additional parameters without affecting authentication', async () => {
const response = await request(app)
.get('/basic-auth?user=test&password=test&additionalParam=value&anotherParam=123')
.auth('test', 'test');

expect(response.status).toBe(200);
expect(response.body).toEqual({
authenticated: true,
user: 'test',
password: 'test',
message: 'Authentication successful',
});
});
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image description CodeRabbit

The test cases are comprehensive and cover a wide range of scenarios including edge cases, different HTTP methods, URL encoding, request body handling, and query parameter handling. The tests also check for both successful and unsuccessful authentication attempts. However, the password is being returned in the response body upon successful authentication which is not a good practice from a security standpoint. Passwords should never be included in the response. This needs to be addressed in the application code rather than the test code.

-            password: 'testpass',

This change should be applied to all instances where the password is included in the response body (lines 35, 54, 73, 92, 108, 246, 260, 278, 300, 315, 331).

Comment thread src/routes/basic-auth.ts
Comment on lines +1 to +58
import { Router } from 'express';
import { HttpStatusCodes } from '../utils/http.js';

const basicAuthRouter = Router();

basicAuthRouter.all('/', (req, res) => {
const { user, password } = req.query;

// Validate that both user and password are provided and non-empty
if (
!user ||
!password ||
typeof user !== 'string' ||
typeof password !== 'string' ||
user.trim() === '' ||
password.trim() === ''
) {
res.status(HttpStatusCodes.BAD_REQUEST).json({
error: {
message: 'Missing user or password query parameter',
},
});
return;
}

const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
res.status(HttpStatusCodes.UNAUTHORIZED).json({
authenticated: false,
message: 'Authentication required',
});
return;
}

const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials as string, 'base64').toString('utf-8');
const [providedUser, providedPassword] = credentials.split(':');

if (providedUser === user && providedPassword === password) {
res.status(HttpStatusCodes.OK).json({
authenticated: true,
message: 'Authentication successful',
user,
password,
});
} else {
res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
res.status(HttpStatusCodes.UNAUTHORIZED).json({
authenticated: false,
message: 'Authentication failed',
user: providedUser,
password: providedPassword,
});
}
});

export { basicAuthRouter };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image description CodeRabbit

The code is generally well-written, but there are a few areas that could be improved for better security and maintainability.

Comment thread src/routes/basic-auth.ts
Comment on lines +7 to +16
const { user, password } = req.query;

// Validate that both user and password are provided and non-empty
if (
!user ||
!password ||
typeof user !== 'string' ||
typeof password !== 'string' ||
user.trim() === '' ||
password.trim() === ''

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image description CodeRabbit

The user and password are being extracted directly from the query parameters. This is not secure as it exposes sensitive information in the URL. It's recommended to use HTTP Basic Authentication which sends credentials in the Authorization header encoded with Base64. The existing code already handles this case starting from line 26, so the extraction of credentials from query parameters can be removed.

-  const { user, password } = req.query;
-
-  // Validate that both user and password are provided and non-empty
-  if (
-    !user ||
-    !password ||
-    typeof user !== 'string' ||
-    typeof password !== 'string' ||
-    user.trim() === '' ||
-    password.trim() === ''
-  ) {
-    res.status(HttpStatusCodes.BAD_REQUEST).json({
-      error: {
-        message: 'Missing user or password query parameter',
-      },
-    });
-    return;
-  }

Comment thread src/routes/basic-auth.ts
Comment on lines +44 to +45
user,
password,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image description CodeRabbit

Returning the user's password in the response is a security risk, even if it's successful authentication. Passwords should never be returned in responses. Remove the password from the response.

-       user,
-       password,

Comment thread src/routes/basic-auth.ts
Comment on lines +52 to +53
user: providedUser,
password: providedPassword,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image description CodeRabbit

Similarly, do not return the provided password when authentication fails.

-       user: providedUser,
-       password: providedPassword,

Comment thread src/routes/basic-auth.ts
Comment on lines +26 to +34
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
res.status(HttpStatusCodes.UNAUTHORIZED).json({
authenticated: false,
message: 'Authentication required',
});
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image description CodeRabbit

This block of code is repeated again in lines 48-50. To follow the DRY principle (Don't Repeat Yourself), you could extract this into a separate function.

+ const sendUnauthorizedResponse = (res) => {
+   res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
+   res.status(HttpStatusCodes.UNAUTHORIZED).json({
+     authenticated: false,
+     message: 'Authentication required',
+   });
+ }

-   if (!authHeader || !authHeader.startsWith('Basic ')) {
-     res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
-     res.status(HttpStatusCodes.UNAUTHORIZED).json({
-       authenticated: false,
-       message: 'Authentication required',
-     });
-     return;
-   }
+   if (!authHeader || !authHeader.startsWith('Basic ')) {
+     sendUnauthorizedResponse(res);
+     return;
+   }

Comment thread src/routes/basic-auth.ts
Comment on lines +48 to +50
res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
res.status(HttpStatusCodes.UNAUTHORIZED).json({
authenticated: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Image description CodeRabbit

Use the sendUnauthorizedResponse function here as well.

-   } else {
-     res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"');
-     res.status(HttpStatusCodes.UNAUTHORIZED).json({
-       authenticated: false,
-       message: 'Authentication failed',
-       user: providedUser,
-       password: providedPassword,
-     });
-   }
+   } else {
+     sendUnauthorizedResponse(res);
+   }

@ryo8000 ryo8000 closed this Jul 12, 2025
@ryo8000 ryo8000 reopened this Jul 12, 2025
@ryo8000 ryo8000 added enhancement New feature or request experiment Experiment labels Aug 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request experiment Experiment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant