Skip to content

Commit

Permalink
feat: add sign-out callback support
Browse files Browse the repository at this point in the history
  • Loading branch information
pamapa committed Feb 9, 2024
1 parent 0c57e8a commit 210f1c0
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 4 deletions.
2 changes: 2 additions & 0 deletions docs/react-oidc-context.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ export const AuthProvider: (props: AuthProviderProps) => JSX.Element;
// @public (undocumented)
export interface AuthProviderBaseProps {
children?: React_2.ReactNode;
matchSignoutCallback?: (args: UserManagerSettings) => boolean;
onRemoveUser?: () => Promise<void> | void;
onSigninCallback?: (user: User | void) => Promise<void> | void;
onSignoutCallback?: () => Promise<void> | void;
skipSigninCallback?: boolean;
}

Expand Down
50 changes: 48 additions & 2 deletions src/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {
import { AuthContext } from "./AuthContext";
import { initialAuthState } from "./AuthState";
import { reducer } from "./reducer";
import { hasAuthParams, signinError } from "./utils";
import { hasAuthParams, signinError, signoutError } from "./utils";

/**
* @public
Expand Down Expand Up @@ -54,6 +54,38 @@ export interface AuthProviderBaseProps {
*/
skipSigninCallback?: boolean;

/**
* Match the redirect uri used for logout (e.g. `post_logout_redirect_uri`)
* This provider will then call automatically the `userManager.signoutCallback`.
*
* HINT:
* Do not call `userManager.signoutRedirect()` within a `React.useEffect`, otherwise the
* logout might be unsuccessful.
*
* ```jsx
* <AuthProvider
* matchSignoutCallback={(args) => {
* window &&
* (window.location.href === args.post_logout_redirect_uri);
* }}
* ```
*/
matchSignoutCallback?: (args: UserManagerSettings) => boolean;

/**
* On sign out callback hook. Can be a async function.
* Here you can change the url after the user is signed out.
* When using this, specifying `matchSignoutCallback` is required.
*
* ```jsx
* const onSignoutCallback = (): void => {
* // go to home after logout
* window.location.pathname = ""
* }
* ```
*/
onSignoutCallback?: () => Promise<void> | void;

/**
* On remove user hook. Can be a async function.
* Here you can change the url after the user is removed.
Expand Down Expand Up @@ -133,6 +165,9 @@ export const AuthProvider = (props: AuthProviderProps): JSX.Element => {
onSigninCallback,
skipSigninCallback,

matchSignoutCallback,
onSignoutCallback,

onRemoveUser,

userManager: userManagerProp = null,
Expand Down Expand Up @@ -194,6 +229,7 @@ export const AuthProvider = (props: AuthProviderProps): JSX.Element => {
didInitialize.current = true;

void (async (): Promise<void> => {
// sign-in
let user: User | void | null = null;
try {
// check if returning back from authority server
Expand All @@ -206,8 +242,18 @@ export const AuthProvider = (props: AuthProviderProps): JSX.Element => {
} catch (error) {
dispatch({ type: "ERROR", error: signinError(error) });
}

// sign-out
try {
if (matchSignoutCallback && matchSignoutCallback(userManager.settings)) {
await userManager.signoutCallback();
onSignoutCallback && (await onSignoutCallback());
}
} catch (error) {
dispatch({ type: "ERROR", error: signoutError(error) });

Check warning on line 253 in src/AuthProvider.tsx

View check run for this annotation

Codecov / codecov/patch

src/AuthProvider.tsx#L253

Added line #L253 was not covered by tests
}
})();
}, [userManager, skipSigninCallback, onSigninCallback]);
}, [userManager, skipSigninCallback, onSigninCallback, onSignoutCallback, matchSignoutCallback]);

// register to userManager events
useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ const normalizeErrorFn = (fallbackMessage: string) => (error: unknown): Error =>
};

export const signinError = normalizeErrorFn("Sign-in failed");
export const signoutError = normalizeErrorFn("Sign-out failed");
31 changes: 31 additions & 0 deletions test/AuthProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,37 @@ describe("AuthProvider", () => {
expect(UserManager.prototype.signinCallback).toHaveBeenCalledTimes(1);
});

it("should handle signoutCallback success and call onSignoutCallback", async () => {
// arrange
const onSignoutCallback = jest.fn();
window.history.pushState(
{},
document.title,
"/signout-callback",
);
expect(window.location.pathname).toBe(
"/signout-callback",
);

const wrapper = createWrapper({
...settingsStub,
post_logout_redirect_uri: "https://www.example.com/signout-callback",
matchSignoutCallback: () => window.location.pathname === "/signout-callback",
onSignoutCallback,
});

// act
act(() => {
renderHook(() => useAuth(), {
wrapper,
});
});

// assert
await waitFor(() => expect(onSignoutCallback).toHaveBeenCalledTimes(1));
expect(UserManager.prototype.signoutCallback).toHaveBeenCalledTimes(1);
});

it("should signinResourceOwnerCredentials when asked", async () => {
// arrange
const wrapper = createWrapper({ ...settingsStub });
Expand Down
6 changes: 4 additions & 2 deletions test/__mocks__/oidc-client-ts.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { UserManager, UserManagerEvents } from "oidc-client-ts";
import type { UserManager, UserManagerEvents, UserManagerSettings } from "oidc-client-ts";

const MockUserManager: typeof UserManager = jest.fn(function (this: { events: Partial<UserManagerEvents> }) {
const MockUserManager: typeof UserManager = jest.fn(function (this: { events: Partial<UserManagerEvents>; settings: Partial<UserManagerSettings> }, args: UserManagerSettings) {
this.events = {
load: jest.fn(),
unload: jest.fn(),
Expand All @@ -24,6 +24,8 @@ const MockUserManager: typeof UserManager = jest.fn(function (this: { events: Pa
removeUserSessionChanged: jest.fn(),
};

this.settings = args;

return this as UserManager;
});
MockUserManager.prototype.clearStaleState = jest.fn().mockResolvedValue(undefined);
Expand Down

0 comments on commit 210f1c0

Please sign in to comment.