Skip to content

Commit

Permalink
CCM-5100: Amplify + Cognito Authentication PoC
Browse files Browse the repository at this point in the history
* Log in form
* Example federation with Auth0 OIDC IDP
* Terraform modules for deploying Cognito and Amplify
* Terraform module for deploying Amplify branch and associated Cognito client app config
* Server and client-side auth
* Redirect handling on successful auth
  • Loading branch information
m-houston committed Jul 1, 2024
1 parent 702a62a commit 790dd2d
Show file tree
Hide file tree
Showing 84 changed files with 22,224 additions and 1,107 deletions.
17 changes: 17 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# These are the settings for the dev user pool
USER_POOL_ID=eu-west-2_fhHtnXS3G
USER_POOL_CLIENT_ID=<client ID>
HOSTED_LOGIN_DOMAIN=nhsnotify-iam-dev-auth-userpool.auth.eu-west-2.amazoncognito.com

NOTIFY_STAGE=nonprod
NOTIFY_ENVIRONMENT=dev
AWS_APP=d1axbs26ewhyx4
AWS_BRANCH=main

# Customise the branch or repository to deploy from an alternative source
#TF_VAR_branch=miho6/CCM-5100-authn-poc
#TF_VAR_repository=https://github.com/m-houston/nhs-notify-iam

# Use a GitHub Personal Access Token to deploy a new Amplify integration (connect to a GitHub repo)
TF_VAR_github_pat=<github_pat_xxxx>

9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,12 @@
!project.code-workspace

# Please, add your custom content below!

# amplify
node_modules
.amplify
amplify_outputs*
amplifyconfiguration*

.next
.env
8 changes: 8 additions & 0 deletions .idea/.gitignore

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

4 changes: 4 additions & 0 deletions .idea/misc.xml

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

8 changes: 8 additions & 0 deletions .idea/modules.xml

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

12 changes: 12 additions & 0 deletions .idea/nhs-notify-iam.iml

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

6 changes: 6 additions & 0 deletions .idea/vcs.xml

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

3 changes: 2 additions & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# This file is for you! Please, updated to the versions agreed by your team.

terraform 1.7.0
terraform 1.8.5
pre-commit 3.6.0
nodejs 18.18.2

# ==============================================================================
# The section below is reserved for Docker image versions.
Expand Down
20 changes: 20 additions & 0 deletions amplify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: 1
backend:
phases:
build:
commands:
- npm ci --cache .npm --prefer-offline
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
phases:
build:
commands:
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- .next/cache/**/*
- .npm/**/*
52 changes: 52 additions & 0 deletions amplify/auth/resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { DeepPartialAmplifyGeneratedConfigs } from '@aws-amplify/plugin-types';
import { ClientConfig } from '@aws-amplify/client-config';

const userPoolId = process.env.USER_POOL_ID!;
const userPoolClientId = process.env.USER_POOL_CLIENT_ID!;
const hostedLoginDomain = process.env.HOSTED_LOGIN_DOMAIN!;

const appId = process.env.AWS_APP_ID!;
const stage = process.env.NOTIFY_STAGE!;
const subdomain = process.env.NOTIFY_SUBDOMAIN!;
const domainName = process.env.NOTIFY_DOMAIN_NAME!;

export const authConfig: DeepPartialAmplifyGeneratedConfigs<ClientConfig> = {
auth: {
aws_region: 'eu-west-2',
user_pool_id: userPoolId,
user_pool_client_id: userPoolClientId,
oauth: {
'identity_providers': [],
'domain': hostedLoginDomain,
'scopes': [
'openid',
'email',
'profile',
'phone',
'aws.cognito.signin.user.admin'
],
'redirect_sign_in_uri': [
`https://${subdomain}.${appId}.amplifyapp.com/auth/`,
`https://${subdomain}.${domainName}/auth/`,
...(stage === 'nonprod' ? ['http://localhost:3000/auth/']: [])
],
'redirect_sign_out_uri': [
`https://${subdomain}.${appId}.amplifyapp.com/`,
`https://${subdomain}.${domainName}/`,
...(stage === 'nonprod' ? ['http://localhost:3000/']: [])
],
'response_type': 'code'
},
username_attributes: ['email'],
standard_required_attributes: ['email'],
user_verification_types: ['email'],
unauthenticated_identities_enabled: false,
password_policy: {
min_length: 8,
require_lowercase: true,
require_uppercase: true,
require_numbers: true,
require_symbols: true,
}
}
};
5 changes: 5 additions & 0 deletions amplify/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineBackend } from '@aws-amplify/backend';
import { authConfig } from './auth/resource';

const backend = defineBackend({});
backend.addOutput(authConfig);
3 changes: 3 additions & 0 deletions amplify/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
17 changes: 17 additions & 0 deletions amplify/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"paths": {
"$amplify/*": [
"../.amplify/generated/*"
]
}
}
}
72 changes: 72 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';

import React from 'react';
import { Authenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import { Col, Container, Footer, Header, Row } from 'nhsuk-react-components';

import ConfigureAmplifyClientSide from '../components/ConfigureAmplify';
import LoginStatus from '../components/LoginStatus';

import 'nhsuk-frontend/dist/nhsuk.css';
import './styles.css';

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Authenticator.Provider>
<ConfigureAmplifyClientSide />
<Header serviceName="Notify">
<Header.Container>
<Header.Logo href="/" />
<Header.Content>
<LoginStatus />
</Header.Content>
</Header.Container>
<Header.Nav>
{/*<Header.NavItem href="/conditions">*/}
{/* Health A-Z*/}
{/*</Header.NavItem>*/}
<Header.NavItem home href="/">
Home
</Header.NavItem>
<Header.NavDropdownMenu />
</Header.Nav>
</Header>
<Container>
<main className="nhsuk-main-wrapper" id="maincontent" role="main">
<Row>
<Col width="full">
{children}
</Col>
</Row>
</main>
</Container>
<Footer>
<Footer.List>
<Footer.ListItem href="https://www.nhs.uk/nhs-sites/">
NHS sites
</Footer.ListItem>
<Footer.ListItem href="https://www.nhs.uk/about-us/">
About us
</Footer.ListItem>
<Footer.ListItem href="https://www.nhs.uk/contact-us/">
Contact us
</Footer.ListItem>
<Footer.ListItem href="https://www.nhs.uk/about-us/sitemap/">
Sitemap
</Footer.ListItem>
<Footer.ListItem href="https://www.nhs.uk/our-policies/">
Our policies
</Footer.ListItem>
</Footer.List>
<Footer.Copyright>
© Crown copyright
</Footer.Copyright>
</Footer>
</Authenticator.Provider>
</body>
</html>
)
}
36 changes: 36 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client';

import React, { useEffect, useState } from 'react';
import { Hub } from '@aws-amplify/core';
import Login from '../components/Login';

export default function Page({ searchParams }: {
searchParams: { [key: string]: string | string[] | undefined }
}) {

const [redirect, setRedirect] = useState<string | undefined>();

useEffect(() => {
return Hub.listen('auth', ({ payload }) => {
switch (payload.event) {
case 'customOAuthState':
try {
const { redirectPath } = JSON.parse(payload.data);
setRedirect(redirectPath);
} catch (err) {
console.error(err);
setRedirect('Invalid redirect path');
}
break;
}
});
}, []);

let redirectPath = redirect || [searchParams.redirect].flat().pop() || '/';
if (redirectPath === '/auth') redirectPath = '/';
if (redirectPath && !redirectPath.match(/^\/[a-z/]*$/)) {
return <h2>Invalid redirect path</h2>;
}

return <Login redirectPath={redirectPath} />
}
18 changes: 18 additions & 0 deletions app/signout/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { AuthGetCurrentUserServer } from '../../utils/amplify-utils';
import Logout from '../../components/Logout';
import { redirect } from 'next/navigation';

export default async function Page() {

const { currentUser, attributes } = await AuthGetCurrentUserServer() ?? {};
if (!currentUser) {
redirect('/');
}

return <>
<h1>Sign out</h1>
<p>You are currently logged in as <strong>{attributes?.email}</strong></p>
<Logout />
</>
}
20 changes: 20 additions & 0 deletions app/status/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import { AuthGetCurrentUserServer } from '../../utils/amplify-utils';

export default async function Page({ searchParams }: {
searchParams: { [key: string]: string | string[] | undefined }
}) {

const { currentUser, attributes } = await AuthGetCurrentUserServer() ?? {};
const redirectPath = [searchParams.redirect].flat().pop() || '/';

return <>
<h1>Hello {currentUser?.username}</h1>
<h2>Login details</h2>
<pre>{JSON.stringify(currentUser?.signInDetails, null, ' ')}</pre>
<h2>Attributes</h2>
<pre>{JSON.stringify(attributes, null, ' ')}</pre>
<h2>Redirect details</h2>
<pre>{JSON.stringify({ redirectPath })}</pre>
</>
}
13 changes: 13 additions & 0 deletions app/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.nhsuk-header__content {
flex-grow: 100;
text-align: right;
}

div.nhsuk-header__content > li.nhsuk-header__navigation-item {
list-style: none;
}

div.nhsuk-header__content > li.nhsuk-header__navigation-item > a {
padding: 0;
display: inline-block;
}
9 changes: 9 additions & 0 deletions components/ConfigureAmplify.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use client';

import { initAmplify } from '../utils/amplify-init';

initAmplify();

export default function ConfigureAmplifyClientSide() {
return null;
}
Loading

0 comments on commit 790dd2d

Please sign in to comment.