Skip to content

Commit 333f1cf

Browse files
feat: add api key auth for comment api endpoint (#103)
1 parent 2cd3494 commit 333f1cf

File tree

7 files changed

+162
-3
lines changed

7 files changed

+162
-3
lines changed

.env.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ LOGIN_USER=username
33
LOGIN_PASSWORD=strongpassword
44
DEFAULT_GITHUB_ORG=Git-Commit-Show
55
ONE_CLA_PER_ORG=true
6+
API_KEY=key_to_protect_endpoints_of_this_app
67
API_POST_GITHUB_COMMENT=http://localhost:3000/api/comment #Put your webhook proxy for PR/issue comment here
78
DOCS_AGENT_API_URL=docs_agent_api_base_url
89
DOCS_AGENT_API_KEY=docs_agent_api_key_here

app.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getWebsiteAddress,
1515
} from "./src/helpers.js";
1616
import DocsAgent from "./src/services/DocsAgent.js";
17+
import { validateApiKey } from "./src/auth.js";
1718

1819
try {
1920
const packageJson = await import("./package.json", {
@@ -261,6 +262,11 @@ const server = http
261262
githubWebhookRequestHandler(req, res);
262263
break;
263264
case "POST /api/comment":
265+
if (!validateApiKey(req)) {
266+
res.writeHead(401);
267+
res.write("API key required");
268+
return res.end();
269+
}
264270
routes.addCommentToGitHubIssueOrPR(req, res);
265271
break;
266272
case "GET /":

docs/api-reference.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,20 @@ The API is served from the configured domain (set via `WEBSITE_ADDRESS` environm
1010

1111
Most endpoints do not require authentication. However, some endpoints require specific authentication:
1212

13+
- **API endpoints**: Require API key authentication via request header (using `API_KEY` environment variable)
1314
- **Download endpoints**: Require username/password authentication via request body (using `LOGIN_USER` and `LOGIN_PASSWORD` environment variables)
1415
- **GitHub webhook endpoints**: Use GitHub webhook secret for verification
1516
- **GitHub API interactions**: Use GitHub App authentication
1617

18+
### API Key Authentication
19+
20+
For endpoints requiring API key authentication, include one of the following headers:
21+
22+
- `X-API-Key: <your-api-key>`
23+
- `Authorization: Bearer <your-api-key>`
24+
25+
The API key must match the value set in the `API_KEY` environment variable. If the API key is missing or invalid, the endpoint will return `401 Unauthorized`.
26+
1727
## Endpoints
1828

1929
### Webhook Endpoints
@@ -42,7 +52,7 @@ GitHub webhook endpoint for receiving GitHub events.
4252
- `issues.opened`: Adds welcome comment to new issues
4353
- `push`: Logs push events
4454

45-
**Note**: For "docs review" label, the app integrates with DocsAgent service if configured. This is limited to repositories specified in the `DOCS_REPOS` environment variable.
55+
**Note**: For "docs review" label, the app integrates with DocsAgent service if configured. This is limited to repositories specified in the `DOCS_REPOS` environment variable. The DocsAgent service uses the `API_POST_GITHUB_COMMENT` environment variable (or defaults to `WEBSITE_ADDRESS/api/comment`) to post review results back to GitHub.
4656

4757
---
4858

@@ -208,6 +218,8 @@ Adds a comment to a GitHub issue or pull request.
208218

209219
**Description**: Adds a comment to a specified GitHub issue or PR (used by external services like docs agent).
210220

221+
**Authentication**: Requires API key authentication via `X-API-Key` or `Authorization: Bearer <key>` header.
222+
211223
**Request Body** (JSON):
212224
```json
213225
{
@@ -221,12 +233,14 @@ Adds a comment to a GitHub issue or pull request.
221233
**Response**:
222234
- `200 OK`: "Comment added to GitHub issue or PR"
223235
- `400 Bad Request`: Missing required parameters
236+
- `401 Unauthorized`: API key missing or invalid
224237
- `500 Internal Server Error`: Failed to add comment
225238

226239
**Example**:
227240
```bash
228241
curl -X POST http://localhost:3000/api/comment \
229242
-H "Content-Type: application/json" \
243+
-H "X-API-Key: your-api-key" \
230244
-d '{"owner":"myorg","repo":"myrepo","issue_number":123,"result":"Review completed"}'
231245
```
232246

@@ -283,6 +297,9 @@ The following environment variables affect API behavior:
283297
- `WEBHOOK_SECRET`: GitHub webhook secret for verification
284298
- `ENTERPRISE_HOSTNAME`: GitHub Enterprise hostname (if applicable)
285299

300+
### API Authentication
301+
- `API_KEY`: API key for protecting API endpoints (e.g., `/api/comment`)
302+
286303
### Download Endpoint Authentication
287304
- `LOGIN_USER`: Username for download authentication
288305
- `LOGIN_PASSWORD`: Password for download authentication
@@ -297,6 +314,7 @@ The following environment variables affect API behavior:
297314
- `DOCS_AGENT_API_LINK_URL`: URL for docs linking endpoint
298315
- `DOCS_AGENT_API_TIMEOUT`: Timeout for DocsAgent API calls (default: 350000ms)
299316
- `DOCS_REPOS`: Comma-separated list of repositories eligible for docs review
317+
- `API_POST_GITHUB_COMMENT`: Webhook URL for DocsAgent to post result back to (defaults to `our WEBSITE_ADDRESS/api/comment` where we have configured github issue/pr comments). Allows use case to use a proxy url for this webhook url in staging server.
300318

301319
### Development & Deployment
302320
- `DEFAULT_GITHUB_ORG`: Default GitHub organization
@@ -305,4 +323,7 @@ The following environment variables affect API behavior:
305323
- `ONE_CLA_PER_ORG`: If "true", one CLA signature is valid for all repos in an org
306324
- `CODESANDBOX_HOST`: CodeSandbox host (for staging environments)
307325
- `HOSTNAME`: Hostname for the application
308-
- `SMEE_URL`: Smee proxy URL for local development
326+
- `SMEE_URL`: Smee proxy URL for local development
327+
328+
### Slack Integration
329+
- `SLACK_DEFAULT_MESSAGE_CHANNEL_WEBHOOK_URL`: Slack webhook URL for sending notifications when PRs are labeled with "product review"

src/auth.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,29 @@ export function isPasswordValid(username, password){
1414
}
1515
loginAttempts[username] = (loginAttempts[username] || 0) + 1;
1616
return false
17+
}
18+
19+
export function validateApiKey(req) {
20+
// Check if API_KEY environment variable is set
21+
if (!process.env.API_KEY) {
22+
console.error("API_KEY environment variable not configured");
23+
return false;
24+
}
25+
26+
// Check for X-API-Key header
27+
const apiKeyHeader = req.headers['x-api-key'];
28+
if (apiKeyHeader && apiKeyHeader === process.env.API_KEY) {
29+
return true;
30+
}
31+
32+
// Check for Authorization: Bearer <key> header
33+
const authHeader = req.headers['authorization'];
34+
if (authHeader && authHeader.startsWith('Bearer ')) {
35+
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
36+
if (token === process.env.API_KEY) {
37+
return true;
38+
}
39+
}
40+
41+
return false;
1742
}

src/services/DocsAgent.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
/**
2-
* Service for interacting with external APIs to get next actions
2+
* Service for interacting with docs agent external APIs to get next actions
3+
*
4+
* @example
5+
* const docsAgent = new DocsAgent();
6+
* const result = await docsAgent.reviewDocs("content", "filepath", { webhookUrl: "webhookUrl", webhookMetadata: { owner: "owner", repo: "repo", issue_number: "issue_number" } });
7+
* console.log(result);
38
*/
49
export class DocsAgent {
510
constructor() {

test/e2e/api-key-auth.test.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* Integration tests for API key authentication functionality.
3+
*/
4+
import { expect, use } from 'chai';
5+
import chaiHttp from 'chai-http';
6+
import { describe, it, before, after } from 'mocha';
7+
8+
const chai = use(chaiHttp);
9+
const SITE_URL = 'http://localhost:' + (process.env.PORT || 3000);
10+
11+
describe('API Key Authentication', function () {
12+
this.timeout(40000);
13+
let agent;
14+
15+
before(function () {
16+
agent = chai.request.agent(SITE_URL);
17+
// Not setting API key here because it's set in the lifecycle test
18+
});
19+
20+
after(function () {
21+
agent.close();
22+
});
23+
24+
describe('POST /api/comment endpoint', function () {
25+
const validPayload = {
26+
owner: 'test-org',
27+
repo: 'test-repo',
28+
issue_number: 123,
29+
result: 'Test comment'
30+
};
31+
32+
it('should return 401 when no API key is provided', async function () {
33+
const res = await agent
34+
.post('/api/comment')
35+
.send(validPayload);
36+
37+
expect(res).to.have.status(401);
38+
expect(res.text).to.equal('API key required');
39+
});
40+
41+
it('should return 401 when invalid X-API-Key is provided', async function () {
42+
const res = await agent
43+
.post('/api/comment')
44+
.set('X-API-Key', 'invalid-key')
45+
.send(validPayload);
46+
47+
expect(res).to.have.status(401);
48+
expect(res.text).to.equal('API key required');
49+
});
50+
51+
it('should return 401 when invalid Authorization Bearer is provided', async function () {
52+
const res = await agent
53+
.post('/api/comment')
54+
.set('Authorization', 'Bearer invalid-key')
55+
.send(validPayload);
56+
57+
expect(res).to.have.status(401);
58+
expect(res.text).to.equal('API key required');
59+
});
60+
61+
it('should accept valid X-API-Key header', async function () {
62+
const res = await agent
63+
.post('/api/comment')
64+
.set('X-API-Key', 'test-api-key')
65+
.send(validPayload);
66+
67+
// Should not return 401 (authentication passed)
68+
// Note: This might return 400/500 due to GitHub API calls in test environment
69+
// but the important thing is that it's not 401
70+
expect(res).to.not.have.status(401);
71+
});
72+
73+
it('should accept valid Authorization Bearer header', async function () {
74+
const res = await agent
75+
.post('/api/comment')
76+
.set('Authorization', 'Bearer test-api-key')
77+
.send(validPayload);
78+
79+
// Should not return 401 (authentication passed)
80+
// Note: This might return 400/500 due to GitHub API calls in test environment
81+
// but the important thing is that it's not 401
82+
expect(res).to.not.have.status(401);
83+
});
84+
85+
it('should prioritize X-API-Key over Authorization header when both are present', async function () {
86+
const res = await agent
87+
.post('/api/comment')
88+
.set('X-API-Key', 'test-api-key')
89+
.set('Authorization', 'Bearer invalid-key')
90+
.send(validPayload);
91+
92+
// Should not return 401 (X-API-Key takes precedence)
93+
expect(res).to.not.have.status(401);
94+
});
95+
});
96+
});

test/lifecycle.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ before(async function () {
1212

1313
if (process.env.RUN_E2E_TESTS === 'true') {
1414
try {
15+
16+
if (!process.env.API_KEY) {
17+
process.env.API_KEY = 'test-api-key';
18+
}
19+
1520
appProcess = spawn('node', ['app.js'], {
1621
env: { ...process.env, NODE_ENV: 'test' },
1722
stdio: 'pipe'

0 commit comments

Comments
 (0)