Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
59230ce
replace okta SDK with requests-oauth2client
stephenkiers Nov 26, 2025
b2db651
add OAuth2 HTTP client for Okta with DPoP support
stephenkiers Nov 26, 2025
c8d9dbf
update Okta schema to use OAuth2 client credentials
stephenkiers Nov 26, 2025
d4ad874
update Okta connector to use OAuth2 HTTP client
stephenkiers Nov 26, 2025
cf36770
update admin-ui for OAuth2 Okta authentication
stephenkiers Nov 26, 2025
b3563f6
update changelog and remove stale TODO
stephenkiers Nov 26, 2025
76bfd65
updated changelog
stephenkiers Nov 26, 2025
ce69e26
explicit multiline, no magic
stephenkiers Nov 26, 2025
6c750d3
removed changelog
stephenkiers Nov 26, 2025
a2aeda9
post merge cleanup
stephenkiers Nov 26, 2025
f1d37ae
security fix; per github security
stephenkiers Nov 26, 2025
00b5a80
fix lint
stephenkiers Nov 26, 2025
3e6da2e
Patch add system wizard
stephenkiers Nov 26, 2025
117ccbb
fix security callout
stephenkiers Nov 26, 2025
be63a27
lint fix
stephenkiers Nov 26, 2025
4f11b24
refactor and cleanup
stephenkiers Nov 26, 2025
c9fca93
cleanup and add merge retry logic pr in
stephenkiers Nov 26, 2025
f487896
FE refagtor
stephenkiers Nov 26, 2025
4c9e4fc
fix tests and env vars
stephenkiers Nov 26, 2025
f13b8a2
graceful skip patterns until 1password is implemented
stephenkiers Nov 26, 2025
cf3989c
expand okta domain compatability
stephenkiers Nov 26, 2025
90d09bb
fix tests and lint
stephenkiers Nov 26, 2025
8784919
fixes and feedback
stephenkiers Nov 27, 2025
14e14cb
add zod
stephenkiers Nov 27, 2025
52e1f11
fix tests
stephenkiers Nov 27, 2025
345480f
prettier
stephenkiers Nov 27, 2025
a2b2fd3
refactors and cleanup
stephenkiers Nov 27, 2025
0088b32
Reuse packages, do not reinvent the wheel.
stephenkiers Nov 27, 2025
61f1eb1
Merge branch 'main' into oauth2-migration/single-pr
dsill-ethyca Dec 11, 2025
fc10dd8
remove unused imports flagged by static checks
dsill-ethyca Dec 11, 2025
c81c44d
Merge branch 'main' into oauth2-migration/single-pr
dsill-ethyca Dec 11, 2025
473d273
Merge branch 'main' into oauth2-migration/single-pr
dsill-ethyca Dec 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/backend_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@ jobs:
DYNAMODB_ACCESS_KEY_ID: op://github-actions/dynamodb/DYNAMODB_ACCESS_KEY_ID
DYNAMODB_ACCESS_KEY: op://github-actions/dynamodb/DYNAMODB_ACCESS_KEY
DYNAMODB_REGION: op://github-actions/dynamodb/DYNAMODB_REGION
OKTA_CLIENT_TOKEN: op://github-actions/ctl/OKTA_CLIENT_TOKEN
OKTA_CLIENT_ID: op://github-actions/okta/OKTA_CLIENT_ID
OKTA_PRIVATE_KEY: op://github-actions/okta/OKTA_PRIVATE_KEY
REDSHIFT_FIDESCTL_PASSWORD: op://github-actions/ctl/REDSHIFT_FIDESCTL_PASSWORD
SNOWFLAKE_FIDESCTL_PASSWORD: op://github-actions/ctl/SNOWFLAKE_FIDESCTL_PASSWORD

Expand Down Expand Up @@ -469,8 +470,9 @@ jobs:
GOOGLE_CLOUD_SQL_POSTGRES_DB_IAM_USER: op://github-actions/gcp-postgres/GOOGLE_CLOUD_SQL_POSTGRES_DB_IAM_USER
GOOGLE_CLOUD_SQL_POSTGRES_INSTANCE_CONNECTION_NAME: op://github-actions/gcp-postgres/GOOGLE_CLOUD_SQL_POSTGRES_INSTANCE_CONNECTION_NAME
GOOGLE_CLOUD_SQL_POSTGRES_KEYFILE_CREDS: op://github-actions/gcp-postgres/GOOGLE_CLOUD_SQL_POSTGRES_KEYFILE_CREDS
OKTA_API_TOKEN: op://github-actions/okta/OKTA_API_TOKEN
OKTA_CLIENT_ID: op://github-actions/okta/OKTA_CLIENT_ID
OKTA_ORG_URL: op://github-actions/okta/OKTA_ORG_URL
OKTA_PRIVATE_KEY: op://github-actions/okta/OKTA_PRIVATE_KEY
RDS_MYSQL_AWS_ACCESS_KEY_ID: op://github-actions/rds-mysql/RDS_MYSQL_AWS_ACCESS_KEY_ID
RDS_MYSQL_AWS_SECRET_ACCESS_KEY: op://github-actions/rds-mysql/RDS_MYSQL_AWS_SECRET_ACCESS_KEY
RDS_MYSQL_DB_INSTANCE: op://github-actions/rds-mysql/RDS_MYSQL_DB_INSTANCE
Expand Down
8 changes: 6 additions & 2 deletions clients/admin-ui/cypress/e2e/config-wizard.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,12 @@ describe("Config Wizard", () => {
cy.getByTestId("okta-btn").click();
// Fill form
cy.getByTestId("authenticate-okta-form");
cy.getByTestId("input-orgUrl").type("https://ethyca.com/");
cy.getByTestId("input-token").type("fakeToken");
cy.getByTestId("input-orgUrl").type("https://dev-12345.okta.com");
cy.getByTestId("input-clientId").type("0oa1abc2def3ghi4jkl5");
cy.getByTestId("input-privateKey").type(
'{"kty":"RSA","kid":"test","n":"test","e":"AQAB","d":"test"}',
{ parseSpecialCharSequences: false },
);
});

it("Allows submitting the form and reviewing the results", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "~/features/connection-type/types";

import { ControlledSelect } from "./ControlledSelect";
import { CustomTextInput } from "./inputs";
import { CustomTextArea, CustomTextInput } from "./inputs";

export type FormFieldProps = {
name: string;
Expand Down Expand Up @@ -91,6 +91,23 @@ export const FormFieldFromSchema = ({
);
}

if (fieldSchema.multiline) {
return (
<CustomTextArea
{...field}
label={fieldSchema.title}
tooltip={fieldSchema.description}
isRequired={isRequired}
placeholder={getPlaceholder()}
variant={layout}
textAreaProps={{
rows: 8,
style: { fontFamily: "monospace", fontSize: "12px" },
}}
/>
);
}

return (
<CustomTextInput
{...field}
Expand Down
104 changes: 87 additions & 17 deletions clients/admin-ui/src/features/config-wizard/AuthenticateOktaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { useState } from "react";
import * as Yup from "yup";

import { useAppDispatch, useAppSelector } from "~/app/hooks";
import { CustomTextInput } from "~/features/common/form/inputs";
import { CustomTextArea, CustomTextInput } from "~/features/common/form/inputs";
import {
isErrorResult,
ParsedError,
parseError,
} from "~/features/common/helpers";
import { useAlert } from "~/features/common/hooks";
import { OKTA_AUTH_DESCRIPTION } from "~/features/integrations/integration-type-info/oktaInfo";
import {
GenerateResponse,
GenerateTypes,
Expand All @@ -30,20 +31,71 @@ import { useGenerateMutation } from "./scanner.slice";
import ScannerError from "./ScannerError";
import ScannerLoading from "./ScannerLoading";

const PRIVATE_KEY_TOOLTIP =
"Private key in JWK (JSON) format. Generate a key in Okta and download the JSON.";

const DEFAULT_SCOPES = ["okta.apps.read"];

const parseScopesFromCsv = (scopes: string | undefined): string[] =>
scopes
? scopes
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: DEFAULT_SCOPES;

const initialValues = {
orgUrl: "",
token: "",
clientId: "",
privateKey: "",
scopes: "okta.apps.read",
};

type FormValues = typeof initialValues;

const ValidationSchema = Yup.object().shape({
orgUrl: Yup.string().required().trim().url().label("URL"),
token: Yup.string()
orgUrl: Yup.string().required().trim().url().label("Organization URL"),
clientId: Yup.string()
.required()
.trim()
.matches(/^[^\s]+$/, "Cannot contain spaces")
.label("Token"),
.label("Client ID"),
privateKey: Yup.string()
.required()
.trim()
.test(
"is-valid-json",
"Private key must be valid JSON. Paste the JWK downloaded from Okta.",
(value) => {
if (!value) {
return true;
}
try {
JSON.parse(value);
return true;
} catch {
return false;
}
},
)
.label("Private Key"),
scopes: Yup.string()
.required()
.trim()
.label("Scopes")
.default("okta.apps.read")
.test(
"valid-scopes",
"Scopes must be a single scope or comma-separated list (e.g., 'okta.apps.read' or 'okta.apps.read, okta.users.read')",
(value) => {
if (!value) {
return true;
}
// Split on comma and check each scope is non-empty and has no internal whitespace
const scopes = value.split(",").map((s) => s.trim());
return scopes.every((scope) => scope.length > 0 && !/\s/.test(scope));
},
),
});

const AuthenticateOktaForm = () => {
Expand Down Expand Up @@ -79,10 +131,15 @@ const AuthenticateOktaForm = () => {
const handleSubmit = async (values: FormValues) => {
setScannerError(undefined);

const config = {
...values,
scopes: parseScopesFromCsv(values.scopes),
};

const result = await generate({
organization_key: organizationKey,
generate: {
config: values,
config,
target: ValidTargets.OKTA,
type: GenerateTypes.SYSTEMS,
},
Expand Down Expand Up @@ -129,23 +186,36 @@ const AuthenticateOktaForm = () => {
{ title: "Authenticate Okta Scanner" },
]}
/>
<Text>
To use the scanner to inventory systems in Okta, you must
first authenticate to your Okta account by providing the
following information:
</Text>
<Text>{OKTA_AUTH_DESCRIPTION}</Text>
</Box>
<Stack>
<CustomTextInput
name="orgUrl"
label="Domain"
tooltip="The URL for your organization's account on Okta"
label="Organization URL"
tooltip="The URL for your organization's Okta account (e.g. https://your-org.okta.com)"
placeholder="https://your-org.okta.com"
/>
<CustomTextInput
name="clientId"
label="Client ID"
tooltip="The OAuth2 client ID from your Okta API Services application"
placeholder="0oa1abc2def3ghi4jkl5"
/>
<CustomTextArea
name="privateKey"
label="Private key"
tooltip={PRIVATE_KEY_TOOLTIP}
placeholder='{"kty":"RSA","kid":"...","n":"...","e":"AQAB","d":"..."}'
textAreaProps={{
rows: 8,
style: { fontFamily: "monospace", fontSize: "12px" },
}}
/>
<CustomTextInput
name="token"
label="Okta token"
type="password"
tooltip="The token generated by Okta for your account."
name="scopes"
label="Scopes"
tooltip="OAuth2 scopes to request. Default is okta.apps.read for application discovery"
placeholder="okta.apps.read"
/>
</Stack>
</>
Expand Down
1 change: 1 addition & 0 deletions clients/admin-ui/src/features/connection-type/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type ConnectionTypeSecretSchemaProperty = {
items?: { $ref: string };
sensitive?: boolean;
multiselect?: boolean;
multiline?: boolean;
options?: string[];
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ const ConfigureMonitorModal = ({
{ isLoading: isSubmittingOktaUpdate },
] = usePutIdentityProviderMonitorMutation();

let isSubmitting: boolean;
if (isOktaIntegration) {
isSubmitting = isEditing ? isSubmittingOktaUpdate : isSubmittingOktaCreate;
} else {
isSubmitting = isSubmittingRegular;
}
const isSubmittingOkta = isEditing
? isSubmittingOktaUpdate
: isSubmittingOktaCreate;
const isSubmitting = isOktaIntegration
? isSubmittingOkta
: isSubmittingRegular;

const { data: databases } = useGetAvailableDatabasesByConnectionQuery({
page: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,10 @@ const MonitorConfigActionsCell = ({
};

const handleExecute = async () => {
// Use Identity Provider Monitor endpoint for Okta, otherwise use regular endpoint
if (isOktaMonitor) {
const result = await executeOktaMonitor({
monitor_config_key: monitorId,
});
toastExecuteResult(result);
} else {
const result = await executeRegularMonitor({
monitor_config_id: monitorId,
});
toastExecuteResult(result);
}
const result = isOktaMonitor
? await executeOktaMonitor({ monitor_config_key: monitorId })
: await executeRegularMonitor({ monitor_config_id: monitorId });
toastExecuteResult(result);
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export const useMonitorConfigTable = ({
statusColumn,
actionsColumn,
];
}, [integration.secrets, isWebsiteMonitor, onEditMonitor]);
}, [integration.secrets, isWebsiteMonitor, isOktaIntegration, onEditMonitor]);
Copy link
Contributor Author

@stephenkiers stephenkiers Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was missed. perhaps a lint rule is needed


return {
// Table state and data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ export const OKTA_DESCRIPTION = (
</>
);

export const OKTA_AUTH_DESCRIPTION =
"Okta integration uses OAuth2 Client Credentials with private_key_jwt for secure authentication. You will need to generate an RSA key in Okta and copy the JSON key to use in Fides.";

const OktaIntegrationOverview = () => (
<>
<InfoHeading text="Overview" />
<InfoText>{OKTA_DESCRIPTION}</InfoText>
<InfoText>
SSO providers manage user authentication and can help identify systems
within your infrastructure. Adding an SSO provider as a data source allows
you to detect connected systems, monitor access patterns, and enhance your
data map for better visibility and control.
<strong>Authentication:</strong> {OKTA_AUTH_DESCRIPTION}
</InfoText>
</>
);
Expand Down
6 changes: 4 additions & 2 deletions clients/admin-ui/src/types/api/models/OktaConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
/* eslint-disable */

/**
* The model for the connection config for Okta
* The model for the connection config for Okta (OAuth2)
*/
export type OktaConfig = {
orgUrl: string;
token: string;
clientId: string;
privateKey: string;
scopes?: Array<string>;
};
2 changes: 1 addition & 1 deletion docs/fides/docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ task_always_eager = true

### Credentials

The credentials section uses custom keys which can be referenced in specific commands that take the --credentials-id option. For example, a command that uses a credential might look like `fides scan dataset db --credentials-id app_postgres`. The credential object itself will be validated at the time of use depending on what type of credential is required. For instance if `fides scan system okta` is used, it will expect the object to contain orgUrl and token key/value pairs. In the case of a typical database like postgres, it will only expect a connection_string. The following is an example of what a credentials section might look like in a given deployment with various applications:
The credentials section uses custom keys which can be referenced in specific commands that take the --credentials-id option. For example, a command that uses a credential might look like `fides scan dataset db --credentials-id app_postgres`. The credential object itself will be validated at the time of use depending on what type of credential is required. For instance if `fides scan system okta` is used, it will expect the object to contain orgUrl, clientId, and privateKey key/value pairs for OAuth2 authentication. In the case of a typical database like postgres, it will only expect a connection_string. The following is an example of what a credentials section might look like in a given deployment with various applications:

```toml title="Example Credentials Section"
[credentials]
Expand Down
3 changes: 2 additions & 1 deletion noxfiles/run_infrastructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@
],
"okta": [
"OKTA_ORG_URL",
"OKTA_API_TOKEN",
"OKTA_CLIENT_ID",
"OKTA_PRIVATE_KEY",
],
}
EXTERNAL_DATASTORES = list(EXTERNAL_DATASTORE_CONFIG.keys())
Expand Down
8 changes: 6 additions & 2 deletions noxfiles/setup_tests_nox.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ def pytest_ctl(session: Session, mark: str, coverage_arg: str) -> None:
"-e",
"AWS_DEFAULT_REGION",
"-e",
"OKTA_CLIENT_TOKEN",
"OKTA_CLIENT_ID",
"-e",
"OKTA_PRIVATE_KEY",
"-e",
"BIGQUERY_CONFIG",
"-e",
Expand Down Expand Up @@ -223,7 +225,9 @@ def pytest_ops(
"-e",
"OKTA_ORG_URL",
"-e",
"OKTA_API_TOKEN",
"OKTA_CLIENT_ID",
"-e",
"OKTA_PRIVATE_KEY",
"-e",
"RDS_MYSQL_AWS_ACCESS_KEY_ID",
"-e",
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ pg8000==1.31.2
nh3==0.2.15
numpy==1.24.4
oauthlib==3.3.1
okta==2.7.0
openpyxl==3.0.9
networkx==3.1
packaging==23.0
Expand All @@ -57,6 +56,7 @@ python-jose[cryptography]==3.3.0
pyyaml==6.0.1
pyahocorasick==2.1.0
redis==3.5.3
requests-oauth2client>=1.5.0
requests-oauthlib==2.0.0
rich-click==1.6.1
sendgrid==6.9.7
Expand Down
Loading
Loading