mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Update ST V3 impl to (rotating) refresh token impl
This commit is contained in:
@@ -24,7 +24,6 @@ export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_RE
|
||||
export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1)
|
||||
export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
|
||||
export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
|
||||
export const getJwtServiceTokenSecret = async () => (await client.getSecret("JWT_SERVICE_TOKEN_SECRET")).secretValue;
|
||||
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
|
||||
export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production";
|
||||
export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
|
||||
|
||||
@@ -28,6 +28,11 @@ declare module "jsonwebtoken" {
|
||||
userId: string;
|
||||
refreshVersion?: number;
|
||||
}
|
||||
export interface ServiceRefreshTokenJwtPayload extends jwt.JwtPayload {
|
||||
serviceTokenDataId: string;
|
||||
authTokenType: string;
|
||||
tokenVersion: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
@@ -24,10 +25,11 @@ import {
|
||||
getUserProjectPermissions
|
||||
} from "../../services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { BadRequestError, ResourceNotFoundError } from "../../../utils/errors";
|
||||
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||
import { EEAuditLogService, EELicenseService } from "../../services";
|
||||
import { getJwtServiceTokenSecret } from "../../../config";
|
||||
import { getAuthSecret } from "../../../config";
|
||||
import { AuthTokenType } from "../../../variables";
|
||||
|
||||
/**
|
||||
* Return project key for service token V3
|
||||
@@ -56,6 +58,99 @@ export const getServiceTokenDataKey = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return access and refresh token as per refresh operation
|
||||
* @param req
|
||||
* @param res
|
||||
*/
|
||||
export const refreshToken = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: {
|
||||
refresh_token
|
||||
}
|
||||
} = await validateRequest(reqValidator.RefreshTokenV3, req);
|
||||
|
||||
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
|
||||
jwt.verify(refresh_token, await getAuthSecret())
|
||||
);
|
||||
|
||||
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError();
|
||||
|
||||
let serviceTokenData = await ServiceTokenDataV3.findOne({
|
||||
_id: new Types.ObjectId(decodedToken.serviceTokenDataId),
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!serviceTokenData) throw UnauthorizedRequestError();
|
||||
|
||||
if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) {
|
||||
// raise alarm
|
||||
throw UnauthorizedRequestError();
|
||||
}
|
||||
|
||||
const response: {
|
||||
refresh_token?: string;
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
token_type: string;
|
||||
} = {
|
||||
refresh_token,
|
||||
access_token: "",
|
||||
expires_in: 0,
|
||||
token_type: "Bearer"
|
||||
};
|
||||
|
||||
if (serviceTokenData.isRefreshTokenRotationEnabled) {
|
||||
serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate(
|
||||
serviceTokenData._id,
|
||||
{
|
||||
$inc: {
|
||||
tokenVersion: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (!serviceTokenData) throw BadRequestError();
|
||||
|
||||
response.refresh_token = createToken({
|
||||
payload: {
|
||||
serviceTokenDataId: serviceTokenData._id.toString(),
|
||||
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
|
||||
tokenVersion: serviceTokenData.tokenVersion
|
||||
},
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
}
|
||||
|
||||
response.access_token = createToken({
|
||||
payload: {
|
||||
serviceTokenDataId: serviceTokenData._id.toString(),
|
||||
authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN,
|
||||
tokenVersion: serviceTokenData.tokenVersion
|
||||
},
|
||||
expiresIn: serviceTokenData.accessTokenTTL,
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
response.expires_in = serviceTokenData.accessTokenTTL;
|
||||
|
||||
await ServiceTokenDataV3.findByIdAndUpdate(
|
||||
serviceTokenData._id,
|
||||
{
|
||||
refreshTokenLastUsed: new Date(),
|
||||
$inc: { refreshTokenUsageCount: 1 }
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).send(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create service token data V3
|
||||
* @param req
|
||||
@@ -71,8 +166,10 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled,
|
||||
encryptedKey, // for ServiceTokenDataV3Key
|
||||
nonce // for ServiceTokenDataV3Key
|
||||
nonce, // for ServiceTokenDataV3Key
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateServiceTokenV3, req);
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
@@ -112,17 +209,21 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
user = req.authData.authPayload._id;
|
||||
}
|
||||
|
||||
const isActive = true;
|
||||
const isActive = false;
|
||||
const serviceTokenData = await new ServiceTokenDataV3({
|
||||
name,
|
||||
user,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
publicKey,
|
||||
usageCount: 0,
|
||||
refreshTokenUsageCount: 0,
|
||||
accessTokenUsageCount: 0,
|
||||
tokenVersion: 1,
|
||||
trustedIps: reformattedTrustedIps,
|
||||
scopes,
|
||||
isActive,
|
||||
expiresAt
|
||||
expiresAt,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
}).save();
|
||||
|
||||
await new ServiceTokenDataV3Key({
|
||||
@@ -133,18 +234,19 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
workspace: new Types.ObjectId(workspaceId)
|
||||
}).save();
|
||||
|
||||
const token = createToken({
|
||||
const refreshToken = createToken({
|
||||
payload: {
|
||||
_id: serviceTokenData._id.toString()
|
||||
serviceTokenDataId: serviceTokenData._id.toString(),
|
||||
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
|
||||
tokenVersion: serviceTokenData.tokenVersion
|
||||
},
|
||||
expiresIn,
|
||||
secret: await getJwtServiceTokenSecret()
|
||||
secret: await getAuthSecret()
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
type: EventType.CREATE_SERVICE_TOKEN_V3,
|
||||
type: EventType.CREATE_SERVICE_TOKEN_V3, // TODO: update
|
||||
metadata: {
|
||||
name,
|
||||
isActive,
|
||||
@@ -160,7 +262,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||
|
||||
return res.status(200).send({
|
||||
serviceTokenData,
|
||||
serviceToken: `stv3.${token}`
|
||||
refreshToken
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,7 +280,9 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
}
|
||||
} = await validateRequest(reqValidator.UpdateServiceTokenV3, req);
|
||||
|
||||
@@ -233,13 +337,15 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps: reformattedTrustedIps,
|
||||
expiresAt
|
||||
expiresAt,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
if (!serviceTokenData) throw BadRequestError({
|
||||
message: "Failed to update service token"
|
||||
});
|
||||
|
||||
@@ -7,11 +7,16 @@ import { serviceTokenDataController } from "../../controllers/v3";
|
||||
router.get(
|
||||
"/me/key",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
serviceTokenDataController.getServiceTokenDataKey
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/me/token",
|
||||
serviceTokenDataController.refreshToken
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
|
||||
@@ -22,8 +22,7 @@ import {
|
||||
import {
|
||||
getAuthSecret,
|
||||
getJwtAuthLifetime,
|
||||
getJwtRefreshLifetime,
|
||||
getJwtServiceTokenSecret
|
||||
getJwtRefreshLifetime
|
||||
} from "../config";
|
||||
import {
|
||||
AuthMode,
|
||||
@@ -43,7 +42,7 @@ import { getUserAgentType } from "../utils/posthog";
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.headers - HTTP request headers object
|
||||
*/
|
||||
export const validateAuthMode = ({
|
||||
export const validateAuthMode = async ({
|
||||
headers,
|
||||
acceptedAuthModes,
|
||||
}: {
|
||||
@@ -84,13 +83,19 @@ export const validateAuthMode = ({
|
||||
authMode = AuthMode.SERVICE_TOKEN;
|
||||
authTokenValue = tokenValue;
|
||||
break;
|
||||
case "stv3":
|
||||
authMode = AuthMode.SERVICE_TOKEN_V3;
|
||||
authTokenValue = parts.slice(1).join(".");
|
||||
break;
|
||||
default:
|
||||
authMode = AuthMode.JWT;
|
||||
default: {
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(tokenValue, await getAuthSecret())
|
||||
);
|
||||
|
||||
if (decodedToken.authTokenType === AuthTokenType.SERVICE_ACCESS_TOKEN) {
|
||||
authMode = AuthMode.SERVICE_ACCESS_TOKEN;
|
||||
} else {
|
||||
authMode = AuthMode.JWT;
|
||||
}
|
||||
|
||||
authTokenValue = tokenValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,23 +259,17 @@ export const getAuthSTDPayload = async ({
|
||||
req: Request,
|
||||
authTokenValue: string;
|
||||
}): Promise<ServiceTokenV3AuthData> => {
|
||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||
jwt.verify(authTokenValue, await getJwtServiceTokenSecret())
|
||||
|
||||
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
|
||||
jwt.verify(authTokenValue, await getAuthSecret())
|
||||
);
|
||||
|
||||
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_ACCESS_TOKEN) throw UnauthorizedRequestError();
|
||||
|
||||
const serviceTokenData = await ServiceTokenDataV3.findOneAndUpdate(
|
||||
{
|
||||
_id: new Types.ObjectId(decodedToken._id),
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
lastUsed: new Date(),
|
||||
$inc: { usageCount: 1 }
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
const serviceTokenData = await ServiceTokenDataV3.findOne({
|
||||
_id: new Types.ObjectId(decodedToken.serviceTokenDataId),
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!serviceTokenData) {
|
||||
throw UnauthorizedRequestError({
|
||||
@@ -288,10 +287,26 @@ export const getAuthSTDPayload = async ({
|
||||
}
|
||||
);
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to authenticate",
|
||||
});
|
||||
} else if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) {
|
||||
// TODO: raise alarm
|
||||
throw UnauthorizedRequestError({
|
||||
message: "Failed to authenticate",
|
||||
});
|
||||
}
|
||||
|
||||
await ServiceTokenDataV3.findByIdAndUpdate(
|
||||
serviceTokenData._id,
|
||||
{
|
||||
accessTokenLastUsed: new Date(),
|
||||
$inc: { accessTokenUsageCount: 1 }
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
actor: {
|
||||
|
||||
@@ -35,7 +35,7 @@ const requireAuth = ({
|
||||
|
||||
// validate auth token against accepted auth modes [acceptedAuthModes]
|
||||
// and return token type [authTokenType] and value [authTokenValue]
|
||||
const { authMode, authTokenValue } = validateAuthMode({
|
||||
const { authMode, authTokenValue } = await validateAuthMode({
|
||||
headers: req.headers,
|
||||
acceptedAuthModes,
|
||||
});
|
||||
@@ -50,7 +50,7 @@ const requireAuth = ({
|
||||
});
|
||||
req.serviceTokenData = authData.authPayload;
|
||||
break;
|
||||
case AuthMode.SERVICE_TOKEN_V3:
|
||||
case AuthMode.SERVICE_ACCESS_TOKEN:
|
||||
authData = await getAuthSTDV3Payload({
|
||||
req,
|
||||
authTokenValue
|
||||
|
||||
@@ -25,9 +25,14 @@ export interface IServiceTokenDataV3 extends Document {
|
||||
user: Types.ObjectId;
|
||||
publicKey: string;
|
||||
isActive: boolean;
|
||||
lastUsed?: Date;
|
||||
usageCount: number;
|
||||
refreshTokenLastUsed?: Date;
|
||||
accessTokenLastUsed?: Date;
|
||||
refreshTokenUsageCount: number;
|
||||
accessTokenUsageCount: number;
|
||||
tokenVersion: number;
|
||||
isRefreshTokenRotationEnabled: boolean;
|
||||
expiresAt?: Date;
|
||||
accessTokenTTL: number;
|
||||
scopes: Array<IServiceTokenV3Scope>;
|
||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||
}
|
||||
@@ -57,19 +62,43 @@ const serviceTokenDataV3Schema = new Schema(
|
||||
default: true,
|
||||
required: true
|
||||
},
|
||||
lastUsed: {
|
||||
refreshTokenLastUsed: {
|
||||
type: Date,
|
||||
required: false
|
||||
},
|
||||
usageCount: {
|
||||
accessTokenLastUsed: {
|
||||
type: Date,
|
||||
required: false
|
||||
},
|
||||
refreshTokenUsageCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true
|
||||
},
|
||||
expiresAt: {
|
||||
accessTokenUsageCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true
|
||||
},
|
||||
tokenVersion: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true
|
||||
},
|
||||
isRefreshTokenRotationEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true
|
||||
},
|
||||
expiresAt: { // consider revising field name
|
||||
type: Date,
|
||||
required: false,
|
||||
expires: 0
|
||||
// expires: 0
|
||||
},
|
||||
accessTokenTTL: { // seconds
|
||||
type: Number,
|
||||
default: 7200,
|
||||
required: true
|
||||
},
|
||||
scopes: {
|
||||
type: [
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AuthMode } from "../../variables";
|
||||
router.get(
|
||||
"/raw",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
secretsController.getSecretsRaw
|
||||
);
|
||||
@@ -15,7 +15,7 @@ router.get(
|
||||
router.get(
|
||||
"/raw/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "query"
|
||||
@@ -29,7 +29,7 @@ router.get(
|
||||
router.post(
|
||||
"/raw/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
@@ -43,7 +43,7 @@ router.post(
|
||||
router.patch(
|
||||
"/raw/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
@@ -57,7 +57,7 @@ router.patch(
|
||||
router.delete(
|
||||
"/raw/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
@@ -71,7 +71,7 @@ router.delete(
|
||||
router.get(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "query"
|
||||
@@ -116,7 +116,7 @@ router.delete(
|
||||
router.post(
|
||||
"/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
@@ -127,7 +127,7 @@ router.post(
|
||||
router.get(
|
||||
"/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "query"
|
||||
@@ -138,7 +138,7 @@ router.get(
|
||||
router.patch(
|
||||
"/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
@@ -149,7 +149,7 @@ router.patch(
|
||||
router.delete(
|
||||
"/:secretName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
|
||||
@@ -60,6 +60,12 @@ import { checkIPAgainstBlocklist } from "../utils/ip";
|
||||
}
|
||||
};
|
||||
|
||||
export const RefreshTokenV3 = z.object({
|
||||
body: z.object({
|
||||
refresh_token: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
export const CreateServiceTokenV3 = z.object({
|
||||
body: z.object({
|
||||
name: z.string().trim(),
|
||||
@@ -80,8 +86,10 @@ export const CreateServiceTokenV3 = z.object({
|
||||
.array()
|
||||
.min(1),
|
||||
expiresIn: z.number().optional(),
|
||||
accessTokenTTL: z.number().int().min(1),
|
||||
encryptedKey: z.string().trim(),
|
||||
nonce: z.string().trim()
|
||||
nonce: z.string().trim(),
|
||||
isRefreshTokenRotationEnabled: z.boolean().default(false)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -108,7 +116,9 @@ export const UpdateServiceTokenV3 = z.object({
|
||||
.array()
|
||||
.min(1)
|
||||
.optional(),
|
||||
expiresIn: z.number().optional()
|
||||
expiresIn: z.number().optional(),
|
||||
accessTokenTTL: z.number().int().min(1).optional(),
|
||||
isRefreshTokenRotationEnabled: z.boolean().optional()
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
// TODO: merge [AuthTokenType] and [AuthMode]
|
||||
|
||||
export enum AuthTokenType {
|
||||
ACCESS_TOKEN = "accessToken",
|
||||
REFRESH_TOKEN = "refreshToken",
|
||||
SIGNUP_TOKEN = "signupToken",
|
||||
MFA_TOKEN = "mfaToken",
|
||||
PROVIDER_TOKEN = "providerToken",
|
||||
API_KEY = "apiKey"
|
||||
SIGNUP_TOKEN = "signupToken", // TODO: remove in favor of claim
|
||||
MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim
|
||||
PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim
|
||||
API_KEY = "apiKey",
|
||||
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||
SERVICE_REFRESH_TOKEN = "serviceRefreshToken"
|
||||
}
|
||||
|
||||
export enum AuthMode {
|
||||
JWT = "jwt",
|
||||
SERVICE_TOKEN = "serviceToken",
|
||||
SERVICE_TOKEN_V3 = "serviceTokenV3",
|
||||
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||
API_KEY = "apiKey"
|
||||
}
|
||||
|
||||
|
||||
@@ -88,14 +88,18 @@ export const useUpdateServiceTokenV3 = () => {
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
}) => {
|
||||
const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, {
|
||||
name,
|
||||
isActive,
|
||||
scopes,
|
||||
trustedIps,
|
||||
expiresIn
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
});
|
||||
|
||||
return serviceTokenData;
|
||||
|
||||
@@ -56,11 +56,15 @@ export type ServiceTokenDataV3 = {
|
||||
name: string;
|
||||
workspace: string;
|
||||
isActive: boolean;
|
||||
lastUsed?: string;
|
||||
usageCount: number;
|
||||
refreshTokenLastUsed?: string;
|
||||
accessTokenLastUsed?: string;
|
||||
refreshTokenUsageCount: number;
|
||||
accessTokenUsageCount: number;
|
||||
scopes: ServiceTokenV3Scope[];
|
||||
trustedIps: ServiceTokenV3TrustedIp[];
|
||||
expiresAt?: string;
|
||||
accessTokenTTL: number;
|
||||
isRefreshTokenRotationEnabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
@@ -74,12 +78,14 @@ export type CreateServiceTokenDataV3DTO = {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
expiresIn?: number;
|
||||
accessTokenTTL: number;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
isRefreshTokenRotationEnabled: boolean;
|
||||
}
|
||||
|
||||
export type CreateServiceTokenDataV3Res = {
|
||||
serviceToken: string;
|
||||
refreshToken: string;
|
||||
serviceTokenData: ServiceTokenDataV3;
|
||||
}
|
||||
|
||||
@@ -92,6 +98,8 @@ export type UpdateServiceTokenDataV3DTO = {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
expiresIn?: number;
|
||||
accessTokenTTL?: number;
|
||||
isRefreshTokenRotationEnabled?: boolean;
|
||||
}
|
||||
|
||||
export type DeleteServiceTokenDataV3DTO = {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Switch,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
@@ -42,7 +43,7 @@ import {
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const expirations = [
|
||||
{ label: "Never", value: undefined },
|
||||
{ label: "Never", value: "" },
|
||||
{ label: "1 day", value: "86400" },
|
||||
{ label: "7 days", value: "604800" },
|
||||
{ label: "1 month", value: "2592000" },
|
||||
@@ -60,6 +61,13 @@ const permissionsMap: {
|
||||
const schema = yup.object({
|
||||
name: yup.string().required("ST V3 name is required"),
|
||||
expiresIn: yup.string(),
|
||||
accessTokenTTL: yup
|
||||
.string()
|
||||
.test("is-positive-integer", "Access Token TTL must be a positive integer", (value) => {
|
||||
const num = parseInt(value, 10);
|
||||
return !Number.isNaN(num) && num > 0 && String(num) === value;
|
||||
})
|
||||
.required("Access Token TTL is required"),
|
||||
scopes: yup
|
||||
.array(
|
||||
yup.object({
|
||||
@@ -79,14 +87,15 @@ const schema = yup.object({
|
||||
.required()
|
||||
.label("Scope"),
|
||||
trustedIps: yup
|
||||
.array(
|
||||
yup.object({
|
||||
ipAddress: yup.string().max(50).required().label("IP Address")
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.required()
|
||||
.label("Trusted IP")
|
||||
.array(
|
||||
yup.object({
|
||||
ipAddress: yup.string().max(50).required().label("IP Address")
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
.required()
|
||||
.label("Trusted IP"),
|
||||
isRefreshTokenRotationEnabled: yup.boolean().default(false)
|
||||
}).required();
|
||||
|
||||
export type FormData = yup.InferType<typeof schema>;
|
||||
@@ -118,6 +127,7 @@ export const AddServiceTokenV3Modal = ({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
accessTokenTTL: "7200",
|
||||
scopes: [{
|
||||
permission: "read",
|
||||
environment: currentWorkspace?.environments?.[0]?.slug,
|
||||
@@ -135,6 +145,8 @@ export const AddServiceTokenV3Modal = ({
|
||||
name: string;
|
||||
scopes: ServiceTokenV3Scope[];
|
||||
trustedIps: ServiceTokenV3TrustedIp[];
|
||||
accessTokenTTL: number;
|
||||
isRefreshTokenRotationEnabled: boolean;
|
||||
};
|
||||
|
||||
if (serviceTokenData) {
|
||||
@@ -163,11 +175,14 @@ export const AddServiceTokenV3Modal = ({
|
||||
return ({
|
||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||
});
|
||||
})
|
||||
}),
|
||||
accessTokenTTL: String(serviceTokenData.accessTokenTTL),
|
||||
isRefreshTokenRotationEnabled: serviceTokenData.isRefreshTokenRotationEnabled
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
name: "",
|
||||
accessTokenTTL: "7200",
|
||||
scopes: [{
|
||||
permission: "read",
|
||||
environment: currentWorkspace?.environments?.[0]?.slug,
|
||||
@@ -186,8 +201,10 @@ export const AddServiceTokenV3Modal = ({
|
||||
const onFormSubmit = async ({
|
||||
name,
|
||||
expiresIn,
|
||||
accessTokenTTL,
|
||||
scopes,
|
||||
trustedIps
|
||||
trustedIps,
|
||||
isRefreshTokenRotationEnabled
|
||||
}: FormData) => {
|
||||
try {
|
||||
const serviceTokenData = popUp?.serviceTokenV3?.data as {
|
||||
@@ -213,7 +230,9 @@ export const AddServiceTokenV3Modal = ({
|
||||
name,
|
||||
scopes: reformattedScopes,
|
||||
trustedIps,
|
||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn)
|
||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
isRefreshTokenRotationEnabled
|
||||
});
|
||||
} else {
|
||||
// create
|
||||
@@ -239,21 +258,23 @@ export const AddServiceTokenV3Modal = ({
|
||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||
});
|
||||
|
||||
const { serviceToken } = await createMutateAsync({
|
||||
const { refreshToken } = await createMutateAsync({
|
||||
name,
|
||||
workspaceId: currentWorkspace._id,
|
||||
publicKey,
|
||||
scopes: reformattedScopes,
|
||||
trustedIps,
|
||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
nonce,
|
||||
isRefreshTokenRotationEnabled
|
||||
});
|
||||
|
||||
const downloadData = {
|
||||
publicKey,
|
||||
privateKey,
|
||||
serviceToken
|
||||
refreshToken
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(downloadData, null, 2)], { type: "application/json" });
|
||||
@@ -476,10 +497,10 @@ export const AddServiceTokenV3Modal = ({
|
||||
<Controller
|
||||
control={control}
|
||||
name="expiresIn"
|
||||
defaultValue="15552000"
|
||||
defaultValue=""
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Expire In`}
|
||||
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Refresh Token Expires In`}
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="mt-4"
|
||||
@@ -499,6 +520,38 @@ export const AddServiceTokenV3Modal = ({
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="7200"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="7200"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 mb-[2.36rem]">
|
||||
<Controller
|
||||
control={control}
|
||||
name="isRefreshTokenRotationEnabled"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
id="label-refresh-token-rotation"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
>
|
||||
Refresh Token Rotation
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
|
||||
@@ -34,6 +34,8 @@ type Props = {
|
||||
name?: string;
|
||||
scopes?: ServiceTokenV3Scope[];
|
||||
trustedIps?: ServiceTokenV3TrustedIp[];
|
||||
accessTokenTTL?: number;
|
||||
isRefreshTokenRotationEnabled?: boolean;
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
@@ -81,10 +83,9 @@ export const ServiceTokenV3Table = ({
|
||||
<Th>Status</Th>
|
||||
<Th>Scopes</Th>
|
||||
<Th>Trusted IPs</Th>
|
||||
{/* <Th># Times Used</Th> */}
|
||||
<Th>Last Used</Th>
|
||||
<Th>Access Token TTL</Th>
|
||||
<Th>Created At</Th>
|
||||
<Th>Expires At</Th>
|
||||
<Th>Valid Until</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
@@ -97,12 +98,12 @@ export const ServiceTokenV3Table = ({
|
||||
_id,
|
||||
name,
|
||||
isActive,
|
||||
lastUsed,
|
||||
// usageCount,
|
||||
scopes,
|
||||
trustedIps,
|
||||
createdAt,
|
||||
expiresAt
|
||||
expiresAt,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
}) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`st-v3-${_id}`}>
|
||||
@@ -160,8 +161,7 @@ export const ServiceTokenV3Table = ({
|
||||
);
|
||||
})}
|
||||
</Td>
|
||||
{/* <Td>{usageCount}</Td> */}
|
||||
<Td>{lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"}</Td>
|
||||
<Td>{accessTokenTTL}</Td>
|
||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||
<Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td>
|
||||
<Td className="flex justify-end">
|
||||
@@ -176,7 +176,9 @@ export const ServiceTokenV3Table = ({
|
||||
serviceTokenDataId: _id,
|
||||
name,
|
||||
scopes,
|
||||
trustedIps
|
||||
trustedIps,
|
||||
accessTokenTTL,
|
||||
isRefreshTokenRotationEnabled
|
||||
});
|
||||
}}
|
||||
size="lg"
|
||||
|
||||
Reference in New Issue
Block a user