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
5 changes: 5 additions & 0 deletions .changeset/quiet-lizards-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Ensures the `users.CreateToken` endpoint checks for the `user-generate-access-token` permission when generating a login token for another user
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,7 @@ const usersEndpoints = API.v1.post(
async function action() {
const user = await getUserFromParams(this.bodyParams);

const data = await generateAccessToken(user._id, this.bodyParams.secret);
const data = await generateAccessToken(user._id, this.bodyParams.secret, this.user);

return API.v1.success({ data });
},
Expand Down
9 changes: 7 additions & 2 deletions apps/meteor/app/lib/server/methods/createToken.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MeteorError, User } from '@rocket.chat/core-services';
import { Authorization, MeteorError, User } from '@rocket.chat/core-services';
import type { IUser } from '@rocket.chat/core-typings';
import { Accounts } from 'meteor/accounts-base';

declare module '@rocket.chat/ddp-client' {
Expand All @@ -10,11 +11,15 @@ declare module '@rocket.chat/ddp-client' {

const { CREATE_TOKENS_FOR_USERS_SECRET } = process.env;

export async function generateAccessToken(userId: string, secret: string) {
export async function generateAccessToken(userId: string, secret: string, caller: IUser) {
if (secret !== CREATE_TOKENS_FOR_USERS_SECRET) {
throw new MeteorError('error-not-authorized', 'Not authorized');
}

if (caller._id !== userId && !(await Authorization.hasPermission(caller._id, 'user-generate-access-token'))) {
throw new MeteorError('error-not-authorized', 'Not authorized');
}

const token = Accounts._generateStampedLoginToken();
Accounts._insertLoginToken(userId, token);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect } from 'chai';
import { beforeEach, describe, it } from 'mocha';
import p from 'proxyquire';
import sinon from 'sinon';

const hasPermissionStub = sinon.stub();

const AccountsMock = {
_generateStampedLoginToken: sinon.stub(),
_insertLoginToken: sinon.stub(),
};

const UserMock = {
ensureLoginTokensLimit: sinon.stub(),
};

const AuthorizationMock = {
hasPermission: hasPermissionStub,
};

class MeteorErrorMock extends Error {
error: string;

constructor(error: string, reason: string) {
super(reason);
this.error = error;
}
}

process.env.CREATE_TOKENS_FOR_USERS_SECRET = 'V@9#mK2$pL8!nQ5^rT1&wX6*jY3%uZ7';

const { generateAccessToken } = p.noCallThru().load('../../../../../../app/lib/server/methods/createToken.ts', {
'@rocket.chat/core-services': { Authorization: AuthorizationMock, MeteorError: MeteorErrorMock, User: UserMock },
'meteor/accounts-base': { Accounts: AccountsMock },
});

describe('generateAccessToken', () => {
beforeEach(() => {
hasPermissionStub.reset();
AccountsMock._generateStampedLoginToken.reset();
AccountsMock._insertLoginToken.reset();
UserMock.ensureLoginTokensLimit.reset();
});

it('should throw if secret does not match', async () => {
await expect(generateAccessToken('targetId', 'wrong-secret', { _id: 'callerId' })).to.be.rejectedWith('Not authorized');
});

it('should throw if caller targets another user and lacks user-generate-access-token permission', async () => {
hasPermissionStub.resolves(false);

await expect(generateAccessToken('targetId', 'V@9#mK2$pL8!nQ5^rT1&wX6*jY3%uZ7', { _id: 'callerId' })).to.be.rejectedWith(
'Not authorized',
);
sinon.assert.calledOnceWithExactly(hasPermissionStub, 'callerId', 'user-generate-access-token');
});

it('should succeed if caller targets another user and has user-generate-access-token permission', async () => {
hasPermissionStub.resolves(true);
AccountsMock._generateStampedLoginToken.returns({ token: 'abc123' });
UserMock.ensureLoginTokensLimit.resolves();

const result = await generateAccessToken('targetId', 'V@9#mK2$pL8!nQ5^rT1&wX6*jY3%uZ7', { _id: 'callerId' });

expect(result).to.deep.equal({ userId: 'targetId', authToken: 'abc123' });
sinon.assert.calledOnceWithExactly(hasPermissionStub, 'callerId', 'user-generate-access-token');
});

it('should succeed without permission check when caller generates token for themselves', async () => {
AccountsMock._generateStampedLoginToken.returns({ token: 'abc123' });
UserMock.ensureLoginTokensLimit.resolves();

const result = await generateAccessToken('userId', 'V@9#mK2$pL8!nQ5^rT1&wX6*jY3%uZ7', { _id: 'userId' });

expect(result).to.deep.equal({ userId: 'userId', authToken: 'abc123' });
sinon.assert.notCalled(hasPermissionStub);
});
});
Loading