fix(ui): only clear the unselected ID-JAG client-auth method on an expressed choice

This commit is contained in:
Tin Chi Lo 2026-07-21 17:12:45 -07:00
parent e571ac7c3e
commit 99ca545920
4 changed files with 54 additions and 8 deletions

View file

@ -20,9 +20,11 @@ const FieldLabel: React.FC<{ label: string; tooltip: string }> = ({ label, toolt
const IdJagFormFields: React.FC<IdJagFormFieldsProps> = ({ isEditing = false }) => {
const placeholderSuffix = isEditing ? " (leave blank to keep existing)" : "";
const form = Form.useFormInstance();
const clientAuthMethod =
(Form.useWatch("id_jag_client_auth_method", form) as "client_secret" | "private_key_jwt" | undefined) ??
"client_secret";
const watchedMethod = Form.useWatch("id_jag_client_auth_method", form) as
| "client_secret"
| "private_key_jwt"
| undefined;
const clientAuthMethod = watchedMethod ?? (isEditing ? undefined : "client_secret");
return (
<>
@ -72,12 +74,14 @@ const IdJagFormFields: React.FC<IdJagFormFieldsProps> = ({ isEditing = false })
/>
}
name="id_jag_client_auth_method"
initialValue="client_secret"
{...(isEditing ? {} : { initialValue: "client_secret" })}
preserve={false}
>
<Select<"client_secret" | "private_key_jwt">
className="rounded-lg"
size="large"
allowClear={isEditing}
placeholder={isEditing ? "Keep existing method" : undefined}
options={[
{ value: "client_secret", label: <span className="font-medium">Client Secret</span> },
{ value: "private_key_jwt", label: <span className="font-medium">Private Key JWT</span> },

View file

@ -582,9 +582,9 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
// The selected ID-JAG client-auth method is authoritative: null the other method's
// fields so the backend's credentials merge cannot keep a stale method alive.
if (restValues.auth_type === AUTH_TYPE.OAUTH2_ID_JAG) {
if (restValues.auth_type === AUTH_TYPE.OAUTH2_ID_JAG && idJagClientAuthMethodRaw) {
const idJagMethodNulls =
(idJagClientAuthMethodRaw ?? "client_secret") === "private_key_jwt"
idJagClientAuthMethodRaw === "private_key_jwt"
? { client_secret: null }
: { client_private_key: null, client_private_key_id: null, client_assertion_signing_alg: null };
payload.credentials = { ...(payload.credentials ?? {}), ...idJagMethodNulls };

View file

@ -393,6 +393,48 @@ describe("MCPServerEdit (auth type switch)", () => {
vi.clearAllMocks();
});
it("keeps the stored ID-JAG client-auth method when saved without expressing a choice", async () => {
vi.mocked(networking.updateMCPServer).mockResolvedValue({
...interactiveOAuthServer,
auth_type: "oauth2_id_jag",
});
render(
<MCPServerEdit
mcpServer={{
...interactiveOAuthServer,
auth_type: "oauth2_id_jag",
token_exchange_endpoint: "https://org.example.com/oauth2/v1/token",
}}
accessToken="access-token"
onCancel={vi.fn()}
onSuccess={vi.fn()}
availableAccessGroups={[]}
/>,
);
await waitFor(() => {
expect(screen.getByPlaceholderText("https://your-org.okta.com/oauth2/v1/token")).toBeInTheDocument();
});
const saveButtons = screen.getAllByRole("button", { name: "Save Changes" });
await act(async () => {
fireEvent.click(saveButtons[0]);
});
await waitFor(() => {
expect(networking.updateMCPServer).toHaveBeenCalledTimes(1);
});
const [, payload] = vi.mocked(networking.updateMCPServer).mock.calls[0];
expect(payload.auth_type).toBe("oauth2_id_jag");
const credentials = payload.credentials ?? {};
expect(credentials.client_private_key).toBeUndefined();
expect(credentials.client_private_key_id).toBeUndefined();
expect(credentials.client_assertion_signing_alg).toBeUndefined();
expect(credentials.client_secret).toBeUndefined();
});
it("renders the ID-JAG arm on edit and nulls its shared fields when switching away", async () => {
vi.mocked(networking.updateMCPServer).mockResolvedValue({
...interactiveOAuthServer,

View file

@ -952,9 +952,9 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
// The selected ID-JAG client-auth method is authoritative: explicit-null the other
// method's stored fields so the backend's credentials merge cannot keep it alive.
if (restValues.auth_type === AUTH_TYPE.OAUTH2_ID_JAG) {
if (restValues.auth_type === AUTH_TYPE.OAUTH2_ID_JAG && idJagClientAuthMethodRaw) {
const idJagMethodNulls =
(idJagClientAuthMethodRaw ?? "client_secret") === "private_key_jwt"
idJagClientAuthMethodRaw === "private_key_jwt"
? { client_secret: null }
: { client_private_key: null, client_private_key_id: null, client_assertion_signing_alg: null };
payload.credentials = { ...(payload.credentials ?? {}), ...idJagMethodNulls };