Skip to content

Commit

Permalink
[BUG] Account level security issue (#3150)
Browse files Browse the repository at this point in the history
* bug fixes

* changes

* changes for single workspace support

* added guards for signup route

* test cases fixes

* Workspace invite and user onboarding flow changes (#3190)

* invite user flow changes

* review comments

* cleanup

* testcase fix
  • Loading branch information
gsmithun4 committed Jun 2, 2022
1 parent 8545689 commit fadf025
Show file tree
Hide file tree
Showing 18 changed files with 788 additions and 543 deletions.
14 changes: 14 additions & 0 deletions frontend/src/App/App.jsx
Expand Up @@ -142,6 +142,20 @@ class App extends React.Component {
/>
)}
/>
<Route
path="/invitations/:token/workspaces/:organizationToken"
render={(props) => (
<Redirect
to={{
pathname: '/confirm',
state: {
token: props.match.params.token,
organizationToken: props.match.params.organizationToken,
},
}}
/>
)}
/>
<Route path="/confirm" component={ConfirmationPage} />
<Route
path="/organization-invitations/:token"
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/ConfirmationPage/ConfirmationPage.jsx
@@ -1,5 +1,5 @@
import React from 'react';
import { userService } from '@/_services';
import { appService } from '@/_services';
import { toast } from 'react-hot-toast';

class ConfirmationPage extends React.Component {
Expand All @@ -23,7 +23,7 @@ class ConfirmationPage extends React.Component {

setPassword = (e) => {
e.preventDefault();
const token = this.props.location.state.token;
const { token, organizationToken } = this.props.location.state;
const { password, organization, role, firstName, lastName, password_confirmation } = this.state;
this.setState({ isLoading: true });

Expand All @@ -43,9 +43,10 @@ class ConfirmationPage extends React.Component {
return;
}

userService
appService
.setPasswordFromToken({
token,
organizationToken,
password,
organization,
role,
Expand Down
109 changes: 54 additions & 55 deletions frontend/src/ConfirmationPage/OrganizationInvitationPage.jsx
@@ -1,5 +1,5 @@
import React from 'react';
import { userService } from '@/_services';
import { appService } from '@/_services';
import { toast } from 'react-hot-toast';

class OrganizationInvitationPage extends React.Component {
Expand Down Expand Up @@ -42,21 +42,22 @@ class OrganizationInvitationPage extends React.Component {
}
}

userService
appService
.acceptInvite({
token,
password,
})
.then(() => {
.then((response) => {
this.setState({ isLoading: false });
toast.success(`Added to the workspace${isSetPassword ? ' and password has been set ' : ' '}successfully.`, {
position: 'top-center',
response.json().then((data) => {
if (!response.ok) {
return toast.error(data?.message || 'Error while setting up your account.', { position: 'top-center' });
}
toast.success(`Added to the workspace${isSetPassword ? ' and password has been set ' : ' '}successfully.`, {
position: 'top-center',
});
this.props.history.push('/login');
});
this.props.history.push('/login');
})
.catch(({ error }) => {
this.setState({ isLoading: false });
toast.error(error, { position: 'top-center' });
});
};

Expand All @@ -73,7 +74,7 @@ class OrganizationInvitationPage extends React.Component {
</div>
<form className="card card-md" action="." method="get" autoComplete="off">
<div className="card-body">
{!this.single_organization && (
{!this.single_organization ? (
<>
<h2 className="card-title text-center mb-2">Already have an account?</h2>
<div className="mb-3">
Expand All @@ -85,53 +86,51 @@ class OrganizationInvitationPage extends React.Component {
Accept invite
</button>
</div>
<div className="org-invite-or">
<h2>
<span>OR</span>
</h2>
</>
) : (
<>
<h2 className="card-title text-center mb-4">Set up your account</h2>
<div className="mb-3">
<label className="form-label">Password</label>
<div className="input-group input-group-flat">
<input
onChange={this.handleChange}
name="password"
type="password"
className="form-control"
autoComplete="off"
/>
<span className="input-group-text"></span>
</div>
</div>
<div className="mb-3">
<label className="form-label">Confirm Password</label>
<div className="input-group input-group-flat">
<input
onChange={this.handleChange}
name="password_confirmation"
type="password"
className="form-control"
autoComplete="off"
/>
<span className="input-group-text"></span>
</div>
</div>
<div className="form-footer">
<p>
By clicking the button below, you agree to our{' '}
<a href="https://tooljet.io/terms">Terms and Conditions</a>.
</p>
<button
className={`btn mt-2 btn-primary w-100 ${isLoading ? ' btn-loading' : ''}`}
onClick={(e) => this.acceptInvite(e, true)}
disabled={isLoading}
>
Finish account setup and accept invite
</button>
</div>
</>
)}
<h2 className="card-title text-center mb-4">Set up your account</h2>
<div className="mb-3">
<label className="form-label">Password</label>
<div className="input-group input-group-flat">
<input
onChange={this.handleChange}
name="password"
type="password"
className="form-control"
autoComplete="off"
/>
<span className="input-group-text"></span>
</div>
</div>
<div className="mb-3">
<label className="form-label">Confirm Password</label>
<div className="input-group input-group-flat">
<input
onChange={this.handleChange}
name="password_confirmation"
type="password"
className="form-control"
autoComplete="off"
/>
<span className="input-group-text"></span>
</div>
</div>
<div className="form-footer">
<p>
By clicking the button below, you agree to our{' '}
<a href="https://tooljet.io/terms">Terms and Conditions</a>.
</p>
<button
className={`btn mt-2 btn-primary w-100 ${isLoading ? ' btn-loading' : ''}`}
onClick={(e) => this.acceptInvite(e, true)}
disabled={isLoading}
>
Finish account setup and accept invite
</button>
</div>
</div>
</form>
</div>
Expand Down
7 changes: 0 additions & 7 deletions frontend/src/ManageOrgUsers/ManageOrgUsers.jsx
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import { authenticationService, organizationService, organizationUserService } from '@/_services';
import { Header } from '@/_components';
import { toast } from 'react-hot-toast';
import { history } from '@/_helpers';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import ReactTooltip from 'react-tooltip';

Expand All @@ -26,7 +25,6 @@ class ManageOrgUsers extends React.Component {
}

validateEmail(email) {
console.log(email);
const re =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase());
Expand Down Expand Up @@ -168,11 +166,6 @@ class ManageOrgUsers extends React.Component {
}
};

logout = () => {
authenticationService.logout();
history.push('/login');
};

generateInvitationURL = (user) => window.location.origin + '/organization-invitations/' + user.invitation_token;

invitationLinkCopyHandler = () => {
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/_services/app.service.js
Expand Up @@ -19,6 +19,8 @@ export const appService = {
setVisibility,
setMaintenance,
setSlug,
setPasswordFromToken,
acceptInvite,
};

function getConfig() {
Expand Down Expand Up @@ -126,3 +128,28 @@ function setSlug(appId, slug) {
const requestOptions = { method: 'PUT', headers: authHeader(), body: JSON.stringify({ app: { slug: slug } }) };
return fetch(`${config.apiUrl}/apps/${appId}`, requestOptions).then(handleResponse);
}

function setPasswordFromToken({ token, password, organization, role, firstName, lastName, organizationToken }) {
const body = {
token,
organizationToken,
password,
organization,
role,
first_name: firstName,
last_name: lastName,
};

const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/set-password-from-token`, requestOptions).then(handleResponse);
}

function acceptInvite({ token, password }) {
const body = {
token,
password,
};

const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/accept-invite`, requestOptions);
}
26 changes: 0 additions & 26 deletions frontend/src/_services/user.service.js
Expand Up @@ -5,10 +5,8 @@ export const userService = {
getAll,
createUser,
deleteUser,
setPasswordFromToken,
updateCurrentUser,
changePassword,
acceptInvite,
getAvatar,
updateAvatar,
};
Expand Down Expand Up @@ -53,30 +51,6 @@ function deleteUser(id) {
return fetch(`${config.apiUrl}/users/${id}`, requestOptions).then(handleResponse);
}

function setPasswordFromToken({ token, password, organization, role, firstName, lastName }) {
const body = {
token,
password,
organization,
role,
first_name: firstName,
last_name: lastName,
};

const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/users/set_password_from_token`, requestOptions).then(handleResponse);
}

function acceptInvite({ token, password }) {
const body = {
token,
password,
};

const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/users/accept-invite`, requestOptions).then(handleResponse);
}

function updateCurrentUser(firstName, lastName) {
const body = { first_name: firstName, last_name: lastName };
const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(body) };
Expand Down
18 changes: 18 additions & 0 deletions server/src/controllers/app.controller.ts
Expand Up @@ -3,6 +3,10 @@ import { User } from 'src/decorators/user.decorator';
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
import { AppAuthenticationDto, AppForgotPasswordDto, AppPasswordResetDto } from '@dto/app-authentication.dto';
import { AuthService } from '../services/auth.service';
import { MultiOrganizationGuard } from 'src/modules/auth/multi-organization.guard';
import { SignupDisableGuard } from 'src/modules/auth/signup-disable.guard';
import { CreateUserDto } from '@dto/user.dto';
import { AcceptInviteDto } from '@dto/accept-organization-invite.dto';

@Controller()
export class AppController {
Expand All @@ -22,6 +26,20 @@ export class AppController {
return await this.authService.switchOrganization(organizationId, user);
}

@UseGuards(MultiOrganizationGuard, SignupDisableGuard)
@Post('set-password-from-token')
async create(@Body() userCreateDto: CreateUserDto) {
await this.authService.setupAccountFromInvitationToken(userCreateDto);
return {};
}

@Post('accept-invite')
async acceptInvite(@Body() acceptInviteDto: AcceptInviteDto) {
await this.authService.acceptOrganizationInvite(acceptInviteDto);
return {};
}

@UseGuards(MultiOrganizationGuard, SignupDisableGuard)
@Post('signup')
async signup(@Body() appAuthDto: AppAuthenticationDto) {
return this.authService.signup(appAuthDto.email);
Expand Down
8 changes: 6 additions & 2 deletions server/src/controllers/organization_users.controller.ts
Expand Up @@ -8,17 +8,21 @@ import { CheckPolicies } from 'src/modules/casl/check_policies.decorator';
import { User as UserEntity } from 'src/entities/user.entity';
import { User } from 'src/decorators/user.decorator';
import { InviteNewUserDto } from '../dto/invite-new-user.dto';
import { OrganizationsService } from '@services/organizations.service';

@Controller('organization_users')
export class OrganizationUsersController {
constructor(private organizationUsersService: OrganizationUsersService) {}
constructor(
private organizationUsersService: OrganizationUsersService,
private organizationsService: OrganizationsService
) {}

// Endpoint for inviting new organization users
@UseGuards(JwtAuthGuard, PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can('inviteUser', UserEntity))
@Post()
async create(@User() user, @Body() inviteNewUserDto: InviteNewUserDto) {
const result = await this.organizationUsersService.inviteNewUser(user, inviteNewUserDto);
const result = await this.organizationsService.inviteNewUser(user, inviteNewUserDto);
return decamelizeKeys({ users: result });
}

Expand Down

0 comments on commit fadf025

Please sign in to comment.