Skip to content

Commit

Permalink
Merge pull request #1378 from authts/backport-on-signout-callback
Browse files Browse the repository at this point in the history
backport on signout callback
  • Loading branch information
pamapa authored Oct 4, 2024
2 parents eb13ab9 + 98cb713 commit f395fec
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 20 deletions.
3 changes: 3 additions & 0 deletions docs/react-oidc-context.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { SigninResourceOwnerCredentialsArgs } from 'oidc-client-ts';
import type { SigninSilentArgs } from 'oidc-client-ts';
import type { SignoutPopupArgs } from 'oidc-client-ts';
import type { SignoutRedirectArgs } from 'oidc-client-ts';
import type { SignoutResponse } from 'oidc-client-ts';
import type { SignoutSilentArgs } from 'oidc-client-ts';
import { User } from 'oidc-client-ts';
import { UserManager } from 'oidc-client-ts';
Expand Down Expand Up @@ -73,8 +74,10 @@ export interface AuthProviderPropsBase extends UserManagerSettings {
children?: React_2.ReactNode;
// @deprecated (undocumented)
implementation?: typeof UserManager | null;
matchSignoutCallback?: (args: UserManagerSettings) => boolean;
onRemoveUser?: () => Promise<void> | void;
onSigninCallback?: (user: User | void) => Promise<void> | void;
onSignoutCallback?: (resp: SignoutResponse | void) => Promise<void> | void;
// @deprecated (undocumented)
onSignoutPopup?: () => Promise<void> | void;
// @deprecated (undocumented)
Expand Down
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"prepare": "husky install"
},
"peerDependencies": {
"oidc-client-ts": "^2.2.1",
"oidc-client-ts": "^2.4.1",
"react": ">=16.8.0"
},
"devDependencies": {
Expand Down
51 changes: 49 additions & 2 deletions src/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import type {
SignoutPopupArgs,
SignoutSilentArgs,
ProcessResourceOwnerPasswordCredentialsArgs,
SignoutResponse,
} from "oidc-client-ts";

import { AuthContext } from "./AuthContext";
import { initialAuthState } from "./AuthState";
import { reducer } from "./reducer";
import { hasAuthParams, loginError } from "./utils";
import { hasAuthParams, loginError, signoutError } from "./utils";

/**
* @public
Expand Down Expand Up @@ -50,6 +51,38 @@ export interface AuthProviderPropsBase extends UserManagerSettings {
*/
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 = (resp: SignoutResponse | void): void => {
* // go to home after logout
* window.location.pathname = ""
* }
* ```
*/
onSignoutCallback?: (resp: SignoutResponse | void) => 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 @@ -140,6 +173,9 @@ export const AuthProvider = (props: AuthProviderProps): JSX.Element => {
onSigninCallback,
skipSigninCallback,

matchSignoutCallback,
onSignoutCallback,

onRemoveUser,
onSignoutRedirect,
onSignoutPopup,
Expand Down Expand Up @@ -204,6 +240,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 @@ -216,8 +253,18 @@ export const AuthProvider = (props: AuthProviderProps): JSX.Element => {
} catch (error) {
dispatch({ type: "ERROR", error: loginError(error) });
}

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

// register to userManager events
React.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 loginError = normalizeErrorFn("Login 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 @@ -105,6 +105,37 @@ describe("AuthProvider", () => {
await waitFor(() => expect(onSigninCallback).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 f395fec

Please sign in to comment.