mirror of
https://github.com/wassname/talk.git
synced 2026-09-11 12:51:33 +08:00
[CORL-929] SSO Key Rotation (Front-end) (#2877)
* feat: initial impl * Create preliminary SSO rotation components CORL-929 * Create rotateSSOKey mutation CORL-929 * Fix sorting by date for SSO keys CORL-929 * Show tool tips beside expiring/expired statuses CORL-929 * Hook up Deactivate and Delete SSO key mutations CORL-929 * Tweak expired tooltip on SSO keys CORL-929 * Replace old SSO key config with key rotation CORL-929 * Fix copy button for SSO key's secret CORL-929 * Refactor SSOKeyCard func's into components All the func's building the sub-components were prime targets for component's with props. CORL-929 * Update tests to match SSO Key rotation CORL-929 * Fix typo in translation id CORL-929 * Test key rotation CORL-929 * Plumb disabled through SSO key rotation CORL-929 * Remove duplicate input/payload pairs from schema For some reason it perfectly duplicated the input/payload pairs for SSO key rotation on rebase. CORL-929 * Remove debug logging CORL-929 * Use switch instead of if's to compute SSO date field CORL-929 * Use switch to compute SSO action button state CORL-929 * Use switch to compute SSO StatusField state CORL-929 * Remove 10 second key rotation option CORL-929 * Use memo in sorting SSO keys CORL-929 Co-authored-by: Wyatt Johnson <wyattjoh@gmail.com>
This commit is contained in:
co-authored by
Wyatt Johnson
parent
2dc63d3a27
commit
9a58673545
@@ -1242,86 +1242,233 @@ integration to register for a new account.
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-4"
|
||||
>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root SSOKeyField-root HorizontalGutter-spacing-2"
|
||||
<p
|
||||
className="FormFieldDescription-root"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
htmlFor="configure-auth-sso-key"
|
||||
To enable integration with your existing authentication system,
|
||||
you will need to create a JWT Token to connect. You can learn
|
||||
more about creating a JWT Token with
|
||||
<a
|
||||
className="ExternalLink-root"
|
||||
href="https://jwt.io/introduction/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Key
|
||||
</label>
|
||||
this introduction
|
||||
</a>
|
||||
. See our
|
||||
|
||||
<a
|
||||
className="ExternalLink-root"
|
||||
href="https://docs.coralproject.net/coral/v5/integrating/sso/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
documentation
|
||||
</a>
|
||||
for additional information on single sign on.
|
||||
</p>
|
||||
<label
|
||||
className="Label-root"
|
||||
htmlFor="configure-auth-sso-rotate-keys"
|
||||
>
|
||||
Keys
|
||||
</label>
|
||||
<div
|
||||
className="Card-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
className="Box-root HorizontalGutter-root HorizontalGutter-full"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignCenter"
|
||||
>
|
||||
<input
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
data-testid="password-field"
|
||||
id="configure-auth-sso-key"
|
||||
name="key"
|
||||
placeholder=""
|
||||
readOnly={true}
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Hide SSO Key"
|
||||
className="SSOKeyCard-keySection"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
<div
|
||||
className="SSOKeyCard-label"
|
||||
>
|
||||
visibility
|
||||
</i>
|
||||
<label
|
||||
className="Label-root"
|
||||
>
|
||||
Key ID
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="TextField-root TextField-fullWidth"
|
||||
>
|
||||
<input
|
||||
className="TextField-input TextField-colorRegular"
|
||||
data-testid="SSO-Key-ID"
|
||||
placeholder=""
|
||||
readOnly={true}
|
||||
type="text"
|
||||
value="kid-01"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="SSOKeyCard-secretSection"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-label"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
>
|
||||
Secret
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexStart Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-fullWidth PasswordField-root"
|
||||
>
|
||||
<div
|
||||
className="PasswordField-wrapper"
|
||||
>
|
||||
<input
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
|
||||
data-testid="password-field"
|
||||
id="configure-auth-sso-rotate-secretField"
|
||||
name="key"
|
||||
placeholder=""
|
||||
readOnly={true}
|
||||
spellCheck={false}
|
||||
type="password"
|
||||
value="secret"
|
||||
/>
|
||||
<div
|
||||
className="PasswordField-icon"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Hide Secret"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
visibility
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorMono Button-variantFlat Button-uppercase"
|
||||
data-color="mono"
|
||||
data-variant="flat"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
aria-label="Copy Secret"
|
||||
className="Icon-root Icon-md"
|
||||
>
|
||||
content_copy
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignFlexEnd"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexStart Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-statusSection"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-label"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
>
|
||||
Status
|
||||
</label>
|
||||
</div>
|
||||
<span
|
||||
className="StatusField-status StatusField-active"
|
||||
data-testid="SSO-Key-Status"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="DateField-label"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
>
|
||||
Active Since
|
||||
</label>
|
||||
</div>
|
||||
<span
|
||||
className="DateField-date"
|
||||
>
|
||||
1/1/2020, 1:00 AM
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Popover-root"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-disabled"
|
||||
data-color="regular"
|
||||
data-variant="regular"
|
||||
disabled={true}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="RotationDropdown-rotate"
|
||||
>
|
||||
Rotate
|
||||
</span>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm"
|
||||
>
|
||||
arrow_drop_down
|
||||
</i>
|
||||
</button>
|
||||
<div
|
||||
aria-hidden={true}
|
||||
aria-labelledby="sso-key-rotate-ariainfo"
|
||||
id="sso-key-rotate"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
className="AriaInfo-root"
|
||||
id="sso-key-rotate-ariainfo"
|
||||
>
|
||||
A dropdown to rotate the SSO key
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="SSOKeyField-warningSection"
|
||||
>
|
||||
<div
|
||||
className="Box-root Flex-root Flex-flex Flex-halfItemGutter Flex-directionRow gutter"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-sm SSOKeyField-warnIcon"
|
||||
>
|
||||
warning
|
||||
</i>
|
||||
<p
|
||||
className="HelperText-root"
|
||||
>
|
||||
When regenerating a key, tokens signed with the previous key will be honored for 30 days.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantRegular Button-uppercase Button-disabled SSOKeyField-regenerateButton"
|
||||
data-color="regular"
|
||||
data-variant="regular"
|
||||
disabled={true}
|
||||
id="configure-auth-sso-regenerate"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Regenerate
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="Box-root HorizontalGutter-root FormField-root HorizontalGutter-spacing-2"
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
CreateTestRendererParams,
|
||||
findParentWithType,
|
||||
replaceHistoryLocation,
|
||||
toJSON,
|
||||
wait,
|
||||
waitForElement,
|
||||
within,
|
||||
@@ -57,11 +58,11 @@ it("renders configure auth", async () => {
|
||||
expect(within(configureContainer).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("regenerate sso key", async () => {
|
||||
it("rotate sso key", async () => {
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Mutation: {
|
||||
regenerateSSOKey: () => {
|
||||
rotateSSOKey: () => {
|
||||
return {
|
||||
settings: pureMerge<typeof settingsWithEmptyAuth>(
|
||||
settingsWithEmptyAuth,
|
||||
@@ -69,8 +70,22 @@ it("regenerate sso key", async () => {
|
||||
auth: {
|
||||
integrations: {
|
||||
sso: {
|
||||
key: "==GENERATED_KEY==",
|
||||
keyGeneratedAt: "2018-11-12T23:26:06.239Z",
|
||||
enabled: true,
|
||||
keys: [
|
||||
{
|
||||
kid: "kid-01",
|
||||
secret: "secret",
|
||||
createdAt: "2015-01-01T00:00:00.000Z",
|
||||
lastUsedAt: "2016-01-01T01:45:00.000Z",
|
||||
rotatedAt: "2016-01-01T01:45:00.000Z",
|
||||
inactiveAt: "2016-01-01T01:45:00.000Z",
|
||||
},
|
||||
{
|
||||
kid: "kid-02",
|
||||
secret: "new-secret",
|
||||
createdAt: "2019-01-01T01:45:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -90,15 +105,40 @@ it("regenerate sso key", async () => {
|
||||
|
||||
act(() => {
|
||||
within(container)
|
||||
.getByText("Regenerate", { selector: "button" })
|
||||
.getByText("Rotate", { selector: "button" })
|
||||
.props.onClick();
|
||||
});
|
||||
|
||||
await wait(() =>
|
||||
expect(within(container).getByLabelText("Key").props.value).toBe(
|
||||
"==GENERATED_KEY=="
|
||||
)
|
||||
);
|
||||
const rotateNow = await waitForElement(() => {
|
||||
return within(container).getByText("Now", { selector: "button" });
|
||||
});
|
||||
|
||||
act(() => {
|
||||
rotateNow.props.onClick();
|
||||
});
|
||||
|
||||
await wait(() => {
|
||||
// Check that we have two SSO Keys that match
|
||||
// our expected key IDs
|
||||
const keyIDs = within(container).getAllByTestID("SSO-Key-ID");
|
||||
const hasOldKey = keyIDs.some(k => k.props.value === "kid-01");
|
||||
const hasNewKey = keyIDs.some(k => k.props.value === "kid-02");
|
||||
expect(hasNewKey).toBe(true);
|
||||
expect(hasOldKey).toBe(true);
|
||||
|
||||
const statuses = within(container).getAllByTestID("SSO-Key-Status");
|
||||
expect(statuses.length).toBe(2);
|
||||
const firstStatus: any = toJSON(statuses[0]);
|
||||
const firstStatusIsActive = firstStatus.children.some(
|
||||
(s: string) => s === "Active"
|
||||
);
|
||||
expect(firstStatusIsActive).toBe(true);
|
||||
const secondStatus: any = toJSON(statuses[1]);
|
||||
const secondStatusIsActive = secondStatus.children.some(
|
||||
(s: string) => s === "Active"
|
||||
);
|
||||
expect(secondStatusIsActive).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("prevents admin lock out", async () => {
|
||||
|
||||
@@ -114,6 +114,16 @@ export const settings = createFixture<GQLSettings>({
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
keys: [
|
||||
{
|
||||
kid: "kid-01",
|
||||
secret: "secret",
|
||||
createdAt: "2020-01-01T01:00:00.000Z",
|
||||
lastUsedAt: undefined,
|
||||
rotatedAt: undefined,
|
||||
inactiveAt: undefined,
|
||||
},
|
||||
],
|
||||
key: "",
|
||||
keyGeneratedAt: null,
|
||||
},
|
||||
@@ -202,6 +212,16 @@ export const settingsWithEmptyAuth = createFixture<GQLSettings>(
|
||||
stream: true,
|
||||
},
|
||||
key: "",
|
||||
keys: [
|
||||
{
|
||||
kid: "kid-01",
|
||||
secret: "secret",
|
||||
createdAt: "2020-01-01T01:00:00.000Z",
|
||||
lastUsedAt: undefined,
|
||||
rotatedAt: undefined,
|
||||
inactiveAt: undefined,
|
||||
},
|
||||
],
|
||||
keyGeneratedAt: null,
|
||||
},
|
||||
google: {
|
||||
|
||||
Reference in New Issue
Block a user