Skip to content

Commit

Permalink
Merge pull request #723 from jetstreamapp/feat/716
Browse files Browse the repository at this point in the history
Add option to disable frontdoor login
  • Loading branch information
paustint authored Feb 10, 2024
2 parents 32d7e6f + 7b81c77 commit 8305758
Show file tree
Hide file tree
Showing 54 changed files with 545 additions and 179 deletions.
4 changes: 2 additions & 2 deletions apps/api/src/app/controllers/sf-query.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NextFunction, Request, Response } from 'express';
import { body, query as queryString } from 'express-validator';
import * as jsforce from 'jsforce';
import * as queryService from '../services/query';
import { sendJson } from '../utils/response.handlers';
import { UserFacingError } from '../utils/error-handler';
import { body, query as queryString } from 'express-validator';
import { sendJson } from '../utils/response.handlers';

export const routeValidators = {
query: [body('query').isString()],
Expand Down
75 changes: 63 additions & 12 deletions apps/api/src/app/controllers/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { logger, mailgun } from '@jetstream/api-config';
import { UserProfileServer } from '@jetstream/types';
import { ENV, logger, mailgun } from '@jetstream/api-config';
import { UserProfileAuth0Ui, UserProfileServer, UserProfileUi, UserProfileUiWithIdentities } from '@jetstream/types';
import { AxiosError } from 'axios';
import * as express from 'express';
import { body, query as queryString } from 'express-validator';
import { deleteUserAndOrgs } from '../db/transactions.db';
import * as userDbService from '../db/user.db';
import * as auth0Service from '../services/auth0';
import { UserFacingError } from '../utils/error-handler';
import { sendJson } from '../utils/response.handlers';

export const routeValidators = {
updateProfile: [body('name').isString().isLength({ min: 1, max: 255 })],
updateProfile: [body('name').isString().isLength({ min: 1, max: 255 }), body('preferences').isObject().optional()],
unlinkIdentity: [queryString('provider').isString().isLength({ min: 1 }), queryString('userId').isString().isLength({ min: 1 })],
resendVerificationEmail: [queryString('provider').isString().isLength({ min: 1 }), queryString('userId').isString().isLength({ min: 1 })],
deleteAccount: [body('reason').isString().optional()],
Expand Down Expand Up @@ -58,16 +59,62 @@ export async function emailSupport(req: express.Request, res: express.Response)
}

export async function getUserProfile(req: express.Request, res: express.Response) {
const user = req.user as UserProfileServer;
sendJson(res, user._json);
const auth0User = req.user as UserProfileServer;

// use fallback locally and on CI
if (ENV.EXAMPLE_USER_OVERRIDE && ENV.EXAMPLE_USER_PROFILE && req.hostname === 'localhost') {
sendJson(res, ENV.EXAMPLE_USER_PROFILE);
return;
}

const user = await userDbService.findByUserId(auth0User.id);
if (!user) {
throw new UserFacingError('User not found');
}
const userProfileUi: UserProfileUi = {
...(auth0User._json as any),
id: user.id,
userId: user.userId,
createdAt: user.createdAt.toISOString(),
updatedAt: user.updatedAt.toISOString(),
preferences: {
skipFrontdoorLogin: user.preferences?.skipFrontdoorLogin,
},
};
sendJson(res, userProfileUi);
}

async function getFullUserProfileFn(sessionUser: UserProfileServer, auth0User?: UserProfileAuth0Ui) {
auth0User = auth0User || (await auth0Service.getUser(sessionUser));
const jetstreamUser = await userDbService.findByUserId(sessionUser.id);
if (!jetstreamUser) {
throw new UserFacingError('User not found');
}
const response: UserProfileUiWithIdentities = {
id: jetstreamUser.id,
userId: sessionUser.id,
name: jetstreamUser.name || '',
email: jetstreamUser.email,
emailVerified: auth0User.email_verified,
username: auth0User.username || '',
nickname: auth0User.nickname,
picture: auth0User.picture,
preferences: {
skipFrontdoorLogin: jetstreamUser.preferences?.skipFrontdoorLogin ?? false,
},
identities: auth0User.identities,
createdAt: jetstreamUser.createdAt.toISOString(),
updatedAt: jetstreamUser.updatedAt.toISOString(),
};
return response;
}

/** Get profile from Auth0 */
export async function getFullUserProfile(req: express.Request, res: express.Response) {
const user = req.user as UserProfileServer;
try {
const auth0User = await auth0Service.getUser(user);
sendJson(res, auth0User);
const response = await getFullUserProfileFn(user);
sendJson(res, response);
} catch (ex) {
if (ex.isAxiosError) {
const error: AxiosError = ex;
Expand All @@ -86,11 +133,14 @@ export async function getFullUserProfile(req: express.Request, res: express.Resp

export async function updateProfile(req: express.Request, res: express.Response) {
const user = req.user as UserProfileServer;
const userProfile = { name: req.body.name };
const userProfile = req.body as UserProfileUiWithIdentities;

try {
// check for name change, if so call auth0 to update
const auth0User = await auth0Service.updateUser(user, userProfile);
sendJson(res, auth0User);
// update name and preferences locally
const response = await getFullUserProfileFn(user, auth0User);
sendJson(res, response);
} catch (ex) {
if (ex.isAxiosError) {
const error: AxiosError = ex;
Expand All @@ -114,7 +164,8 @@ export async function unlinkIdentity(req: express.Request, res: express.Response
const userId = req.query.userId as string;

const auth0User = await auth0Service.unlinkIdentity(user, { provider, userId });
sendJson(res, auth0User);
const response = await getFullUserProfileFn(user, auth0User);
sendJson(res, response);
} catch (ex) {
if (ex.isAxiosError) {
const error: AxiosError = ex;
Expand All @@ -136,8 +187,8 @@ export async function resendVerificationEmail(req: express.Request, res: express
const provider = req.query.provider as string;
const userId = req.query.userId as string;
try {
const auth0User = await auth0Service.resendVerificationEmail(user, { provider, userId });
sendJson(res, auth0User);
await auth0Service.resendVerificationEmail(user, { provider, userId });
sendJson(res);
} catch (ex) {
if (ex.isAxiosError) {
const error: AxiosError = ex;
Expand Down
68 changes: 63 additions & 5 deletions apps/api/src/app/db/user.db.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,70 @@
import { ENV, logger, prisma } from '@jetstream/api-config';
import { UserProfileServer } from '@jetstream/types';
import { User } from '@prisma/client';
import { Prisma, User } from '@prisma/client';

const userSelect: Prisma.UserSelect = {
appMetadata: true,
createdAt: true,
email: true,
id: true,
name: true,
nickname: true,
picture: true,
preferences: {
select: {
skipFrontdoorLogin: true,
},
},
updatedAt: true,
userId: true,
};

/**
* Find by Auth0 userId, not Jetstream Id
*/
async function findByUserId(userId: string) {
return await prisma.user.findUnique({
export async function findByUserId(userId: string) {
let user = await prisma.user.findUnique({
where: { userId: userId },
select: userSelect,
});
// ensure user preference exists if not already created
if (user && !user.preferences) {
await prisma.userPreference.create({
data: {
userId: user.userId,
skipFrontdoorLogin: false,
},
});
user = await prisma.user.findUnique({
where: { userId: userId },
select: userSelect,
});
}
return user;
}

export async function updateUser(user: UserProfileServer, data: { name: string }): Promise<User> {
export async function updateUser(
user: UserProfileServer,
data: { name: string; preferences: { skipFrontdoorLogin: boolean } }
): Promise<User> {
try {
const existingUser = await prisma.user.findUnique({
where: { userId: user.id },
select: { id: true, preferences: { select: { skipFrontdoorLogin: true } } },
});
const skipFrontdoorLogin = data.preferences.skipFrontdoorLogin ?? (existingUser?.preferences?.skipFrontdoorLogin || false);
const updatedUser = await prisma.user.update({
where: { userId: user.id },
data: { name: data.name },
data: {
name: data.name,
preferences: {
upsert: {
create: { skipFrontdoorLogin },
update: { skipFrontdoorLogin },
},
},
},
select: userSelect,
});
return updatedUser;
} catch (ex) {
Expand All @@ -36,7 +85,14 @@ export async function createOrUpdateUser(user: UserProfileServer): Promise<{ cre
where: { userId: user.id },
data: {
appMetadata: JSON.stringify(user._json[ENV.AUTH_AUDIENCE!]),
preferences: {
connectOrCreate: {
create: { skipFrontdoorLogin: false },
where: { userId: user.id },
},
},
},
select: userSelect,
});
logger.debug('[DB][USER][UPDATED] %s', user.id, { userId: user.id, id: existingUser.id });
return { created: false, user: updatedUser };
Expand All @@ -49,7 +105,9 @@ export async function createOrUpdateUser(user: UserProfileServer): Promise<{ cre
nickname: user._json.nickname,
picture: user._json.picture,
appMetadata: JSON.stringify(user._json[ENV.AUTH_AUDIENCE!]),
preferences: { create: { skipFrontdoorLogin: false } },
},
select: userSelect,
});
logger.debug('[DB][USER][CREATED] %s', user.id, { userId: user.id, id: createdUser.id });
return { created: true, user: createdUser };
Expand Down
11 changes: 7 additions & 4 deletions apps/api/src/app/services/auth0.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ENV, logger } from '@jetstream/api-config';
import { UserProfileAuth0Identity, UserProfileAuth0Ui, UserProfileServer } from '@jetstream/types';
import { UserProfileAuth0Identity, UserProfileAuth0Ui, UserProfileServer, UserProfileUiWithIdentities } from '@jetstream/types';
import axios, { AxiosError } from 'axios';
import { addHours, addSeconds, formatISO, isBefore } from 'date-fns';
import * as userDb from '../db/user.db';
Expand Down Expand Up @@ -76,10 +76,13 @@ export async function updateUserLastActivity(user: UserProfileServer, lastActivi
).data;
}

export async function updateUser(user: UserProfileServer, userProfile: { name: string }): Promise<UserProfileAuth0Ui> {
export async function updateUser(user: UserProfileServer, userProfile: UserProfileUiWithIdentities): Promise<UserProfileAuth0Ui> {
await initAuthorizationToken(user);
// update on Auth0
await axiosAuth0.patch<UserProfileAuth0Ui>(`/api/v2/users/${user.id}`, userProfile);

if (user.displayName !== userProfile.name) {
// update on Auth0 if name changed (not allowed for OAuth connections)
await axiosAuth0.patch<UserProfileAuth0Ui>(`/api/v2/users/${user.id}`, { name: userProfile.name });
}
// update locally
await userDb.updateUser(user, userProfile);
// re-fetch user from Auth0
Expand Down
15 changes: 5 additions & 10 deletions apps/docs/docs/getting-started/_org-troubleshooting-table.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,8 @@ export const OAuthSettingsSolution = ({ children }) => (

If you have issues adding your org, here are some likely causes and solutions.

| Problem | Possible Causes | Solution |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| You are unable to login, your username and password is not accepted from Salesforce | Your org may have a login restriction to only allow access by logging in using the custom domain. | This setting can be found in Salesforce under `Setup``My Domain``Policies``Prevent login from https://login.salesforce.com`. <br /> <br /> If this is set to true, then you will want to use the **Custom Login URL** option and provide custom domain shown on the **Current My Domain URL** on the setup page. |
| You receive an error message after successfully logging in | <OAuthSettingsList /> | <OAuthSettingsSolution /> |

:::important

Jetstream uses a wide range of IP addresses, so you may need to relax IP address restrictions for the Jetstream Connected App.

:::
| Problem | Possible Causes | Solution |
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| You are unable to login, your username and password is not accepted from Salesforce | Your org may have a login restriction to only allow access by logging in using the custom domain. | This setting can be found in Salesforce under `Setup``My Domain``Policies``Prevent login from https://login.salesforce.com`. <br /> <br /> If this is set to true, then you will want to use the **Custom Login URL** option and provide custom domain shown on the **Current My Domain URL** on the setup page. |
| You receive an error message after successfully logging in | <OAuthSettingsList /> | <OAuthSettingsSolution /> |
| When you click a Salesforce link, you are required to "Choose a Verification Method" to continue to Salesforce. | You may have very strict session settings in Salesforce, such as "High Assurance". | By default, Jetstream uses [Frontdoor](https://help.salesforce.com/s/articleView?id=sf.security_frontdoorjsp.htm&type=5) to login using your existing Jetstream session. <br /> <br /> This can be disabled by navigating to your settings and enabling the option to **Don't Auto-Login on Link Clicks**. <br /> <br /> <img src={require('./settings-dont-auto-login.png').default} alt="Disable Auto-Login" /> |
2 changes: 1 addition & 1 deletion apps/docs/docs/getting-started/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ You will be asked to choose an org type which is used to determine the which Sal

### Jetstream IP Addresses

If you need to whitelist IP addresses for your org, Jetream will use one of the following three ip addresses:
If you need to whitelist IP addresses for your org, Jetstream will use one of the following three ip addresses:

- `3.134.238.10`
- `3.129.111.220`
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions apps/jetstream/src/app/app-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ export const salesforceOrgsOmitSelectedState = selector({
},
});

export const selectSkipFrontdoorAuth = selector({
key: 'selectSkipFrontdoorAuth',
get: ({ get }) => {
const userProfile = get(userProfileState);
return userProfile?.preferences?.skipFrontdoorLogin || false;
},
});

export const salesforceOrgsById = selector({
key: 'salesforceOrgsById',
get: ({ get }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import escapeRegExp from 'lodash/escapeRegExp';
import type { editor } from 'monaco-editor';
import { Fragment, FunctionComponent, MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { STORAGE_KEYS, applicationCookieState, selectedOrgState } from '../../app-state';
import { STORAGE_KEYS, applicationCookieState, selectSkipFrontdoorAuth, selectedOrgState } from '../../app-state';
import { useAmplitude } from '../core/analytics';
import AnonymousApexFilter from './AnonymousApexFilter';
import AnonymousApexHistory from './AnonymousApexHistory';
Expand Down Expand Up @@ -55,7 +55,8 @@ export const AnonymousApex: FunctionComponent<AnonymousApexProps> = () => {
const logRef = useRef<editor.IStandaloneCodeEditor>();
const { trackEvent } = useAmplitude();
const rollbar = useRollbar();
const [{ serverUrl }] = useRecoilState(applicationCookieState);
const { serverUrl } = useRecoilValue(applicationCookieState);
const skipFrontDoorAuth = useRecoilValue(selectSkipFrontdoorAuth);
const selectedOrg = useRecoilValue<SalesforceOrgUi>(selectedOrgState);
const [apex, setApex] = useState(() => localStorage.getItem(STORAGE_KEYS.ANONYMOUS_APEX_STORAGE_KEY) || '');
const [results, setResults] = useState('');
Expand Down Expand Up @@ -273,6 +274,7 @@ export const AnonymousApex: FunctionComponent<AnonymousApexProps> = () => {
className="slds-m-right_x-small"
serverUrl={serverUrl}
org={selectedOrg}
skipFrontDoorAuth={skipFrontDoorAuth}
returnUrl="/_ui/common/apex/debug/ApexCSIPage"
omitIcon
title="Open developer console"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import {
import classNames from 'classnames';
import { FunctionComponent, useState } from 'react';
import { Link } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { applicationCookieState, selectedOrgState } from '../../app-state';
import { useRecoilValue } from 'recoil';
import { applicationCookieState, selectSkipFrontdoorAuth, selectedOrgState } from '../../app-state';
import { RequireMetadataApiBanner } from '../core/RequireMetadataApiBanner';
import { useAmplitude } from '../core/analytics';
import * as fromJetstreamEvents from '../core/jetstream-events';
Expand Down Expand Up @@ -60,7 +60,9 @@ export const AutomationControlEditor: FunctionComponent<AutomationControlEditorP
const { trackEvent } = useAmplitude();

const selectedOrg = useRecoilValue<SalesforceOrgUi>(selectedOrgState);
const [{ serverUrl, defaultApiVersion, google_apiKey, google_appId, google_clientId }] = useRecoilState(applicationCookieState);
const { serverUrl, defaultApiVersion, google_apiKey, google_appId, google_clientId } = useRecoilValue(applicationCookieState);
const skipFrontdoorLogin = useRecoilValue(selectSkipFrontdoorAuth);

const selectedSObjects = useRecoilValue(fromAutomationCtlState.selectedSObjectsState);
const selectedAutomationTypes = useRecoilValue(fromAutomationCtlState.selectedAutomationTypes);

Expand Down Expand Up @@ -368,6 +370,7 @@ export const AutomationControlEditor: FunctionComponent<AutomationControlEditorP
>
<AutomationControlEditorTable
serverUrl={serverUrl}
skipFrontdoorLogin={skipFrontdoorLogin}
selectedOrg={selectedOrg}
rows={visibleRows}
quickFilterText={quickFilterText}
Expand Down
Loading

0 comments on commit 8305758

Please sign in to comment.