Skip to content

Commit

Permalink
Patch service account UI in audit logs, add lastUsed for API keys and…
Browse files Browse the repository at this point in the history
… service accounts/tokens
  • Loading branch information
dangtony98 committed Apr 6, 2023
1 parent d547532 commit a7880db
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 138 deletions.
1 change: 1 addition & 0 deletions backend/src/controllers/v2/apiKeyDataController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => {

apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
user: req.user._id,
secretHash
Expand Down
1 change: 1 addition & 0 deletions backend/src/controllers/v2/serviceAccountsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const createServiceAccount = async (req: Request, res: Response) => {
organization: new Types.ObjectId(organizationId),
user: req.user,
publicKey,
lastUsed: new Date(),
expiresAt,
secretHash
}).save();
Expand Down
20 changes: 3 additions & 17 deletions backend/src/controllers/v2/serviceTokenDataController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
environment,
user,
serviceAccount,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
Expand All @@ -102,7 +103,6 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
permissions
}).save();


// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);

Expand All @@ -123,25 +123,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData;
try {
const { serviceTokenDataId } = req.params;

serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
const { serviceTokenDataId } = req.params;

} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete service token data'
});
}
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);

return res.status(200).send({
serviceTokenData
});
}

function UnauthorizedRequestError(arg0: { message: string; }) {
throw new Error('Function not implemented.');
}
2 changes: 1 addition & 1 deletion backend/src/ee/controllers/v1/workspaceController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
.skip(offset)
.limit(limit)
.populate('actions')
.populate('user');
.populate('user serviceAccount serviceTokenData');

} catch (err) {
Sentry.setUser({ email: req.user.email });
Expand Down
212 changes: 97 additions & 115 deletions backend/src/helpers/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import {
Expand Down Expand Up @@ -102,25 +103,17 @@ const getAuthUserPayload = async ({
}: {
authTokenValue: string;
}) => {
let user;
try {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, getJwtAuthSecret())
);
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, getJwtAuthSecret())
);

user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');

if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });

if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });

} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate JWT token'
});
}
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });

return user;
}
Expand All @@ -136,41 +129,38 @@ const getAuthSTDPayload = async ({
}: {
authTokenValue: string;
}) => {
let serviceTokenData;
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);

// TODO: optimize double query
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');

if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired service token'
});
}

const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);

serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag');

if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
let serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');

} catch (err) {
if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
message: 'Failed to authenticate expired service token'
});
}

const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});

serviceTokenData = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
}, {
lastUsed: new Date()
}, {
new: true
})
.select('+encryptedKey +iv +tag');

if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });

return serviceTokenData;
}

Expand Down Expand Up @@ -204,7 +194,6 @@ const getAuthSAAKPayload = async ({
}

/**
* TODO: deprecate API keys
* Return API key data payload corresponding to API key [authTokenValue]
* @param {Object} obj
* @param {String} obj.authTokenValue - API key value
Expand All @@ -215,33 +204,44 @@ const getAuthAPIKeyPayload = async ({
}: {
authTokenValue: string;
}) => {
let user;
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);

const apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate<{user: IUser}>('user', '+publicKey');

if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired API key'
});
}
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);

const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
let apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate<{user: IUser}>('user', '+publicKey');

user = apiKeyData.user;
} catch (err) {
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
message: 'Failed to authenticate expired API key'
});
}

const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});

apiKeyData = await APIKeyData.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
}, {
lastUsed: new Date()
}, {
new: true
});

if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
}

const user = await User.findById(apiKeyData.user).select('+publicKey');

if (!user) {
throw AccountNotFoundError({
message: 'Failed to find user'
});
}

Expand All @@ -257,30 +257,23 @@ const getAuthAPIKeyPayload = async ({
* @return {String} obj.refreshToken - issued refresh token
*/
const issueAuthTokens = async ({ userId }: { userId: string }) => {
let token: string;
let refreshToken: string;
try {
// issue tokens
token = createToken({
payload: {
userId
},
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
});

refreshToken = createToken({
payload: {
userId
},
expiresIn: getJwtRefreshLifetime(),
secret: getJwtRefreshSecret()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to issue tokens');
}
// issue tokens
const token = createToken({
payload: {
userId
},
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
});

const refreshToken = createToken({
payload: {
userId
},
expiresIn: getJwtRefreshLifetime(),
secret: getJwtRefreshSecret()
});

return {
token,
Expand All @@ -294,19 +287,14 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
* @param {String} obj.userId - id of user whose tokens are cleared.
*/
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
try {
// increment refreshVersion on user by 1
User.findOneAndUpdate({
_id: userId
}, {
$inc: {
refreshVersion: 1
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
// increment refreshVersion on user by 1
User.findOneAndUpdate({
_id: userId
}, {
$inc: {
refreshVersion: 1
}
});
};

/**
Expand All @@ -326,15 +314,9 @@ const createToken = ({
expiresIn: string | number;
secret: string;
}) => {
try {
return jwt.sign(payload, secret, {
expiresIn
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create a token');
}
return jwt.sign(payload, secret, {
expiresIn
});
};

export {
Expand Down
4 changes: 4 additions & 0 deletions backend/src/models/apiKeyData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Schema, model, Types } from 'mongoose';
export interface IAPIKeyData {
name: string;
user: Types.ObjectId;
lastUsed: Date;
expiresAt: Date;
secretHash: string;
}
Expand All @@ -18,6 +19,9 @@ const apiKeyDataSchema = new Schema<IAPIKeyData>(
ref: 'User',
required: true
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date
},
Expand Down
6 changes: 5 additions & 1 deletion backend/src/models/serviceAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface IServiceAccount extends Document {
organization: Types.ObjectId;
user: Types.ObjectId;
publicKey: string;
lastUsed: Date;
expiresAt: Date;
secretHash: string;
}
Expand All @@ -30,6 +31,9 @@ const serviceAccountSchema = new Schema<IServiceAccount>(
type: String,
required: true
},
lastUsed: {
type: Date
},
expiresAt: {
type: Date
},
Expand All @@ -44,6 +48,6 @@ const serviceAccountSchema = new Schema<IServiceAccount>(
}
);

const ServiceAccount = model<IServiceAccount>('ServiceAcount', serviceAccountSchema);
const ServiceAccount = model<IServiceAccount>('ServiceAccount', serviceAccountSchema);

export default ServiceAccount;
Loading

0 comments on commit a7880db

Please sign in to comment.