Skip to content

Commit

Permalink
auth and cognito fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
revmischa committed Jul 2, 2024
1 parent f1e0b79 commit 8d272ca
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 69 deletions.
10 changes: 6 additions & 4 deletions .env
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
PRISMA_CONNECTION_LIMIT=15
CREATE_AURORA_DATABASE=true

# for local dev environments which use docker-compose, you can disable running migrations in AWS by setting this to false
# if you want to automatically run DB migrations on deployed environments
# (does not apply to local dev)
RUN_DB_MIGRATIONS=true

# set this for non-local-dev stages to your web URL
# WEB_DOMAIN=
# e.g. WEB_DOMAIN=d3s9sdc942lbai.cloudfront.net

# set this to enable a bastion EC2 host you can tunnel through to connect to the database
# create a keypair in your desired account and region and add the name here
# SSH_KEYPAIR_NAME=

# set this for non-local-dev stages to your web URL
# WEB_URL=
4 changes: 3 additions & 1 deletion .env.prod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
IS_PRODUCTION=true

# WEB_URL=https://mysite.com
# assumes you have a hosted zone set up in Route53
# HOSTED_ZONE=mysite.com
# WEB_DOMAIN=mysite.com
85 changes: 48 additions & 37 deletions stacks/auth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { Cognito, StackContext, use } from 'sst/constructs';
import { Duration } from 'aws-cdk-lib';
import { StringAttribute, UserPoolClientIdentityProvider } from 'aws-cdk-lib/aws-cognito';
import { AaaaRecord, ARecord, RecordTarget } from 'aws-cdk-lib/aws-route53';
import { UserPoolDomainTarget } from 'aws-cdk-lib/aws-route53-targets';
import { Dns } from './dns';
import { WEB_URL } from './config';
import { Cognito, StackContext, use } from 'sst/constructs'
import { Duration } from 'aws-cdk-lib'
import { StringAttribute, UserPoolClientIdentityProvider } from 'aws-cdk-lib/aws-cognito'
import { AaaaRecord, ARecord, RecordTarget } from 'aws-cdk-lib/aws-route53'
import { UserPoolDomainTarget } from 'aws-cdk-lib/aws-route53-targets'
import { Dns } from './dns'
import { WEB_DOMAIN } from './config'
import { HttpUserPoolAuthorizer } from 'aws-cdk-lib/aws-apigatewayv2-authorizers'

const ALLOWED_HOSTS = [
'http://localhost:6001',
...(WEB_DOMAIN ? [`https://${WEB_DOMAIN}`] : []),
/// ... add frontend hosts here
];
const ALLOWED_URLS = ['/login', '/api/auth/callback/cognito'];
]
const ALLOWED_URLS = ['/login', '/api/auth/callback/cognito']

export function Auth({ stack, app }: StackContext) {
const dns = use(Dns);
const { certificateGlobal, domainName, hostedZone } = use(Dns)

const callbackUrls = ALLOWED_HOSTS.flatMap((h) => ALLOWED_URLS.map((url) => h + url));
const callbackUrls = ALLOWED_HOSTS.flatMap((h) => ALLOWED_URLS.map((url) => h + url))

const auth = new Cognito(stack, 'Auth', {
triggers: {
Expand All @@ -42,39 +44,43 @@ export function Auth({ stack, app }: StackContext) {
},
},
},
});
})

const userPool = auth.cdk.userPool;
const userPool = auth.cdk.userPool

let cognitoDomainName
// custom domain
const domainName = dns.domainName;
if (dns.hostedZone && dns.certificateGlobal && domainName) {
const authDomain = 'auth.' + domainName;
const domain = userPool.addDomain('CustomDomain', {
if (hostedZone && certificateGlobal && domainName) {
cognitoDomainName = `${app.stage}-auth.${domainName}`
const customDomain = userPool.addDomain('CustomDomain', {
customDomain: {
domainName: authDomain,
certificate: dns.certificateGlobal,
domainName: cognitoDomainName,
certificate: certificateGlobal,
},
});
})
new ARecord(stack, 'Domain4', {
zone: dns.hostedZone,
target: RecordTarget.fromAlias(new UserPoolDomainTarget(domain)),
recordName: authDomain,
});
zone: hostedZone,
target: RecordTarget.fromAlias(new UserPoolDomainTarget(customDomain)),
recordName: cognitoDomainName,
})
new AaaaRecord(stack, 'Domain6', {
zone: dns.hostedZone,
target: RecordTarget.fromAlias(new UserPoolDomainTarget(domain)),
recordName: authDomain,
});
zone: hostedZone,
target: RecordTarget.fromAlias(new UserPoolDomainTarget(customDomain)),
recordName: cognitoDomainName,
})
} else {
// default auth domain
// must be set for cognito to work at all unless a custom domain is specified
// must be globally unique
// feel free to edit this
const resourcePrefix = `auth-${app.name}-${app.stage}`
const cognitoDomain = userPool.addDomain('CognitoDomain', {
cognitoDomain: { domainPrefix: resourcePrefix },
})
const cognitoBaseUrl = cognitoDomain.baseUrl().replace('https://', '')
cognitoDomainName = cognitoBaseUrl
}

// get cognito domain
const cognitoDomain = userPool.addDomain('CognitoDomain', {
cognitoDomain: { domainPrefix: `${app.name}-${app.stage}` },
});
const cognitoBaseUrl = cognitoDomain.baseUrl().replace('https://', '');
const cognitoDomainName = dns.hostedZone ? `${app.stage}-auth.${dns.hostedZone.zoneName}` : cognitoBaseUrl;

// create cognito client
const webClient = userPool.addClient('WebClient', {
supportedIdentityProviders: [UserPoolClientIdentityProvider.COGNITO],
Expand All @@ -83,12 +89,17 @@ export function Auth({ stack, app }: StackContext) {
callbackUrls: callbackUrls,
logoutUrls: callbackUrls,
},
});
})
const httpApiAuthorizer = new HttpUserPoolAuthorizer('HttpUserPoolAuthorizer', userPool, {
userPoolClients: [webClient],
})
stack.addOutputs({ UserPoolId: userPool.userPoolId, WebClientId: webClient.userPoolClientId })

return {
userPool,
domainName,
webClient,
cognitoDomainName,
};
httpApiAuthorizer,
}
}
7 changes: 4 additions & 3 deletions stacks/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const IS_PRODUCTION = process.env.IS_PRODUCTION === 'true';
export const RUN_DB_MIGRATIONS = process.env.RUN_DB_MIGRATIONS === 'true';
export const WEB_URL = process.env.WEB_URL;
export const IS_PRODUCTION = process.env.IS_PRODUCTION === 'true'
export const RUN_DB_MIGRATIONS = process.env.RUN_DB_MIGRATIONS === 'true'
export const WEB_DOMAIN = process.env.WEB_DOMAIN
export const HOSTED_ZONE_NAME = process.env.HOSTED_ZONE_NAME
21 changes: 12 additions & 9 deletions stacks/dns.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import { StackContext } from 'sst/constructs';
import { DnsValidatedCertificate, ICertificate } from 'aws-cdk-lib/aws-certificatemanager';
import { HostedZone } from 'aws-cdk-lib/aws-route53';
import { StackContext } from 'sst/constructs'
import { DnsValidatedCertificate, ICertificate } from 'aws-cdk-lib/aws-certificatemanager'
import { HostedZone } from 'aws-cdk-lib/aws-route53'
import { HOSTED_ZONE_NAME, WEB_DOMAIN } from './config'

export function Dns({ stack, app }: StackContext) {
// route53 zone
const hostedZoneName = process.env['HOSTED_ZONE_NAME'];
const hostedZoneName = HOSTED_ZONE_NAME

// assumes the hosted zone already exists in route53
const hostedZone = hostedZoneName
? HostedZone.fromLookup(stack, 'Zone', {
domainName: hostedZoneName,
})
: undefined;
: undefined

// certificate (in our region)
let certificateRegional: ICertificate | undefined, certificateGlobal: ICertificate | undefined;
let certificateRegional: ICertificate | undefined, certificateGlobal: ICertificate | undefined

if (hostedZoneName && hostedZone) {
certificateRegional = new DnsValidatedCertificate(stack, 'RegionalCertificate', {
domainName: hostedZoneName,
hostedZone,
subjectAlternativeNames: [`*.${hostedZoneName}`],
});
})
// cert in us-east-1, required for cloudfront, cognito
certificateGlobal =
app.region === 'us-east-1'
Expand All @@ -30,8 +33,8 @@ export function Dns({ stack, app }: StackContext) {
hostedZone,
subjectAlternativeNames: [`*.${hostedZoneName}`],
region: 'us-east-1',
});
})
}

return { certificateRegional, certificateGlobal, hostedZone, domainName: hostedZoneName };
return { certificateRegional, certificateGlobal, hostedZone, domainName: hostedZoneName }
}
10 changes: 5 additions & 5 deletions stacks/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NextjsSite, StackContext, use } from 'sst/constructs'
import { AppSyncApi } from './appSyncApi'
import { Auth } from './auth'
import { Dns } from './dns'
import { IS_PRODUCTION, WEB_URL } from './config'
import { IS_PRODUCTION, WEB_DOMAIN } from './config'
import { Secrets } from './secrets'

export function Web({ stack, app }: StackContext) {
Expand All @@ -12,8 +12,8 @@ export function Web({ stack, app }: StackContext) {
const dns = use(Dns)
const isLocal = app.local

if (!isLocal && !process.env.SST_STAGE && !WEB_URL) {
console.warn(`Please set WEB_URL in .env.${app.stage} to the URL of your frontend site.`)
if (!isLocal && !process.env.SST_STAGE && !WEB_DOMAIN) {
console.warn(`Please set WEB_DOMAIN in .env.${app.stage} to the hostname of your frontend site.`)
}

const allSecrets = Object.values(configSecrets)
Expand All @@ -39,13 +39,13 @@ export function Web({ stack, app }: StackContext) {
memorySize: 1536,
environment: {
NEXTAUTH_SECRET: secrets.secretValueFromJson('AUTH_SECRET').toString(),
NEXTAUTH_URL: isLocal ? 'http://localhost:6001' : WEB_URL ?? 'https://set-me-in-.env',
NEXTAUTH_URL: isLocal ? 'http://localhost:6001' : WEB_DOMAIN ? `https://${WEB_DOMAIN}` : 'https://set-me-in-.env',

NEXT_PUBLIC_REGION: stack.region,
NEXT_PUBLIC_APPSYNC_ENDPOINT: appSyncApi.api.url,
NEXT_PUBLIC_COGNITO_CLIENT_ID: webClient.userPoolClientId,
NEXT_PUBLIC_COGNITO_USER_POOL_ID: userPool.userPoolId,
NEXT_PUBLIC_COGNITO_DOMAIN_NAME: cognitoDomainName,
NEXT_PUBLIC_COGNITO_DOMAIN_NAME: cognitoDomainName || '',
},
})

Expand Down
4 changes: 3 additions & 1 deletion web/app/lib/ui/Authentication/Authentication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ interface Props {
export const Authentication = async ({ className }: Props) => {
const session = await auth()

return <div className={className}>{session ? <LogoutButton userEmail={session.user.email} /> : <LoginButton />}</div>
return (
<div className={className}>{session ? <LogoutButton userEmail={session?.user?.email} /> : <LoginButton />}</div>
)
}
8 changes: 6 additions & 2 deletions web/auth.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@ import type { NextAuthConfig, Session } from 'next-auth'
import CognitoProvider from 'next-auth/providers/cognito'
import { COGNITO_CLIENT_ID, COGNITO_USER_POOL_ID, REGION } from './config'

if (process.env.NEXTAUTH_URL && process.env.NEXTAUTH_URL.includes('set-me-in-.env')) {
console.error(`Please set WEB_DOMAIN in .env.${process.env.SST_STAGE} to the URL of your frontend site.`)
}

export const authConfig = {
trustHost: true, // trust X-Forwarded-Host from CloudFront
providers: [
CognitoProvider({
clientId: COGNITO_CLIENT_ID,
issuer: `https://cognito-idp.${REGION}.amazonaws.com/${COGNITO_USER_POOL_ID}`,
token: true,

client: {
token_endpoint_auth_method: 'none',
},
checks: ['pkce', 'state', 'nonce'], // https://github.com/nextauthjs/next-auth/discussions/3551
checks: ['nonce', 'pkce', 'state'], // https://github.com/nextauthjs/next-auth/discussions/3551
}),
],
callbacks: {
Expand Down
11 changes: 5 additions & 6 deletions web/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// public - available for frontend
export const REGION = process.env['NEXT_PUBLIC_REGION'];
export const APPSYNC_ENDPOINT = process.env['NEXT_PUBLIC_APPSYNC_ENDPOINT'];
export const COGNITO_CLIENT_ID = process.env['NEXT_PUBLIC_COGNITO_CLIENT_ID'];
export const COGNITO_USER_POOL_ID = process.env['NEXT_PUBLIC_COGNITO_USER_POOL_ID'];
export const COGNITO_DOMAIN_NAME = process.env['NEXT_PUBLIC_COGNITO_DOMAIN_NAME'];
export const COGNITO_CLIENT_SECRET = process.env['NEXT_PUBLIC_COGNITO_CLIENT_SECRET'];
export const REGION = process.env['NEXT_PUBLIC_REGION']
export const APPSYNC_ENDPOINT = process.env['NEXT_PUBLIC_APPSYNC_ENDPOINT']
export const COGNITO_CLIENT_ID = process.env['NEXT_PUBLIC_COGNITO_CLIENT_ID']
export const COGNITO_USER_POOL_ID = process.env['NEXT_PUBLIC_COGNITO_USER_POOL_ID']
export const COGNITO_DOMAIN_NAME = process.env['NEXT_PUBLIC_COGNITO_DOMAIN_NAME']
1 change: 0 additions & 1 deletion web/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const nextConfig = {
'./**/@swc/core-linux-x64-gnu*',
'./**/@swc/core-linux-x64-musl*',
'./**/@esbuild*',
'./**/webpack*',
'./**/rollup*',
'./**/terser*',
'./**/sharp*',
Expand Down

0 comments on commit 8d272ca

Please sign in to comment.