diff --git a/cloud/.env.example b/cloud/.env.example
index 68dfff3a6..b31adf032 100644
--- a/cloud/.env.example
+++ b/cloud/.env.example
@@ -1,2 +1,7 @@
DOMAIN_NAME=your.domain
HOSTED_ZONE_ID=YOUR_AWS_HOSTED_ZONE_ID
+
+# Enable Azure IdP
+#IDP_NAME=azure
+#AZURE_APPLICATION_ID=[your-azure-application-id]
+#AZURE_TENANT_ID=[your-azure-tenant-id]
diff --git a/cloud/bin/cloud.ts b/cloud/bin/cloud.ts
index b2c3ea3f0..56827702a 100644
--- a/cloud/bin/cloud.ts
+++ b/cloud/bin/cloud.ts
@@ -49,8 +49,6 @@ const authStack = new AuthStack(app, generateStackName('auth'), {
description: generateDescription('Auth stack'),
env,
tags,
- authDomainName: certificateStack.authDomainName,
- certificate: certificateStack.cloudFrontCert,
hostedZone: hostedZoneStack.hostedZone,
});
diff --git a/cloud/lib/auth-stack.ts b/cloud/lib/auth-stack.ts
index 963916986..8f4b27ec6 100644
--- a/cloud/lib/auth-stack.ts
+++ b/cloud/lib/auth-stack.ts
@@ -1,27 +1,21 @@
-import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
import {
- AllowedMethods,
- CachePolicy,
- Distribution,
- OriginRequestPolicy,
- PriceClass,
- ResponseHeadersPolicy,
- ViewerProtocolPolicy,
-} from 'aws-cdk-lib/aws-cloudfront';
-import { HttpOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
-import { Mfa, OAuthScope, UserPool } from 'aws-cdk-lib/aws-cognito';
-import { ARecord, IHostedZone, RecordTarget } from 'aws-cdk-lib/aws-route53';
-import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets';
+ Mfa,
+ OAuthScope,
+ ProviderAttribute,
+ UserPool,
+ UserPoolClientIdentityProvider,
+ UserPoolIdentityProviderSaml,
+ UserPoolIdentityProviderSamlMetadata,
+} from 'aws-cdk-lib/aws-cognito';
+import { IHostedZone } from 'aws-cdk-lib/aws-route53';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { CfnOutput, Duration, RemovalPolicy, Stack, StackProps, Tags } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
-import { resourceId, stageName } from './resourceNamingUtils';
+import { appName, resourceId, stageName } from './resourceNamingUtils';
type AuthStackProps = StackProps & {
- certificate: ICertificate;
hostedZone: IHostedZone;
- authDomainName: string;
};
export class AuthStack extends Stack {
@@ -36,12 +30,14 @@ export class AuthStack extends Stack {
const stage = stageName(scope);
const generateResourceId = resourceId(scope);
- const { certificate, env, hostedZone, authDomainName } = props;
+ const { env, hostedZone } = props;
if (!env?.region) {
throw new Error('Region not defined in stack env, cannot continue!');
}
- // Cognito UserPool
+ /*
+ User Pool - including attribute claims and password policy
+ */
const userPool = new UserPool(this, generateResourceId('userpool'), {
enableSmsRole: false,
mfa: Mfa.OFF,
@@ -69,12 +65,50 @@ export class AuthStack extends Stack {
Tags.of(userPool).add(key, value);
});
- new CfnOutput(this, 'UserPool.Id', {
- value: `urn:amazon:cognito:sp:${userPool.userPoolId}`,
+ /*
+ Identity Providers - currently only Cognito itself plus Azure
+ */
+ const supportedIdentityProviders = [UserPoolClientIdentityProvider.COGNITO];
+
+ if (process.env.IDP_NAME?.toUpperCase() === 'AZURE') {
+ const { AZURE_APPLICATION_ID, AZURE_TENANT_ID } = process.env;
+ if (!AZURE_APPLICATION_ID) throw new Error('Missing env var AZURE_APPLICATION_SECRET');
+ if (!AZURE_TENANT_ID) throw new Error('Missing env var AZURE_TENANT_ID');
+ const idp = new UserPoolIdentityProviderSaml(this, generateResourceId('azure-idp'), {
+ name: 'Azure',
+ userPool,
+ metadata: UserPoolIdentityProviderSamlMetadata.url(
+ `https://login.microsoftonline.com/${AZURE_TENANT_ID}/federationmetadata/2007-06/federationmetadata.xml?appid=${AZURE_APPLICATION_ID}`
+ ),
+ attributeMapping: {
+ email: ProviderAttribute.other(
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'
+ ),
+ familyName: ProviderAttribute.other(
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'
+ ),
+ givenName: ProviderAttribute.other(
+ 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'
+ ),
+ },
+ });
+ supportedIdentityProviders.push(UserPoolClientIdentityProvider.custom(idp.providerName));
+ }
+
+ /*
+ User Pool Domain - where authentication service is reachable
+ */
+ const userPoolDomain = userPool.addDomain(generateResourceId('userpool-domain'), {
+ cognitoDomain: {
+ domainPrefix: appName.toLowerCase(),
+ },
});
+ /*
+ User Pool Client - defines Auth flow and token validity
+ */
const logoutUrls = [`https://${hostedZone.zoneName}`];
- const callbackUrls = logoutUrls.concat(`https://api.${hostedZone.zoneName}/oauth2/idpresponse`);
+ const replyUrl = `${userPoolDomain.baseUrl()}/saml2/idpresponse`;
const userPoolClient = userPool.addClient(generateResourceId('userpool-client'), {
authFlows: {
userSrp: true,
@@ -84,9 +118,10 @@ export class AuthStack extends Stack {
authorizationCodeGrant: true,
},
scopes: [OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE],
- callbackUrls,
+ callbackUrls: logoutUrls.concat(replyUrl),
logoutUrls,
},
+ supportedIdentityProviders,
accessTokenValidity: Duration.minutes(60),
idTokenValidity: Duration.minutes(60),
refreshTokenValidity: Duration.days(14),
@@ -94,47 +129,16 @@ export class AuthStack extends Stack {
preventUserExistenceErrors: true,
});
- userPool.addDomain(generateResourceId('userpool-domain'), {
- cognitoDomain: {
- domainPrefix: generateResourceId('auth'),
- },
+ new CfnOutput(this, 'UserPool.Id', {
+ value: `urn:amazon:cognito:sp:${userPool.userPoolId}`,
+ });
+ new CfnOutput(this, 'UserPool.ReplyURL', {
+ value: replyUrl,
});
-
new CfnOutput(this, 'UserPoolClient.Id', {
value: userPoolClient.userPoolClientId,
});
- /*
- Use a CloudFront distribution to proxy Cognito.
-
- Why? Turns out a custom userpool domain won't work due to CORS failures :(
- Cognito does that by creating a CloudFront distro under the hood, so we're
- not adding any costs by configuring this manually.
- */
- const cognitoProxyDistribution = new Distribution(this, generateResourceId('cognito-proxy'), {
- certificate,
- domainNames: [authDomainName],
- enableLogging: true,
- defaultBehavior: {
- origin: new HttpOrigin(`cognito-idp.${env.region}.amazonaws.com`),
- allowedMethods: AllowedMethods.ALLOW_ALL,
- cachePolicy: CachePolicy.CACHING_DISABLED,
- viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
- originRequestPolicy: OriginRequestPolicy.ALL_VIEWER_AND_CLOUDFRONT_2022,
- responseHeadersPolicy:
- ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT_AND_SECURITY_HEADERS,
- },
- priceClass: PriceClass.PRICE_CLASS_100,
- });
-
- new ARecord(this, generateResourceId('arecord-auth-proxy'), {
- zone: hostedZone,
- recordName: authDomainName,
- target: RecordTarget.fromAlias(new CloudFrontTarget(cognitoProxyDistribution)),
- deleteExisting: true,
- comment: 'DNS A Record for Cognito auth proxy',
- });
-
// SSM Parameters accessed in auth edge lambda, as cannot use ENV vars.
this.parameterNameUserPoolId = `/${stage}/userpool-id`;
new StringParameter(this, generateResourceId('parameter-userpool-id'), {
diff --git a/cloud/lib/certificate-stack.ts b/cloud/lib/certificate-stack.ts
index 203faca55..7bbd1deae 100644
--- a/cloud/lib/certificate-stack.ts
+++ b/cloud/lib/certificate-stack.ts
@@ -16,7 +16,6 @@ type CertificateStackProps = StackProps & {
export class CertificateStack extends Stack {
public readonly apiDomainName: string;
- public readonly authDomainName: string;
public readonly cloudFrontCert: ICertificate;
public readonly loadBalancerCert: ICertificate;
@@ -27,13 +26,11 @@ export class CertificateStack extends Stack {
const generateResourceId = resourceId(scope);
const validation = CertificateValidation.fromDns(hostedZone);
this.apiDomainName = `api.${hostedZone.zoneName}`;
- this.authDomainName = `auth.${hostedZone.zoneName}`;
// Yes this is deprecated, but CDK currently gives us no way to use
// Permissions Boundaries with cross-region resources, so ...
this.cloudFrontCert = new DnsValidatedCertificate(this, generateResourceId('cert-ui'), {
domainName: hostedZone.zoneName,
- subjectAlternativeNames: [this.authDomainName],
hostedZone,
validation,
region: 'us-east-1',
diff --git a/frontend/.env.example b/frontend/.env.example
index b52fdf1d5..32cc1fe05 100644
--- a/frontend/.env.example
+++ b/frontend/.env.example
@@ -7,4 +7,3 @@ VITE_BACKEND_URL=http://localhost:3000/api
#VITE_COGNITO_USERPOOL_ID=YOUR_USERPOOL_ID
#VITE_COGNITO_USERPOOL_CLIENT=YOUR_CLIENT_ID
#VITE_COGNITO_USERPOOL_DOMAIN=YOUR_AUTH_DOMAIN
-#VITE_COGNITO_USERPOOL_ENDPOINT=https://${VITE_COGNITO_USERPOOL_DOMAIN}
diff --git a/frontend/src/assets/icons/Azure.svg b/frontend/src/assets/icons/Azure.svg
new file mode 100644
index 000000000..7151406b9
--- /dev/null
+++ b/frontend/src/assets/icons/Azure.svg
@@ -0,0 +1,23 @@
+
\ No newline at end of file
diff --git a/frontend/src/components/AuthProviders/CognitoAuthenticatedApp.tsx b/frontend/src/components/AuthProviders/CognitoAuthenticatedApp.tsx
index cd2f78026..e38f26c68 100644
--- a/frontend/src/components/AuthProviders/CognitoAuthenticatedApp.tsx
+++ b/frontend/src/components/AuthProviders/CognitoAuthenticatedApp.tsx
@@ -1,11 +1,20 @@
-import { fetchAuthSession } from '@aws-amplify/auth';
+import { fetchAuthSession, signInWithRedirect } from '@aws-amplify/auth';
import {
+ Alert,
+ Button,
+ Fieldset,
+ Flex,
+ PasswordField,
+ Tabs,
+ TextField,
+ useAuthenticator,
withAuthenticator,
WithAuthenticatorProps,
} from '@aws-amplify/ui-react';
import { Amplify } from 'aws-amplify';
-import { PropsWithChildren } from 'react';
+import { FormEvent, useEffect, useState } from 'react';
+import AzureLogo from '@src/assets/icons/Azure.svg';
import BotAvatar from '@src/assets/images/BotAvatarDefault.svg';
import App from '@src/components/App';
import { interceptRequest } from '@src/service/backendService';
@@ -28,10 +37,10 @@ Amplify.configure({
Cognito: {
userPoolId: import.meta.env.VITE_COGNITO_USERPOOL_ID,
userPoolClientId: import.meta.env.VITE_COGNITO_USERPOOL_CLIENT,
- userPoolEndpoint: import.meta.env.VITE_COGNITO_USERPOOL_ENDPOINT,
loginWith: {
oauth: {
domain: import.meta.env.VITE_COGNITO_USERPOOL_DOMAIN,
+ providers: [{ custom: 'Azure' }],
redirectSignIn: [import.meta.env.VITE_COGNITO_REDIRECT_URL],
redirectSignOut: [import.meta.env.VITE_COGNITO_REDIRECT_URL],
responseType: 'code',
@@ -70,6 +79,7 @@ const CognitoAuthenticatedApp = withAuthenticator(
components: {
SignIn: {
Header: WelcomeHeader,
+ Footer: () => null,
},
ForgotPassword: {
Header: ResetPasswordHeader,
@@ -94,28 +104,179 @@ const CognitoAuthenticatedApp = withAuthenticator(
function WelcomeHeader() {
return (
-
If you have a Scott Logic email address, sign in via SSO:
+ +