From 40945b6eba616e99bf8806b11ca8cad1fda9a0a8 Mon Sep 17 00:00:00 2001 From: Embbnux Ji Date: Thu, 15 Aug 2024 14:23:59 +0800 Subject: [PATCH] feat: support authorization with RingCentral jwt token (#107) * feat: support authorization with jwt token * chore: add jwt auth doc --- docs/customize-authorization.md | 39 ++++++++++++++++++++++++++---- src/adapter.ts | 2 ++ src/app.tsx | 5 ++-- src/brands/rc/index.ts | 6 ++--- src/modules/OAuth/index.ts | 42 ++++++++++++++++++++++++++++++++- src/modules/Phone/index.ts | 2 ++ 6 files changed, 86 insertions(+), 10 deletions(-) diff --git a/docs/customize-authorization.md b/docs/customize-authorization.md index 30f4ced..fc6f691 100644 --- a/docs/customize-authorization.md +++ b/docs/customize-authorization.md @@ -1,10 +1,41 @@ -# Customize Authorization +# Alternative authorization methods -In widget, it popups a window with [RingCentral authorization uri](https://developers.ringcentral.com/api-reference/Authorization) from [authorization code with PKCE flow](https://medium.com/ringcentral-developers/use-authorization-code-pkce-for-ringcentral-api-in-client-app-e9108f04b5f0) for user login. But you can also implement the popup window out of the widget, and pass authorization code to widget after you get authorization callback. +In the widget, it popups a window with [RingCentral authorization uri](https://developers.ringcentral.com/api-reference/Authorization) from [authorization code with PKCE flow](https://medium.com/ringcentral-developers/use-authorization-code-pkce-for-ringcentral-api-in-client-app-e9108f04b5f0) when user click login button. This is the recommended authorization method for applications like those built on top of RingCX Embeddable. Therefore, no changes are necessary to enable authorization and usage of RingCX Embeddable. However, some developers in specific and rare circumstances may wish to utilize a different method of authorization. This guide will instruct developers on how to do so. + +## JWT flow + +Developers can login to RingCX Embeddable using the [JWT auth flow](https://developers.ringcentral.com/guide/authentication/jwt/quick-start) if they so choose. A JWT token is bind to a RingCentral user when it is generated. If developer shares the same JWT token to multiple users, it means that every user of RingCX Embeddable will be logged in as the same RingCentral user, which may undermine the value of RingCentral's audit trail and security practices. Please use at your own risk. For different users, developers need to help them to generate different JWT tokens with their own RingCentral credentials. + +!!! warning "JWT auth flow in Embeddable is experimental" + While the JWT auth flow itself is not experimental, its usage within the context of RingCX Embeddable is. This is due to the fact that using JWT in this way is beyond the intended design of Embeddable, and could be problematic in some circumstances. + + JWT also requires you to expose your client secret, which if exposed publicly could expose you to some security risks. + +### Adapter JS way + +```js + (function() { + var rcs = document.createElement("script"); + rcs.src = "https://ringcentral.github.io/engage-voice-embeddable/adapter.js?clientId=YOU_OWN_JWT_APP_CLIENT_ID&clientSecret=YOUR_OWN_JWT_APP_CLIENT_SECRET&jwt=JWT_TOKEN"; + var rcs0 = document.getElementsByTagName("script")[0]; + rcs0.parentNode.insertBefore(rcs, rcs0); + })(); +``` + +### Iframe way + +```html + +``` + +## Authorization code flow with PKCE + +This section is for developers who want to manage login popup window. **Prerequisites**: [Customize `clientId`](customize-client-id.md) -## Get authorization uri +### Get authorization uri For authorization code with PKCE, the widget will create `code_verifier` and `code_challenge` pair for generating oauth uri and exchanging token. So we need to get oauth login URI from it. @@ -35,7 +66,7 @@ window.addEventListener('message', (e) => { }); ``` -## Pass RingCentral authorization code: +### Pass RingCentral authorization code: ```js document.querySelector("#engage-voice-embeddable-adapter-frame").contentWindow.postMessage({ diff --git a/src/adapter.ts b/src/adapter.ts index e66bfa9..bd8fcc4 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -32,6 +32,7 @@ const { disableLoginPopup, enablePopup, popupPageUri, + jwt, } = parseUri(paramsUri); function obj2uri(obj) { @@ -56,6 +57,7 @@ const appUri = `${appUrl}?${obj2uri({ disableLoginPopup, fromAdapter: 1, fromPopup, + jwt, _t: Date.now(), })}`; diff --git a/src/app.tsx b/src/app.tsx index cadad78..c414b63 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -27,6 +27,7 @@ const { evServer, disableLoginPopup, fromPopup, + jwt, } = pathParams; if (clientId) { @@ -48,8 +49,7 @@ const phone = createPhone({ sdkConfig, brandConfig, evSdkConfig, - redirectUri, - authConfig, + redirectUri: redirectUri || authConfig.redirectUri, prefix, version, buildHash, @@ -57,6 +57,7 @@ const phone = createPhone({ runTimeEnvironment: process.env.NODE_ENV, disableLoginPopup: !!disableLoginPopup, fromPopup, + jwt, }); const store = createStore(phone.reducer); diff --git a/src/brands/rc/index.ts b/src/brands/rc/index.ts index 43812d7..5e73eef 100644 --- a/src/brands/rc/index.ts +++ b/src/brands/rc/index.ts @@ -3,7 +3,7 @@ export default { brandCode: 'rc', code: 'rc', name: 'RingCentral', - appName: 'RingCentral Engage Cloud Contact Center', - fullName: 'RingCentral Engage Cloud Contact Center', - application: 'RingCentral Engage Cloud Contact Center', + appName: 'RingCX Embeddable', + fullName: 'RingCentral RingCX Embeddable', + application: 'RingCX Embeddable', }; diff --git a/src/modules/OAuth/index.ts b/src/modules/OAuth/index.ts index 29bbbf7..a61be66 100644 --- a/src/modules/OAuth/index.ts +++ b/src/modules/OAuth/index.ts @@ -1,6 +1,7 @@ import { Module } from '@ringcentral-integration/commons/lib/di'; import OAuthBase from '@ringcentral-integration/widgets/modules/OAuth'; - +import { loginStatus } from '@ringcentral-integration/commons/modules/Auth/loginStatus'; +import { watch } from '@ringcentral-integration/core'; import messageTypes from '../../enums/messageTypes'; @Module({ @@ -10,6 +11,8 @@ import messageTypes from '../../enums/messageTypes'; ] }) export default class OAuth extends OAuthBase { + protected _userLogout = false; + openOAuthPage() { if (this._deps.oAuthOptions.disableLoginPopup) { window.parent.postMessage({ @@ -20,5 +23,42 @@ export default class OAuth extends OAuthBase { } super.openOAuthPage(); } + + override onInitOnce() { + super.onInitOnce(); + watch( + this, + () => [ + this.ready, + this._deps.auth.loginStatus, + ], + async () => { + if (!this.ready) { + return; + } + if (this._deps.auth.loginStatus === loginStatus.beforeLogout) { + // Do not jwt login after logout + this._userLogout = true; + } + if (this._userLogout) { + return; + } + if ( + !this._deps.auth.notLoggedIn + ) { + return; + } + if (this._deps.oAuthOptions.jwt) { + this._deps.auth.setLogin(); + this._deps.client.service.platform().login({ + jwt: this._deps.oAuthOptions.jwt, + }); + } + }, + { + multiple: true, + }, + ); + } } diff --git a/src/modules/Phone/index.ts b/src/modules/Phone/index.ts index c94502d..d0ee26e 100644 --- a/src/modules/Phone/index.ts +++ b/src/modules/Phone/index.ts @@ -393,6 +393,7 @@ export function createPhone({ disableLoginPopup, redirectUri, fromPopup, + jwt, }) { const appVersion = buildHash ? `${version} (${buildHash})` : version; const usePKCE = sdkConfig.clientId && !sdkConfig.clientSecret; @@ -465,6 +466,7 @@ export function createPhone({ extralUIOptions: ['hide_remember_me', 'hide_tos', '-old_ui'], disableLoginPopup, redirectUri, + jwt, }, }, { provide: 'AuthOptions', useValue: { usePKCE } },