mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #3242 from akhilmhdh/feat/metadata-oidc
Feat/metadata OIDC
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
import "@fastify/request-context";
|
||||
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
reqId: string;
|
||||
}
|
||||
}
|
||||
6
backend/src/@types/fastify.d.ts
vendored
6
backend/src/@types/fastify.d.ts
vendored
@@ -100,6 +100,12 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
|
||||
declare module "@fastify/request-context" {
|
||||
interface RequestContextData {
|
||||
reqId: string;
|
||||
identityAuthInfo?: {
|
||||
identityId: string;
|
||||
oidc?: {
|
||||
claims: Record<string, string>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
|
||||
if (!hasMappingField) {
|
||||
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.jsonb("claimMetadataMapping");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasMappingField = await knex.schema.hasColumn(TableName.IdentityOidcAuth, "claimMetadataMapping");
|
||||
if (hasMappingField) {
|
||||
await knex.schema.alterTable(TableName.IdentityOidcAuth, (t) => {
|
||||
t.dropColumn("claimMetadataMapping");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,8 @@ export const IdentityOidcAuthsSchema = z.object({
|
||||
boundSubject: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedCaCertificate: zodBuffer.nullable().optional()
|
||||
encryptedCaCertificate: zodBuffer.nullable().optional(),
|
||||
claimMetadataMapping: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
export type TIdentityOidcAuths = z.infer<typeof IdentityOidcAuthsSchema>;
|
||||
|
||||
@@ -12,7 +12,6 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const SecretSharingSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedValue: z.string().nullable().optional(),
|
||||
type: z.string(),
|
||||
iv: z.string().nullable().optional(),
|
||||
tag: z.string().nullable().optional(),
|
||||
hashedHex: z.string().nullable().optional(),
|
||||
@@ -27,7 +26,8 @@ export const SecretSharingSchema = z.object({
|
||||
lastViewedAt: z.date().nullable().optional(),
|
||||
password: z.string().nullable().optional(),
|
||||
encryptedSecret: zodBuffer.nullable().optional(),
|
||||
identifier: z.string().nullable().optional()
|
||||
identifier: z.string().nullable().optional(),
|
||||
type: z.string().default("share")
|
||||
});
|
||||
|
||||
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||
|
||||
@@ -978,6 +978,7 @@ interface AddIdentityOidcAuthEvent {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@@ -1002,6 +1003,7 @@ interface UpdateIdentityOidcAuthEvent {
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import { MongoQuery } from "@ucast/mongo2js";
|
||||
import handlebars from "handlebars";
|
||||
|
||||
@@ -317,14 +318,17 @@ export const permissionServiceFactory = ({
|
||||
|
||||
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
|
||||
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
|
||||
objectify(
|
||||
identityProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
)
|
||||
);
|
||||
|
||||
const identityAuthInfo = requestContext.get("identityAuthInfo");
|
||||
const unescapedMetadata = objectify(
|
||||
identityProjectPermission.metadata,
|
||||
(i) => i.key,
|
||||
(i) => i.value
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as Record<string, any>;
|
||||
if (identityAuthInfo?.identityId === identityId && identityAuthInfo) {
|
||||
unescapedMetadata.auth = identityAuthInfo;
|
||||
}
|
||||
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(unescapedMetadata);
|
||||
const interpolateRules = templatedRules(
|
||||
{
|
||||
identity: {
|
||||
|
||||
@@ -329,6 +329,7 @@ export const OIDC_AUTH = {
|
||||
boundIssuer: "The unique identifier of the identity provider issuing the JWT.",
|
||||
boundAudiences: "The list of intended recipients.",
|
||||
boundClaims: "The attributes that should be present in the JWT for it to be valid.",
|
||||
claimMetadataMapping: "The attributes that should be present in the permission metadata from the JWT.",
|
||||
boundSubject: "The expected principal that is the subject of the JWT.",
|
||||
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
|
||||
accessTokenTTL: "The lifetime for an access token in seconds.",
|
||||
@@ -342,6 +343,7 @@ export const OIDC_AUTH = {
|
||||
boundIssuer: "The new unique identifier of the identity provider issuing the JWT.",
|
||||
boundAudiences: "The new list of intended recipients.",
|
||||
boundClaims: "The new attributes that should be present in the JWT for it to be valid.",
|
||||
claimMetadataMapping: "The new attributes that should be present in the permission metadata from the JWT.",
|
||||
boundSubject: "The new expected principal that is the subject of the JWT.",
|
||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
|
||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||
|
||||
34
backend/src/lib/template/dot-access.ts
Normal file
34
backend/src/lib/template/dot-access.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Safely retrieves a value from a nested object using dot notation path
|
||||
*/
|
||||
export const getStringValueByDot = (
|
||||
obj: Record<string, unknown> | null | undefined,
|
||||
path: string,
|
||||
defaultValue?: string
|
||||
): string | undefined => {
|
||||
// Handle null or undefined input
|
||||
if (!obj) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const parts = path.split(".");
|
||||
let current: unknown = obj;
|
||||
|
||||
for (const part of parts) {
|
||||
const isObject = typeof current === "object" && !Array.isArray(current) && current !== null;
|
||||
if (!isObject) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (!Object.hasOwn(current as object, part)) {
|
||||
// Check if the property exists as an own property
|
||||
return defaultValue;
|
||||
}
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
|
||||
if (typeof current !== "string") {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requestContext } from "@fastify/request-context";
|
||||
import { FastifyRequest } from "fastify";
|
||||
import fp from "fastify-plugin";
|
||||
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||
@@ -137,6 +138,12 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||
identityName: identity.name,
|
||||
authMethod: null
|
||||
};
|
||||
if (token?.identityAuth?.oidc) {
|
||||
requestContext.set("identityAuthInfo", {
|
||||
identityId: identity.identityId,
|
||||
oidc: token?.identityAuth?.oidc
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AuthMode.SERVICE_TOKEN: {
|
||||
|
||||
@@ -23,6 +23,7 @@ const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({
|
||||
boundIssuer: true,
|
||||
boundAudiences: true,
|
||||
boundClaims: true,
|
||||
claimMetadataMapping: true,
|
||||
boundSubject: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
@@ -104,6 +105,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
boundIssuer: z.string().min(1).describe(OIDC_AUTH.ATTACH.boundIssuer),
|
||||
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.ATTACH.boundAudiences),
|
||||
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.boundClaims),
|
||||
claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.ATTACH.claimMetadataMapping).optional(),
|
||||
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.ATTACH.boundSubject),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
@@ -161,6 +163,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
boundIssuer: identityOidcAuth.boundIssuer,
|
||||
boundAudiences: identityOidcAuth.boundAudiences,
|
||||
boundClaims: identityOidcAuth.boundClaims as Record<string, string>,
|
||||
claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record<string, string>,
|
||||
boundSubject: identityOidcAuth.boundSubject as string,
|
||||
accessTokenTTL: identityOidcAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
|
||||
@@ -200,6 +203,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
boundIssuer: z.string().min(1).describe(OIDC_AUTH.UPDATE.boundIssuer),
|
||||
boundAudiences: validateOidcAuthAudiencesField.describe(OIDC_AUTH.UPDATE.boundAudiences),
|
||||
boundClaims: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.boundClaims),
|
||||
claimMetadataMapping: validateOidcBoundClaimsField.describe(OIDC_AUTH.UPDATE.claimMetadataMapping).optional(),
|
||||
boundSubject: z.string().optional().default("").describe(OIDC_AUTH.UPDATE.boundSubject),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
@@ -258,6 +262,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
boundIssuer: identityOidcAuth.boundIssuer,
|
||||
boundAudiences: identityOidcAuth.boundAudiences,
|
||||
boundClaims: identityOidcAuth.boundClaims as Record<string, string>,
|
||||
claimMetadataMapping: identityOidcAuth.claimMetadataMapping as Record<string, string>,
|
||||
boundSubject: identityOidcAuth.boundSubject as string,
|
||||
accessTokenTTL: identityOidcAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
|
||||
|
||||
@@ -7,4 +7,9 @@ export type TIdentityAccessTokenJwtPayload = {
|
||||
clientSecretId: string;
|
||||
identityAccessTokenId: string;
|
||||
authTokenType: string;
|
||||
identityAuth: {
|
||||
oidc?: {
|
||||
claims: Record<string, string>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
@@ -177,8 +178,9 @@ export const identityJwtAuthServiceFactory = ({
|
||||
if (identityJwtAuth.boundClaims) {
|
||||
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
|
||||
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
|
||||
if (!tokenData[claimKey]) {
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: token has no ${claimKey} field`
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
@@ -77,7 +78,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
|
||||
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
|
||||
{
|
||||
httpsAgent: requestAgent
|
||||
httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
|
||||
}
|
||||
);
|
||||
const jwksUri = discoveryDoc.jwks_uri;
|
||||
@@ -91,7 +92,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
|
||||
const client = new JwksClient({
|
||||
jwksUri,
|
||||
requestAgent
|
||||
requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
|
||||
});
|
||||
|
||||
const { kid } = decodedToken.header;
|
||||
@@ -108,7 +109,6 @@ export const identityOidcAuthServiceFactory = ({
|
||||
message: `Access denied: ${error.message}`
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -135,10 +135,16 @@ export const identityOidcAuthServiceFactory = ({
|
||||
if (identityOidcAuth.boundClaims) {
|
||||
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
|
||||
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: token has no ${claimKey} field`
|
||||
});
|
||||
}
|
||||
|
||||
// handle both single and multi-valued claims
|
||||
if (
|
||||
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry))
|
||||
) {
|
||||
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(value, claimEntry))) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: OIDC claim not allowed."
|
||||
});
|
||||
@@ -146,6 +152,20 @@ export const identityOidcAuthServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const filteredClaims: Record<string, string> = {};
|
||||
if (identityOidcAuth.claimMetadataMapping) {
|
||||
Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
|
||||
const claimKey = (identityOidcAuth.claimMetadataMapping as Record<string, string>)[permissionKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: token has no ${claimKey} field`
|
||||
});
|
||||
}
|
||||
filteredClaims[permissionKey] = value;
|
||||
});
|
||||
}
|
||||
|
||||
const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
@@ -167,7 +187,12 @@ export const identityOidcAuthServiceFactory = ({
|
||||
{
|
||||
identityId: identityOidcAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
|
||||
identityAuth: {
|
||||
oidc: {
|
||||
claims: filteredClaims
|
||||
}
|
||||
}
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||
@@ -188,6 +213,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
@@ -254,6 +280,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
@@ -274,6 +301,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
@@ -335,6 +363,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
|
||||
@@ -7,6 +7,7 @@ export type TAttachOidcAuthDTO = {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@@ -21,6 +22,7 @@ export type TUpdateOidcAuthDTO = {
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { Identity } from "@app/hooks/api/identities/types";
|
||||
|
||||
import { User } from "../types";
|
||||
import {
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
TGetServerRootKmsEncryptionDetails,
|
||||
TServerConfig
|
||||
} from "./types";
|
||||
import { Identity } from "@app/hooks/api/identities/types";
|
||||
|
||||
export const adminStandaloneKeys = {
|
||||
getUsers: "get-users",
|
||||
|
||||
@@ -462,6 +462,7 @@ export const useUpdateIdentityOidcAuth = () => {
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject
|
||||
}) => {
|
||||
const {
|
||||
@@ -478,7 +479,8 @@ export const useUpdateIdentityOidcAuth = () => {
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
accessTokenTrustedIps,
|
||||
claimMetadataMapping
|
||||
}
|
||||
);
|
||||
|
||||
@@ -504,6 +506,7 @@ export const useAddIdentityOidcAuth = () => {
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
@@ -524,7 +527,8 @@ export const useAddIdentityOidcAuth = () => {
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
accessTokenTrustedIps,
|
||||
claimMetadataMapping
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ export type IdentityOidcAuth = {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@@ -208,6 +209,7 @@ export type AddIdentityOidcAuthDTO = {
|
||||
boundIssuer: string;
|
||||
boundAudiences: string;
|
||||
boundClaims: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
@@ -225,6 +227,7 @@ export type UpdateIdentityOidcAuthDTO = {
|
||||
boundIssuer?: string;
|
||||
boundAudiences?: string;
|
||||
boundClaims?: Record<string, string>;
|
||||
claimMetadataMapping?: Record<string, string>;
|
||||
boundSubject?: string;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
|
||||
@@ -28,13 +28,13 @@ import {
|
||||
useGetServerRootKmsEncryptionDetails,
|
||||
useUpdateServerConfig
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
||||
|
||||
import { AuthPanel } from "./components/AuthPanel";
|
||||
import { EncryptionPanel } from "./components/EncryptionPanel";
|
||||
import { IntegrationPanel } from "./components/IntegrationPanel";
|
||||
import { RateLimitPanel } from "./components/RateLimitPanel";
|
||||
import { UserPanel } from "./components/UserPanel";
|
||||
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
||||
|
||||
enum TabSections {
|
||||
Settings = "settings",
|
||||
|
||||
@@ -52,6 +52,15 @@ const schema = z.object({
|
||||
value: z.string()
|
||||
})
|
||||
),
|
||||
claimMetadataMapping: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.nullable(),
|
||||
boundSubject: z.string().optional().default("")
|
||||
});
|
||||
|
||||
@@ -99,7 +108,6 @@ export const IdentityOidcAuthForm = ({
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
fields: boundClaimsFields,
|
||||
append: appendBoundClaimField,
|
||||
@@ -109,6 +117,15 @@ export const IdentityOidcAuthForm = ({
|
||||
name: "boundClaims"
|
||||
});
|
||||
|
||||
const {
|
||||
fields: claimMetadataMappingFields,
|
||||
append: appendClaimMetadataMappingField,
|
||||
remove: removeClaimMetadataMappingField
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: "claimMetadataMapping"
|
||||
});
|
||||
|
||||
const {
|
||||
fields: accessTokenTrustedIpsFields,
|
||||
append: appendAccessTokenTrustedIp,
|
||||
@@ -126,6 +143,12 @@ export const IdentityOidcAuthForm = ({
|
||||
key,
|
||||
value
|
||||
})),
|
||||
claimMetadataMapping: data?.claimMetadataMapping
|
||||
? Object.entries(data.claimMetadataMapping).map(([key, value]) => ({
|
||||
key,
|
||||
value
|
||||
}))
|
||||
: undefined,
|
||||
boundSubject: data.boundSubject,
|
||||
accessTokenTTL: String(data.accessTokenTTL),
|
||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||
@@ -164,6 +187,7 @@ export const IdentityOidcAuthForm = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
claimMetadataMapping,
|
||||
boundSubject
|
||||
}: FormData) => {
|
||||
try {
|
||||
@@ -180,6 +204,9 @@ export const IdentityOidcAuthForm = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
|
||||
claimMetadataMapping: claimMetadataMapping
|
||||
? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value]))
|
||||
: undefined,
|
||||
boundSubject,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
@@ -194,6 +221,9 @@ export const IdentityOidcAuthForm = ({
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims: Object.fromEntries(boundClaims.map((entry) => [entry.key, entry.value])),
|
||||
claimMetadataMapping: claimMetadataMapping
|
||||
? Object.fromEntries(claimMetadataMapping.map((entry) => [entry.key, entry.value]))
|
||||
: undefined,
|
||||
boundSubject,
|
||||
organizationId: orgId,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
@@ -449,6 +479,84 @@ export const IdentityOidcAuthForm = ({
|
||||
Add Claims
|
||||
</Button>
|
||||
</div>
|
||||
{claimMetadataMappingFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`claimMetadataMapping.${index}.key`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Claim Metadata Mapping" : undefined}
|
||||
icon={
|
||||
index === 0 ? (
|
||||
<Tooltip
|
||||
className="text-center"
|
||||
content="Map token claims to metadata. This can later be accessed in attribute based permission as identity.metadata.oidc.claims.<>."
|
||||
>
|
||||
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="property"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`claimMetadataMapping.${index}.value`}
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => field.onChange(e)}
|
||||
placeholder="key1.nested-key2"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => removeClaimMetadataMappingField(index)}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() =>
|
||||
appendClaimMetadataMappingField({
|
||||
key: "",
|
||||
value: ""
|
||||
})
|
||||
}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add Mapping
|
||||
</Button>
|
||||
</div>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
|
||||
@@ -111,6 +111,26 @@ export const ViewIdentityOidcAuthContent = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay className="col-span-2" label="Claim Metadata Mapping">
|
||||
{data.claimMetadataMapping && Object.keys(data.claimMetadataMapping).length && (
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-2"
|
||||
content={
|
||||
<pre className="whitespace-pre-wrap rounded bg-mineshaft-600 p-2">
|
||||
{JSON.stringify(data.claimMetadataMapping, null, 2)}
|
||||
</pre>
|
||||
}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</IdentityAuthFieldDisplay>
|
||||
</ViewIdentityContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -306,7 +306,10 @@ export const SecretItem = memo(
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div key="actions" className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2">
|
||||
<div
|
||||
key="actions"
|
||||
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
|
||||
>
|
||||
<Tooltip content="Copy secret">
|
||||
<IconButton
|
||||
isDisabled={secret.secretValueHidden}
|
||||
|
||||
87
sink/oidc-server/main.js
Normal file
87
sink/oidc-server/main.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import Provider from "oidc-provider";
|
||||
import express from "express";
|
||||
|
||||
const configuration = {
|
||||
jwks: {
|
||||
keys: [
|
||||
{
|
||||
kty: "RSA",
|
||||
use: "sig",
|
||||
alg: "RS256",
|
||||
d: "EF2Kky61jzvMYQ_B6ImXzCsQ8uQzbFJrGnB2azlpr_CFStjjUVKP4EKrSCVEasD6SGNJV2QSiNJr7j05nvuGmHMKa__rbU8fqP4qbDahUgCgWOq-zS5tGK6Ifk4II_cZ_V1F-TnrvmcOKMWBiSV-p8i72KpXXucbHGNRwASVs7--M55wp_m1UsybI2jSQ4IgyvGzTnvMmQ_GsX-XoD8u0zGU_4eN3DGc8l6hdxxuSymH0fEeL1Aj0LoCj6teRGF37a2sBQdU6mkNNAuyyirkoDqGZCGJToQLqX4F1FafnzjeIgfdneRa-vuaV380Hhr2rorWnQyBqOO27M5O_VAkJbfRaWJVrXTJ69ZgkU4GPdeYdklVL0HkU6laziTNqNMeAjnt4m51sWokVyJpvdWcb_vJ4NSCsRo7kHOz7g-UvWTXa8UW0DTDliq_TJ3rN4Gv0vn9tBlFfaeuLPpK4VNmRRDRXY_fcuzlnQwYExL9a4V_vCyGmabdb7PrUFPBcjR5",
|
||||
dp: "SX52TkZEc_eLIk5gYrKjAC643LJIw1RxMBWWewRSGLn_rbrH1he3hy7AGDUV6Uon7zkNh9R5GBVuxmlluBRAGbrhIXAAf8sWeyma3F6FIAt-MH_VkfW5K2p88PLOyVGljlv8-Z3wzdKYOlDP4yFU18LqGMqaRSDLDGhILkuZhjLYA40sfYJeJTi_HVP5UyWL4ohayqUWCT2W3DgeDDThYHmufOaqlrSLhUst6uez_cDz0BXAYIZvUuPVL_n1-_px",
|
||||
dq: "K1KYU77I6yyPA2u32rc0exp_TCG59hhpWxrmXN8yTXWyq_xYBhCJA_nHdY8UV25Hmd7q0iX2i8y2cCAFNWA5UWiSiNg9-fKRLI2nz53IM4dGfssOLwUk66wzX8r_u3XiLZsO7XNNtQZdcZmF0YuNTtzEdiNDhaOyHiwwHgShL36WNmUn00mZR__G5Qk60VvI8vsbvJU9xRnWuEVS1wRgyD7v6Nl9nIxb8N7oibCdTJLmgnRXPWvArsW0cJ-NURfr",
|
||||
e: "AQAB",
|
||||
n: "2QwX-NBMkQYedGpbPvHL7Ca0isvfmLC7lSc8XSOCLmCUIf6Bk_pdCNx2kxsmT81IoA8CfvJLHQj5vWKoVDFMLfwo4IujvsC3m2IrEg6jERE-YHfC3W5jKZtmzQYpfx5vC2_XTmcyPigtyaNVsftGfycES3B_tvphNsFmQcJjVGOsJQXXqh_TDv6FMcH4m9pngyw6wfe3GgAKA0dRTSfD0h7wLdNCeuid53lLpkQypTNdZ6_PiCMu2gr_cH5M0MPZtBb2TW12_2zOabExK1lI5-HvdPtbMT4Qzs2nd2NkjcWmlbKRZzq6IzyWt7W2EnfZDsi61PHECtTb-EQN2icl8Wnsp-0Bw66yviAOj0gn3X5hRLx-TknT_PnWMou17l5GoAojKDezcTW0iLlrfs2ixFlY28u7WklUN8uYhHvwgON6fsdefG-3bPpiRLBPZ_tgXa4doALsCwfXu2oz0vYktk31A-UYv92uJsKSUbK0_8ODTN0rslCqCYN_1a_aVt2P",
|
||||
p: "--L5BX8juLlGJk8hdPgEUmJjD7SsZuMrdq3cSibkkbaWUE5CQQ7vhLPr2dWCS1jUnY9WyoCx9QCZvhTHjORX50ykkOyBso9VJjWvYPjsrPpF7_Y6V0dKlblDmbbmRT9BW-MgjbwTivu3c2OpMXh2XLF-FOTq3t3Brs7SRnhTkD6GBDFf3X95J0PF7NELa9z2-kzPSDYz3k-9FepXnRPBM_ViDzlRw4eKUdylVuhzGbC2TRSmab9BRP0wipQKd-f5",
|
||||
q: "3Jd5CRJpQV3xUi3FiHHAwcjfsRkfXMrxfaXt0PjX2xWzxscYiDcyCF6VhHTAGsiq5SOtCp3l5mg6A9PzdR53AzM2-706D82fMwiUZvsLOVTepXkgriP_xw7rDlkOeAvjB80sL2G9scFliTzzRZ8I8E79A8DxZihfB75AIN9ijklEihnwxfhp2EgO5MYEyQRcqU1TT8wD8ekLMzd-kJUWyTz3BogiVJH__BQoB6kaDyjvQoxBgwh0hi72t9H5XqPH",
|
||||
qi: "cwK0jhzwbu8BaTmTQhwfGiqwNN3v9F4nUQ4dtnBYRI6zlki4cLb2Mf9-VhyEsUYhhdTm8R7RwO9m5Xct3gEfozdk35wuvkVwkZgL3Uho5asao0xi4aENeUk5DCkU-paO3yLSDhIs9YYuYIDjUX6QuMCPjomypuE3SRm-Dg1PGOxYvX3w_P-0kd5iBFrm4jwGTZViFOr8tl_dXgDRDWDgofOYOYcmUv2_0zt1aO3j5dhEpwdkyuDMLfVZNpJQyopJ",
|
||||
kid: "f262a3214213d194c92991d6735b153b",
|
||||
},
|
||||
],
|
||||
},
|
||||
features: {
|
||||
clientCredentials: {
|
||||
enabled: true,
|
||||
},
|
||||
introspection: {
|
||||
enabled: true,
|
||||
},
|
||||
resourceIndicators: {
|
||||
enabled: true,
|
||||
getResourceServerInfo(ctx, resourceIndicator) {
|
||||
if (resourceIndicator === "urn:api") {
|
||||
return {
|
||||
scope: "read",
|
||||
audience: "urn:api",
|
||||
accessTokenTTL: 1 * 60 * 60, // 1 hour
|
||||
accessTokenFormat: "jwt",
|
||||
};
|
||||
}
|
||||
|
||||
throw new errors.InvalidTarget();
|
||||
},
|
||||
},
|
||||
},
|
||||
clients: [
|
||||
{
|
||||
client_id: "app",
|
||||
client_secret: "a_secret",
|
||||
grant_types: ["client_credentials"],
|
||||
redirect_uris: [],
|
||||
response_types: [],
|
||||
},
|
||||
{
|
||||
client_id: "oidc_client",
|
||||
client_secret: "a_different_secret",
|
||||
grant_types: ["authorization_code"],
|
||||
response_types: ["code"],
|
||||
redirect_uris: ["http://localhost:3001/cb"],
|
||||
},
|
||||
],
|
||||
claims: {
|
||||
profile: [
|
||||
"birthdate",
|
||||
"family_name",
|
||||
"gender",
|
||||
"given_name",
|
||||
"locale",
|
||||
"middle_name",
|
||||
"name",
|
||||
"nickname",
|
||||
"picture",
|
||||
"preferred_username",
|
||||
"profile",
|
||||
"updated_at",
|
||||
"website",
|
||||
"zoneinfo",
|
||||
],
|
||||
email: ["email", "email_verified"],
|
||||
},
|
||||
};
|
||||
|
||||
const oidc = new Provider("http://localhost:3000", configuration);
|
||||
|
||||
const app = express();
|
||||
app.use("/oidc", oidc.callback());
|
||||
app.listen(3000);
|
||||
1708
sink/oidc-server/package-lock.json
generated
Normal file
1708
sink/oidc-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
sink/oidc-server/package.json
Normal file
21
sink/oidc-server/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "oidc-server",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node main.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"form-data": "^4.0.2",
|
||||
"jose": "^6.0.10",
|
||||
"oidc-provider": "^8.8.1"
|
||||
}
|
||||
}
|
||||
80
sink/oidc-server/test-infisical.js
Normal file
80
sink/oidc-server/test-infisical.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import axios from "axios";
|
||||
import { Buffer } from "buffer";
|
||||
import querystring from "querystring";
|
||||
|
||||
// Configuration
|
||||
const config = {
|
||||
issuer: "http://localhost:3000/oidc",
|
||||
tokenEndpoint: "http://localhost:3000/oidc/token",
|
||||
clientId: "app",
|
||||
clientSecret: "a_secret",
|
||||
};
|
||||
|
||||
// Client credentials flow for machine identity
|
||||
async function getMachineToken() {
|
||||
try {
|
||||
// Use application/x-www-form-urlencoded format as required by the OIDC spec
|
||||
const data = querystring.stringify({
|
||||
grant_type: "client_credentials",
|
||||
scope: "read",
|
||||
resource: "urn:api",
|
||||
});
|
||||
|
||||
const authHeader =
|
||||
"Basic " +
|
||||
Buffer.from(`${config.clientId}:${config.clientSecret}`).toString(
|
||||
"base64",
|
||||
);
|
||||
|
||||
const response = await axios.post(config.tokenEndpoint, data, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization: authHeader,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Successfully obtained token:");
|
||||
console.log("Access Token:", response.data.access_token);
|
||||
console.log("Token Type:", response.data.token_type);
|
||||
console.log("Expires In:", response.data.expires_in, "seconds");
|
||||
console.log("Scope:", response.data.scope);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error obtaining token:");
|
||||
if (error.response && error.response.data) {
|
||||
console.error(error.response.data);
|
||||
} else {
|
||||
console.error(error.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Test the machine identity authentication
|
||||
async function testMachineIdentity() {
|
||||
try {
|
||||
// Get token using client credentials
|
||||
const token = await getMachineToken();
|
||||
|
||||
const loginData = querystring.stringify({
|
||||
identityId: "5d81d5cc-602f-4af7-b242-ab7c1331b430",
|
||||
jwt: token.access_token,
|
||||
});
|
||||
|
||||
const response = await axios({
|
||||
method: "post",
|
||||
url: `http://localhost:8080/api/v1/auth/oidc-auth/login`,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
data: loginData,
|
||||
});
|
||||
console.log(response.data);
|
||||
} catch (error) {
|
||||
console.error("Error in test:", error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testMachineIdentity();
|
||||
Reference in New Issue
Block a user