- Print
- DarkLight
- PDF
OAuth2 Client
- Print
- DarkLight
- PDF
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:
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.Set up Google API credentials: In the Google API Console, create authorization credentials to obtain your Client ID and Client Secret.
Configure authorized URLs: In the project’s Google API console, configure the following:
Authorized JavaScript origins: Set the authorized JavaScript origins to the URL of your Rocket.Chat workspace.
Authorized redirect URIs: Set the authorized redirect URIs to the app's API URL. 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 './OAuth2Service';
import { OAuthCommand } from './OAuthCommand';
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.getLogger()));
}
}
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 which will be created in the project root folder. These files each have two methods:
OAuth2Service
which manages the OAuth2 operations.OAuthCommand
registers the user commands that interact with the OAuth2 service.
OAuth service setup
Next, create the OAuth2Service.ts
file and implement the service that will handle OAuth2 operations:
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 } 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): Promise<any> {
try {
const association = new RocketChatAssociationRecord(RocketChatAssociationModel.USER, user.id);
const [tokenData] = await read.getPersistenceReader().readByAssociation(association);
if (tokenData) {
this.app.getLogger().debug(`Token data retrieved for user ${user.username}:`, tokenData);
this.app.getLogger().info(`Access token retrieved for user: ${user.username}`);
return tokenData;
} else {
this.app.getLogger().warn(`No access token found for user: ${user.username}`);
return null;
}
} catch (error) {
this.app.getLogger().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): 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;
this.app.getLogger().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);
this.app.getLogger().info(`Access token stored for user: ${user.username}`);
} else {
this.app.getLogger().error(`Failed to get access token: ${response.content}`);
}
} catch (error) {
this.app.getLogger().error(`Failed to handle OAuth callback for user: ${user.username}`, error);
}
}
}
Here, the createOAuth2Client
method takes in two parameters:
app: The app itself.
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 applicationrefreshUserAccessToken
: 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
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';
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 logger: ILogger) {}
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);
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.logger.error('Error executing OAuth command:', error);
await modify.getNotifier().notifyUser(
user,
modify.getCreator().startMessage()
.setText(`An error occurred while processing the OAuth command: ${error.message}`)
.setRoom(context.getRoom())
.getMessage()
);
}
}
}
The slash command here is oauth
which accepts different arguments (token
, refresh
, revoke
) to perform corresponding actions.
To see this app in action, deploy the app and try using these slash commands 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.