Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -200,3 +207,25 @@ const handleLogout = () => {
// In your button
<button onClick={handleLogout}>Logout</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:

```
<iframe src="https://app.deriv.com/front-channel?iss=https://oauth.deriv.com&sid=..."></iframe>
<iframe src="https://smarttrader.deriv.com/front-channel?iss=https://oauth.deriv.com&sid=..."></iframe>
<iframe src="https://hub.deriv.com/tradershub/front-channel?iss=https://oauth.deriv.com&sid=..."></iframe>
```

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.
1 change: 1 addition & 0 deletions src/oidc/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export enum OIDCErrorType {
AuthenticationRequestFailed = 'AuthenticationRequestFailed',
AccessTokenRequestFailed = 'AccessTokenRequestFailed',
LegacyTokenRequestFailed = 'LegacyTokenRequestFailed',
RevokeTokenRequestFailed = 'RevokeTokenRequestFailed',
UserManagerCreationFailed = 'UserManagerCreationFailed',
OneTimeCodeMissing = 'OneTimeCodeMissing',
FailedToRemoveSession = 'FailedToRemoveSession',
Expand Down
47 changes: 47 additions & 0 deletions src/oidc/oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,53 @@ export const requestLegacyToken = async (accessToken: string): Promise<LegacyTok
}
};

/**
* Revokes legacy tokens by making a POST request to the legacy token revocation endpoint.
* Once these tokens are revoked, they can no longer be used for authentication.
*
* @param {string[]} tokens - An array of legacy tokens (a1-... format) to be revoked. Maximum 20 tokens allowed.
* All tokens must belong to the same user and app_id.
* @returns {Promise<void>} 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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got these error codes from the backend clickup card

*
* @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<void> => {
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
Expand Down
Loading