Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@
"expiry-map": "^2.0.0",
"express": "^4.21.2",
"express-rate-limit": "^5.5.1",
"express-session": "^1.19.0",
"fastq": "^1.17.1",
"fflate": "^0.8.2",
"file-type": "^16.5.4",
Expand Down Expand Up @@ -256,6 +257,10 @@
"object-path": "^0.11.8",
"overlayscrollbars": "^2.11.4",
"overlayscrollbars-react": "^0.5.6",
"passport": "^0.7.0",
"passport-facebook": "^3.0.0",
"passport-facebook2": "^1.0.3",
"passport-github2": "^0.1.12",
"path": "^0.12.7",
"path-to-regexp": "^6.3.0",
"pino": "^8.21.0",
Expand Down Expand Up @@ -349,6 +354,7 @@
"@types/ejson": "^2.2.2",
"@types/express": "^4.17.25",
"@types/express-rate-limit": "^5.1.3",
"@types/express-session": "^1",
"@types/google-libphonenumber": "^7.4.30",
"@types/gravatar": "^1.8.6",
"@types/he": "^1.2.3",
Expand Down Expand Up @@ -378,6 +384,9 @@
"@types/oauth2-server": "^3.0.18",
"@types/object-path": "^0.11.4",
"@types/parseurl": "^1.3.3",
"@types/passport": "^1.0.17",
"@types/passport-facebook": "^3.0.4",
"@types/passport-github2": "^1.2.9",
"@types/prometheus-gc-stats": "^0.6.4",
"@types/proxy-from-env": "^1.0.4",
"@types/proxyquire": "^1.3.31",
Expand Down
50 changes: 50 additions & 0 deletions apps/meteor/server/configuration/configurePassport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Users } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
import express from 'express';
import session from 'express-session';
import { WebApp } from 'meteor/webapp';
import passport from 'passport';

import type { ICachedSettings } from '../../app/settings/server/CachedSettings';
import { configureOAuthServices } from '../lib/oauth/configureOAuthServices';
import { createOAuthServiceConfig } from '../lib/oauth/createOAuthServiceConfig';
import { getOAuthServices } from '../lib/oauth/getOAuthServices';

export const oAuthRouter = express();

oAuthRouter.use(
session({
name: 'oauth',
secret: Random.secret(),
Comment thread
yash-rajpal marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: check with @cardoso if this is FIPS-compliant.

Comment thread
yash-rajpal marked this conversation as resolved.
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 5 * 60 * 1000, // 5 minutes
},
}),
);

oAuthRouter.use(passport.initialize());
oAuthRouter.use(passport.session());

export const configurePassport = (settings: ICachedSettings) => {
passport.serializeUser((user: any, done) => {
done(null, user._id);
});

passport.deserializeUser(async (id, done) => {
const user = await Users.findOneById(id as string);
// we don’t actually use this user later
done(null, user);
Comment thread
yash-rajpal marked this conversation as resolved.
});

settings.watchByRegex(/^(Accounts_OAuth_)[a-z0-9_]+$/i, () => {
const services = getOAuthServices(settings);
const oauthServiceConfigs = createOAuthServiceConfig(settings, services);
configureOAuthServices(oauthServiceConfigs);
});

WebApp.rawConnectHandlers.use(oAuthRouter);
};
2 changes: 2 additions & 0 deletions apps/meteor/server/configuration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { configureCORS } from './configureCORS';
import { configureDirectReply } from './configureDirectReply';
import { configureIRC } from './configureIRC';
import { configureLogLevel } from './configureLogLevel';
import { configurePassport } from './configurePassport';
import { configureSMTP } from './configureSMTP';
import { configureLDAP } from './ldap';
import { configureOAuth } from './oauth';
Expand All @@ -28,5 +29,6 @@ export async function configureServer(settings: ICachedSettings) {
configureDirectReply(settings),
configureSMTP(settings),
configureIRC(settings),
configurePassport(settings),
Comment thread
yash-rajpal marked this conversation as resolved.
]);
}
89 changes: 89 additions & 0 deletions apps/meteor/server/lib/oauth/configureOAuthServices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { type IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import passport from 'passport';
import type { Profile, DoneCallback } from 'passport';

import type { OAuthServiceConfig } from './createOAuthServiceConfig';
import { oAuthRouter } from '../../configuration/configurePassport';

export const configureOAuthServices = (oauthServiceConfig: OAuthServiceConfig[]) => {
oauthServiceConfig.forEach((config) => {
const Strategy = config.strategy;

passport.unuse(config.provider);

passport.use(
config.provider,
new Strategy(
{
clientID: config.clientId,
clientSecret: config.clientSecret,
callbackURL: `${Meteor.absoluteUrl()}oauth/${config.provider}/callback`,
state: true,
pkce: true,
scope: config.scope,
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;

// eslint-disable-next-line @typescript-eslint/await-thenable
const user = await Accounts.updateOrCreateUserFromExternalService(
config.provider,
{
accessToken,
refreshToken,
name: profile.displayName,
email: profile?.emails?.[0]?.value,
...restProfile,
..._json,
},
{},
);

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);
Comment thread
yash-rajpal marked this conversation as resolved.
},
),
);

oAuthRouter.get(
`/oauth/${config.provider}`,
passport.authenticate(config.provider, { scope: config.scope, prompt: 'consent', failureRedirect: '/login' }),
);
oAuthRouter.get(
`/oauth/${config.provider}/callback`,
passport.authenticate(config.provider, { failureRedirect: '/login' }),
Comment thread
yash-rajpal marked this conversation as resolved.
async (req, res) => {
const oAuthUser = req.user as IUser;

if (!oAuthUser) {
return res.redirect('/login');
}

const stampedToken = Accounts._generateStampedLoginToken();
Accounts._insertLoginToken(oAuthUser._id, stampedToken);

res.redirect(`/home?resumeToken=${stampedToken.token}`);
Comment thread
yash-rajpal marked this conversation as resolved.

req.session.destroy((err) => {
if (err) {
console.error('Error destroying session', err);
}
});
},
Comment thread
yash-rajpal marked this conversation as resolved.
Dismissed
);
});
};
25 changes: 25 additions & 0 deletions apps/meteor/server/lib/oauth/createOAuthServiceConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { capitalize } from '@rocket.chat/string-helpers';
import type { Strategy } from 'passport';

import { OAuthConfigs } from './oauthConfigs';
import { type ICachedSettings } from '../../../app/settings/server/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) => {
return {
provider: service,
strategy: OAuthConfigs[service].strategy,
clientId: settings.get<string>(`Accounts_OAuth_${capitalize(service)}_id`),
Comment thread
yash-rajpal marked this conversation as resolved.
clientSecret: settings.get<string>(`Accounts_OAuth_${capitalize(service)}_secret`),
scope: OAuthConfigs[service].scope,
};
});
};
33 changes: 33 additions & 0 deletions apps/meteor/server/lib/oauth/getOAuthServices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { isTruthy } from '@rocket.chat/tools';

import { OAuthConfigs } from './oauthConfigs';
import { type ICachedSettings } from '../../../app/settings/server/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);
};
21 changes: 21 additions & 0 deletions apps/meteor/server/lib/oauth/oauthConfigs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Strategy } from 'passport';
import { Strategy as FacebookStrategy } from 'passport-facebook';
import { Strategy as GitHubStrategy } from 'passport-github2';

export type OAuthConfig = {
strategy: new (...args: any[]) => Strategy;
scope: string[];
};

export const OAuthConfigs: Record<string, OAuthConfig> = {
Comment thread
yash-rajpal marked this conversation as resolved.
github: {
strategy: GitHubStrategy,
scope: ['user:email'],
},
facebook: {
strategy: FacebookStrategy,
scope: ['email'],
},
} as const;

export type Provider = keyof typeof OAuthConfigs;
2 changes: 1 addition & 1 deletion packages/core-typings/src/IUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export interface IUser extends IRocketChatRecord {
isOAuthUser?: boolean; // client only field
__rooms?: string[];
inactiveReason?: 'deactivated' | 'pending_approval' | 'idle_too_long';

providerId?: string;
abacAttributes?: IAbacAttributeDefinition[];
}

Expand Down
Loading
Loading