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

[Issue #2643] Initial auth #3492

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
24 changes: 14 additions & 10 deletions documentation/infra/environment-variables-and-secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ Applications may need application specific configuration as environment variable

> ⚠️ Note: Do not put sensitive information such as credentials as regular environment variables. The method described in this section will embed the environment variables and their values in the ECS task definition's container definitions, so anyone with access to view the task definition will be able to see the values of the environment variables. For configuring secrets, see the section below on [Secrets](#secrets)

Environment variables are defined in the `app-config` module in the [environment-variables.tf file](/infra/app/app-config/env-config/environment-variables.tf). Modify the `default_extra_environment_variables` map to define extra environment variables specific to the application. Map keys define the environment variable name, and values define the default value for the variable across application environments. For example:
Environment variables are defined in the `app-config` module in the [environment_variables.tf file](/infra/app/app-config/env-config/environment_variables.tf). Modify the `default_extra_environment_variables` map to define extra environment variables specific to the application. Map keys define the environment variable name, and values define the default value for the variable across application environments. For example:

```terraform
# environment-variables.tf
# environment_variables.tf

locals {
default_extra_environment_variables = {
Expand Down Expand Up @@ -40,19 +40,23 @@ module "dev_config" {

Secrets are a specific category of environment variables that need to be handled sensitively. Examples of secrets are authentication credentials such as API keys for external services. Secrets first need to be stored in AWS SSM Parameter Store as a `SecureString`. This section then describes how to make those secrets accessible to the ECS task as environment variables through the `secrets` configuration in the container definition (see AWS documentation on [retrieving Secrets Manager secrets through environment variables](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/secrets-envvar-secrets-manager.html)).

Secrets are defined in the same file that non-sensitive environment variables are defined, in the `app-config` module in the [environment-variables.tf file](/infra/app/app-config/env-config/environment-variables.tf). Modify the `secrets` list to define the secrets that the application will have access to. For each secret, `name` defines the environment variable name, and `ssm_param_name` defines the SSM parameter name that stores the secret value. For example:
Secrets are defined in the same file that non-sensitive environment variables are defined, in the `app-config` module in the [environment_variables.tf file](/infra/app/app-config/env-config/environment_variables.tf). Modify the `secrets` map to define the secrets that the application will have access to. For each secret, the map key defines the environment variable name. The `manage_method` property, which can be set to `"generated"` or `"manual"`, defines whether or not to generate a random secret or to reference an existing secret that was manually created and stored into AWS SSM. The `secret_store_name` property defines the SSM parameter name that stores the secret value. If `manage_method = "generated"`, then `secret_store_name` is where terraform will store the secret. If `manage_method = "manual"`, then `secret_store_name` is where terraform will look for the existing secret. For example:

```terraform
# environment-variables.tf
# environment_variables.tf

locals {
secrets = [
{
name = "SOME_API_KEY"
ssm_param_name = "/${var.app_name}-${var.environment}/secret-sauce"
secrets = {
GENERATED_SECRET = {
manage_method = "generated"
secret_store_name = "/${var.app_name}-${var.environment}/generated-secret"
}
]
MANUALLY_CREATED_SECRET = {
manage_method = "manual"
secret_store_name = "/${var.app_name}-${var.environment}/manually-created-secret"
}
}
}
```

> ⚠️ Make sure you store the secret in SSM Parameter Store before you try to add secrets to your application service, or else the service won't be able to start since the ECS Task Executor won't be able to fetch the configured secret.
> ⚠️ For secrets with `manage_method = "manual"`, make sure you store the secret in SSM Parameter Store _before_ you try to add configure your application service with the secrets, or else the service won't be able to start since the ECS Task Executor won't be able to fetch the configured secret.
4 changes: 4 additions & 0 deletions frontend/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ API_URL=http://localhost:8080
# This is also hardcoded and checked-in on the API
API_AUTH_TOKEN=LOCAL_AUTH_12345678

AUTH_LOGIN_URL=http://localhost:8080/v1/users/login

# Boolean to switch between mock data and live API call data
# Default is a live API call
USE_SEARCH_MOCK_DATA=false
Expand All @@ -29,3 +31,5 @@ USE_SEARCH_MOCK_DATA=false
# DO NOT COMMIT THESE VALUES TO GITHUB
NEW_RELIC_APP_NAME=
NEW_RELIC_LICENSE_KEY=

SESSION_SECRET=extraSecretSessionSecretValueSssh
10 changes: 0 additions & 10 deletions frontend/.env.production
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,3 @@
# This file is checked into your git repo and provides defaults for non-sensitive
# env vars when running `next dev` only.
# Learn more: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables

# If you deploy to a subpath, change this to the subpath so relative paths work correctly.
NEXT_PUBLIC_BASE_PATH=

# Put the following secrets into a .env.local file for local development
SENDY_API_KEY=
SENDY_API_URL=
SENDY_LIST_ID=

API_URL=http://api.simpler.grants.gov
10 changes: 10 additions & 0 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ const nextConfig = {
},
],
},
// don't cache the api
{
source: "/api/:path*",
headers: [
{
key: "Cache-Control",
value: "no-store, must-revalidate",
},
],
},
];
},
basePath,
Expand Down
9 changes: 9 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dayjs": "^1.11.13",
"focus-trap-react": "^10.2.3",
"isomorphic-dompurify": "^2.15.0",
"jose": "^5.9.6",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"newrelic": "^12.7.0",
Expand Down
38 changes: 17 additions & 21 deletions frontend/src/app/[locale]/dev/feature-flags/FeatureFlagsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,20 @@ import { Button, Table } from "@trussworks/react-uswds";
* View for managing feature flags
*/
export default function FeatureFlagsTable() {
const { featureFlagsManager, mounted, setFeatureFlag } = useFeatureFlags();

if (!mounted) {
return null;
}
const { setFeatureFlag, featureFlags } = useFeatureFlags();

return (
<Table>
<thead>
<tr>
<th scope="col">Status</th>
<th scope="col">Feature Flag</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{Object.entries(featureFlagsManager.featureFlags).map(
([featureName, enabled]) => (
<>
<Table>
<thead>
<tr>
<th scope="col">Status</th>
<th scope="col">Feature Flag</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{Object.entries(featureFlags).map(([featureName, enabled]) => (
<tr key={featureName}>
<td
data-testid={`${featureName}-status`}
Expand All @@ -38,7 +34,7 @@ export default function FeatureFlagsTable() {
<td>
<Button
data-testid={`enable-${featureName}`}
disabled={enabled}
disabled={!!enabled}
onClick={() => setFeatureFlag(featureName, true)}
type="button"
>
Expand All @@ -54,9 +50,9 @@ export default function FeatureFlagsTable() {
</Button>
</td>
</tr>
),
)}
</tbody>
</Table>
))}
</tbody>
</Table>
</>
);
}
3 changes: 1 addition & 2 deletions frontend/src/app/[locale]/dev/feature-flags/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Metadata } from "next";
import FeatureFlagsTable from "src/app/[locale]/dev/feature-flags/FeatureFlagsTable";

import Head from "next/head";
import React from "react";

import FeatureFlagsTable from "./FeatureFlagsTable";

export function generateMetadata() {
const meta: Metadata = {
title: "Feature flag manager",
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/app/[locale]/error/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Metadata } from "next";

import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { GridContainer } from "@trussworks/react-uswds";

import ServerErrorAlert from "src/components/ServerErrorAlert";

export async function generateMetadata() {
const t = await getTranslations();
const meta: Metadata = {
title: t("ErrorPages.generic_error.page_title"),
description: t("Index.meta_description"),
};
return meta;
}

// not a NextJS error page - this is here to be redirected to manually in cases
// where Next's error handling situation doesn't quite do what we need.
const TopLevelError = () => {
const t = useTranslations("Errors");
return (
<GridContainer>
<ServerErrorAlert callToAction={t("try_again")} />
</GridContainer>
);
};

export default TopLevelError;
4 changes: 2 additions & 2 deletions frontend/src/app/[locale]/not-found.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import BetaAlert from "src/components/BetaAlert";
export async function generateMetadata() {
const t = await getTranslations();
const meta: Metadata = {
title: t("ErrorPages.page_not_found.title"),
title: t("ErrorPages.page_not_found.page_title"),
description: t("Index.meta_description"),
};
return meta;
Expand All @@ -24,7 +24,7 @@ export default function NotFound() {
<>
<BetaAlert />
<GridContainer className="padding-y-1 tablet:padding-y-3 desktop-lg:padding-y-15 measure-2">
<h1 className="nj-h1">{t("title")}</h1>
<h1>{t("title")}</h1>
<p className="margin-bottom-2">{t("message_content_1")}</p>
<Link className="usa-button" href="/" key="returnToHome">
{t("visit_homepage_button")}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/[locale]/opportunity/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Metadata } from "next";
import NotFound from "src/app/[locale]/not-found";
import { fetchOpportunity } from "src/app/api/fetchers";
import { OPPORTUNITY_CRUMBS } from "src/constants/breadcrumbs";
import { ApiRequestError, parseErrorStatus } from "src/errors";
import withFeatureFlag from "src/hoc/search/withFeatureFlag";
import withFeatureFlag from "src/hoc/withFeatureFlag";
import { fetchOpportunity } from "src/services/fetch/fetchers/fetchers";
import { Opportunity } from "src/types/opportunity/opportunityResponseTypes";
import { WithFeatureFlagProps } from "src/types/uiTypes";

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/[locale]/process/ProcessNext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const ProcessNext = () => {
>
<IconListIcon>
<USWDSIcon
className="usa-icon text-base"
className="text-base"
name="check_circle_outline"
/>
</IconListIcon>
Expand Down
10 changes: 2 additions & 8 deletions frontend/src/app/[locale]/search/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import QueryProvider from "src/app/[locale]/search/QueryProvider";
import { ServerSideSearchParams } from "src/types/searchRequestURLTypes";
import { Breakpoints } from "src/types/uiTypes";
import { Breakpoints, ErrorProps } from "src/types/uiTypes";
import { convertSearchParamsToProperTypes } from "src/utils/search/convertSearchParamsToProperTypes";

import { useTranslations } from "next-intl";
Expand All @@ -13,12 +13,6 @@ import SearchBar from "src/components/search/SearchBar";
import SearchFilters from "src/components/search/SearchFilters";
import ServerErrorAlert from "src/components/ServerErrorAlert";

interface ErrorProps {
// Next's error boundary also includes a reset function as a prop for retries,
// but it was not needed as users can retry with new inputs in the normal page flow.
error: Error & { digest?: string };
}

export interface ParsedError {
message: string;
searchInputs: ServerSideSearchParams;
Expand Down Expand Up @@ -54,7 +48,7 @@ function createBlankParsedError(): ParsedError {
};
}

export default function Error({ error }: ErrorProps) {
export default function SearchError({ error }: ErrorProps) {
const t = useTranslations("Search");

// The error message is passed as an object that's been stringified.
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/[locale]/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Metadata } from "next";
import QueryProvider from "src/app/[locale]/search/QueryProvider";
import withFeatureFlag from "src/hoc/search/withFeatureFlag";
import withFeatureFlag from "src/hoc/withFeatureFlag";
import { LocalizedPageProps } from "src/types/intl";
import { SearchParamsTypes } from "src/types/search/searchRequestTypes";
import { Breakpoints } from "src/types/uiTypes";
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/app/[locale]/unauthorized/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Metadata } from "next";

import { useTranslations } from "next-intl";
import { getTranslations } from "next-intl/server";
import { Alert, GridContainer } from "@trussworks/react-uswds";

export async function generateMetadata() {
const t = await getTranslations();
const meta: Metadata = {
title: t("ErrorPages.unauthorized.page_title"),
description: t("Index.meta_description"),
};
return meta;
}

const Unauthorized = () => {
const t = useTranslations("Errors");
return (
<GridContainer>
<Alert type="error" heading={t("unauthorized")} headingLevel="h4">
{t("authorization_fail")}
</Alert>
</GridContainer>
);
};

export default Unauthorized;
28 changes: 28 additions & 0 deletions frontend/src/app/[locale]/user/LogoutButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

/*

Delete this file when we build an actual Users page

*/
import { useRouter } from "next/navigation";
import { Button } from "@trussworks/react-uswds";

const makeLogoutRequest = async (push: (location: string) => void) => {
const response = await fetch("/api/auth/logout", { method: "POST" });
if (response.status === 200) {
push("/user?message=logged out");
return;
}
push("/user?message=log out error");
};

export const LogoutButton = () => {
const router = useRouter();
return (
// eslint-disable-next-line
<Button type="button" onClick={() => makeLogoutRequest(router.push)}>
Logout
</Button>
);
};
40 changes: 40 additions & 0 deletions frontend/src/app/[locale]/user/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Metadata } from "next";
import { LocalizedPageProps } from "src/types/intl";

import { getTranslations } from "next-intl/server";
import { GridContainer } from "@trussworks/react-uswds";

import { LogoutButton } from "./LogoutButton";

export async function generateMetadata({
params: { locale },
}: LocalizedPageProps) {
const t = await getTranslations({ locale });
const meta: Metadata = {
title: t("User.pageTitle"),
description: t("Index.meta_description"),
};
return meta;
}

// this is a placeholder page used as temporary landing page for login redirects.
// Note that this page only functions to display the message passed down in query params from
// the /api/auth/callback route, and it does not handle errors.
// How to handle errors or failures from the callback route in the UI will need to be revisited
// later on, but note that throwing to an error page won't be an option, as that produces a 500
// response in the client.
export default async function UserDisplay({
searchParams,
params: { locale },
}: LocalizedPageProps & { searchParams: { message?: string } }) {
const { message } = searchParams;

const t = await getTranslations({ locale, namespace: "User" });
return (
<GridContainer>
<h1>{t("heading")}</h1>
{message && <div>{message}</div>}
<LogoutButton />
</GridContainer>
);
}
Loading