Add basic authentication endpoint#92
Conversation
WalkthroughThis update introduces a new endpoint Changes
Uplevel your code reviews with CodeRabbit ProCodeRabbit ProIf 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. |
There was a problem hiding this comment.
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
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
@coderabbitaiin 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: ignoreanywhere in the PR description to pause further reviews from the bot.
| 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', | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
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).
| 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 }; |
| 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() === '' |
There was a problem hiding this comment.
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;
- }| user, | ||
| password, |
| user: providedUser, | ||
| password: providedPassword, |
| 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; | ||
| } |
There was a problem hiding this comment.
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;
+ }| res.setHeader('WWW-Authenticate', 'Basic realm="Access to /basic-auth"'); | ||
| res.status(HttpStatusCodes.UNAUTHORIZED).json({ | ||
| authenticated: false, |
There was a problem hiding this comment.
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);
+ }
Summary by CodeRabbit
/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.