Skip to content

Commit d16a671

Browse files
authored
Merge pull request #3 from StackExchange/CSRF-JWT-TOKENS
Encrypting SO Tokens before setting them up as cookies
2 parents 024e14b + 70be6ae commit d16a671

File tree

15 files changed

+191
-216
lines changed

15 files changed

+191
-216
lines changed

app-config.yaml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,9 @@ organization:
88
stackoverflow:
99
baseUrl: ${STACK_OVERFLOW_INSTANCE_URL}
1010
# teamName: ${STACK_OVERFLOW_TEAM_NAME}
11-
apiAccessToken: ${STACK_OVERFLOW_ENTERPRISE_ACCESS_TOKEN}
11+
apiAccessToken: ${STACK_OVERFLOW_API_ACCESS_TOKEN}
1212
clientId: ${STACK_OVERFLOW_CLIENT_ID}
13-
redirectUri: http://redirectmeto.com/http://localhost:3000/stack-overflow-teams/
14-
schedule:
15-
{
16-
frequency: { minutes: 20 },
17-
timeout: { minutes: 5 },
18-
initialDelay: { seconds: 3 },
19-
}
13+
redirectUri: ${STACK_OVERFLOW_REDIRECT_URI}
2014

2115
backend:
2216
# Used for enabling authentication, secret is shared by all backend plugins

packages/app/src/components/search/SearchPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import {
1616
} from '@backstage/plugin-search-react';
1717
import { CatalogIcon, Content, Header, Page } from '@backstage/core-components';
1818
import { useApi } from '@backstage/core-plugin-api';
19-
import { StackOverflowIcon, StackOverflowSearchResultListItem } from 'backstage-plugin-stack-overflow-teams';
19+
import {
20+
StackOverflowIcon,
21+
StackOverflowSearchResultListItem,
22+
} from 'backstage-plugin-stack-overflow-teams';
2023

2124
const useStyles = makeStyles((theme: Theme) => ({
2225
bar: {

packages/backend/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ backend.add(import('@backstage/plugin-kubernetes-backend'));
5757

5858
// StackOverflow
5959

60-
backend.add(import('backstage-plugin-stack-overflow-teams-backend'));
60+
backend.add(import('backstage-plugin-stack-overflow-teams-backend')); // Teams Backend
61+
backend.add(import('backstage-stack-overflow-teams-collator')); // Optional questions collator
6162

62-
backend.add(import('backstage-stack-overflow-teams-collator'));
6363
backend.start();

plugins/search-backend-module-stack-overflow-teams-collator/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ To use any of the functionality this plugin provides, you need to start by confi
1313

1414
```yaml
1515
stackoverflow:
16-
baseUrl: https://stackoverflowteams.com # alternative: your Stack Overflow Enterprise site
16+
baseUrl: https://api.stackoverflowteams.com # alternative: your Stack Overflow Enterprise site
1717
teamName: $STACK_OVERFLOW_TEAM_NAME # optional if you are on Enterprise
1818
apiAccessToken: $STACK_OVERFLOW_API_ACCESS_TOKEN
1919
```

plugins/search-backend-module-stack-overflow-teams-collator/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "backstage-stack-overflow-teams-collator",
3-
"version": "1.0.0",
3+
"version": "1.0.1",
44
"description": "A module for the search backend that exports stack overflow for teams modules",
55
"backstage": {
66
"role": "backend-plugin-module",

plugins/search-backend-module-stack-overflow-teams-collator/src/collators/StackOverflowQuestionsCollatorFactory.ts

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,16 @@ export class StackOverflowQuestionsCollatorFactory
8080
private readonly apiAccessToken: string | undefined;
8181
private readonly teamName: string | undefined;
8282
private readonly logger: LoggerService;
83-
private readonly referrer: string = 'Backstage_Plugin'
83+
private readonly referrer: string = 'Backstage_Plugin';
8484
public readonly type: string = 'stack-overflow';
85+
public readonly stackOverflowTeamsAPI: string =
86+
'https://api.stackoverflowteams.com';
8587

86-
// Helper function to force API Version 3.
87-
private forceAPIv3(baseUrl: string) {
88-
return `${new URL(baseUrl).origin}/api/v3`;
89-
}
88+
private forceOriginUrl = (baseUrl: string): string =>
89+
`${new URL(baseUrl).origin}`;
9090

9191
private constructor(options: StackOverflowQuestionsCollatorFactoryOptions) {
92-
this.baseUrl = this.forceAPIv3(options.baseUrl)
92+
this.baseUrl = this.forceOriginUrl(options.baseUrl);
9393
this.apiAccessToken = options.apiAccessToken;
9494
this.teamName = options.teamName;
9595
this.logger = options.logger.child({ documentType: this.type });
@@ -103,14 +103,11 @@ export class StackOverflowQuestionsCollatorFactory
103103
};
104104
}
105105

106-
107106
static fromConfig(
108107
config: Config,
109108
options: StackOverflowQuestionsCollatorFactoryOptions,
110109
) {
111-
const apiAccessToken = config.getString(
112-
'stackoverflow.apiAccessToken',
113-
);
110+
const apiAccessToken = config.getString('stackoverflow.apiAccessToken');
114111
const teamName = config.getOptionalString('stackoverflow.teamName');
115112
const baseUrl = config.getString('stackoverflow.baseUrl');
116113
const requestParams = config
@@ -122,7 +119,7 @@ export class StackOverflowQuestionsCollatorFactory
122119
apiAccessToken,
123120
teamName,
124121
requestParams,
125-
...options
122+
...options,
126123
});
127124
}
128125

@@ -149,25 +146,31 @@ export class StackOverflowQuestionsCollatorFactory
149146
let requestUrl;
150147

151148
if (this.teamName) {
152-
requestUrl = `${this.baseUrl}/teams/${this.teamName}/questions${params}`;
149+
const basePath =
150+
this.baseUrl === this.stackOverflowTeamsAPI ? '/v3' : '/api/v3';
151+
requestUrl = `${this.baseUrl}${basePath}/teams/${this.teamName}/questions${params}`;
153152
} else {
154-
requestUrl = `${this.baseUrl}/questions${params}`;
153+
requestUrl = `${this.baseUrl}/api/v3/questions${params}`;
155154
}
156155

157156
let page = 1;
158-
let totalPages = 1
159-
const pageSize = this.requestParams.pageSize || 50
160-
this.logger.warn('Starting collating Stack Overflow for Teams questions, wait for the success message')
157+
let totalPages = 1;
158+
const pageSize = this.requestParams.pageSize || 50;
159+
this.logger.warn(
160+
'Starting collating Stack Overflow for Teams questions, wait for the success message',
161+
);
161162
while (page <= totalPages) {
162-
const res = await fetch(`${requestUrl}&page=${page}&pageSize=${pageSize}`, {
163-
headers: {
164-
Authorization: `Bearer ${this.apiAccessToken}`,
163+
const res = await fetch(
164+
`${requestUrl}&page=${page}&pageSize=${pageSize}`,
165+
{
166+
headers: {
167+
Authorization: `Bearer ${this.apiAccessToken}`,
168+
},
165169
},
166-
}
167-
);
170+
);
168171

169172
const data = await res.json();
170-
totalPages = data.totalPages
173+
totalPages = data.totalPages;
171174

172175
for (const question of data.items ?? []) {
173176
const tags =
@@ -192,11 +195,11 @@ export class StackOverflowQuestionsCollatorFactory
192195
viewCount: question.viewCount,
193196
isAnswered: question.isAnswered,
194197
bounty: question.bounty,
195-
creationDate: question.creationDate,
196-
lastActivityDate: question.lastActivityDate,
198+
creationDate: question.creationDate,
199+
lastActivityDate: question.lastActivityDate,
197200
};
198201
}
199-
page++
202+
page++;
200203
}
201204
}
202205
}

plugins/stack-overflow-teams-backend/README.md

Lines changed: 14 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,109 +6,56 @@ Backend counterpart of the Stack Overflow for Teams Plugin.
66

77
The **Stack Overflow for Teams Backend plugin** is responsible for:
88

9-
10-
11-
- **Indexing all questions** from the private Stack Overflow instance (an enhanced version of the existing community plugins in the Backstage repository).
12-
13-
- **Handling API requests** via ``createStackOverflowApi`` and ``createStackOverflowService`` to the Stack Overflow instance for retrieving:
14-
15-
- `/users`
16-
17-
- `/tags`
18-
19-
- `/questions`
20-
21-
- Posting new questions via `/questions`
22-
23-
- **Managing OAuth authentication flow** to securely access Stack Overflow private instances via ``createStackOverflowAuth``
24-
25-
9+
- **Indexing all questions** from the private Stack Overflow instance (an enhanced version of the existing community plugins in the Backstage repository).
10+
- **Handling API requests** via ``createStackOverflowApi`` and ``createStackOverflowService`` to the Stack Overflow instance for retrieving:
11+
- `/users`
12+
- `/tags`
13+
- `/questions`
14+
- Posting new questions via `/questions`
15+
- **Managing OAuth authentication flow** to securely access Stack Overflow private instances via ``createStackOverflowAuth``
16+
- **Encrypts** the Stack Overflow Token before sending it as an http-only cookie to the frontend.
2617

2718
## OAuth Authentication Flow
2819

29-
30-
31-
The backend is the only component that directly utilizes **Stack Overflow access tokens** for requests.
32-
33-
20+
The backend is the only component that directly utilizes the **encrypted Stack Overflow access tokens** for requests.
3421

3522
### **Authorization Flow Details**
3623

37-
38-
39-
![image](https://github.com/user-attachments/assets/1a7df089-c3c6-49a4-8761-38479e89214a)
40-
41-
42-
4324
#### **`/auth/start`**
4425

4526
- Generates **PKCE Code Verifier**.
46-
4727
- Hashes Code Verifier to obtain **Code Challenge**.
48-
4928
- Generates a **state** (random string).
50-
5129
- Stores **Code Verifier** and **State** in a **secure, HTTP-only cookie** accessible only to the server.
5230

53-
54-
5531
#### **`/callback`**
5632

5733
- Retrieves the stored **Code Verifier** and **State**.
58-
5934
- Validates that the received **state** matches the one from Stack Overflow's query string parameter.
60-
6135
- The backend requests an **Access Token** using the stored **Code Verifier**.
62-
63-
- Stores the **Stack Overflow Access Token** in a **secure, HTTP-only cookie**.
64-
65-
66-
36+
- Backend **encrypts the token**, using the JWT secret stored in memory.
37+
- Stores the **encrypted Stack Overflow Access Token** in a **secure, HTTP-only cookie**.
6738

6839
## Installation
6940

70-
71-
7241
This plugin is installed via the `backstage-plugin-stack-overflow-teams-backend` package. To install it to your backend package, run the following command:
7342

74-
75-
7643
```bash
77-
78-
# From your root directory
79-
80-
yarn --cwd packages/backend add backstage-plugin-stack-overflow-teams-backend
81-
44+
yarn --cwd packages/backend add backstage-plugin-stack-overflow-teams-backend
8245
```
8346

84-
85-
8647
Then add the plugin to your backend in `packages/backend/src/index.ts`:
8748

88-
89-
9049
```ts
91-
92-
const backend = createBackend();
50+
const backend = createBackend();
9351

9452
// ...
9553

9654
backend.add(import('backstage-plugin-stack-overflow-teams-backend'));
97-
9855
```
9956

100-
101-
10257
## Development
10358

104-
105-
106-
This plugin backend can be started in a standalone mode from directly in this
107-
108-
package with `yarn start`. It is a limited setup that is most convenient when
109-
110-
developing the plugin backend itself.
111-
112-
59+
This plugin backend can be started in a standalone mode from directly in this package with `yarn start`. It is a limited setup that is most convenient when developing the plugin backend itself.
11360

11461
If you want to run the entire project, including the frontend, run `yarn dev` from the root directory.

plugins/stack-overflow-teams-backend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "backstage-plugin-stack-overflow-teams-backend",
3-
"version": "1.0.1",
3+
"version": "1.0.3",
44
"main": "src/index.ts",
55
"types": "src/index.ts",
66
"license": "Apache-2.0",
@@ -42,13 +42,15 @@
4242
"@backstage/plugin-search-common": "^1.2.17",
4343
"express": "^4.17.1",
4444
"express-promise-router": "^4.1.0",
45+
"jsonwebtoken": "^9.0.2",
4546
"qs": "^6.14.0",
4647
"zod": "^3.22.4"
4748
},
4849
"devDependencies": {
4950
"@backstage/backend-test-utils": "^1.2.0",
5051
"@backstage/cli": "^0.29.4",
5152
"@types/express": "^4.17.6",
53+
"@types/jsonwebtoken": "^9",
5254
"@types/qs": "^6",
5355
"@types/supertest": "^2.0.12",
5456
"msw": "^2.7.3",

plugins/stack-overflow-teams-backend/src/plugin.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
coreServices,
33
createBackendPlugin,
44
} from '@backstage/backend-plugin-api';
5+
import { randomBytes } from 'crypto'
56
import { createRouter } from './router';
67
import { createStackOverflowService } from './services/StackOverflowService';
78
import { StackOverflowConfig } from './services/StackOverflowService';
@@ -11,6 +12,9 @@ import { StackOverflowConfig } from './services/StackOverflowService';
1112
*
1213
* @public
1314
*/
15+
16+
const JWT_SECRET = randomBytes(64).toString('hex')
17+
1418
export const stackOverflowTeamsPlugin = createBackendPlugin({
1519
pluginId: 'stack-overflow-teams',
1620
register(env) {
@@ -38,6 +42,7 @@ export const stackOverflowTeamsPlugin = createBackendPlugin({
3842
stackOverflowConfig,
3943
logger,
4044
stackOverflowService,
45+
jwtSecret: JWT_SECRET
4146
}),
4247
);
4348
},

0 commit comments

Comments
 (0)