diff --git a/README.md b/README.md
index 9c5d4ee..1569293 100644
--- a/README.md
+++ b/README.md
@@ -178,12 +178,19 @@ This logout process combines two parts: clearing OAuth session cookies through t
4. Once the logout is completed, the cookie logged_state will be set to false.
+5. Once the user has succesfully logged out of Hydra, we should revoke their legacy tokens (e.g. a1-...). We can do this by calling the function `revokeLegacyTokens` and passing in the list of legacy tokens to revoke. Typically you should do this inside your logout handler, which this is invoked after Hydra has successfully logged the user's session out.
+
```typescript
-import { OAuth2Logout } from '@deriv-com/auth-client';
+import { OAuth2Logout, revokeLegacyTokens } from '@deriv-com/auth-client';
// we clean up everything related to the user here, for now it's just user's account
// later on we should clear user tokens as well
const logout = useCallback(async () => {
+ const clientAccounts = localStorage.getItem('client.accounts') || []
+ const tokens = Object.values(clientAccounts).map(account => account.token);
+ // revoke the legacy tokens,
+ await revokeLegacyTokens(tokens);
+ // then call your application's post-logout cleanup functions
await apiManager.logout();
updateLoginAccounts([]);
updateCurrentLoginAccount({
@@ -200,3 +207,25 @@ const handleLogout = () => {
// In your button
```
+
+## Logout Front Channels
+
+Front channels are pages in your application which sole responsibility is to clear local/session storage of your authentication data like `client.accounts` or `loginid` when its rendered. This will be used and invoked by OIDC in an iframe during the logout process to log your application out and clear its local/session storage, so that when we land in the application, it will already be in a logged out state. Typically we register front channels under the route `/front-channel`.
+
+For instance, assume that you are already logged in on Deriv App, SmartTrader and Traders Hub Outsystems, and that these applications have front channels implemented on routes like:
+
+- `https://app.deriv.com/front-channel`
+- `https://smarttrader.deriv.com/front-channel` and
+- `https://hub.deriv.com/tradershub/front-channel`
+
+Then assume that you are logging out from Deriv.app. When you click the Logout button, in the background Hydra checks any of its registered applications that has the front channels logout route and has a session, and automatically invokes the route using an iframe like:
+
+```
+
+
+
+```
+
+Which will automatically clear the local/session storage for authentication data within SmartTrader and Traders Hub Outsystems, so that when the user navigates to SmartTrader or Traders Hub Outsystems, they are already in a logged out state since their `client.accounts` is already cleared off the local storage.
+
+Once the route and page is implemented, you will need to notify the authentication squad or DevOps to register the front channel logout URI in order for Hydra to invoke it during the logout flow.
diff --git a/src/components/SilentCallback/SilentCallback.tsx b/src/components/SilentCallback/SilentCallback.tsx
new file mode 100644
index 0000000..00ba998
--- /dev/null
+++ b/src/components/SilentCallback/SilentCallback.tsx
@@ -0,0 +1,50 @@
+import { useCallback, useEffect } from 'react';
+import { requestLegacyToken, requestOidcToken } from '../../oidc';
+
+type SilentCallbackProps = {
+ /** URI to redirect to the silent callback page. **/
+ redirectSilentCallbackUri?: string;
+};
+
+export const SilentCallback = ({ redirectSilentCallbackUri }: SilentCallbackProps) => {
+ const fetchTokens = useCallback(async () => {
+ try {
+ const { accessToken } = await requestOidcToken({
+ redirectCallbackUri: redirectSilentCallbackUri,
+ });
+
+ if (accessToken) {
+ const legacyTokens = await requestLegacyToken(accessToken);
+
+ window.parent.postMessage({
+ event: 'login_successful',
+ value: legacyTokens,
+ });
+ }
+ } catch (err) {
+ console.error('unable to exchange tokens during silent login', err);
+ window.parent.postMessage({
+ event: 'login_error',
+ value: err,
+ });
+ }
+ }, [redirectSilentCallbackUri]);
+
+ useEffect(() => {
+ const params = new URLSearchParams(window.location.search);
+ const oneTimeCode = params.get('code');
+ const errorType = params.get('error');
+
+ if (errorType === 'login_required') {
+ window.parent.postMessage({
+ event: 'login_required',
+ });
+ } else {
+ if (oneTimeCode) {
+ fetchTokens();
+ }
+ }
+ }, []);
+
+ return <>>;
+};
diff --git a/src/components/SilentCallback/index.ts b/src/components/SilentCallback/index.ts
new file mode 100644
index 0000000..3015d9b
--- /dev/null
+++ b/src/components/SilentCallback/index.ts
@@ -0,0 +1 @@
+export { SilentCallback } from './SilentCallback';
diff --git a/src/oidc/error.ts b/src/oidc/error.ts
index 66acda8..6ddf721 100644
--- a/src/oidc/error.ts
+++ b/src/oidc/error.ts
@@ -3,6 +3,7 @@ export enum OIDCErrorType {
AuthenticationRequestFailed = 'AuthenticationRequestFailed',
AccessTokenRequestFailed = 'AccessTokenRequestFailed',
LegacyTokenRequestFailed = 'LegacyTokenRequestFailed',
+ RevokeTokenRequestFailed = 'RevokeTokenRequestFailed',
UserManagerCreationFailed = 'UserManagerCreationFailed',
OneTimeCodeMissing = 'OneTimeCodeMissing',
FailedToRemoveSession = 'FailedToRemoveSession',
diff --git a/src/oidc/oidc.ts b/src/oidc/oidc.ts
index a8db3c0..937f062 100644
--- a/src/oidc/oidc.ts
+++ b/src/oidc/oidc.ts
@@ -31,6 +31,10 @@ type RequestOidcAuthenticationOptions = {
postLogoutRedirectUri?: string;
};
+type requestOidcSilentAuthenticationOptions = {
+ redirectSilentCallbackUri: string;
+};
+
type RequestOidcTokenOptions = {
redirectCallbackUri?: string;
postLogoutRedirectUri?: string;
@@ -144,6 +148,25 @@ export const requestOidcAuthentication = async (options: RequestOidcAuthenticati
}
};
+export const requestOidcSilentAuthentication = async (options: requestOidcSilentAuthenticationOptions) => {
+ const { redirectSilentCallbackUri } = options;
+
+ try {
+ const userManager = await createUserManager({
+ redirectCallbackUri: redirectSilentCallbackUri,
+ });
+
+ await userManager.signinSilent({
+ extraQueryParams: {
+ brand: 'deriv',
+ },
+ });
+ return { userManager };
+ } catch (error) {
+ console.error('Authentication failed:', error);
+ }
+};
+
/**
* Requests access tokens from the authorization server. * The returned access tokens will be used to fetch the original tokens that can be passed to the `authorize` endpoint.
*
@@ -244,6 +267,53 @@ export const requestLegacyToken = async (accessToken: string): Promise} A promise that resolves when the tokens are successfully revoked
+ *
+ * @throws {OIDCError} With type `RevokeTokenRequestFailed` if:
+ * - The request fails due to network issues (500 Internal Server Error)
+ * - The tokens array is empty or invalid format (400 Bad Request - InvalidPayload)
+ * - The tokens are invalid, already revoked, or belong to different users/app_ids (400 Bad Request - InvalidToken)
+ * - The number of tokens exceeds the maximum limit of 20 (400 Bad Request - InvalidTokenCount)
+ * - Rate limit is exceeded - more than 5 requests per minute (429 Too Many Requests - RateLimit)
+ *
+ * @example
+ * ```typescript
+ * try {
+ * const legacyTokens = [
+ * 'a1-....',
+ * 'a1-....'
+ * ];
+ * await revokeLegacyTokens(legacyTokens);
+ * // Tokens successfully revoked
+ * } catch (error) {
+ * if (error instanceof OIDCError) {
+ * // Handle specific revocation errors
+ * console.error(error.message);
+ * }
+ * }
+ * ```
+ */
+export const revokeLegacyTokens = async (tokens: string[]): Promise => {
+ const { serverUrl } = getServerInfo();
+
+ try {
+ await fetch(`https://${serverUrl}/oauth2/legacy/tokens/revoke`, {
+ method: 'POST',
+ body: JSON.stringify(tokens),
+ });
+ } catch (error) {
+ console.error('unable to request legacy tokens: ', error);
+ if (error instanceof Error) throw new OIDCError(OIDCErrorType.RevokeTokenRequestFailed, error.message);
+ throw new OIDCError(OIDCErrorType.RevokeTokenRequestFailed, 'unable to revoke legacy tokens');
+ }
+};
+
/**
* Creates a UserManager instance that will be used to manage and call the OIDC flow
* @param options - Configuration options for the OIDC token request