Merge pull request #1112 from Infisical/auth-jwt-standardization

API Key V2
This commit is contained in:
BlackMagiq
2023-10-23 12:30:00 +01:00
committed by GitHub
31 changed files with 800 additions and 37 deletions
+2
View File
@@ -1,9 +1,11 @@
import * as usersController from "./usersController";
import * as secretsController from "./secretsController";
import * as workspacesController from "./workspacesController";
import * as authController from "./authController";
import * as signupController from "./signupController";
export {
usersController,
authController,
secretsController,
signupController,
@@ -0,0 +1,18 @@
import { Request, Response } from "express";
import { APIKeyDataV2 } from "../../models";
/**
* Return API keys belonging to current user.
* @param req
* @param res
* @returns
*/
export const getMyAPIKeys = async (req: Request, res: Response) => {
const apiKeyData = await APIKeyDataV2.find({
user: req.user._id
});
return res.status(200).send({
apiKeyData
});
}
@@ -0,0 +1,101 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { APIKeyDataV2 } from "../../../models/apiKeyDataV2";
import { validateRequest } from "../../../helpers/validation";
import { BadRequestError } from "../../../utils/errors";
import * as reqValidator from "../../../validation";
import { createToken } from "../../../helpers";
import { AuthTokenType } from "../../../variables";
import { getAuthSecret } from "../../../config";
/**
* Create API key data v2
* @param req
* @param res
*/
export const createAPIKeyData = async (req: Request, res: Response) => {
const {
body: {
name
}
} = await validateRequest(reqValidator.CreateAPIKeyV3, req);
const apiKeyData = await new APIKeyDataV2({
name,
user: req.user._id,
usageCount: 0,
}).save();
const apiKey = createToken({
payload: {
authTokenType: AuthTokenType.API_KEY,
apiKeyDataId: apiKeyData._id.toString(),
userId: req.user._id.toString()
},
secret: await getAuthSecret()
});
return res.status(200).send({
apiKeyData,
apiKey
});
}
/**
* Update API key data v2 with id [apiKeyDataId]
* @param req
* @param res
*/
export const updateAPIKeyData = async (req: Request, res: Response) => {
const {
params: { apiKeyDataId },
body: {
name,
}
} = await validateRequest(reqValidator.UpdateAPIKeyV3, req);
const apiKeyData = await APIKeyDataV2.findOneAndUpdate(
{
_id: new Types.ObjectId(apiKeyDataId),
user: req.user._id
},
{
name
},
{
new: true
}
);
if (!apiKeyData) throw BadRequestError({
message: "Failed to update API key"
});
return res.status(200).send({
apiKeyData
});
}
/**
* Delete API key data v2 with id [apiKeyDataId]
* @param req
* @param res
*/
export const deleteAPIKeyData = async (req: Request, res: Response) => {
const {
params: { apiKeyDataId }
} = await validateRequest(reqValidator.DeleteAPIKeyV3, req);
const apiKeyData = await APIKeyDataV2.findOneAndDelete({
_id: new Types.ObjectId(apiKeyDataId),
user: req.user._id
});
if (!apiKeyData) throw BadRequestError({
message: "Failed to delete API key"
});
return res.status(200).send({
apiKeyData
});
}
+3 -1
View File
@@ -1,5 +1,7 @@
import * as serviceTokenDataController from "./serviceTokenDataController";
import * as apiKeyDataController from "./apiKeyDataController";
export {
serviceTokenDataController
serviceTokenDataController,
apiKeyDataController
}
@@ -30,7 +30,7 @@ import { EEAuditLogService, EELicenseService } from "../../services";
import { getJwtServiceTokenSecret } from "../../../config";
/**
* Return project key for service token
* Return project key for service token V3
* @param req
* @param res
*/
@@ -57,7 +57,7 @@ export const getServiceTokenDataKey = async (req: Request, res: Response) => {
}
/**
* Create service token data
* Create service token data V3
* @param req
* @param res
* @returns
@@ -165,7 +165,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
}
/**
* Update service token data with id [serviceTokenDataId]
* Update service token V3 data with id [serviceTokenDataId]
* @param req
* @param res
* @returns
+31
View File
@@ -0,0 +1,31 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../../middleware";
import { AuthMode } from "../../../variables";
import { apiKeyDataController } from "../../controllers/v3";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
apiKeyDataController.createAPIKeyData
);
router.patch(
"/:apiKeyDataId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
apiKeyDataController.updateAPIKeyData
);
router.delete(
"/:apiKeyDataId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
apiKeyDataController.deleteAPIKeyData
);
export default router;
+3 -1
View File
@@ -1,5 +1,7 @@
import serviceTokenData from "./serviceTokenData";
import apiKeyData from "./apiKeyData";
export {
serviceTokenData
serviceTokenData,
apiKeyData
}
+37 -16
View File
@@ -4,6 +4,7 @@ import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import {
APIKeyData,
APIKeyDataV2,
ITokenVersion,
IUser,
ServiceTokenData,
@@ -105,6 +106,7 @@ export const validateAuthMode = ({
/**
* Return user payload corresponding to JWT token [authTokenValue]
* that is either for browser / CLI or API Key
* @param {Object} obj
* @param {String} obj.authTokenValue - JWT token value
* @returns {User} user - user corresponding to JWT token
@@ -120,7 +122,41 @@ export const getAuthUserPayload = async ({
jwt.verify(authTokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.ACCESS_TOKEN) throw UnauthorizedRequestError();
if (
decodedToken.authTokenType !== AuthTokenType.ACCESS_TOKEN &&
decodedToken.authTokenType !== AuthTokenType.API_KEY
) {
throw UnauthorizedRequestError();
}
if (decodedToken.authTokenType === AuthTokenType.ACCESS_TOKEN) {
const tokenVersion = await TokenVersion.findOneAndUpdate({
_id: new Types.ObjectId(decodedToken.tokenVersionId),
user: decodedToken.userId
}, {
lastUsed: new Date(),
});
if (!tokenVersion) throw UnauthorizedRequestError();
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError();
} else if (decodedToken.authTokenType === AuthTokenType.API_KEY) {
const apiKeyData = await APIKeyDataV2.findOneAndUpdate(
{
_id: new Types.ObjectId(decodedToken.apiKeyDataId),
user: new Types.ObjectId(decodedToken.userId)
},
{
lastUsed: new Date(),
$inc: { usageCount: 1 }
},
{
new: true
}
);
if (!apiKeyData) throw UnauthorizedRequestError();
}
const user = await User.findOne({
_id: new Types.ObjectId(decodedToken.userId),
@@ -130,21 +166,6 @@ export const getAuthUserPayload = async ({
if (!user?.publicKey) throw UnauthorizedRequestError({ message: "Failed to authenticate user with partially set up account" });
const tokenVersion = await TokenVersion.findOneAndUpdate({
_id: new Types.ObjectId(decodedToken.tokenVersionId),
user: user._id,
}, {
lastUsed: new Date(),
});
if (!tokenVersion) throw UnauthorizedRequestError({
message: "Failed to validate access token",
});
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({
message: "Failed to validate access token",
});
return {
actor: {
type: ActorType.USER,
+5 -1
View File
@@ -29,6 +29,7 @@ import {
secretApprovalRequest as v1SecretApprovalRequest,
secretScanning as v1SecretScanningRouter
} from "./ee/routes/v1";
import { apiKeyData as v3apiKeyDataRouter } from "./ee/routes/v3";
import { serviceTokenData as v3ServiceTokenDataRouter } from "./ee/routes/v3";
import {
auth as v1AuthRouter,
@@ -68,6 +69,7 @@ import {
auth as v3AuthRouter,
secrets as v3SecretsRouter,
signup as v3SignupRouter,
users as v3UsersRouter,
workspaces as v3WorkspacesRouter
} from "./routes/v3";
import { healthCheck } from "./routes/status";
@@ -180,7 +182,8 @@ const main = async () => {
app.use("/api/v1/organizations", eeOrganizationsRouter);
app.use("/api/v1/sso", eeSSORouter);
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
app.use("/api/v3/service-token", v3ServiceTokenDataRouter);
app.use("/api/v3/api-key", v3apiKeyDataRouter); // new
app.use("/api/v3/service-token", v3ServiceTokenDataRouter); // new
// v1 routes
app.use("/api/v1/signup", v1SignupRouter);
@@ -226,6 +229,7 @@ const main = async () => {
app.use("/api/v3/secrets", v3SecretsRouter);
app.use("/api/v3/workspaces", v3WorkspacesRouter);
app.use("/api/v3/signup", v3SignupRouter);
app.use("/api/v3/users", v3UsersRouter);
// api docs
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile));
+38
View File
@@ -0,0 +1,38 @@
import { Document, Schema, Types, model } from "mongoose";
export interface IAPIKeyDataV2 extends Document {
_id: Types.ObjectId;
name: string;
user: Types.ObjectId;
lastUsed?: Date
usageCount: number;
expiresAt?: Date;
}
const apiKeyDataV2Schema = new Schema(
{
name: {
type: String,
required: true
},
user: {
type: Schema.Types.ObjectId,
ref: "User",
required: true
},
lastUsed: {
type: Date,
required: false
},
usageCount: {
type: Number,
default: 0,
required: true
}
},
{
timestamps: true
}
);
export const APIKeyDataV2 = model<IAPIKeyDataV2>("APIKeyDataV2", apiKeyDataV2Schema);
+4 -3
View File
@@ -24,9 +24,10 @@ export * from "./user";
export * from "./userAction";
export * from "./workspace";
export * from "./serviceTokenData"; // TODO: deprecate
export * from "./apiKeyData";
export * from "./serviceTokenDataV3";
export * from "./serviceTokenDataV3Key";
export * from "./apiKeyData"; // TODO: deprecate
export * from "./apiKeyDataV2";
export * from "./loginSRPDetail";
export * from "./tokenVersion";
export * from "./webhooks";
export * from "./serviceTokenDataV3";
export * from "./serviceTokenDataV3Key";
+1
View File
@@ -54,6 +54,7 @@ const serviceTokenDataV3Schema = new Schema(
},
isActive: {
type: Boolean,
default: true,
required: true
},
lastUsed: {
+4 -4
View File
@@ -6,7 +6,7 @@ import {
import { AuthMode } from "../../variables";
import { serviceTokenDataController } from "../../controllers/v2";
router.get(
router.get( // TODO: deprecate (moving to ST V3)
"/",
requireAuth({
acceptedAuthModes: [AuthMode.SERVICE_TOKEN]
@@ -14,7 +14,7 @@ router.get(
serviceTokenDataController.getServiceTokenData
);
router.post(
router.post( // TODO: deprecate (moving to ST V3)
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
@@ -22,7 +22,7 @@ router.post(
serviceTokenDataController.createServiceTokenData
);
router.delete(
router.delete( // TODO: deprecate (moving to ST V3)
"/:serviceTokenDataId",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
@@ -30,4 +30,4 @@ router.delete(
serviceTokenDataController.deleteServiceTokenData
);
export default router;
export default router;
+1 -1
View File
@@ -36,7 +36,7 @@ router.get(
usersController.getMyOrganizations
);
router.get(
router.get( // TODO: deprecate (moving to API Key V2)
"/me/api-keys",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
+2
View File
@@ -1,10 +1,12 @@
import auth from "./auth";
import users from "./users";
import secrets from "./secrets";
import workspaces from "./workspaces";
import signup from "./signup";
export {
auth,
users,
secrets,
signup,
workspaces
+15
View File
@@ -0,0 +1,15 @@
import express from "express";
const router = express.Router();
import { requireAuth } from "../../middleware";
import { AuthMode } from "../../variables";
import { usersController } from "../../controllers/v3";
router.get(
"/me/api-keys",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
usersController.getMyAPIKeys
);
export default router;
+22
View File
@@ -0,0 +1,22 @@
import { z } from "zod";
export const CreateAPIKeyV3 = z.object({
body: z.object({
name: z.string().trim()
})
});
export const UpdateAPIKeyV3 = z.object({
params: z.object({
apiKeyDataId: z.string().trim()
}),
body: z.object({
name: z.string().trim()
})
});
export const DeleteAPIKeyV3 = z.object({
params: z.object({
apiKeyDataId: z.string().trim()
})
});
+1
View File
@@ -10,3 +10,4 @@ export * from "./secrets";
export * from "./serviceAccount";
export * from "./serviceTokenData";
export * from "./serviceTokenDataV3";
export * from "./apiKeyDataV3";
+2 -1
View File
@@ -3,7 +3,8 @@ export enum AuthTokenType {
REFRESH_TOKEN = "refreshToken",
SIGNUP_TOKEN = "signupToken",
MFA_TOKEN = "mfaToken",
PROVIDER_TOKEN = "providerToken"
PROVIDER_TOKEN = "providerToken",
API_KEY = "apiKey"
}
export enum AuthMode {