Skip to content

Commit

Permalink
Build out the web app (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
casewalker authored Oct 23, 2023
1 parent b7e7cfc commit 5661f65
Show file tree
Hide file tree
Showing 32 changed files with 6,275 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NEXT_PUBLIC_SAML_GENERATION_URL="https://example.com/generateSaml"
NEXT_PUBLIC_COGNITO_HOSTED_UI_URL="https://example.com"
NEXT_PUBLIC_COGNITO_CLIENT_ID="cl1en4_1D"
NEXT_PUBLIC_COGNITO_REDIRECT_URI="http://localhost:3000"
NEXT_PUBLIC_SSO_DEFAULT_DURATION=28800
NEXT_PUBLIC_AWS_SAML_ENDPOINT="https://example.com/saml"
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Automated testing

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20.x
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Run Cypress tests
uses: cypress-io/github-action@v6
with:
browser: chrome
start: npm run dev
wait-on: "http://localhost:3000"
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env.development.local
.env.production.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,53 @@
# aws-connect-sso-web-app
# AWS Connect SSO Web App

This project is intended to run from AWS Amplify, using Cognito as the log in
mechanism with users configured to have specialized groups representing their
Call Center permissions for various AWS Connect instances. This works in concert
with the Lambda defined for
[generating SAML Responses for Connect](https://github.com/newjersey/custom-aws-idp).
Then, once a user logs in to Cognito, this site should help them (within zero to
two clicks) get federated and logged in to their desired Connect instance.

## Configuration

This site relies on some environment variables:

- `NEXT_PUBLIC_SAML_GENERATION_URL`

The URL for the APIGateway connection to the SAML Response generating Lambda
mentioned above

- `NEXT_PUBLIC_COGNITO_HOSTED_UI_URL`

The base URL for the Hosted UI Cognito sign-in page associated with this
UserPool's application (the current working UserPool is "_Cognito for Connect
Call Centers_" (ID: _us-east-1_jKQHCtx7s_) and the "App integration" client is
"_ReactAppClient_")

- `NEXT_PUBLIC_COGNITO_CLIENT_ID`

The Client ID of the App Integration - App Client (from the "_ReactAppClient_"
client)

- `NEXT_PUBLIC_COGNITO_REDIRECT_URI`

The URL of this web app

**Note:** This must be configured here as well as inside Cognito, in the
UserPool, in the App client, under "_Hosted UI_", under "_Allowed callback
URLs_"

- `NEXT_PUBLIC_SSO_DEFAULT_DURATION`

The session duration to use for the SSO session requested by the generated
SAML (in seconds)

- `NEXT_PUBLIC_AWS_SAML_ENDPOINT`

The SAML endpoint for Connect SSO sessions (should be
"https://signin.aws.amazon.com/saml")

## Misc

This is a [Next.js](https://nextjs.org/) project bootstrapped with
[`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
75 changes: 75 additions & 0 deletions components/CallCenterPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useState } from "react";
import generateSaml from "../pages/api/generateSaml";
import { CallCenterSubmitState, SsoDetails } from "../pages/api/types";
import styles from "../styles/Home.module.css";

interface Props {
cognitoGroups: string[];
idToken: string;
submitState: CallCenterSubmitState;
setSubmitState: React.Dispatch<React.SetStateAction<CallCenterSubmitState>>;
setSsoDetails: React.Dispatch<React.SetStateAction<SsoDetails | undefined>>;
setError: React.Dispatch<React.SetStateAction<string | undefined>>;
}

/**
* Pick one call center with a radio button type of form when a user is
* configured with multiple call centers.
*
* @param cognitoGroups The groups (call centers) this user belongs to
* @param idToken User's Cognito token necessary for calling `generateSaml`
* @param submitState Enum state variable denoting the current submit-state for
* a call center choice
* @param setSubmitState Setter for the above state variable
* @param setSsoDetails Setter for the output of `generateSaml`
* @param setError Setter in case there are errors while calling `generateSaml`
*/
export default function CallCenterPicker({
cognitoGroups,
idToken,
submitState,
setSubmitState,
setSsoDetails,
setError,
}: Props) {
const [currentPick, setCurrentPick] = useState<string | undefined>(undefined);
const onRadioGroupSubmit = () => {
setSubmitState(CallCenterSubmitState.USER_SUBMITTED);
generateSaml(currentPick!, idToken)
.then((output) => setSsoDetails(output))
.catch((error) => setError(error.message));
};

return (
<>
<h2>Choose a Call Center to connect to:</h2>
<div>
{cognitoGroups.map((group: string) => (
<div key={group}>
<input
type="radio"
name="currentGroupChoice"
value={group}
id={group}
checked={currentPick === group}
onChange={(e) => setCurrentPick(e.target.value)}
/>
<label htmlFor={group}>{group}</label>
</div>
))}
</div>
<div>
<button
className={styles.button}
disabled={
currentPick == undefined ||
submitState === CallCenterSubmitState.USER_SUBMITTED
}
onClick={onRadioGroupSubmit}
>
Connect
</button>
</div>
</>
);
}
58 changes: 58 additions & 0 deletions components/SelfSubmittingSsoForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useEffect, useRef } from "react";
import { AWS_SAML_ENDPOINT } from "../pages/api/constants";
import { SsoDetails } from "../pages/api/types";

interface Props {
ssoDetails: SsoDetails | undefined;
}

/**
* A special form designed to POST the SAML SSO details to AWS. It is also
* designed to auto-submit the form when the `ssoDetails` become defined. SAML
* logins work best when the POST as well as subsequent interactions are managed
* by the browser, but a user with only one configured Call Center should not
* have to click anything to submit this form. Thus, the form is intentionally
* hidden and set to auto-submit utilizing an effect hook.
*
* @param ssoDetails State variable containing the details used by the SSO form
*/
export default function SelfSubmittingSsoForm({ ssoDetails }: Props) {
// Refs seem to need `null` instead of `undefined`
const formRef = useRef<HTMLFormElement | null>(null);
const samlResponseRef = useRef<HTMLInputElement | null>(null);
const relayStateRef = useRef<HTMLInputElement | null>(null);

// Auto-submit the SAML form once ssoDetails are present
useEffect(() => {
if (
samlResponseRef.current?.value != null &&
samlResponseRef.current.value.length > 0 &&
relayStateRef.current?.value != null &&
relayStateRef.current.value.length > 0
) {
formRef.current?.submit();
}
}, [samlResponseRef.current?.value, relayStateRef.current?.value]);

return (
<form
action={AWS_SAML_ENDPOINT}
encType="application/x-www-form-urlencoded"
method="POST"
ref={formRef}
>
<input
name="SAMLResponse"
value={ssoDetails?.SAMLResponse ?? ""}
type="hidden"
ref={samlResponseRef}
/>
<input
name="RelayState"
value={ssoDetails?.RelayState ?? ""}
type="hidden"
ref={relayStateRef}
/>
</form>
);
}
18 changes: 18 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig } from "cypress";

export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
},
component: {
devServer: {
framework: "next",
bundler: "webpack",
},
},
chromeWebSecurity: false,
retries: {
runMode: 2,
openMode: 0,
},
});
Loading

0 comments on commit 5661f65

Please sign in to comment.