snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
267
apps/meteor/server/lib/oauth/addOAuthService.ts
Normal file
267
apps/meteor/server/lib/oauth/addOAuthService.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/* eslint no-multi-spaces: 0 */
|
||||
/* eslint comma-spacing: 0 */
|
||||
import { capitalize } from '@rocket.chat/tools';
|
||||
|
||||
import { settingsRegistry } from '../../settings';
|
||||
|
||||
export async function addOAuthService(name: string, values: { [k: string]: string | boolean | undefined } = {}): Promise<void> {
|
||||
name = name.toLowerCase().replace(/[^a-z0-9_]/g, '');
|
||||
name = capitalize(name);
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}`, values.enabled || false, {
|
||||
type: 'boolean',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Enable',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-url`, values.serverURL || '', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'URL',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-token_path`, values.tokenPath || '/oauth/token', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Token_Path',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-token_sent_via`, values.tokenSentVia || 'payload', {
|
||||
type: 'select',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Token_Sent_Via',
|
||||
persistent: true,
|
||||
values: [
|
||||
{ key: 'header', i18nLabel: 'Header' },
|
||||
{ key: 'payload', i18nLabel: 'Payload' },
|
||||
],
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-identity_token_sent_via`, values.identityTokenSentVia || 'default', {
|
||||
type: 'select',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Identity_Token_Sent_Via',
|
||||
persistent: true,
|
||||
values: [
|
||||
{ key: 'default', i18nLabel: 'Same_As_Token_Sent_Via' },
|
||||
{ key: 'header', i18nLabel: 'Header' },
|
||||
{ key: 'payload', i18nLabel: 'Payload' },
|
||||
],
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-identity_path`, values.identityPath || '/me', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Identity_Path',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-authorize_path`, values.authorizePath || '/oauth/authorize', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Authorize_Path',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-scope`, values.scope || 'openid', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Scope',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-access_token_param`, values.accessTokenParam || 'access_token', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Access_Token_Param',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-id`, values.clientId || '', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_id',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-secret`, values.clientSecret || '', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Secret',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-login_style`, values.loginStyle || 'popup', {
|
||||
type: 'select',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Login_Style',
|
||||
persistent: true,
|
||||
values: [
|
||||
{ key: 'redirect', i18nLabel: 'Redirect' },
|
||||
{ key: 'popup', i18nLabel: 'Popup' },
|
||||
{ key: '', i18nLabel: 'Default' },
|
||||
],
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-button_label_text`, values.buttonLabelText || '', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-button_label_color`, values.buttonLabelColor || '#FFFFFF', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color',
|
||||
persistent: true,
|
||||
alert: 'OAuth_button_colors_alert',
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-button_color`, values.buttonColor || '#1d74f5', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Button_Color',
|
||||
persistent: true,
|
||||
alert: 'OAuth_button_colors_alert',
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-key_field`, values.keyField || 'username', {
|
||||
type: 'select',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Key_Field',
|
||||
persistent: true,
|
||||
values: [
|
||||
{ key: 'username', i18nLabel: 'Username' },
|
||||
{ key: 'email', i18nLabel: 'Email' },
|
||||
],
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-username_field`, values.usernameField || '', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Username_Field',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-email_field`, values.emailField || '', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Email_Field',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-name_field`, values.nameField || '', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Name_Field',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-avatar_field`, values.avatarField || '', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Avatar_Field',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-roles_claim`, values.rolesClaim || 'roles', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Roles_Claim',
|
||||
enterprise: true,
|
||||
invalidValue: 'roles',
|
||||
modules: ['oauth-enterprise'],
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-groups_claim`, values.groupsClaim || 'groups', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Groups_Claim',
|
||||
enterprise: true,
|
||||
invalidValue: 'groups',
|
||||
modules: ['oauth-enterprise'],
|
||||
});
|
||||
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-channels_admin`, values.channelsAdmin || 'rocket.cat', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Channel_Admin',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-map_channels`, values.mapChannels || false, {
|
||||
type: 'boolean',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Map_Channels',
|
||||
enterprise: true,
|
||||
invalidValue: false,
|
||||
modules: ['oauth-enterprise'],
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-merge_roles`, values.mergeRoles || false, {
|
||||
type: 'boolean',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Merge_Roles',
|
||||
enterprise: true,
|
||||
invalidValue: false,
|
||||
modules: ['oauth-enterprise'],
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-roles_to_sync`, values.rolesToSync || '', {
|
||||
type: 'string',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Roles_To_Sync',
|
||||
i18nDescription: 'Accounts_OAuth_Custom_Roles_To_Sync_Description',
|
||||
enterprise: true,
|
||||
enableQuery: {
|
||||
_id: `Accounts_OAuth_Custom-${name}-merge_roles`,
|
||||
value: true,
|
||||
},
|
||||
invalidValue: '',
|
||||
modules: ['oauth-enterprise'],
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-merge_users`, values.mergeUsers || false, {
|
||||
type: 'boolean',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Merge_Users',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-merge_users_distinct_services`, values.mergeUsersDistinctServices || false, {
|
||||
type: 'boolean',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Merge_Users_Distinct_Services',
|
||||
i18nDescription: 'Accounts_OAuth_Custom_Merge_Users_Distinct_Services_Description',
|
||||
enableQuery: {
|
||||
_id: `Accounts_OAuth_Custom-${name}-merge_users`,
|
||||
value: true,
|
||||
},
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${name}-show_button`, values.showButton || true, {
|
||||
type: 'boolean',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Show_Button_On_Login_Page',
|
||||
persistent: true,
|
||||
});
|
||||
await settingsRegistry.add(
|
||||
`Accounts_OAuth_Custom-${name}-groups_channel_map`,
|
||||
values.channelsMap || '{\n\t"rocket-admin": "admin",\n\t"tech-support": "support"\n}',
|
||||
{
|
||||
type: 'code',
|
||||
multiline: true,
|
||||
code: 'application/json',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${name}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Channel_Map',
|
||||
persistent: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
62
apps/meteor/server/lib/oauth/addPassportCustomOAuth.ts
Normal file
62
apps/meteor/server/lib/oauth/addPassportCustomOAuth.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { OAuthConfiguration } from '@rocket.chat/core-typings';
|
||||
import passport from 'passport';
|
||||
import type { DoneCallback, Profile } from 'passport';
|
||||
|
||||
import { allowPassportOAuthMiddleware } from './allowPassportOAuthMiddleware';
|
||||
import { passportOAuthCallback } from './passportOAuthCallback';
|
||||
import { removeOAuthRoutes } from './removeOAuthRoutes';
|
||||
import { verifyFunction } from './verifyFunction';
|
||||
import { oAuthRouter } from '../../configuration/configurePassport';
|
||||
import { settings } from '../../settings';
|
||||
import { CustomOAuthStrategy } from '../auth-providers/custom-oauth/customOAuth';
|
||||
|
||||
export const addPassportCustomOAuth = (
|
||||
serviceName: string,
|
||||
config: Partial<OAuthConfiguration & { clientSecret: string }>,
|
||||
isCustomOAuth: boolean = false,
|
||||
) => {
|
||||
passport.unuse(serviceName);
|
||||
removeOAuthRoutes(serviceName);
|
||||
|
||||
if (!config.clientId || !config.clientSecret || !config.serverURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
passport.use(
|
||||
serviceName,
|
||||
new CustomOAuthStrategy(
|
||||
serviceName,
|
||||
config as OAuthConfiguration & { clientSecret: string },
|
||||
(accessToken: string, refreshToken: string, profile: Profile, done: DoneCallback) =>
|
||||
verifyFunction(accessToken, refreshToken, profile, done, serviceName),
|
||||
),
|
||||
);
|
||||
|
||||
const siteUrl = settings.get<string>('Site_Url').replace(/\/$/, '');
|
||||
|
||||
oAuthRouter.get(
|
||||
`/oauth/${serviceName}`,
|
||||
allowPassportOAuthMiddleware(serviceName, isCustomOAuth),
|
||||
(req, _res, next) => {
|
||||
const { loginClient } = req.query;
|
||||
if (loginClient === 'mobile' || loginClient === 'desktop') {
|
||||
req.session.loginClient = loginClient;
|
||||
req.session.save(() => {
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
//delete stale value from previous sessions if any
|
||||
delete req.session.loginClient;
|
||||
next();
|
||||
}
|
||||
},
|
||||
passport.authenticate(serviceName, { scope: config.scope, prompt: 'consent', failureRedirect: '/login', keepSessionInfo: true }),
|
||||
);
|
||||
|
||||
oAuthRouter.get(
|
||||
`/_oauth/${serviceName}`,
|
||||
allowPassportOAuthMiddleware(serviceName, isCustomOAuth),
|
||||
passport.authenticate(serviceName, { failureRedirect: '/login', failWithError: true, keepSessionInfo: true }),
|
||||
passportOAuthCallback(siteUrl, config.loginStyle ? config.loginStyle : undefined),
|
||||
);
|
||||
};
|
||||
20
apps/meteor/server/lib/oauth/allowPassportOAuthMiddleware.ts
Normal file
20
apps/meteor/server/lib/oauth/allowPassportOAuthMiddleware.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { capitalize } from '@rocket.chat/tools';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export const allowPassportOAuthMiddleware =
|
||||
(service: string, isCustomOAuth: boolean = false) =>
|
||||
(_req: Request, _res: Response, next: NextFunction) => {
|
||||
const isPassportFlowEnabled = settings.get<boolean>('Accounts_OAuth_Use_Modern_Flow');
|
||||
const settingPrefix = `${isCustomOAuth ? 'Accounts_OAuth_Custom-' : 'Accounts_OAuth_'}`;
|
||||
const isOAuthServiceEnabled = settings.get<boolean>(
|
||||
`${settingPrefix}${service === 'github_enterprise' ? 'GitHub_Enterprise' : capitalize(service)}`,
|
||||
);
|
||||
|
||||
if (!isPassportFlowEnabled || !isOAuthServiceEnabled) {
|
||||
next('router');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
92
apps/meteor/server/lib/oauth/configureOAuthServices.ts
Normal file
92
apps/meteor/server/lib/oauth/configureOAuthServices.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Users } from '@rocket.chat/models';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import passport from 'passport';
|
||||
import type { Profile, DoneCallback } from 'passport';
|
||||
|
||||
import { allowPassportOAuthMiddleware } from './allowPassportOAuthMiddleware';
|
||||
import type { OAuthServiceConfig } from './createOAuthServiceConfig';
|
||||
import { passportOAuthCallback } from './passportOAuthCallback';
|
||||
import { removeOAuthRoutes } from './removeOAuthRoutes';
|
||||
import { oAuthRouter } from '../../configuration/configurePassport';
|
||||
import type { ICachedSettings } from '../../settings/CachedSettings';
|
||||
|
||||
export const configureOAuthServices = (oauthServiceConfig: OAuthServiceConfig[], settings: ICachedSettings) => {
|
||||
oauthServiceConfig.forEach((config) => {
|
||||
const Strategy = config.strategy;
|
||||
const siteUrl = settings.get<string>('Site_Url').replace(/\/$/, '');
|
||||
|
||||
passport.unuse(config.provider);
|
||||
removeOAuthRoutes(config.provider);
|
||||
|
||||
passport.use(
|
||||
config.provider,
|
||||
new Strategy(
|
||||
{
|
||||
...config,
|
||||
clientID: config.clientId,
|
||||
clientSecret: config.clientSecret,
|
||||
consumerKey: config.clientId,
|
||||
consumerSecret: config.clientSecret,
|
||||
callbackURL: `${siteUrl}/_oauth/${config.provider}`,
|
||||
state: true,
|
||||
pkce: true,
|
||||
profileFields: ['id', 'displayName', 'emails'],
|
||||
},
|
||||
async (accessToken: string, refreshToken: string, profile: Profile, done: DoneCallback) => {
|
||||
const profileWithRaw = profile as Profile & { _json?: Record<string, unknown>; _raw?: string };
|
||||
const { _json, _raw, ...restProfile } = profileWithRaw;
|
||||
|
||||
const user = await Accounts.updateOrCreateUserFromExternalService(
|
||||
config.provider,
|
||||
{
|
||||
accessToken,
|
||||
refreshToken,
|
||||
name: profile.displayName,
|
||||
...restProfile,
|
||||
..._json,
|
||||
email: profile?.emails?.[0]?.value,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
if (!user?.userId || typeof user?.userId !== 'string') {
|
||||
return done(new Error('User not found'));
|
||||
}
|
||||
|
||||
const userFromDB = await Users.findOneById(user.userId);
|
||||
|
||||
if (!userFromDB) {
|
||||
return done(new Error('User not found'));
|
||||
}
|
||||
|
||||
return done(null, userFromDB);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
oAuthRouter.get(
|
||||
`/oauth/${config.provider}`,
|
||||
allowPassportOAuthMiddleware(config.provider),
|
||||
(req, _res, next) => {
|
||||
const { loginClient } = req.query;
|
||||
if (loginClient === 'mobile' || loginClient === 'desktop') {
|
||||
req.session.loginClient = loginClient;
|
||||
req.session.save(() => {
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
//delete stale value from previous sessions if any
|
||||
delete req.session.loginClient;
|
||||
next();
|
||||
}
|
||||
},
|
||||
passport.authenticate(config.provider, { scope: config.scope, prompt: 'consent', failureRedirect: '/login', keepSessionInfo: true }),
|
||||
);
|
||||
oAuthRouter.get(
|
||||
`/_oauth/${config.provider}`,
|
||||
allowPassportOAuthMiddleware(config.provider),
|
||||
passport.authenticate(config.provider, { failureRedirect: '/login', failWithError: true, keepSessionInfo: true }),
|
||||
passportOAuthCallback(siteUrl),
|
||||
);
|
||||
});
|
||||
};
|
||||
58
apps/meteor/server/lib/oauth/createOAuthServiceConfig.ts
Normal file
58
apps/meteor/server/lib/oauth/createOAuthServiceConfig.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { capitalize, isTruthy } from '@rocket.chat/tools';
|
||||
import type { Strategy } from 'passport';
|
||||
|
||||
import { OAuthConfigs } from './oauthConfigs';
|
||||
import { type ICachedSettings } from '../../settings/CachedSettings';
|
||||
|
||||
export type OAuthServiceConfig = {
|
||||
provider: string;
|
||||
strategy: new (...args: any[]) => Strategy;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scope?: string[];
|
||||
};
|
||||
|
||||
export const createOAuthServiceConfig = (settings: ICachedSettings, services: string[]): OAuthServiceConfig[] => {
|
||||
return services
|
||||
.map((service) => {
|
||||
if (!OAuthConfigs[service]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (service === 'github_enterprise') {
|
||||
const clientId = settings.get<string>('Accounts_OAuth_GitHub_Enterprise_id');
|
||||
const clientSecret = settings.get<string>('Accounts_OAuth_GitHub_Enterprise_secret');
|
||||
const serverUrl = settings.get<string>('API_GitHub_Enterprise_URL');
|
||||
|
||||
if (!clientId || !clientSecret || !serverUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
provider: service,
|
||||
clientId,
|
||||
clientSecret,
|
||||
authorizationURL: `${serverUrl}/login/oauth/authorize`,
|
||||
tokenURL: `${serverUrl}/login/oauth/access_token`,
|
||||
userProfileURL: `${serverUrl}/api/v3/user`,
|
||||
strategy: OAuthConfigs.github_enterprise.strategy,
|
||||
scope: OAuthConfigs.github_enterprise.scope,
|
||||
};
|
||||
}
|
||||
|
||||
const clientId = settings.get<string>(`Accounts_OAuth_${capitalize(service)}_id`);
|
||||
const clientSecret = settings.get<string>(`Accounts_OAuth_${capitalize(service)}_secret`);
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
provider: service,
|
||||
clientId,
|
||||
clientSecret,
|
||||
...OAuthConfigs[service],
|
||||
};
|
||||
})
|
||||
.filter(isTruthy);
|
||||
};
|
||||
34
apps/meteor/server/lib/oauth/getOAuthServices.ts
Normal file
34
apps/meteor/server/lib/oauth/getOAuthServices.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { isTruthy } from '@rocket.chat/tools';
|
||||
|
||||
import { OAuthConfigs } from './oauthConfigs';
|
||||
import { type ICachedSettings } from '../../settings/CachedSettings';
|
||||
|
||||
export const getOAuthServices = (settings: ICachedSettings) => {
|
||||
const services = settings.getByRegexp(/^(Accounts_OAuth_|Accounts_OAuth_Custom-)[a-z0-9_]+$/i);
|
||||
const filteredServices = services.filter(([, value]) => typeof value === 'boolean' && value === true);
|
||||
return filteredServices
|
||||
.map(([key, value]) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let serviceName = key.replace('Accounts_OAuth_', '');
|
||||
if (serviceName === 'Meteor') {
|
||||
serviceName = 'meteor-developer';
|
||||
}
|
||||
|
||||
if (/Accounts_OAuth_Custom-/.test(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serviceKey = serviceName.toLowerCase();
|
||||
|
||||
const oauthConfig = OAuthConfigs[serviceKey];
|
||||
if (!oauthConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
return serviceKey;
|
||||
})
|
||||
.filter(isTruthy);
|
||||
};
|
||||
56
apps/meteor/server/lib/oauth/initCustomOAuthServices.ts
Normal file
56
apps/meteor/server/lib/oauth/initCustomOAuthServices.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { addOAuthService } from './addOAuthService';
|
||||
|
||||
export async function initCustomOAuthServices(): Promise<void> {
|
||||
// Add settings for custom OAuth providers to the settings so they get
|
||||
// automatically added when they are defined in ENV variables
|
||||
for await (const key of Object.keys(process.env)) {
|
||||
if (/Accounts_OAuth_Custom_[a-zA-Z0-9_-]+$/.test(key)) {
|
||||
// Most all shells actually prohibit the usage of - in environment variables
|
||||
// So this will allow replacing - with _ and translate it back to the setting name
|
||||
let name = key.replace('Accounts_OAuth_Custom_', '');
|
||||
|
||||
if (name.indexOf('_') > -1) {
|
||||
name = name.replace(name.substr(name.indexOf('_')), '');
|
||||
}
|
||||
|
||||
const serviceKey = `Accounts_OAuth_Custom_${name}`;
|
||||
|
||||
if (key === serviceKey) {
|
||||
const values = {
|
||||
enabled: process.env[`${serviceKey}`] === 'true',
|
||||
clientId: process.env[`${serviceKey}_id`],
|
||||
clientSecret: process.env[`${serviceKey}_secret`],
|
||||
serverURL: process.env[`${serviceKey}_url`],
|
||||
tokenPath: process.env[`${serviceKey}_token_path`],
|
||||
identityPath: process.env[`${serviceKey}_identity_path`],
|
||||
authorizePath: process.env[`${serviceKey}_authorize_path`],
|
||||
scope: process.env[`${serviceKey}_scope`],
|
||||
accessTokenParam: process.env[`${serviceKey}_access_token_param`],
|
||||
buttonLabelText: process.env[`${serviceKey}_button_label_text`],
|
||||
buttonLabelColor: process.env[`${serviceKey}_button_label_color`],
|
||||
loginStyle: process.env[`${serviceKey}_login_style`],
|
||||
buttonColor: process.env[`${serviceKey}_button_color`],
|
||||
tokenSentVia: process.env[`${serviceKey}_token_sent_via`],
|
||||
identityTokenSentVia: process.env[`${serviceKey}_identity_token_sent_via`],
|
||||
keyField: process.env[`${serviceKey}_key_field`],
|
||||
usernameField: process.env[`${serviceKey}_username_field`],
|
||||
nameField: process.env[`${serviceKey}_name_field`],
|
||||
emailField: process.env[`${serviceKey}_email_field`],
|
||||
rolesClaim: process.env[`${serviceKey}_roles_claim`],
|
||||
groupsClaim: process.env[`${serviceKey}_groups_claim`],
|
||||
channelsMap: process.env[`${serviceKey}_groups_channel_map`],
|
||||
channelsAdmin: process.env[`${serviceKey}_channels_admin`],
|
||||
mergeUsers: process.env[`${serviceKey}_merge_users`] === 'true',
|
||||
mergeUsersDistinctServices: process.env[`${serviceKey}_merge_users_distinct_services`] === 'true',
|
||||
mapChannels: process.env[`${serviceKey}_map_channels`],
|
||||
mergeRoles: process.env[`${serviceKey}_merge_roles`] === 'true',
|
||||
rolesToSync: process.env[`${serviceKey}_roles_to_sync`],
|
||||
showButton: process.env[`${serviceKey}_show_button`] === 'true',
|
||||
avatarField: process.env[`${serviceKey}_avatar_field`],
|
||||
};
|
||||
|
||||
await addOAuthService(name, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
apps/meteor/server/lib/oauth/logger.ts
Normal file
3
apps/meteor/server/lib/oauth/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
|
||||
export const logger = new Logger('rocketchat:lib');
|
||||
36
apps/meteor/server/lib/oauth/oauthConfigs.ts
Normal file
36
apps/meteor/server/lib/oauth/oauthConfigs.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Strategy as XStrategy } from '@rocket.chat/passport-x';
|
||||
import type { Strategy } from 'passport';
|
||||
import { Strategy as FacebookStrategy } from 'passport-facebook';
|
||||
import { Strategy as GitHubStrategy } from 'passport-github2';
|
||||
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
|
||||
|
||||
export type OAuthConfig = {
|
||||
strategy: new (...args: any[]) => Strategy;
|
||||
scope?: string[];
|
||||
includeEmail?: boolean;
|
||||
};
|
||||
|
||||
export const OAuthConfigs: Record<string, OAuthConfig> = {
|
||||
github: {
|
||||
strategy: GitHubStrategy,
|
||||
scope: ['user:email'],
|
||||
},
|
||||
facebook: {
|
||||
strategy: FacebookStrategy,
|
||||
scope: ['email'],
|
||||
},
|
||||
google: {
|
||||
strategy: GoogleStrategy,
|
||||
scope: ['email', 'profile'],
|
||||
},
|
||||
twitter: {
|
||||
strategy: XStrategy,
|
||||
includeEmail: true,
|
||||
},
|
||||
github_enterprise: {
|
||||
strategy: GitHubStrategy,
|
||||
scope: ['user:email'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Provider = keyof typeof OAuthConfigs;
|
||||
108
apps/meteor/server/lib/oauth/passportOAuthCallback.ts
Normal file
108
apps/meteor/server/lib/oauth/passportOAuthCallback.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { LoginCodes } from '@rocket.chat/models';
|
||||
import type { Request, Response } from 'express';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
|
||||
import { doesUserRequire2FA } from './twoFactorAuth';
|
||||
|
||||
const logger = new Logger('OAuth');
|
||||
|
||||
type LaunchStyle = 'popup' | 'redirect' | undefined;
|
||||
|
||||
const createLoginToken = async (userId: string) => {
|
||||
const stampedToken = Accounts._generateStampedLoginToken();
|
||||
await Accounts._insertLoginToken(userId, stampedToken);
|
||||
|
||||
return stampedToken.token;
|
||||
};
|
||||
|
||||
const sendPopupMessage = (res: Response, payload: unknown) => {
|
||||
const nonce = crypto.randomBytes(16).toString('base64');
|
||||
res.setHeader('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
|
||||
|
||||
res.send(`
|
||||
<script nonce="${nonce}">
|
||||
window.opener.postMessage(
|
||||
${JSON.stringify(payload)},
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
</script>
|
||||
`);
|
||||
};
|
||||
|
||||
const handlePopupStyleOAuth = async (res: Response, oAuthUser: IUser, loginClient?: string) => {
|
||||
const secondFactorMethod = doesUserRequire2FA(oAuthUser);
|
||||
|
||||
if (secondFactorMethod) {
|
||||
const challengeId = await secondFactorMethod.sendTwoFactorChallenge(oAuthUser);
|
||||
sendPopupMessage(res, {
|
||||
oauthPopupCommand: 'redirect',
|
||||
path: `/2fa/${secondFactorMethod.method}/${challengeId}${loginClient ? `?loginClient=${loginClient}` : ''}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = await createLoginToken(oAuthUser._id);
|
||||
|
||||
if (loginClient) {
|
||||
sendPopupMessage(res, {
|
||||
oauthPopupCommand: 'redirect',
|
||||
path: `home?resumeToken=${token}&userId=${oAuthUser._id}&loginClient=${loginClient}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
sendPopupMessage(res, { oauthPopupCommand: 'oauth-login-with-token', token });
|
||||
};
|
||||
|
||||
export const passportOAuthCallback =
|
||||
(siteUrl: string, launchStyle: LaunchStyle = 'redirect') =>
|
||||
async (req: Request, res: Response) => {
|
||||
const oAuthUser = req.user as IUser;
|
||||
|
||||
if (!oAuthUser) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
|
||||
const { loginClient } = req.session;
|
||||
|
||||
if (launchStyle === 'popup') {
|
||||
await handlePopupStyleOAuth(res, oAuthUser, loginClient);
|
||||
return;
|
||||
}
|
||||
|
||||
const secondFactorMethod = doesUserRequire2FA(oAuthUser);
|
||||
|
||||
if (secondFactorMethod) {
|
||||
const challengeId = await secondFactorMethod.sendTwoFactorChallenge(oAuthUser);
|
||||
const twoFARedirectUrl = new URL(`/2fa/${secondFactorMethod.method}/${challengeId}`, siteUrl);
|
||||
|
||||
if (loginClient) {
|
||||
twoFARedirectUrl.searchParams.set('loginClient', loginClient);
|
||||
}
|
||||
|
||||
return res.redirect(twoFARedirectUrl.toString());
|
||||
}
|
||||
|
||||
const loginCode = await LoginCodes.createCode(oAuthUser._id);
|
||||
|
||||
const redirectUrl = new URL(`/home`, siteUrl);
|
||||
|
||||
redirectUrl.searchParams.set('loginCode', loginCode);
|
||||
|
||||
if (loginClient) {
|
||||
redirectUrl.searchParams.set('loginClient', loginClient);
|
||||
}
|
||||
|
||||
setImmediate(() => res.redirect(redirectUrl.toString()));
|
||||
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
logger.error({ msg: 'Error destroying OAuth session', err });
|
||||
}
|
||||
});
|
||||
};
|
||||
19
apps/meteor/server/lib/oauth/removeOAuthRoutes.ts
Normal file
19
apps/meteor/server/lib/oauth/removeOAuthRoutes.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { oAuthRouter } from '../../configuration/configurePassport';
|
||||
|
||||
/**
|
||||
* Removes every route previously registered for an OAuth service from the OAuth router.
|
||||
*
|
||||
* OAuth services are re-registered whenever their settings change, but express only ever
|
||||
* *appends* to `router.stack` and dispatches on a first-match-wins basis. Without this,
|
||||
* re-registering a service leaves the previous handlers in place and they keep serving the
|
||||
* requests, using the configuration that was captured in their closures at registration time
|
||||
* (`loginStyle`, `scope`, ...). Callers must invoke this before registering the routes again.
|
||||
*/
|
||||
export const removeOAuthRoutes = (serviceName: string): void => {
|
||||
const paths = [`/oauth/${serviceName}`, `/_oauth/${serviceName}`];
|
||||
|
||||
const router = oAuthRouter;
|
||||
|
||||
// Only route layers are dropped; middleware layers registered via `router.use`
|
||||
router.stack = router.stack.filter((layer) => !layer.route || !paths.includes(layer.route.path));
|
||||
};
|
||||
9
apps/meteor/server/lib/oauth/removeOAuthService.ts
Normal file
9
apps/meteor/server/lib/oauth/removeOAuthService.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ServiceConfiguration } from 'meteor/service-configuration';
|
||||
|
||||
export async function removeOAuthService(mainSettingId: string): Promise<void> {
|
||||
const serviceName = mainSettingId.replace('Accounts_OAuth_Custom-', '');
|
||||
|
||||
await ServiceConfiguration.configurations.removeAsync({
|
||||
service: serviceName.toLowerCase(),
|
||||
});
|
||||
}
|
||||
37
apps/meteor/server/lib/oauth/twoFactorAuth.ts
Normal file
37
apps/meteor/server/lib/oauth/twoFactorAuth.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
|
||||
import { getRememberDate } from '../2fa/code';
|
||||
import { EmailCheckForOAuth } from '../2fa/code/EmailCheckForOAuth';
|
||||
import { TOTPCheckForOAuth } from '../2fa/code/TOTPCheckForOAuth';
|
||||
|
||||
export const emailCheckForOAuth = new EmailCheckForOAuth();
|
||||
export const totpCheckForOAuth = new TOTPCheckForOAuth();
|
||||
|
||||
const twoFACheckMethodsForOAuth = {
|
||||
[emailCheckForOAuth.method]: emailCheckForOAuth,
|
||||
[totpCheckForOAuth.method]: totpCheckForOAuth,
|
||||
};
|
||||
|
||||
export const getTwoFAMethodForOAuth = (method: 'email' | 'totp') => {
|
||||
return twoFACheckMethodsForOAuth[method];
|
||||
};
|
||||
|
||||
const getSecondFactorMethod = (user: IUser) => {
|
||||
return Array.from(Object.values(twoFACheckMethodsForOAuth)).find((method) => method.isEnabled(user));
|
||||
};
|
||||
|
||||
export const doesUserRequire2FA = (user: IUser) => {
|
||||
const rememberAfterRegistration = getRememberDate(user.createdAt);
|
||||
|
||||
if (rememberAfterRegistration && rememberAfterRegistration > new Date()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const secondFactorMethod = getSecondFactorMethod(user);
|
||||
|
||||
if (!secondFactorMethod) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return secondFactorMethod;
|
||||
};
|
||||
135
apps/meteor/server/lib/oauth/updateOAuthServices.ts
Normal file
135
apps/meteor/server/lib/oauth/updateOAuthServices.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import type {
|
||||
FacebookOAuthConfiguration,
|
||||
ILoginServiceConfiguration,
|
||||
LinkedinOAuthConfiguration,
|
||||
OAuthConfiguration,
|
||||
TwitterOAuthConfiguration,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import { LoginServiceConfiguration } from '@rocket.chat/models';
|
||||
|
||||
import { addPassportCustomOAuth } from './addPassportCustomOAuth';
|
||||
import { logger } from './logger';
|
||||
import { settings } from '../../settings/cached';
|
||||
import { CustomOAuth } from '../auth-providers/custom-oauth/custom_oauth_server';
|
||||
import { notifyOnLoginServiceConfigurationChanged, notifyOnLoginServiceConfigurationChangedByService } from '../notifyListener';
|
||||
|
||||
export async function updateOAuthServices(): Promise<void> {
|
||||
const services = settings.getByRegexp(/^(Accounts_OAuth_|Accounts_OAuth_Custom-)[a-z0-9_]+$/i);
|
||||
const filteredServices = services.filter(([, value]) => typeof value === 'boolean');
|
||||
for await (const [key, value] of filteredServices) {
|
||||
if (key === 'Accounts_OAuth_Use_Modern_Flow') {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug({ oauth_updated: key });
|
||||
let serviceName = key.replace('Accounts_OAuth_', '');
|
||||
if (serviceName === 'Meteor') {
|
||||
serviceName = 'meteor-developer';
|
||||
}
|
||||
if (/Accounts_OAuth_Custom-/.test(key)) {
|
||||
serviceName = key.replace('Accounts_OAuth_Custom-', '');
|
||||
}
|
||||
|
||||
const serviceKey = serviceName.toLowerCase();
|
||||
|
||||
if (value === true) {
|
||||
const data: Partial<ILoginServiceConfiguration & Omit<OAuthConfiguration, '_id'>> = {
|
||||
clientId: settings.get(`${key}_id`),
|
||||
secret: settings.get(`${key}_secret`),
|
||||
};
|
||||
|
||||
if (/Accounts_OAuth_Custom-/.test(key)) {
|
||||
data.custom = true;
|
||||
data.clientId = settings.get(`${key}-id`);
|
||||
data.secret = settings.get(`${key}-secret`);
|
||||
data.serverURL = settings.get(`${key}-url`);
|
||||
data.tokenPath = settings.get(`${key}-token_path`);
|
||||
data.identityPath = settings.get(`${key}-identity_path`);
|
||||
data.authorizePath = settings.get(`${key}-authorize_path`);
|
||||
data.scope = settings.get(`${key}-scope`);
|
||||
data.accessTokenParam = settings.get(`${key}-access_token_param`);
|
||||
data.buttonLabelText = settings.get(`${key}-button_label_text`);
|
||||
data.buttonLabelColor = settings.get(`${key}-button_label_color`);
|
||||
data.loginStyle = settings.get(`${key}-login_style`);
|
||||
data.buttonColor = settings.get(`${key}-button_color`);
|
||||
data.tokenSentVia = settings.get(`${key}-token_sent_via`);
|
||||
data.identityTokenSentVia = settings.get(`${key}-identity_token_sent_via`);
|
||||
data.keyField = settings.get(`${key}-key_field`);
|
||||
data.usernameField = settings.get(`${key}-username_field`);
|
||||
data.emailField = settings.get(`${key}-email_field`);
|
||||
data.nameField = settings.get(`${key}-name_field`);
|
||||
data.avatarField = settings.get(`${key}-avatar_field`);
|
||||
data.rolesClaim = settings.get(`${key}-roles_claim`);
|
||||
data.groupsClaim = settings.get(`${key}-groups_claim`);
|
||||
data.channelsMap = settings.get(`${key}-groups_channel_map`);
|
||||
data.channelsAdmin = settings.get(`${key}-channels_admin`);
|
||||
data.mergeUsers = settings.get(`${key}-merge_users`);
|
||||
data.mergeUsersDistinctServices = settings.get(`${key}-merge_users_distinct_services`);
|
||||
data.mapChannels = settings.get(`${key}-map_channels`);
|
||||
data.mergeRoles = settings.get(`${key}-merge_roles`);
|
||||
data.rolesToSync = settings.get(`${key}-roles_to_sync`);
|
||||
data.showButton = settings.get(`${key}-show_button`);
|
||||
|
||||
const config = {
|
||||
serverURL: data.serverURL,
|
||||
tokenPath: data.tokenPath,
|
||||
identityPath: data.identityPath,
|
||||
authorizePath: data.authorizePath,
|
||||
scope: data.scope,
|
||||
loginStyle: data.loginStyle,
|
||||
tokenSentVia: data.tokenSentVia,
|
||||
identityTokenSentVia: data.identityTokenSentVia,
|
||||
keyField: data.keyField,
|
||||
usernameField: data.usernameField,
|
||||
emailField: data.emailField,
|
||||
nameField: data.nameField,
|
||||
avatarField: data.avatarField,
|
||||
rolesClaim: data.rolesClaim,
|
||||
groupsClaim: data.groupsClaim,
|
||||
mapChannels: data.mapChannels,
|
||||
channelsMap: data.channelsMap,
|
||||
channelsAdmin: data.channelsAdmin,
|
||||
mergeUsers: data.mergeUsers,
|
||||
mergeUsersDistinctServices: data.mergeUsersDistinctServices,
|
||||
mergeRoles: data.mergeRoles,
|
||||
rolesToSync: data.rolesToSync,
|
||||
accessTokenParam: data.accessTokenParam,
|
||||
showButton: data.showButton,
|
||||
clientSecret: data.secret,
|
||||
clientId: data.clientId,
|
||||
};
|
||||
|
||||
new CustomOAuth(serviceKey, config);
|
||||
addPassportCustomOAuth(serviceKey, config, true);
|
||||
}
|
||||
if (serviceName === 'Facebook') {
|
||||
(data as FacebookOAuthConfiguration).appId = data.clientId as string;
|
||||
delete data.clientId;
|
||||
}
|
||||
if (serviceName === 'Twitter') {
|
||||
(data as TwitterOAuthConfiguration).consumerKey = data.clientId as string;
|
||||
delete data.clientId;
|
||||
}
|
||||
|
||||
if (serviceName === 'Linkedin') {
|
||||
(data as LinkedinOAuthConfiguration).clientConfig = {
|
||||
requestPermissions: ['openid', 'email', 'profile'],
|
||||
};
|
||||
}
|
||||
|
||||
if (serviceName === 'Nextcloud') {
|
||||
data.buttonLabelText = settings.get('Accounts_OAuth_Nextcloud_button_label_text');
|
||||
data.buttonLabelColor = settings.get('Accounts_OAuth_Nextcloud_button_label_color');
|
||||
data.buttonColor = settings.get('Accounts_OAuth_Nextcloud_button_color');
|
||||
}
|
||||
|
||||
await LoginServiceConfiguration.createOrUpdateService(serviceKey, data);
|
||||
void notifyOnLoginServiceConfigurationChangedByService(serviceKey);
|
||||
} else {
|
||||
const service = await LoginServiceConfiguration.removeByService(serviceName);
|
||||
if (service) {
|
||||
void notifyOnLoginServiceConfigurationChanged({ _id: service._id }, 'removed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
apps/meteor/server/lib/oauth/verifyFunction.ts
Normal file
40
apps/meteor/server/lib/oauth/verifyFunction.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Users } from '@rocket.chat/models';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import type { DoneCallback, Profile } from 'passport';
|
||||
|
||||
export const verifyFunction = async (
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
profile: Profile,
|
||||
done: DoneCallback,
|
||||
serviceName: string,
|
||||
) => {
|
||||
const profileWithRaw = profile as Profile & { _json?: Record<string, unknown>; _raw?: string };
|
||||
const { _json, _raw, ...restProfile } = profileWithRaw;
|
||||
|
||||
const user = await Accounts.updateOrCreateUserFromExternalService(
|
||||
serviceName,
|
||||
{
|
||||
accessToken,
|
||||
refreshToken,
|
||||
name: profile.displayName,
|
||||
...profile,
|
||||
...restProfile,
|
||||
..._json,
|
||||
email: profile?.emails?.[0]?.value,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
if (!user?.userId || typeof user?.userId !== 'string') {
|
||||
return done(new Error('User not found'));
|
||||
}
|
||||
|
||||
const userFromDB = await Users.findOneById(user.userId);
|
||||
|
||||
if (!userFromDB) {
|
||||
return done(new Error('User not found'));
|
||||
}
|
||||
|
||||
return done(null, userFromDB);
|
||||
};
|
||||
Reference in New Issue
Block a user