Skip to content

Commit c7fb920

Browse files
committed
Complete v1 support for API key auth mode
1 parent 8c7c41e commit c7fb920

File tree

8 files changed

+119
-16
lines changed

8 files changed

+119
-16
lines changed

backend/src/app.ts

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
secret as v2SecretRouter,
3939
workspace as v2WorkspaceRouter,
4040
serviceTokenData as v2ServiceTokenDataRouter,
41+
apiKeyData as v2APIKeyDataRouter,
4142
} from './routes/v2';
4243

4344
import { getLogger } from './utils/logger';
@@ -94,6 +95,7 @@ app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
9495
app.use('/api/v2/workspace', v2WorkspaceRouter);
9596
app.use('/api/v2/secret', v2SecretRouter);
9697
app.use('/api/v2/service-token-data', v2ServiceTokenDataRouter);
98+
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
9799

98100
//* Handle unrouted requests and respond with proper error message as well as status code
99101
app.use((req, res, next)=>{

backend/src/controllers/v2/apiKeyDataController.ts

+30-4
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,36 @@ import {
1010
} from '../../config';
1111

1212
/**
13-
* Create new API key for user with id [req.user._id]
13+
* Return API key data for user with id [req.user_id]
14+
* @param req
15+
* @param res
16+
* @returns
17+
*/
18+
export const getAPIKeyData = async (req: Request, res: Response) => {
19+
let apiKeyData;
20+
try {
21+
apiKeyData = await APIKeyData.find({
22+
user: req.user._id
23+
});
24+
} catch (err) {
25+
Sentry.setUser({ email: req.user.email });
26+
Sentry.captureException(err);
27+
return res.status(400).send({
28+
message: 'Failed to get API key data'
29+
});
30+
}
31+
32+
return res.status(200).send({
33+
apiKeyData
34+
});
35+
}
36+
37+
/**
38+
* Create new API key data for user with id [req.user._id]
1439
* @param req
1540
* @param res
1641
*/
17-
export const createAPIKey = async (req: Request, res: Response) => {
42+
export const createAPIKeyData = async (req: Request, res: Response) => {
1843
let apiKey, apiKeyData;
1944
try {
2045
const { name, expiresIn } = req.body;
@@ -30,7 +55,7 @@ export const createAPIKey = async (req: Request, res: Response) => {
3055
expiresAt,
3156
user: req.user._id,
3257
secretHash
33-
});
58+
}).save();
3459

3560
// return api key data without sensitive data
3661
apiKeyData = await APIKeyData.findById(apiKeyData._id);
@@ -40,10 +65,11 @@ export const createAPIKey = async (req: Request, res: Response) => {
4065
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
4166

4267
} catch (err) {
68+
console.error(err);
4369
Sentry.setUser({ email: req.user.email });
4470
Sentry.captureException(err);
4571
return res.status(400).send({
46-
message: 'Failed to create service token data'
72+
message: 'Failed to API key data'
4773
});
4874
}
4975

backend/src/helpers/auth.ts

+60-9
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,25 @@ import * as Sentry from '@sentry/node';
33
import bcrypt from 'bcrypt';
44
import {
55
User,
6-
ServiceTokenData
6+
ServiceTokenData,
7+
APIKeyData
78
} from '../models';
89
import {
910
JWT_AUTH_LIFETIME,
1011
JWT_AUTH_SECRET,
1112
JWT_REFRESH_LIFETIME,
12-
JWT_REFRESH_SECRET,
13-
SALT_ROUNDS
13+
JWT_REFRESH_SECRET
1414
} from '../config';
1515
import {
1616
AccountNotFoundError,
1717
ServiceTokenDataNotFoundError,
18-
UnauthorizedRequestError,
19-
BadRequestError
18+
APIKeyDataNotFoundError,
19+
UnauthorizedRequestError
2020
} from '../utils/errors';
2121

22+
// TODO 1: check if API key works
23+
// TODO 2: optimize middleware
24+
2225
/**
2326
* Validate that auth token value [authTokenValue] falls under one of
2427
* accepted auth modes [acceptedAuthModes].
@@ -40,6 +43,9 @@ const validateAuthMode = ({
4043
case 'st':
4144
authMode = 'serviceToken';
4245
break;
46+
case 'ak':
47+
authMode = 'apiKey';
48+
break;
4349
default:
4450
authMode = 'jwt';
4551
break;
@@ -106,18 +112,18 @@ const getAuthSTDPayload = async ({
106112

107113
// TODO: optimize double query
108114
serviceTokenData = await ServiceTokenData
109-
.findById(TOKEN_IDENTIFIER, 'secretHash expiresAt');
115+
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
110116

111-
if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
117+
if (!serviceTokenData) {
118+
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
119+
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
112120
// case: service token expired
113121
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
114122
throw UnauthorizedRequestError({
115123
message: 'Failed to authenticate expired service token'
116124
});
117125
}
118126

119-
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
120-
121127
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
122128
if (!isMatch) throw UnauthorizedRequestError({
123129
message: 'Failed to authenticate service token'
@@ -136,6 +142,50 @@ const getAuthSTDPayload = async ({
136142
return serviceTokenData;
137143
}
138144

145+
/**
146+
* Return API key data payload corresponding to API key [authTokenValue]
147+
* @param {Object} obj
148+
* @param {String} obj.authTokenValue - API key value
149+
* @returns {APIKeyData} apiKeyData - API key data
150+
*/
151+
const getAuthAPIKeyPayload = async ({
152+
authTokenValue
153+
}: {
154+
authTokenValue: string;
155+
}) => {
156+
let user;
157+
try {
158+
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
159+
160+
const apiKeyData = await APIKeyData
161+
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
162+
.populate('user', '+publicKey');
163+
164+
if (!apiKeyData) {
165+
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
166+
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
167+
// case: API key expired
168+
await APIKeyData.findByIdAndDelete(apiKeyData._id);
169+
throw UnauthorizedRequestError({
170+
message: 'Failed to authenticate expired API key'
171+
});
172+
}
173+
174+
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
175+
if (!isMatch) throw UnauthorizedRequestError({
176+
message: 'Failed to authenticate API key'
177+
});
178+
179+
user = apiKeyData.user;
180+
} catch (err) {
181+
throw UnauthorizedRequestError({
182+
message: 'Failed to authenticate API key'
183+
});
184+
}
185+
186+
return user;
187+
}
188+
139189
/**
140190
* Return newly issued (JWT) auth and refresh tokens to user with id [userId]
141191
* @param {Object} obj
@@ -229,6 +279,7 @@ export {
229279
validateAuthMode,
230280
getAuthUserPayload,
231281
getAuthSTDPayload,
282+
getAuthAPIKeyPayload,
232283
createToken,
233284
issueTokens,
234285
clearTokens

backend/src/middleware/requireAuth.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { User, ServiceTokenData } from '../models';
44
import {
55
validateAuthMode,
66
getAuthUserPayload,
7-
getAuthSTDPayload
7+
getAuthSTDPayload,
8+
getAuthAPIKeyPayload
89
} from '../helpers/auth';
910
import { BadRequestError } from '../utils/errors';
1011

@@ -53,6 +54,11 @@ const requireAuth = ({
5354
authTokenValue: AUTH_TOKEN_VALUE
5455
});
5556
break;
57+
case 'apiKey':
58+
req.user = await getAuthAPIKeyPayload({
59+
authTokenValue: AUTH_TOKEN_VALUE
60+
});
61+
break;
5662
default:
5763
req.user = await getAuthUserPayload({
5864
authTokenValue: AUTH_TOKEN_VALUE

backend/src/routes/v2/apiKeyData.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import {
77
import { body } from 'express-validator';
88
import { apiKeyDataController } from '../../controllers/v2';
99

10+
router.get(
11+
'/',
12+
requireAuth({
13+
acceptedAuthModes: ['jwt']
14+
}),
15+
apiKeyDataController.getAPIKeyData
16+
);
17+
1018
router.post(
1119
'/',
1220
requireAuth({
@@ -15,7 +23,7 @@ router.post(
1523
body('name').exists().trim(),
1624
body('expiresIn'), // measured in ms
1725
validateRequest,
18-
apiKeyDataController.createAPIKey
26+
apiKeyDataController.createAPIKeyData
1927
);
2028

2129
export default router;

backend/src/routes/v2/workspace.ts

-1
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,4 @@ router.get(
7171
workspaceController.getWorkspaceServiceTokenData
7272
);
7373

74-
7574
export default router;

backend/src/types/express/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ declare global {
1616
serviceToken: any;
1717
accessToken: any;
1818
serviceTokenData: any;
19+
apiKeyData: any;
1920
query?: any;
2021
}
2122
}

backend/src/utils/errors.ts

+10
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,14 @@ export const ServiceTokenDataNotFoundError = (error?: Partial<RequestErrorContex
143143
stack: error?.stack
144144
})
145145

146+
//* ----->[API KEY DATA ERRORS]<-----
147+
export const APIKeyDataNotFoundError = (error?: Partial<RequestErrorContext>) => new RequestError({
148+
logLevel: error?.logLevel ?? LogLevel.ERROR,
149+
statusCode: error?.statusCode ?? 404,
150+
type: error?.type ?? 'service_token_data_not_found_error',
151+
message: error?.message ?? 'The requested service token data was not found',
152+
context: error?.context,
153+
stack: error?.stack
154+
})
155+
146156
//* ----->[MISC ERRORS]<-----

0 commit comments

Comments
 (0)