A JavaScript demo project for RingCentral token management

Tyler Liu
RingCentral Developers
5 min readSep 9, 2021

There are several articles and tutorials on this topic that go along with this article: A minimal project to implement RingCentral Auth Code Flow in JavaScript, RingCentral Token Management, Use Authorization Code + PKCE for RingCentral API in Client App, and the official pages for authentication.

There are a couple tricky parts that I wanted to cover that help with how to obtain a token in the first place, especially how to properly implement the 3-legged OAuth in a frontend application and how to properly store and manage tokens. Today I am going to give you a comprehensive demo application.

PKCE for OAuth

For this demo project, we only need the RingCentral app clientId. We don’t need the clientSecret. It is a bad practice to release any application with clientSecret to client side. Because as its name suggests, we should keep it secret.

Thanks to PKCE for OAuth, we can do OAuth without clientSecret. For more details, please refer to Embbnux Ji’s article: Use Authorization Code + PKCE for RingCentral API in Client App

Initialize RingCentral SDK

Thanks to PKCE for OAuth, we don’t need theclientSecret .

// initialize RingCentral SDK
const rcsdk = new SDK({
server: process.env.RINGCENTRAL_SERVER_URL,
clientId: process.env.RINGCENTRAL_CLIENT_ID,
});
const platform = rcsdk.platform();

What should the redirect URI be?

In theory, the redirect URI could be any page in your website. It could be a dedicated page like https://example.com/callback.html. The advantage of using a dedicated page is that it is dedicated and won’t mess up the other parts of your website.

But if your app is a Single Page Application (or SPA for short), by default there is one and only one page. You may want to keep it like that and refuse to create any new pages. It is perfectly fine to use an existing page as the redirect URI.

If your app is a SPA and you want to use the only page as redirect URI. You can simply do this:

const redirectUri = window.location.origin + window.location.pathname;

The good thing about the code snippet above is you don’t need to hard code it. Changing the hostname of your app won’t break it because it is dynamically generated.

Where to save the RingCentral token?

It is a good idea to save the RingCentral token if your app is a web application. Without saving the RingCentral token, you will need users to go through the 3-legged auth process every time they start your application, and also when they refresh the page.

There are several options for client-side data storage: Cookies, localStorage, IndexedDB and WebSQL. In this demo project I would like to use localForage:

localForage is a fast and simple storage library for JavaScript. localForage improves the offline experience of your web app by using asynchronous storage (IndexedDB or WebSQL) with a simple, localStorage-like API.

localForage uses localStorage in browsers with no IndexedDB or WebSQL support.

It is very easy to use:

// to write:
await localforage.setItem('key', {hello: 'world'});
// to read:
const obj = await localforage.getItem('key');
if(obj === null) {
// you will get null if it doesn't exist
} else {
// do whatever with obj
}

Add a login link

First we check if we already have a token, if not, we add a login to the page so that user can click and start the 3-legged OAuth process:

const token = await localforage.getItem(tokenKey);
if(token === null) {
addLoginLink();
}

Now let’s define the addLoginLink method:

const loginUrl = platform.loginUrl({
redirectUri,
usePKCE: true,
});
const link = document.createElement('a');
link.href = '#';
link.innerText = 'Login';
document.body.appendChild(link);

It’s pretty straightforward. But do pay attention to usePKCE: true. OK, we have the link, how will it react when a user clicks it?

link.onclick = () => {
const popupWindow = window.open(
loginUrl,
'Login RingCentral',
'width=300,height=400'
);
const handle = setInterval(async () => {
const code = (await localforage.getItem(authCodeKey)) as string;
if (code !== null) {
popupWindow?.close();
link.remove();
localforage.removeItem(authCodeKey);
clearInterval(handle);
await platform.login({code, redirect_uri: redirectUri});
await fetchCallLogs();
}
}, 1000);
};

We create a popup window which redirects user to the the login URL, then we check the localforage every second to see if we could get the code. If we get it, then we can invoke platform.login to get a RingCentral token. And after that we can make API calls because we have the token ready.

Wait a minute! “We check the localforage every second to see if we could get the code”. Who will write the code into localforage? I know you may have this question if you are closely following me so far.

Well, somewhere in your code, you need this:

// fetch code from query parameters and save it
const urlSearchParams = new URLSearchParams(
new URL(window.location.href).search
);
const code = urlSearchParams.get('code');
if (code !== null) {
await localforage.setItem(authCodeKey, code);
return; // because this is the popup window.
}

But why do we save it instead of using it directly to exchange for a token? Because the window which could access the code query parameter is the pop up window. We would like to keep main logics in the main window instead. So we just save the code and quit the pop up window. The main window detects the existence of code.

Save token whenever login success or refresh success

This one is tricky and you will need to listen to two events: events.refreshSuccess and events.loginSuccess :

// save token whenever login success or refresh success
const saveToken = async (res: Response) => {
const token = await res.clone().json();
await localforage.setItem(tokenKey, token);
};
platform.on(events.refreshSuccess, async res => {
saveToken(res);
});
platform.on(events.loginSuccess, async res => {
saveToken(res);
});

Reuse existing token

Please note that, RingCentral SDK (@ringcentral/sdk) auto refreshes token for you if access token expired. But the refresh will not succeed if the refresh token is auto expired. In such a case, we need to show the login link again to ask the user to login again.

Refresh tokens by default expire in 7 days. So in theory the user will never need to login again as long as he uses the service at least once per week.

// load saved token and fetch call logs.
const token = (await localforage.getItem(tokenKey)) as AuthData;
if (token === null) {
addLoginLink();
} else {
platform.auth().setData(token);
try {
await fetchCallLogs();
} catch (e) {
console.log(e); // most likely refresh token expired
addLoginLink();
}
}

Source Code

You can get source code about the project we built in this article. Please let us know what you think by leaving your questions and comments below. To learn even more about other features we have make sure to visit our developer site and if you’re ever stuck make sure to go to our developer forum.

Want to stay up to date and in the know about new APIs and features? Join our Game Changer Program and earn great rewards for building your skills and learning more about RingCentral!

--

--