OAuth2 Client

Prev Next

OAuth2 is an open standard that allows applications to access user information without exposing passwords. Rocket.Chat OAuth2 Client simplifies this process by handling the OAuth2 flow with third-party services like Google, GitHub, and others directly within Rocket.Chat.

This document demonstrates using OAuth2 to interact with Google APIs in a Rocket.Chat app.

Prerequisites

Ensure you have the following:

  1. Create and deploy the app: Start by creating a new Rocket.Chat app. For this example, name it OAuth, and then deploy the app to your workspace.

  2. Set up Google API credentials: In the Google API Console, create authorization credentials to obtain your Client ID and Client Secret. For this, you will need to create a client app of type web application. Once the client app is created, make sure to note the ID and secret.

  3. Configure authorized URLs: In the project’s Google API console, configure the following:

    1. Authorized JavaScript origins: Set the authorized JavaScript origins to the URL of your Rocket.Chat workspace.

    2. Authorized redirect URIs: You can get this value once the app is deployed on Rocket.Chat. You can find this URL by navigating to the app within your Rocket.Chat workspace, select Details, and then view the APIs section.

OAuth2 client setup

To set up the OAuth2 client in the Rocket.Chat app, start by importing the necessary modules into the app's main class:

import { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors';
// New files to be created in the project root folder
import { OAuth2Service } from './commands/OAuth2Service';
import { OAuthCommand } from './commands/OAuthCommand';
import { App } from '@rocket.chat/apps-engine/definition/App';

export class OAuthApp extends App {
    private oauth2Service: OAuth2Service;

    protected async extendConfiguration(configuration: IConfigurationExtend): Promise<void> {
        const oauthConfig = {
            alias: 'test',
            accessTokenUri: 'https://oauth2.googleapis.com/token',
            authUri: 'https://accounts.google.com/o/oauth2/v2/auth',
            refreshTokenUri: 'https://oauth2.googleapis.com/token',
            revokeTokenUri: 'https://oauth2.googleapis.com/revoke',
            defaultScopes: ['profile', 'email'],
        };

        this.oauth2Service = new OAuth2Service(this, oauthConfig);
        await this.oauth2Service.setup(configuration);

        // Register the slash command and pass the logger
        configuration.slashCommands.provideSlashCommand(new OAuthCommand(this.oauth2Service, this));
    }
}

The code above defines the OAuth2 configuration, which includes the authorization, token endpoints, client ID, client secret, and scopes. The code also imports two files used to manage OAuth operations and register slash commands.

OAuth service setup

Next, create the OAuth2Service.ts file and implement the service that will handle OAuth2 operations. It is recommended that you create a folder called commands in the project root, and add the file to the new folder.

Add the following code to the OAuth2Service.ts file:

import { IOAuth2Client } from '@rocket.chat/apps-engine/definition/oauth2/IOAuth2';
import { createOAuth2Client } from '@rocket.chat/apps-engine/definition/oauth2/OAuth2';
import { IConfigurationExtend, IPersistence, IRead, IHttp, ILogger } from '@rocket.chat/apps-engine/definition/accessors';
import { IUser } from '@rocket.chat/apps-engine/definition/users';
import { RocketChatAssociationRecord, RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata';

export class OAuth2Service {
    private oauthClient: IOAuth2Client;

    constructor(private app: any, private config: any) {
        this.oauthClient = createOAuth2Client(this.app, this.config);
    }

    public async setup(configuration: IConfigurationExtend): Promise<void> {
        try {
            await this.oauthClient.setup(configuration);
        } catch (error) {
            this.app.getLogger().error('[OAuth2Service] setup error', error);
        }
    }

    public async getUserAuthorizationUrl(user: IUser): Promise<string> {
        const url = await this.oauthClient.getUserAuthorizationUrl(user);
        return url.toString();
    }

    public async getAccessTokenForUser(user: IUser, read: IRead, logger: ILogger): Promise<any> {
        try {
            const association = new RocketChatAssociationRecord(RocketChatAssociationModel.USER, user.id);
            const [tokenData] = await read.getPersistenceReader().readByAssociation(association);
            if (tokenData) {
                logger.debug(`Token data retrieved for user ${user.username}:`, tokenData);
                logger.info(`Access token retrieved for user: ${user.username}`);
                return tokenData;
            } else {
                logger.warn(`No access token found for user: ${user.username}`);
                return null;
            }
        } catch (error) {
            logger.error(`Failed to get access token for user: ${user.username}`, error);
            throw error;
        }
    }
    public async refreshUserAccessToken(user: IUser, persis: IPersistence): Promise<void> {
        await this.oauthClient.refreshUserAccessToken(user, persis);
    }

    public async revokeUserAccessToken(user: IUser, persis: IPersistence): Promise<void> {
        await this.oauthClient.revokeUserAccessToken(user, persis);
    }

    public async handleOAuthCallback(user: IUser, code: string, http: IHttp, persis: IPersistence, logger: ILogger): Promise<void> {
        try {
            const response = await http.post(this.config.accessTokenUri, {
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                data: `code=${code}&client_id=${this.config.clientId}&client_secret=${this.config.clientSecret}&redirect_uri=${this.config.redirectUri}&grant_type=authorization_code`,
            });

            if (response.statusCode === 200 && response.data) {
                const tokenData = response.data;
                logger.debug(`Token data to be stored for user ${user.username}:`, tokenData);
                const association = new RocketChatAssociationRecord(RocketChatAssociationModel.USER, user.id);
                await persis.updateByAssociation(association, tokenData, true);
                logger.info(`Access token stored for user: ${user.username}`);
            } else {
                logger.error(`Failed to get access token: ${response.content}`);
            }
        } catch (error) {
            logger.error(`Failed to handle OAuth callback for user: ${user.username}`, error);
        }
    }
}

Here, the createOAuth2Client method takes in two parameters:

  1. app: The app itself.

  2. options: An object with props as configuration - see the definition documentation for more details.

The setup() method configures the OAuth2Client which is used to access multiple methods like getAccessTokenForUser, revokeUserAccessToken etc., that will handle user-specific OAuth2 operations.

  • getAccessTokenForUser: Gets the token information for a specific user, if available. This receives the user instance as a parameter and returns data about the authenticated user.

  • getUserAuthorizationUrl: Returns the authorization URL to which the user must be redirected to authorize access to the application

  • refreshUserAccessToken: Refreshes the user's access token. This is useful when the user access token has expired.

  • revokeUserAccessToken: This function revokes the user's access token in the service provider. When successfully executed, users must be authenticated again before using the service.

OAuth command setup

To enable users to interact with the OAuth2 setup, create a new file named OAuthCommand.ts in the commands folder and define the following slash command:

import { IHttp, IModify, IPersistence, IRead } from '@rocket.chat/apps-engine/definition/accessors';
import { ISlashCommand, SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands';
import { OAuth2Service } from './OAuth2Service';
import { ILogger } from '@rocket.chat/apps-engine/definition/accessors';
import { App } from '@rocket.chat/apps-engine/definition/App';

export class OAuthCommand implements ISlashCommand {
    public command = 'oauth';
    public i18nParamsExample = '';
    public i18nDescription = 'OAuth command for testing';
    public providesPreview = false;

    constructor(private readonly oauth2Service: OAuth2Service, private readonly app: App) { }

    public async executor(context: SlashCommandContext, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise<void> {
        const user = context.getSender();
        const args = context.getArguments();

        try {
            if (args[0] === 'token') {
                // Retrieve and display the access token
                const tokenData = await this.oauth2Service.getAccessTokenForUser(user, read, this.app.getLogger());
                if (tokenData && tokenData.token) {
                    await modify.getNotifier().notifyUser(
                        user,
                        modify.getCreator().startMessage()
                            .setText(`Access token: ${tokenData.token}`)
                            .setRoom(context.getRoom())
                            .getMessage()
                    );
                } else {
                    await modify.getNotifier().notifyUser(
                        user,
                        modify.getCreator().startMessage()
                            .setText('No access token found. Please authorize the app first.')
                            .setRoom(context.getRoom())
                            .getMessage()
                    );
                }
            } else if (args[0] === 'refresh') {
                await this.oauth2Service.refreshUserAccessToken(user, persis);
                await modify.getNotifier().notifyUser(
                    user,
                    modify.getCreator().startMessage()
                        .setText('Access token refreshed successfully.')
                        .setRoom(context.getRoom())
                        .getMessage()
                );
            } else if (args[0] === 'revoke') {
                await this.oauth2Service.revokeUserAccessToken(user, persis);
                await modify.getNotifier().notifyUser(
                    user,
                    modify.getCreator().startMessage()
                        .setText('Access token revoked successfully.')
                        .setRoom(context.getRoom())
                        .getMessage()
                );
            } else {
                const authUrl = await this.oauth2Service.getUserAuthorizationUrl(user);
                await modify.getNotifier().notifyUser(
                    user,
                    modify.getCreator().startMessage()
                        .setText(`Please authorize the app by visiting: ${authUrl}`)
                        .setRoom(context.getRoom())
                        .getMessage()
                );
            }
        } catch (error) {
            this.app.getLogger().error('Error executing OAuth command:', error);
            const message = error instanceof Error ? error.message : String(error);
            await modify.getNotifier().notifyUser(
                user,
                modify.getCreator().startMessage()
                    .setText(`An error occurred while processing the OAuth command: ${message}`)
                    .setRoom(context.getRoom())
                    .getMessage()
            );
        }
    }
}

The slash command here is oauth which accepts different arguments (token, refresh, revoke) to perform corresponding actions.

Deploy the app

In your terminal, go to the app folder project and run the following command:

rc-apps deploy --url <server_url> -u <user> -p <pwd>
  • Replace <server_url> with the URL of your Rocket.Chat workspace.

  • Replace <user> and <pwd> with your Rocket.Chat username and password.

After running the command, your app is deployed to the workspace.

Alternatively, run the rc-apps package command to generate a .zip file of your app. This file can be uploaded manually to your Rocket.Chat workspace as a private app.

Test the app

  1. Once the app is available in the workspace, go to the APIs section in the app's Details tab. Get the callback URL and paste it in the Authorized redirect URIs setting in your Google client web app. For example, the URL can look like this: https://test.rocket.chat/api/apps/public/3ec6e34b-a605-44c2-9cf3-e35b4d08ded0/test-callback. Now you can create the web app.

  2. Next, go to the app's Settings tab in Rocket.Chat. Enter the client ID and secret you get after creating the web app on the Google console. Save the changes.

Try using these slash commands in any room in the workspace:

  • /oauth to authorize the app.

  • /oauth token to get the authorization token.

  • /oauth refesh to refresh the token.

  • /oauth revoke to revoke the access.

When the application is successfully authorized, you can verify the app through the third-party apps connected to your Google account. Likewise, when you revoke this access, the app should be subsequently removed.