diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts new file mode 100644 index 000000000..c6d65d836 --- /dev/null +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-fns.ts @@ -0,0 +1,4 @@ +import picomatch from "picomatch"; + +export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) => + policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue); diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 457e93058..4c687e86c 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -28,6 +28,7 @@ import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identit import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; import { TOrgBotDALFactory } from "../org/org-bot-dal"; import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal"; +import { doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns"; import { TAttachOidcAuthDTO, TGetOidcAuthDTO, @@ -123,7 +124,7 @@ export const identityOidcAuthServiceFactory = ({ }) as Record; if (identityOidcAuth.boundSubject) { - if (tokenData.sub !== identityOidcAuth.boundSubject) { + if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) { throw new ForbiddenRequestError({ message: "Access denied: OIDC subject not allowed." }); @@ -131,7 +132,11 @@ export const identityOidcAuthServiceFactory = ({ } if (identityOidcAuth.boundAudiences) { - if (!identityOidcAuth.boundAudiences.split(", ").includes(tokenData.aud)) { + if ( + !identityOidcAuth.boundAudiences + .split(", ") + .some((policyValue) => doesFieldValueMatchOidcPolicy(tokenData.aud, policyValue)) + ) { throw new ForbiddenRequestError({ message: "Access denied: OIDC audience not allowed." }); @@ -142,7 +147,9 @@ export const identityOidcAuthServiceFactory = ({ Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => { const claimValue = (identityOidcAuth.boundClaims as Record)[claimKey]; // handle both single and multi-valued claims - if (!claimValue.split(", ").some((claimEntry) => tokenData[claimKey] === claimEntry)) { + if ( + !claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(tokenData[claimKey], claimEntry)) + ) { throw new ForbiddenRequestError({ message: "Access denied: OIDC claim not allowed." }); diff --git a/docs/documentation/platform/identities/oidc-auth/general.mdx b/docs/documentation/platform/identities/oidc-auth/general.mdx index ac9b4f2d7..776d175a4 100644 --- a/docs/documentation/platform/identities/oidc-auth/general.mdx +++ b/docs/documentation/platform/identities/oidc-auth/general.mdx @@ -93,7 +93,12 @@ In the following steps, we explore how to create and use identities to access th - Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time. - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. - Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address. + + The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible. + + + To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. diff --git a/docs/documentation/platform/identities/oidc-auth/github.mdx b/docs/documentation/platform/identities/oidc-auth/github.mdx index 4a9e2c671..47352a339 100644 --- a/docs/documentation/platform/identities/oidc-auth/github.mdx +++ b/docs/documentation/platform/identities/oidc-auth/github.mdx @@ -92,8 +92,8 @@ In the following steps, we explore how to create and use identities to access th - Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time. - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. - Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address. - If you are unsure about what to configure for the subject, audience, and claims fields you can use [github/actions-oidc-debugger](https://github.com/github/actions-oidc-debugger) to get the appropriate values. Alternatively, you can fetch the JWT from the workflow and inspect the fields manually. + The `subject`, `audiences`, and `claims` fields support glob pattern matching; however, we highly recommend using hardcoded values whenever possible. To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx index 30663d470..fdfb17e0d 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm.tsx @@ -1,12 +1,13 @@ import { useEffect } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; -import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2"; +import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2"; import { useOrganization, useSubscription } from "@app/context"; import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api"; import { IdentityAuthMethod } from "@app/hooks/api/identities"; @@ -258,7 +259,19 @@ export const IdentityOidcAuthForm = ({ control={control} name="boundSubject" render={({ field, fieldState: { error } }) => ( - + This field supports glob patterns} + > + + + } + > )} @@ -267,7 +280,19 @@ export const IdentityOidcAuthForm = ({ control={control} name="boundAudiences" render={({ field, fieldState: { error } }) => ( - + This field supports glob patterns} + > + + + } + > )} @@ -282,6 +307,16 @@ export const IdentityOidcAuthForm = ({ This field supports glob patterns} + > + + + ) : undefined + } isError={Boolean(error)} errorText={error?.message} >