# Impersonation

Impersonation is the ability to log in as a specific user from the Teams List page. While impersonating, you can see the app exactly as that user does. In our app, this is particularly useful for logging in as a specific team owner.<br>

However, you cannot log in as all owners; they can disable this option in their profile settings. We have the `impersonationAllowed` field in the `userEntity` that controls this:

```typescript
    @Column({ default: true })
    public impersonationAllowed: boolean;
```

On UI you can switch this option on Profile Setting page:

<figure><img src="/files/HRmrLrRPnYrltoa4p4OA" alt=""><figcaption></figcaption></figure>

It is also other factors, with which you cannot impersonate user:

* Your two-factor authentication is disabled
* You are trying to impersonate yourself
* You are trying to impersonate user with **Super Admin** role
* You are trying to impersonate another **Admin** while you are not **Super Admin**
* You are trying to impersonate other users when you are already impersonating someone

In api/src/auth/controller/**two-factor-auth.controller.ts** you can find **impersonate** endpoint:

```typescript
    @UseGuards(JwtTwoFactorGuard, AdminGuard)
    @Post('impersonate')
    async impersonate(@Body() body: TwoFactorImpersonationDto, @GetUser() impersonator: JwtUserPayload) {
        const isCodeValid = await this.twoFactorAuthService.verifyTwoFactorAuthCode(body.code, impersonator);
        if (!isCodeValid) {
            throw new HttpException(
                {
                    message: 'Code is not valid!',
                    description: 'An incorrect code or code time has expired. Please try again.',
                    code: 'INVALID_TWO_FACTOR_AUTH_CODE',
                },
                HttpStatus.BAD_REQUEST
            );
        }

        return await this.twoFactorAuthService.impesonateUser(body.userId, body.teamId, impersonator);
    }
```

As you can see, at first we should pass two-factor verification for security reasons. On the higher level we have **JwtTwoFactorGuard** which will check if user 2FA is enabled and **AdminGuard** which will check if user is **Admin** or **Super Admin.**

After that we move to the service which will handle security checks and if all passed, we will generate impersonation token:

```typescript
    async impesonateUser(userId: number, teamId: string, impersonator: JwtUserPayload) {
        const userToImpersonate = await this.userRepository.findOne({
            where: { id: userId },
            relations: ['userRoles', 'userRoles.role'],
        });

        if (!userToImpersonate) {
            throw new HttpException(
                {
                    message: 'Error',
                    description: 'User not found',
                    code: 'USER_NOT_FOUND',
                },
                HttpStatus.BAD_REQUEST
            );
        }

        // avoiding double impersonation
        if (impersonator.impersonatedBy) {
            throw new ForbiddenException('Access denied');
        }

        if (!userToImpersonate.impersonationAllowed) {
            throw new HttpException(
                {
                    message: 'Error',
                    description: 'User disabled impersonation ability',
                    code: 'USER_IMPERSONATION_DISABLED',
                },
                HttpStatus.BAD_REQUEST
            );
        }

        const userRolesNames = userToImpersonate.userRoles.map(roleEntity => roleEntity.role.name);

        if (
            userRolesNames.includes(Role.SuperAdmin) ||
            (userRolesNames.includes(Role.Admin) && !impersonator.roles.includes(Role.SuperAdmin))
        ) {
            throw new ForbiddenException('Access denied');
        }

        return await this.generateImpersonationAccessToken(userId, teamId, impersonator);
    }
```

In *generateImpersonationAccessToken* function we will create impersonation record, with randomly generated 24 characters code and user who done impersonation,  timestamp will be generated automatically:

```typescript
const impersonationRecord = this.impersonationRepository.create({
            user: impersonator,
            code: generateRandomCode(24),
        });
```

Next, we will generate JWT token with some extra fields:

```typescript
const payload: Partial<JwtUserPayload> = {
            id: userWithRole.id,
            email: userWithRole.email,
            roles: userWithRole.userRoles.map(userRole => userRole.role.name),
            isTwoFactorEnable: false,
            impersonatedBy: impersonator.email,
            impersonatedAt: impersonationRecord.impersonatedAt.toISOString(),
            impersonationCode: impersonationRecord.code,
            impersonatorTeamId: impersonator.teamId,
            isTwoFactorAuthenticated: false,
        };

        let membership;

        if (teamId) {
            membership = userWithRole.memberships.find(membership => membership.team.id === teamId);
        } else {
            membership = userWithRole.memberships[0];
        }

        if (membership) {
            const { team, role } = membership;

            payload.teamId = teamId;
            payload.teamRole = role;
            payload.subscriptionTier = team?.subscription?.tier.priority;
        } else {
            throw ForbiddenException;
        }

        const token = await sign(payload, this.tokenSecret, {
            expiresIn: TokensExpiredTime.impersonationToken,
        });
```

As you can see we have 4 extra fields: **impersonatedBy, impersonatedAt, impersonationCode, impersonatorTeamId.** The **impersonatorTeamId** value is used to return impersonator to the team from where he ded impersonation. **isTwoFactorEnable** and **isTwoFactorAuthenticated** should be always setted to false, in case user has 2FA enabled, this logic will allow us to skip 2FA on this user. To regular field we pass user which will be impersonated credentials that will allow backend to identify impersonator like other user.

{% hint style="danger" %}
Token is valid for the next 30 minutes.

While impersonation we will have some functionality blocked.

On frontend `currentUser`in context will have `impersonatedBy` field by which we know when to disable specific actions and show impersonation alert.
{% endhint %}

Changing teams and creating new teams will be blocked both in the UI and on the backend. The reason for this is that changing teams requires generating new tokens, which would break the impersonation logic. To forbid that we can add simple condition:

```typescript
if (user.impersonatedAt || user.impersonatedBy || user.impersonationCode) {
            throw new ForbiddenException();
        }
```

Additionally, during impersonation, we will be able to leave comments on businesses and leads, but they will be posted under the impersonator's name, with the prefix 'Support team.'  The same will apply to audit logs. This logic will allow us to clearly identify who performed the actions.

<figure><img src="/files/BpNSpupMC18LO2GhpPCa" alt=""><figcaption><p>Display on frontend</p></figcaption></figure>

`AuditLoggerEntity`, `BusinessCommentEntity` and `LeadCommentEntity` have ***isImpersonated*** field to identify which record was created while impersonation.

On backend we handle that scenario with one condition (similar for both logs and comments):

```typescript
        if (user.impersonatedBy) {
            const impersonator = await this.userRepository.findOne({ where: { email: user.impersonatedBy } });

            newLog.user = impersonator;
            newLog.userEmail = impersonator.email;
            newLog.isImpersonated = true;
        }
```

To return back, we will use the `cancelImpersonation` function in the `two-factor-auth.controller`. We will find the impersonation record by user, code, and timestamp to ensure we find the correct record (we normalize `user.impersonatedAt` and truncate the timestamp to get accurate results, since JWT tokens don't support the timestamp format used in PostgreSQL).  After that, we generate tokens for the admin, allowing them to return to their profile and team.

```typescript
const normalizedImpersonatedAt = dayjs(user.impersonatedAt).utc().format('YYYY-MM-DDTHH:mm:ss');

        const impersonationRecord = await this.impersonationRepository
            .createQueryBuilder('impersonation')
            .leftJoinAndSelect('impersonation.user', 'user')
            .where("DATE_TRUNC('second', impersonation.impersonatedAt) = :date", { date: normalizedImpersonatedAt })
            .andWhere('impersonation.code = :code', { code: user.impersonationCode })
            .andWhere('impersonation.user = :user', { user: impersonator.id })
            .getOne();

        if (!impersonationRecord) {
            throw new ForbiddenException('Access denied');
        }

        const impersonatorAccessToken = await this.authService.generateAccessToken({
            userId: impersonationRecord.user.id,
            isTwoFactorAuthenticated: impersonator.isTwoFactorEnable,
            teamId: user.impersonatorTeamId,
        });

        return { accessToken: impersonatorAccessToken, refreshToken: impersonator.refreshToken };
```

You can see full process of impersonation in block schemas:

<figure><img src="/files/N649quuoGRICLz0GsBIp" alt=""><figcaption><p>Start of impersonation</p></figcaption></figure>

<figure><img src="/files/Nk1VoydMig5pKEh5nRhw" alt=""><figcaption><p>End of impersonating</p></figcaption></figure>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://intercode.gitbook.io/intercode-saas-kit/features/impersonation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
