Skip to content

Commit f41a93f

Browse files
authored
Use public key to validate access to the cloud templates (#1619)
Fixes OPS-2983.
1 parent 5a8c407 commit f41a93f

11 files changed

Lines changed: 125 additions & 195 deletions

File tree

packages/react-ui/src/app/routes/cloud-connection/cloud-connection-page.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,14 @@ const CloudConnectionPage = () => {
3333
if (!flags || isLoading) {
3434
return;
3535
}
36-
const { FRONTEGG_URL, FRONTEGG_CLIENT_ID, FRONTEGG_APP_ID } = flags;
36+
const { FRONTEGG_URL } = flags;
3737

38-
if (!FRONTEGG_URL || !FRONTEGG_CLIENT_ID || !FRONTEGG_APP_ID) {
38+
if (!FRONTEGG_URL) {
3939
navigate('/');
4040
return;
4141
}
4242

43-
const app = initializeFrontegg(
44-
FRONTEGG_URL as string,
45-
FRONTEGG_CLIENT_ID as string,
46-
FRONTEGG_APP_ID as string,
47-
);
43+
const app = initializeFrontegg(FRONTEGG_URL as string);
4844

4945
app.ready(() => {
5046
app.store.subscribe(() => {

packages/react-ui/src/app/routes/cloud-connection/cloud-logout-page.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,14 @@ const CloudLogoutPage = () => {
3030
if (!flags || isLoading) {
3131
return;
3232
}
33-
const { FRONTEGG_URL, FRONTEGG_CLIENT_ID, FRONTEGG_APP_ID } = flags;
33+
const { FRONTEGG_URL } = flags;
3434

35-
if (!FRONTEGG_URL || !FRONTEGG_CLIENT_ID || !FRONTEGG_APP_ID) {
35+
if (!FRONTEGG_URL) {
3636
navigate('/');
3737
return;
3838
}
3939

40-
const app = initializeFrontegg(
41-
FRONTEGG_URL as string,
42-
FRONTEGG_CLIENT_ID as string,
43-
FRONTEGG_APP_ID as string,
44-
);
40+
const app = initializeFrontegg(FRONTEGG_URL as string);
4541

4642
Cookies.remove('cloud-token');
4743
Cookies.remove('cloud-refresh-token');
Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,21 @@
1-
import { initialize } from '@frontegg/js';
1+
import { FronteggApp, initialize } from '@frontegg/js';
22

33
export const additionalFronteggParams = {
44
// Google OAuth 2.0: Prompt the user to select an account. https://developers.google.com/identity/protocols/oauth2/web-server#creatingclient
55
prompt: 'select_account',
66
};
77

8-
export const initializeFrontegg = (
9-
FRONTEGG_URL: string,
10-
FRONTEGG_CLIENT_ID: string,
11-
FRONTEGG_APP_ID: string,
12-
) =>
13-
initialize({
8+
export function initializeFrontegg(url: string, tenant?: string): FronteggApp {
9+
const tenantResolver = tenant ? () => ({ tenant }) : undefined;
10+
11+
return initialize({
1412
contextOptions: {
15-
baseUrl: FRONTEGG_URL as string,
16-
clientId: FRONTEGG_CLIENT_ID as string,
17-
appId: FRONTEGG_APP_ID as string,
13+
baseUrl: url,
14+
tenantResolver,
1815
},
1916
authOptions: {
2017
keepSessionAlive: true,
2118
},
2219
hostedLoginBox: true,
2320
});
21+
}

packages/server/api/src/app/flags/flag.service.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -251,18 +251,6 @@ export const flagService = {
251251
created,
252252
updated,
253253
},
254-
{
255-
id: FlagId.FRONTEGG_CLIENT_ID,
256-
value: system.get(AppSystemProp.FRONTEGG_CLIENT_ID),
257-
created,
258-
updated,
259-
},
260-
{
261-
id: FlagId.FRONTEGG_APP_ID,
262-
value: system.get(AppSystemProp.FRONTEGG_APP_ID),
263-
created,
264-
updated,
265-
},
266254
{
267255
id: FlagId.CLOUD_CONNECTION_PAGE_ENABLED,
268256
value: system.getBoolean(AppSystemProp.CLOUD_CONNECTION_PAGE_ENABLED),

packages/server/api/src/app/flow-template/cloud-template.controller.ts

Lines changed: 14 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,29 @@ import {
22
FastifyPluginAsyncTypebox,
33
Type,
44
} from '@fastify/type-provider-typebox';
5-
import { IdentityClient } from '@frontegg/client';
65
import { AppSystemProp, logger, system } from '@openops/server-shared';
76
import { ALL_PRINCIPAL_TYPES, OpenOpsId } from '@openops/shared';
8-
import { getCloudToken, getCloudUser } from '../user-info/cloud-auth';
7+
import { allowAllOriginsHookHandler } from '../helper/allow-all-origins-hook-handler';
8+
import { getVerifiedUser } from '../user-info/cloud-auth';
99
import { flowTemplateService } from './flow-template.service';
1010

1111
export const cloudTemplateController: FastifyPluginAsyncTypebox = async (
1212
app,
1313
) => {
14-
const fronteggClientId = system.get(AppSystemProp.FRONTEGG_CLIENT_ID);
15-
const fronteggApiKey = system.get(AppSystemProp.FRONTEGG_API_KEY);
14+
const publicKey = system.get(AppSystemProp.FRONTEGG_PUBLIC_KEY);
15+
const connectionPageEnabled = system.getBoolean(
16+
AppSystemProp.CLOUD_CONNECTION_PAGE_ENABLED,
17+
);
1618

17-
if (!fronteggClientId || !fronteggApiKey) {
19+
if (!publicKey || !connectionPageEnabled) {
1820
logger.info(
1921
'Missing Frontegg configuration, disabling cloud templates API',
2022
);
2123
return;
2224
}
2325

24-
const identityClient = new IdentityClient({
25-
FRONTEGG_CLIENT_ID: fronteggClientId,
26-
FRONTEGG_API_KEY: fronteggApiKey,
27-
});
28-
2926
// cloud templates are available on any origin
30-
app.addHook('onSend', (request, reply, payload, done) => {
31-
void reply.header(
32-
'Access-Control-Allow-Origin',
33-
request.headers.origin || request.headers['Ops-Origin'] || '*',
34-
);
35-
void reply.header('Access-Control-Allow-Methods', 'GET,OPTIONS');
36-
void reply.header(
37-
'Access-Control-Allow-Headers',
38-
'Content-Type,Ops-Origin,Authorization',
39-
);
40-
void reply.header('Access-Control-Allow-Credentials', 'true');
41-
42-
if (request.method === 'OPTIONS') {
43-
return reply.status(204).send();
44-
}
45-
46-
done(null, payload);
47-
return;
48-
});
27+
app.addHook('onSend', allowAllOriginsHookHandler);
4928

5029
app.get(
5130
'/',
@@ -70,23 +49,7 @@ export const cloudTemplateController: FastifyPluginAsyncTypebox = async (
7049
},
7150
},
7251
async (request) => {
73-
const token = getCloudToken(request);
74-
75-
if (!(await getCloudUser(identityClient, token))) {
76-
return flowTemplateService.getFlowTemplates({
77-
search: request.query.search,
78-
tags: request.query.tags,
79-
services: request.query.services,
80-
domains: request.query.domains,
81-
blocks: request.query.blocks,
82-
projectId: request.principal.projectId,
83-
organizationId: request.principal.organization.id,
84-
cloudTemplates: true,
85-
isSample: true,
86-
version: request.query.version,
87-
categories: request.query.categories,
88-
});
89-
}
52+
const user = getVerifiedUser(request, publicKey);
9053

9154
return flowTemplateService.getFlowTemplates({
9255
search: request.query.search,
@@ -97,6 +60,7 @@ export const cloudTemplateController: FastifyPluginAsyncTypebox = async (
9760
projectId: request.principal.projectId,
9861
organizationId: request.principal.organization.id,
9962
cloudTemplates: true,
63+
isSample: !user,
10064
version: request.query.version,
10165
categories: request.query.categories,
10266
});
@@ -120,13 +84,14 @@ export const cloudTemplateController: FastifyPluginAsyncTypebox = async (
12084
},
12185
},
12286
async (request, reply) => {
123-
const token = getCloudToken(request);
124-
if (!(await getCloudUser(identityClient, token))) {
87+
const user = getVerifiedUser(request, publicKey);
88+
89+
if (!user) {
12590
const template = await flowTemplateService.getFlowTemplate(
12691
request.params.id,
12792
);
12893

129-
return template?.isSample ? template : reply.status(404).send();
94+
return template?.isSample ? template : reply.status(403).send();
13095
}
13196

13297
return flowTemplateService.getFlowTemplate(request.params.id);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { onSendHookHandler } from 'fastify/types/hooks';
2+
3+
export const allowAllOriginsHookHandler: onSendHookHandler = (
4+
request,
5+
reply,
6+
payload,
7+
done,
8+
) => {
9+
void reply.header(
10+
'Access-Control-Allow-Origin',
11+
request.headers.origin || request.headers['Ops-Origin'] || '*',
12+
);
13+
14+
void reply.header('Access-Control-Allow-Methods', 'GET,OPTIONS');
15+
16+
void reply.header(
17+
'Access-Control-Allow-Headers',
18+
'Content-Type,Ops-Origin,Authorization',
19+
);
20+
21+
void reply.header('Access-Control-Allow-Credentials', 'true');
22+
23+
if (request.method === 'OPTIONS') {
24+
return reply.status(204).send();
25+
}
26+
27+
done(null, payload);
28+
return;
29+
};
Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,28 @@
1-
import { IdentityClient } from '@frontegg/client';
2-
import {
3-
IAccessToken,
4-
IEntityWithRoles,
5-
} from '@frontegg/client/dist/src/clients/identity/types';
61
import { FastifyRequest } from 'fastify';
2+
import jwt, { JwtPayload } from 'jsonwebtoken';
73

84
const CLOUD_TOKEN_COOKIE_NAME = 'cloud-token';
95

10-
export const getCloudToken = (request: FastifyRequest): string | undefined => {
6+
const getCloudToken = (request: FastifyRequest): string | undefined => {
117
let token = request.headers.authorization?.replace('Bearer ', '');
128
if (!token) {
139
token = request.cookies[CLOUD_TOKEN_COOKIE_NAME];
1410
}
1511
return token;
1612
};
1713

18-
export async function getCloudUser(
19-
identityClient: IdentityClient,
20-
token?: string,
21-
): Promise<null | IEntityWithRoles | IAccessToken> {
14+
export function getVerifiedUser(
15+
request: FastifyRequest,
16+
publicKey: string,
17+
): string | JwtPayload | undefined {
18+
const token = getCloudToken(request);
2219
if (!token) {
23-
return null;
20+
return undefined;
2421
}
2522

2623
try {
27-
const user = await identityClient.validateIdentityOnToken(token);
28-
return user;
29-
} catch (e) {
30-
// eslint-disable-next-line no-console
31-
console.error(e);
32-
return null;
24+
return jwt.verify(token, publicKey, { algorithms: ['RS256'] });
25+
} catch {
26+
return undefined;
3327
}
3428
}

packages/server/api/src/app/user-info/user-info.module.ts

Lines changed: 9 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,28 @@
11
import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox';
2-
import { IdentityClient } from '@frontegg/client';
32
import { AppSystemProp, logger, system } from '@openops/server-shared';
43
import { ALL_PRINCIPAL_TYPES } from '@openops/shared';
5-
import { getCloudToken, getCloudUser } from './cloud-auth';
4+
import { allowAllOriginsHookHandler } from '../helper/allow-all-origins-hook-handler';
5+
import { getVerifiedUser } from './cloud-auth';
66

77
export const userInfoModule: FastifyPluginAsyncTypebox = async (app) => {
88
await app.register(userInfoController, { prefix: '/v1/user-info' });
99
};
1010

1111
export const userInfoController: FastifyPluginAsyncTypebox = async (app) => {
12-
const fronteggClientId = system.get(AppSystemProp.FRONTEGG_CLIENT_ID);
13-
const fronteggApiKey = system.get(AppSystemProp.FRONTEGG_API_KEY);
12+
const publicKey = system.get(AppSystemProp.FRONTEGG_PUBLIC_KEY);
13+
const connectionPageEnabled = system.getBoolean(
14+
AppSystemProp.CLOUD_CONNECTION_PAGE_ENABLED,
15+
);
1416

15-
if (!fronteggClientId || !fronteggApiKey) {
17+
if (!publicKey || !connectionPageEnabled) {
1618
logger.info(
1719
'Missing Frontegg configuration, disabling cloud templates API',
1820
);
1921
return;
2022
}
2123

22-
const identityClient = new IdentityClient({
23-
FRONTEGG_CLIENT_ID: fronteggClientId,
24-
FRONTEGG_API_KEY: fronteggApiKey,
25-
});
26-
2724
// user-info is available on any origin
28-
app.addHook('onSend', (request, reply, payload, done) => {
29-
void reply.header(
30-
'Access-Control-Allow-Origin',
31-
request.headers.origin || request.headers['Ops-Origin'] || '*',
32-
);
33-
void reply.header('Access-Control-Allow-Methods', 'GET,OPTIONS');
34-
void reply.header(
35-
'Access-Control-Allow-Headers',
36-
'Content-Type,Ops-Origin,Authorization',
37-
);
38-
void reply.header('Access-Control-Allow-Credentials', 'true');
39-
if (request.method === 'OPTIONS') {
40-
return reply.status(204).send();
41-
}
42-
43-
done(null, payload);
44-
return;
45-
});
25+
app.addHook('onSend', allowAllOriginsHookHandler);
4626

4727
app.get(
4828
'/',
@@ -53,8 +33,7 @@ export const userInfoController: FastifyPluginAsyncTypebox = async (app) => {
5333
},
5434
},
5535
async (request, reply) => {
56-
const token = getCloudToken(request);
57-
const user = await getCloudUser(identityClient, token);
36+
const user = getVerifiedUser(request, publicKey);
5837

5938
if (!user) {
6039
return reply.status(401).send();

0 commit comments

Comments
 (0)