Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional Auth0 integration #229

Merged
merged 12 commits into from
Oct 19, 2020
2 changes: 2 additions & 0 deletions spotlight-client/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
REACT_APP_AUTH_ENABLED={FLAG}
REACT_APP_AUTH_ENV={ENV_NAME}
19 changes: 19 additions & 0 deletions spotlight-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ If you have followed the [setup instructions](../README.md#getting-set-up) in th

We suggest installing a linting package for your preferred code editor that hooks into [eslint](#yarn-lint). We recommend [linter-eslint](https://atom.io/packages/linter-eslint) if you're using Atom.

#### Environment variables

Second and last, set up your environment variables. Copy the `.env.example` file and set variables accordingly per environment. The app can be deployed to both staging and production environments. Staging relies on environment variables stored in `.env.development` and production relies on variables in `.env.production`. Local relies on `.env.development.local`.

Expected environment variables include:

- `REACT_APP_AUTH_ENABLED` - set to `true` or `false` to toggle Auth0 protection per environment. Currently only used in staging to make the entire site private. No need to enable this locally unless you are developing or testing something auth-related.
- `REACT_APP_AUTH_ENV` - a string indicating the "auth environment" used to point to the correct Auth0 tenant. Either "development" (which also covers staging) or "production".
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true and relevant? I thought we only had one auth environment for this app.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes you are right ... I adapted this from the pulse readme and was a little unsure of how to handle the production case. But after sleeping on it, it seems pretty obvious to me that the docs should just reflect the current state of things (just the one environment) and not worry about being future-proof. I'll update this!


(Note that variables must be prefixed with `REACT_APP_` to be available inside the client application.)

The build process, as described below, ensures that the proper values are compiled and included in the static bundle at build time, for the right environment.

### Running the application locally

`yarn dev` will start a Webpack development server on port `3000` and open the homepage in your browser.
Expand All @@ -21,6 +34,12 @@ The development servers will remain active until you either close your terminal

**Note:** The frontend server does not need to be restarted when frontend source code is modified. The assets will automatically be recompiled and the browser will be refreshed.

### Authentication

This app may optionally be authenticated via [Auth0](https://auth0.com/). Auth0 settings can be inspected in the `AuthProvider` component, which wraps the entire application in a global React context using the [`@auth0/auth0-react`](https://www.npmjs.com/package/@auth0/auth0-react) library.

There is no per-view authentication; enabling auth (via environment variable, as described above) protects the entire application. We currently only enable this on our staging environment. If you are setting this app up completely fresh, you will need to create your own Auth0 account on the staging site in order to access it.

## Deploys

Not yet implemented! Instructions will be found here once a process is in place.
Expand Down
49 changes: 49 additions & 0 deletions spotlight-client/auth0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Auth0 Integration

This application uses Auth0 as its authentication and authorization service. Auth0 provides a number
of configuration options and can integrate with apps in a variety of ways.

If you are so inclined to set up an Auth0 tenant, either in the event of disaster recovery or to
support a new environment, these instructions should help.

## Initial Setup

Follow the initial setup instructions provided by Auth0 for creating a new account, or creating a
new tenant within an existing account. Specifically, follow the quickstart for React apps
[here](https://auth0.com/docs/quickstart/spa/react). Whether you are doing this only to test the
app in full auth mode or for production purposes, you should at least start by configuring all of
the various urls in that quickstart with the set of localhost urls, e.g. `http://localhost:3000` for
the callback urls and logout urls.

When going through the quickstart, you do not need to perform any of the coding-related steps.
However, it would still be wise to read these steps to understand how the system works as a whole.

### Connections

At present, the app uses only the _Database_ connection, which provides basic username-password
credential authentication. Other connections may be added in the future.

## Rules and Hooks

Auth0 has a system of [rules](https://auth0.com/docs/rules) and [hooks](https://auth0.com/docs/hooks)
for expanding functionality. The names, order, and actual code for rules and hooks are configured in
the Auth0 dashboard itself. However, for the sake of tracking updates, we commit those same rules
and hooks in `auth0/`, even though the files therein _are not_ used by the app in any way.

### Usage

For each file in `auth0/rules/`, create a new rule. The name does not matter beyond reminding
you what is in each rule at a glance. Copy and paste the full content of the file into the rule's
code and save it. Order the rules in the same order they are in within the `/rules/` folder.

Do the same thing for `auth0/hooks`, but creating a new hook for each file instead of a new
rule. There are different kinds of hooks that execute at different points in the authentication
workflow. Each hook file should have a suffix indicating which type of hook it should be created as.

## Logging

Auth0 maintains good logging for all interactions with Auth0 APIs. For compliance reasons,
specifically the need to store authentication logs for longer retention periods, we copy Auth0 logs
to segment. You can set this up by creating an Auth0 [extension](https://auth0.com/docs/extensions).
If you are in a situation where you need to do this for Recidiviz, speak to someone internally about
how to configure this.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 Recidiviz, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

/**
@param {object} user - The user being created
@param {string} user.tenant - Auth0 tenant name
@param {string} user.username - user name
@param {string} user.password - user's password
@param {string} user.email - email
@param {boolean} user.emailVerified - is e-mail verified?
@param {string} user.phoneNumber - phone number
@param {boolean} user.phoneNumberVerified - is phone number verified?
@param {object} context - Auth0 connection and other context info
@param {string} context.requestLanguage - language of the client agent
@param {object} context.connection - information about the Auth0 connection
@param {object} context.connection.id - connection id
@param {object} context.connection.name - connection name
@param {object} context.connection.tenant - connection tenant
@param {object} context.webtask - webtask context
@param {function} cb - function (error, response)
*/
module.exports = function (user, context, cb) {
const response = {};

const whitelist = []; // add authorized domains here
const userHasAccess = whitelist.some(function (domain) {
const emailSplit = user.email.split("@");
return emailSplit[emailSplit.length - 1].toLowerCase() === domain;
});

if (userHasAccess) {
response.user = user;
cb(null, response);
} else {
cb("Access denied.", null);
}
};
1 change: 1 addition & 0 deletions spotlight-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"test": "react-scripts test"
},
"dependencies": {
"@auth0/auth0-react": "^1.1.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"react": "^16.13.1",
Expand Down
77 changes: 74 additions & 3 deletions spotlight-client/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,84 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import React from "react";
import { Auth0Provider, useAuth0 } from "@auth0/auth0-react";
import { render } from "@testing-library/react";
import React from "react";
import App from "./App";
import getAuthSettings from "./AuthProvider/getAuthSettings";
import isAuthEnabled from "./utils/isAuthEnabled";

test("does not explode", () => {
const { getByText } = render(<App />);
const { getByRole } = render(<App />);
// seems like a pretty safe bet this word will always be there somewhere!
const websiteName = getByText(/spotlight/i);
const websiteName = getByRole("heading", /spotlight/i);
expect(websiteName).toBeInTheDocument();
});

jest.mock("./AuthProvider/getAuthSettings");
const getAuthSettingsMock = getAuthSettings as jest.MockedFunction<
typeof getAuthSettings
>;

jest.mock("./utils/isAuthEnabled");
const isAuthEnabledMock = isAuthEnabled as jest.MockedFunction<
typeof isAuthEnabled
>;

// Although mocking the Auth0 library is not necessarily a great practice,
// it has a number of side effects (e.g. issuing XHRs, navigating to new URLs)
// that are challenging to handle or even simulate in this test environment,
// so this seemed like the better solution here
jest.mock("@auth0/auth0-react", () => {
return {
Auth0Provider: jest.fn(),
useAuth0: jest.fn(),
};
});

test("require auth", async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing test 🚀

// mock the environment configuration to enable auth
const MOCK_DOMAIN = "test.local";
const MOCK_CLIENT_ID = "abcdef";
getAuthSettingsMock.mockReturnValue({
domain: MOCK_DOMAIN,
clientId: MOCK_CLIENT_ID,
});
isAuthEnabledMock.mockReturnValue(true);

// mock the auth0 provider
const PROVIDER_TEST_ID = "Auth0Provider";
(Auth0Provider as jest.Mock).mockImplementation(({ children }) => {
// here we mock just enough to verify that the context provider is being included;
// we can use this as a proxy for the relationship between provider and hook.
return <div data-testid={PROVIDER_TEST_ID}>{children}</div>;
});

// mock the auth0 hook
const mockLoginWithRedirect = jest.fn();
(useAuth0 as jest.Mock).mockReturnValue({
// this isn't the full hook API, just what's relevant to this test
isAuthenticated: false,
loginWithRedirect: mockLoginWithRedirect,
});

// now we test
const { queryByRole, getByTestId } = render(<App />);

// verify that we have included an Auth0Provider
expect(getByTestId(PROVIDER_TEST_ID)).toBeInTheDocument();

// verify that we supplied that provider with
// the settings designated by our mock environment
expect((Auth0Provider as jest.Mock).mock.calls[0][0]).toEqual(
expect.objectContaining({ domain: MOCK_DOMAIN, clientId: MOCK_CLIENT_ID })
);

// verify that we have initiated an Auth0 login
expect(mockLoginWithRedirect.mock.calls.length).toBe(1);

// application contents should not have been rendered unauthed
expect(queryByRole("heading", /spotlight/i)).toBeNull();

jest.restoreAllMocks();
});
20 changes: 13 additions & 7 deletions spotlight-client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@
// =============================================================================

import React from "react";
import AuthProvider from "./AuthProvider";
import AuthWall from "./AuthWall";

function App(): JSX.Element {
const App: React.FC = () => {
return (
<div>
<header>
<h1>Spotlight</h1>
</header>
</div>
<AuthProvider>
<AuthWall>
<div>
<header>
<h1>Spotlight</h1>
</header>
</div>
</AuthWall>
</AuthProvider>
);
}
};

export default App;
44 changes: 44 additions & 0 deletions spotlight-client/src/AuthProvider/AuthProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 Recidiviz, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import { Auth0Provider } from "@auth0/auth0-react";
import React from "react";
import isAuthEnabled from "../utils/isAuthEnabled";
import getAuthSettings from "./getAuthSettings";

/**
* If auth is enabled for the current environment, wraps its children
* in an Auth0Provider to enable the Auth0 React context.
* If auth is disabled, renders its children unwrapped.
*/
const AuthProvider: React.FC = ({ children }) => {
const authSettings = getAuthSettings();
if (isAuthEnabled() && authSettings) {
return (
<Auth0Provider
domain={authSettings.domain}
clientId={authSettings.clientId}
redirectUri={window.location.href}
>
{children}
</Auth0Provider>
);
}
return <>{children}</>;
};

export default AuthProvider;
38 changes: 38 additions & 0 deletions spotlight-client/src/AuthProvider/getAuthSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 Recidiviz, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

type Auth0Settings = {
domain: string;
clientId: string;
};

let AUTH_SETTINGS: Auth0Settings | undefined;

// NOTE: there is no production auth requirement!
if (process.env.REACT_APP_AUTH_ENV === "development") {
AUTH_SETTINGS = {
domain: "recidiviz-spotlight-staging.us.auth0.com",
clientId: "ID9plpd8j4vaUin9rPTGxWlJoknSkDX1",
};
}

/**
* Returns the auth settings configured for the current environment, if any.
*/
export default function getAuthSettings(): typeof AUTH_SETTINGS {
return AUTH_SETTINGS;
}
18 changes: 18 additions & 0 deletions spotlight-client/src/AuthProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 Recidiviz, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

export { default } from "./AuthProvider";
Loading