Compare commits

...

16 Commits

Author SHA1 Message Date
Maidul Islam
36144d8c42 add proper docs 2025-03-18 17:21:48 -04:00
=
c487b2b34a feat: updated doc 2025-03-18 23:44:50 +05:30
=
8e20531b40 feat: changed key to be required 2025-03-18 23:05:31 +05:30
=
8ead2aa774 feat: updated documentation on identity oidc auth permission 2025-03-18 20:54:12 +05:30
=
1b2128e3cc feat: updated code to auth field in permission for identity 2025-03-18 20:53:51 +05:30
Maidul Islam
78f83cb478 remove default open 2025-03-17 21:51:47 -04:00
Maidul Islam
c8a871de7c fix lint 2025-03-17 19:47:08 -04:00
Maidul Islam
64c0951df3 add new line so there is no change 2025-03-17 19:38:50 -04:00
Maidul Islam
c185414a3c bring back .env example 2025-03-17 19:38:05 -04:00
Maidul Islam
f9695741f1 Minor changes to oidc claims and mappings
- Made the claims expanded by default (it looked off when they were closed)
- Moved claims from advanced to geneal tab and kept the mapping in the advanced tab
- Added better description for the tooltip

question: i feel like it would be better to access metadata like: `{{identity.auth.oidc.claim.<...>}}` instead of like how it is now: `{{identity.metadata.auth.oidc.claim.<...>}}`? What do you think
2025-03-17 19:36:40 -04:00
Maidul Islam
a7fe79c046 Merge pull request #3242 from akhilmhdh/feat/metadata-oidc
Feat/metadata OIDC
2025-03-17 16:55:50 -04:00
=
9eb89bb46d fix: null causing ui error 2025-03-17 23:52:11 +05:30
=
c4da1ce32d feat: resolved PR feedbacks 2025-03-17 23:38:24 +05:30
=
2ef77c737a feat: added a simple oidc server 2025-03-14 22:11:23 +05:30
=
0f31fa3128 feat: updated form for oidc auth 2025-03-14 22:11:23 +05:30
=
1da5a5f417 feat: completed backend code for oidc permission inject 2025-03-14 22:11:22 +05:30
38 changed files with 2448 additions and 213 deletions

View File

@@ -1,7 +0,0 @@
import "@fastify/request-context";
declare module "@fastify/request-context" {
interface RequestContextData {
reqId: string;
}
}

View File

@@ -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>;
};
};
}
}

View File

@@ -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");
});
}
}

View File

@@ -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>;

View File

@@ -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>;

View File

@@ -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;

View File

@@ -131,12 +131,12 @@ function validateOrgSSO(actorAuthMethod: ActorAuthMethod, isOrgSsoEnforced: TOrg
}
}
const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
const escapeHandlebarsMissingDict = (obj: Record<string, string>, key: string) => {
const handler = {
get(target: Record<string, string>, prop: string) {
if (!(prop in target)) {
if (!Object.hasOwn(target, prop)) {
// eslint-disable-next-line no-param-reassign
target[prop] = `{{identity.metadata.${prop}}}`; // Add missing key as an "own" property
target[prop] = `{{${key}.${prop}}}`; // Add missing key as an "own" property
}
return target[prop];
}
@@ -145,4 +145,4 @@ const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
return new Proxy(obj, handler);
};
export { escapeHandlebarsMissingMetadata, isAuthMethodSaml, validateOrgSSO };
export { escapeHandlebarsMissingDict, isAuthMethodSaml, validateOrgSSO };

View File

@@ -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";
@@ -22,7 +23,7 @@ import { TServiceTokenDALFactory } from "@app/services/service-token/service-tok
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
import { TPermissionDALFactory } from "./permission-dal";
import { escapeHandlebarsMissingMetadata, validateOrgSSO } from "./permission-fns";
import { escapeHandlebarsMissingDict, validateOrgSSO } from "./permission-fns";
import {
TBuildOrgPermissionDTO,
TBuildProjectPermissionDTO,
@@ -243,20 +244,22 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
),
"identity.metadata"
);
const templateValue = {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
};
const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
}
identity: templateValue
},
{ data: false }
);
@@ -317,21 +320,26 @@ 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 unescapedIdentityAuthInfo = requestContext.get("identityAuthInfo");
const unescapedMetadata = objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
);
const identityAuthInfo =
unescapedIdentityAuthInfo?.identityId === identityId && unescapedIdentityAuthInfo
? escapeHandlebarsMissingDict(unescapedIdentityAuthInfo as never, "identity.auth")
: {};
const metadataKeyValuePair = escapeHandlebarsMissingDict(unescapedMetadata, "identity.metadata");
const templateValue = {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair,
auth: identityAuthInfo
};
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
}
identity: templateValue
},
{ data: false }
);
@@ -424,20 +432,22 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
userProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
),
"identity.metadata"
);
const templateValue = {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
};
const interpolateRules = templatedRules(
{
identity: {
id: userProjectPermission.userId,
username: userProjectPermission.username,
metadata: metadataKeyValuePair
}
identity: templateValue
},
{ data: false }
);
@@ -469,21 +479,22 @@ export const permissionServiceFactory = ({
const rules = buildProjectPermissionRules(rolePermissions.concat(additionalPrivileges));
const templatedRules = handlebars.compile(JSON.stringify(rules), { data: false });
const metadataKeyValuePair = escapeHandlebarsMissingMetadata(
const metadataKeyValuePair = escapeHandlebarsMissingDict(
objectify(
identityProjectPermission.metadata,
(i) => i.key,
(i) => i.value
)
),
"identity.metadata"
);
const templateValue = {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
};
const interpolateRules = templatedRules(
{
identity: {
id: identityProjectPermission.identityId,
username: identityProjectPermission.username,
metadata: metadataKeyValuePair
}
identity: templateValue
},
{ data: false }
);

View File

@@ -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.",

View 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;
};

View File

@@ -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: {

View File

@@ -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,

View File

@@ -7,4 +7,9 @@ export type TIdentityAccessTokenJwtPayload = {
clientSecretId: string;
identityAccessTokenId: string;
authTokenType: string;
identityAuth: {
oidc?: {
claims: Record<string, string>;
};
};
};

View File

@@ -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`
});

View File

@@ -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,

View File

@@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 KiB

View File

@@ -0,0 +1,68 @@
---
title: "Machine identities"
description: "Learn how to set metadata and leverage authentication attributes for machine identities."
---
Machine identities can have metadata set manually, just like users. In addition, during the machine authentication process (e.g., via OIDC), extra attributes called claims—are provided, which can be used in your ABAC policies.
#### Setting Metadata on Machine Identities
<Tabs>
<Tab title="Manually Configure Metadata">
<Steps>
<Step title="Navigate to the Access Control page on the organization sidebar and select a machine identity.">
<img src="/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-1.png" />
</Step>
<Step title="On the machine identity page, click the pencil icon to edit the selected identity.">
<img src="/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-2.png" />
</Step>
<Step title="Add metadata via key-value pairs and update the machine identity.">
<img src="/documentation/platform/access-controls/abac/images/add-metadata-on-machine-identity-3.png" />
</Step>
</Steps>
</Tab>
</Tabs>
#### Accessing Attributes From Machine Identity Login
When machine identities authenticate, they may receive additional payloads/attributes from the service provider.
For methods like OIDC, these come as claims in the token and can be made available in your policies.
<Tabs>
<Tab title="OIDC Login Attributes">
1. Navigate to the Identity Authentication settings and select the OIDC Auth Method.
2. In the **Advanced section**, locate the Claim Mapping configuration.
3. Map the OIDC claims to permission attributes by specifying:
- **Attribute Name:** The identifier to be used in your policies (e.g., department).
- **Claim Path:** The dot notation path to the claim in the OIDC token (e.g., user.department).
For example, if your OIDC provider returns:
```json
{
"sub": "machine456",
"name": "Service A",
"user": {
"department": "engineering",
"role": "service"
}
}
```
You might map:
- **department:** to `user.department`
- **role:** to `user.role`
Once configured, these attributes become available in your policies using the following format:
```
{{ identity.auth.oidc.claims.<permission claim name> }}
```
<img src="/images/platform/access-controls/abac-policy-oidc-format.png" />
</Tab>
<Tab title="Other Authentication Method Attributes">
At the moment we only support OIDC claims. Payloads on other authentication methods are not yet accessible.
</Tab>
</Tabs>

View File

@@ -0,0 +1,39 @@
---
title: "Users identities"
description: "How to set and use metadata attributes on user identities for ABAC."
---
User identities can have metadata attributes assigned directly. These attributes (such as location or department) are used to define dynamic access policies.
#### Setting Metadata on Users
<Tabs>
<Tab title="Manually Configure Metadata">
<Steps>
<Step title="Navigate to the Access Control page on the organization sidebar and select a user.">
<img src="/images/platform/access-controls/add-metadata-step1.png" />
</Step>
<Step title="On the User Page, click the pencil icon to edit the selected user.">
<img src="/images/platform/access-controls/add-metadata-step2.png" />
</Step>
<Step title="Add metadata via key-value pairs and update the user identity.">
<img src="/images/platform/access-controls/add-metadata-step3.png" />
</Step>
</Steps>
</Tab>
<Tab title="Automatically Populate Metadata">
For organizations using SAML for **user logins**, Infisical automatically maps metadata attributes from SAML assertions to user identities on every login. This enables dynamic policies based on the user's SAML attributes.
</Tab>
</Tabs>
#### Applying ABAC Policies with User Metadata
Attribute-based access controls are currently only available for polices defined on Secrets Manager projects.
You can set ABAC permissions to dynamically set access to environments, folders, secrets, and secret tags.
<img src="/images/platform/access-controls/example-abac-1.png" />
In your policies, metadata values are accessed as follows:
- **User ID:** `{{ identity.id }}` (always available)
- **Username:** `{{ identity.username }}` (always available)
- **Metadata Attributes:** `{{ identity.metadata.<metadata-key-name> }}` (available if set)

View File

@@ -0,0 +1,15 @@
---
title: "Overview"
description: "Learn the basics of ABAC for both users and machine identities."
---
Infisical's Attribute-based Access Controls (ABAC) enable dynamic, attribute-driven permissions for both users and machine identities. ABAC enforces fine-grained, context-aware access controls using metadata attributes—stored as key-value pairs—either attached to identities or provided during authentication.
<CardGroup cols={2}>
<Card title="Users" icon="square-1" href="./managing-user-metadata">
Manage user metadata manually or automatically via SAML logins.
</Card>
<Card title="Machine Identities" icon="square-2" href="./managing-machine-identity-attributes">
Set metadata manually like users and access additional attributes provided during machine authentication (for example, OIDC claims).
</Card>
</CardGroup>

View File

@@ -1,65 +0,0 @@
---
title: "Attribute-based Access Controls"
description: "Learn how to use ABAC to manage permissions based on identity attributes."
---
Infisical's Attribute-based Access Controls (ABAC) allow for dynamic, attribute-driven permissions for both user and machine identities.
ABAC policies use metadata attributes—stored as key-value pairs on identities—to enforce fine-grained permissions that are context aware.
In ABAC, access controls are defined using metadata attributes, such as location or department, which can be set directly on user or machine identities.
During policy execution, these attributes are evaluated, and determine whether said actor can access the requested resource or perform the requested operation.
## Project-level Permissions
Attribute-based access controls are currently available for polices defined on projects. You can set ABAC permissions to control access to environments, folders, secrets, and secret tags.
### Setting Metadata on Identities
<Tabs>
<Tab title="Manually Configure Metadata">
<Steps>
<Step title="Navigate to the Access Control page on the organization sidebar and select an identity (user or machine).">
<img src="/images/platform/access-controls/add-metadata-step1.png" />
</Step>
<Step title="On the Identity Page, click the pencil icon to edit the selected identity.">
<img src="/images/platform/access-controls/add-metadata-step2.png" />
</Step>
<Step title="Add metadata via key-value pairs and update the identity.">
<img src="/images/platform/access-controls/add-metadata-step3.png" />
</Step>
</Steps>
</Tab>
<Tab title="Automatically Populate Metadata">
For organizations using SAML for login, Infisical automatically maps metadata attributes from SAML assertions to user identities.
This makes it easy to create policies that dynamically adapt based on the SAML users attributes.
</Tab>
</Tabs>
## Defining ABAC Policies
<img src="/images/platform/access-controls/example-abac-1.png" />
ABAC policies make use of identity metadata to define dynamic permissions. Each attribute must start and end with double curly-brackets `{{ <attribute-name> }}`.
The following attributes are available within project permissions:
- **User ID**: `{{ identity.id }}`
- **Username**: `{{ identity.username }}`
- **Metadata Attributes**: `{{ identity.metadata.<metadata-key-name> }}`
During policy execution, these placeholders are replaced by their actual values prior to evaluation.
### Example Use Case
#### Location-based Access Control
Suppose you want to restrict access to secrets within a specific folder based on a user's geographic region.
You could assign a `location` attribute to each user (e.g., `identity.metadata.location`).
You could then structure your folders to align with this attribute and define permissions accordingly.
For example, a policy might restrict access to folders matching the user's location attribute in the following pattern:
```
/appA/{{ identity.metadata.location }}
```
Using this structure, users can only access folders that correspond to their configured `location` attribute.
Consequently, if a users attribute changes due to relocation, no policies need to be changed to gain access to the folders associated with their new location.

View File

@@ -18,7 +18,7 @@ To make sure that users and machine identities are only accessing the resources
<Card
title="Attribute-based Access Control"
href="./attribute-based-access-controls"
href="/documentation/platform/access-controls/abac"
icon="address-book"
color="#000000"
>

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

View File

@@ -150,7 +150,14 @@
"pages": [
"documentation/platform/access-controls/overview",
"documentation/platform/access-controls/role-based-access-controls",
"documentation/platform/access-controls/attribute-based-access-controls",
{
"group": "Attribute based access controls",
"pages": [
"documentation/platform/access-controls/abac/overview",
"documentation/platform/access-controls/abac/managing-user-metadata",
"documentation/platform/access-controls/abac/managing-machine-identity-attributes"
]
},
"documentation/platform/access-controls/additional-privileges",
"documentation/platform/access-controls/temporary-access",
"documentation/platform/access-controls/access-requests",

View File

@@ -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",

View File

@@ -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
}
);

View File

@@ -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;

View File

@@ -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",

View File

@@ -46,12 +46,22 @@ const schema = z.object({
caCert: z.string().trim().default(""),
boundIssuer: z.string().min(1),
boundAudiences: z.string().optional().default(""),
boundClaims: z.array(
z.object({
key: z.string(),
value: z.string()
})
),
boundClaims: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.default([]),
claimMetadataMapping: z
.array(
z.object({
key: z.string(),
value: z.string()
})
)
.default([]),
boundSubject: z.string().optional().default("")
});
@@ -96,10 +106,11 @@ export const IdentityOidcAuthForm = ({
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
boundClaims: [],
claimMetadataMapping: []
}
});
const {
fields: boundClaimsFields,
append: appendBoundClaimField,
@@ -109,6 +120,15 @@ export const IdentityOidcAuthForm = ({
name: "boundClaims"
});
const {
fields: claimMetadataMappingFields,
append: appendClaimMetadataMappingField,
remove: removeClaimMetadataMappingField
} = useFieldArray({
control,
name: "claimMetadataMapping"
});
const {
fields: accessTokenTrustedIpsFields,
append: appendAccessTokenTrustedIp,
@@ -126,6 +146,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),
@@ -149,7 +175,8 @@ export const IdentityOidcAuthForm = ({
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
claimMetadataMapping: []
});
}
}, [data]);
@@ -164,6 +191,7 @@ export const IdentityOidcAuthForm = ({
boundIssuer,
boundAudiences,
boundClaims,
claimMetadataMapping,
boundSubject
}: FormData) => {
try {
@@ -180,6 +208,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 +225,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),
@@ -223,7 +257,9 @@ export const IdentityOidcAuthForm = ({
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["accessTokenTrustedIps", "caCert", "boundClaims"].includes(Object.keys(fields)[0])
["accessTokenTrustedIps", "caCert", "claimMetadataMapping"].includes(
Object.keys(fields)[0]
)
? IdentityFormTab.Advanced
: IdentityFormTab.Configuration
);
@@ -313,63 +349,6 @@ export const IdentityOidcAuthForm = ({
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
{boundClaimsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@@ -449,6 +428,155 @@ export const IdentityOidcAuthForm = ({
Add Claims
</Button>
</div>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
{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 ? "Token Claim Mapping" : undefined}
icon={
index === 0 ? (
<Tooltip
className="text-center"
content={
<div className="w-[180px]">
<p>Map OIDC token claims to metadata fields</p>
<p className="mt-2 text-sm">Example:</p>
<p className="mt-1 text-sm">
&apos;role&apos; &apos;token.groups&apos;
</p>
<p className="mt-1 text-xs text-gray-400">
Becomes: identity.metadata.oidc.claims.role
</p>
</div>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" />
</Tooltip>
) : undefined
}
isError={Boolean(error)}
errorText={error?.message}
>
<Input
value={field.value}
onChange={(e) => field.onChange(e)}
placeholder="Field name"
/>
</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="Token claim"
/>
</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 Token Mapping
</Button>
</div>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@@ -519,24 +647,21 @@ export const IdentityOidcAuthForm = ({
</div>
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Add"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
>
Cancel
</Button>
<div className="mt-8 flex justify-between">
<div className="flex items-center">
<Button
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
variant="outline_bg"
className="mr-4"
isDisabled={isSubmitting}
>
Cancel
</Button>
<Button type="submit" isLoading={isSubmitting}>
{isUpdate ? "Update" : "Add"} Auth Method
</Button>
</div>
</div>
</form>
);

View File

@@ -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>
);
};

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

View 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();