Skip to content

Commit

Permalink
Allowing apps to register authenticated routes
Browse files Browse the repository at this point in the history
  • Loading branch information
d-gubert committed Jun 20, 2022
1 parent 63d4e30 commit c6e8b3f
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 42 deletions.
33 changes: 33 additions & 0 deletions apps/meteor/app/api/server/middlewares/authentication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Request, Response, NextFunction } from 'express';

import { Users } from '../../../models/server';
import { oAuth2ServerAuth } from '../../../oauth2-server-config/server/oauth/oauth2-server';

export type AuthenticationMiddlewareConfig = {
rejectUnauthorized: boolean;
};

export const defaultAuthenticationMiddlewareConfig = {
rejectUnauthorized: true,
};

export function authenticationMiddleware(config: AuthenticationMiddlewareConfig = defaultAuthenticationMiddlewareConfig) {
return (req: Request, res: Response, next: NextFunction): void => {
const { 'x-user-id': userId, 'x-auth-token': authToken } = req.headers;

if (userId && authToken) {
req.user = Users.findOneByIdAndLoginToken(userId, authToken);
} else {
req.user = oAuth2ServerAuth(req)?.user;
}

if (config.rejectUnauthorized && !req.user) {
res.status(401).send('Unauthorized');
return;
}

req.userId = req.user?._id;

next();
};
}
23 changes: 17 additions & 6 deletions apps/meteor/app/apps/server/bridges/api.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { Meteor } from 'meteor/meteor';
import express, { Response, Request, IRouter, RequestHandler } from 'express';
import express, { Response, Request, IRouter, RequestHandler, NextFunction } from 'express';
import { WebApp } from 'meteor/webapp';
import { ApiBridge } from '@rocket.chat/apps-engine/server/bridges/ApiBridge';
import { IApiRequest, IApiEndpoint, IApi } from '@rocket.chat/apps-engine/definition/api';
import { AppApi } from '@rocket.chat/apps-engine/server/managers/AppApi';
import { RequestMethod } from '@rocket.chat/apps-engine/definition/accessors';

import { AppServerOrchestrator } from '../orchestrator';
import { authenticationMiddleware } from '../../../api/server/middlewares/authentication';

const apiServer = express();

apiServer.disable('x-powered-by');

WebApp.connectHandlers.use(apiServer);

type RequestWithPrivateHash = Request & {
interface IRequestWithPrivateHash extends Request {
_privateHash?: string;
content?: any;
};
}

export class AppApisBridge extends ApiBridge {
appRouters: Map<string, IRouter>;
Expand All @@ -27,7 +28,7 @@ export class AppApisBridge extends ApiBridge {
super();
this.appRouters = new Map();

apiServer.use('/api/apps/private/:appId/:hash', (req: RequestWithPrivateHash, res: Response) => {
apiServer.use('/api/apps/private/:appId/:hash', (req: IRequestWithPrivateHash, res: Response) => {
const notFound = (): Response => res.sendStatus(404);

const router = this.appRouters.get(req.params.appId);
Expand Down Expand Up @@ -73,7 +74,7 @@ export class AppApisBridge extends ApiBridge {
}

if (router[method] instanceof Function) {
router[method](routePath, Meteor.bindEnvironment(this._appApiExecutor(endpoint, appId)));
router[method](routePath, this._authMiddlewares(endpoint, appId), Meteor.bindEnvironment(this._appApiExecutor(endpoint, appId)));
}
}

Expand All @@ -85,6 +86,15 @@ export class AppApisBridge extends ApiBridge {
}
}

private _authMiddlewares(endpoint: IApiEndpoint, _appId: string): RequestHandler {
const authFunction = authenticationMiddleware();
return Meteor.bindEnvironment((req: IRequestWithPrivateHash, res: Response, next: NextFunction) => {
if (!endpoint.authRequired) return next();

return authFunction(req, res, next);
});
}

private _verifyApi(api: IApi, endpoint: IApiEndpoint): void {
if (typeof api !== 'object') {
throw new Error('Invalid Api parameter provided, it must be a valid IApi object.');
Expand All @@ -96,14 +106,15 @@ export class AppApisBridge extends ApiBridge {
}

private _appApiExecutor(endpoint: IApiEndpoint, appId: string): RequestHandler {
return (req: RequestWithPrivateHash, res: Response): void => {
return (req: IRequestWithPrivateHash, res: Response): void => {
const request: IApiRequest = {
method: req.method.toLowerCase() as RequestMethod,
headers: req.headers as { [key: string]: string },
query: (req.query as { [key: string]: string }) || {},
params: req.params || {},
content: req.body,
privateHash: req._privateHash,
user: req.user && this.orch.getConverters()?.get('users')?.convertToApp(req.user),
};

this.orch
Expand Down
12 changes: 5 additions & 7 deletions apps/meteor/app/apps/server/communication/uikit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { WebApp } from 'meteor/webapp';
import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit';
import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata';

import { Users } from '../../../models/server';
import { settings } from '../../../settings/server';
import { Apps, AppServerOrchestrator } from '../orchestrator';
import { UiKitCoreApp } from '../../../../server/sdk';
import { authenticationMiddleware } from '../../../api/server/middlewares/authentication';

const apiServer = express();

Expand Down Expand Up @@ -51,16 +51,14 @@ Meteor.startup(() => {
settings.get('API_Enable_Rate_Limiter') !== true ||
(process.env.NODE_ENV === 'development' && settings.get('API_Enable_Rate_Limiter_Dev') !== true),
});

router.use(apiLimiter);
});

router.use((req, res, next) => {
const { 'x-user-id': userId, 'x-auth-token': authToken, 'x-visitor-token': visitorToken } = req.headers;
router.use(authenticationMiddleware({ rejectUnauthorized: false }));

if (userId && authToken) {
req.body.user = Users.findOneByIdAndLoginToken(userId, authToken);
req.body.userId = req.body.user._id;
}
router.use((req, res, next) => {
const { 'x-visitor-token': visitorToken } = req.headers;

if (visitorToken) {
req.body.visitor = Apps.getConverters()?.get('visitors').convertByToken(visitorToken);
Expand Down
53 changes: 24 additions & 29 deletions apps/meteor/app/oauth2-server-config/server/oauth/oauth2-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Mongo } from 'meteor/mongo';
import { WebApp } from 'meteor/webapp';
import { OAuth2Server } from 'meteor/rocketchat:oauth2-server';
import { Request, Response } from 'express';
import { IUser } from '@rocket.chat/core-typings';

import { Users } from '../../../models/server';
import { OAuthApps } from '../../../models/server/raw';
Expand All @@ -25,6 +26,28 @@ function getAccessToken(accessToken: string): any {
});
}

export function oAuth2ServerAuth(partialRequest: {
headers: Record<string, any>;
query: Record<string, any>;
}): { user: IUser } | undefined {
const headerToken = partialRequest.headers.authorization?.replace('Bearer ', '');
const queryToken = partialRequest.query.access_token;

const accessToken = getAccessToken(headerToken || queryToken);

if (accessToken?.expires != null && accessToken?.expires !== 0 && accessToken?.expires < new Date()) {
return;
}

const user = Users.findOne(accessToken.userId);

if (user == null) {
return;
}

return { user };
}

oauth2server.app.disable('x-powered-by');
oauth2server.routes.disable('x-powered-by');

Expand Down Expand Up @@ -57,33 +80,5 @@ oauth2server.routes.get('/oauth/userinfo', function (req: Request, res: Response
});

API.v1.addAuthMethod(function () {
const headerToken = this.request.headers.authorization;
const getToken = this.request.query.access_token;

let token: string | undefined;
if (headerToken != null) {
const matches = headerToken.match(/Bearer\s(\S+)/);
if (matches) {
token = matches[1];
} else {
token = undefined;
}
}
const bearerToken = token || getToken;
if (bearerToken == null) {
return;
}

const accessToken = getAccessToken(bearerToken);
if (accessToken == null) {
return;
}
if (accessToken.expires != null && accessToken.expires !== 0 && accessToken.expires < new Date()) {
return;
}
const user = Users.findOne(accessToken.userId);
if (user == null) {
return;
}
return { user };
return oAuth2ServerAuth(this.request);
});
11 changes: 11 additions & 0 deletions apps/meteor/definition/externals/express.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import 'express';

import { IUser } from '@rocket.chat/core-typings';

declare module 'express' {
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
export interface Request {
userId?: string;
user?: IUser;
}
}

0 comments on commit c6e8b3f

Please sign in to comment.