Skip to content

Commit

Permalink
feat (cdk): wrote infrastructure for provisioning auth and identity p…
Browse files Browse the repository at this point in the history
…roviders (#32)

* feat(webapp): updated UI icons for OAuth sign in buttons.
* fix(webapp): fixed pyodide type errors related to globalThis..
* feat(webapp): added a loading spinner for OAuth sign in buttons.
* refactor(webapp): improved descriptiveness of the auth stack while initializing.
* feat(cdk): made sure environment variables are formatted correctly.
* feat(webapp): improved the UI design for the login screen.
* feat(cdk): added local-env-info.json.
* feat: temporary storage.
* feat(webapp): added google federated login.
* feat(webapp): exported aws cdk values to json.
* feat(cdk): added facebook federated login.
* feat(cdk): added apple federated login.
* docs: added instructions for setup.
* Added instructions for setup like setting environment variables.
  • Loading branch information
hwelsters authored Dec 20, 2023
1 parent 9a02ea8 commit da57a73
Show file tree
Hide file tree
Showing 26 changed files with 2,471 additions and 86 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Documentation
## Setting up
### CDK
A file `apps/snakecode-cdk/.env` should be created with the following content.

```bash
GOOGLE_CLIENT_ID=[YOUR GOOGLE CLIENT ID]
GOOGLE_CLIENT_SECRET=[YOUR GOOGLE CLIENT SECRET]

FACEBOOK_CLIENT_ID=[YOUR FACEBOOK CLIENT ID]
FACEBOOK_CLIENT_SECRET=[YOUR FACEBOOK CLIENT SECRET]

APPLE_CLIENT_ID=[YOUR APPLE CLIENT ID]
APPLE_CLIENT_SECRET=[YOUR APPLE CLIENT SECRET]
APPLE_KEY_ID=[YOUR APPLE KEY ID]
APPLE_PRIVATE_KEY=[YOUR APPLE PRIVATE KEY]
APPLE_TEAM_ID=[YOUR APPLE TEAM ID]
```

To get these values, create an OAuth 2.0 Client ID in your Google Developer Console.
4 changes: 3 additions & 1 deletion apps/snakecode-cdk/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ node_modules
cdk.out

!jest.config.js
!.eslintrc.js
!.eslintrc.js

.env
21 changes: 21 additions & 0 deletions apps/snakecode-cdk/lib/constants/Env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import dotenv from 'dotenv'
import { cleanEnv, str } from 'envalid'

dotenv.config()

// Log an error message and exit (in Node) if any required env variables are missing
const Env = cleanEnv(process.env, {
GOOGLE_CLIENT_ID: str(),
GOOGLE_CLIENT_SECRET: str(),

FACEBOOK_CLIENT_ID: str(),
FACEBOOK_CLIENT_SECRET: str(),

APPLE_CLIENT_ID: str(),
APPLE_CLIENT_SECRET: str(),
APPLE_KEY_ID: str(),
APPLE_PRIVATE_KEY: str(),
APPLE_TEAM_ID: str(),
})

export default Env
93 changes: 74 additions & 19 deletions apps/snakecode-cdk/lib/stacks/AmplifyAuthStack.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import type { StackProps } from 'aws-cdk-lib'
import { Duration, NestedStack } from 'aws-cdk-lib'
import { AccountRecovery, CfnIdentityPool, CfnIdentityPoolRoleAttachment, UserPool, UserPoolClient, VerificationEmailStyle } from 'aws-cdk-lib/aws-cognito'
import { AccountRecovery, CfnIdentityPool, CfnIdentityPoolRoleAttachment, ProviderAttribute, UserPool, UserPoolClient, UserPoolClientIdentityProvider, UserPoolIdentityProviderApple, UserPoolIdentityProviderFacebook, UserPoolIdentityProviderGoogle, VerificationEmailStyle } from 'aws-cdk-lib/aws-cognito'
import { FederatedPrincipal, Role } from 'aws-cdk-lib/aws-iam'
import type { Construct } from 'constructs'

import type { AmplifyAuthConfiguration } from '@snakecode/models'
import { APP_NAME } from '@snakecode/models'
import { APP_NAME, BASE_URL } from '@snakecode/models'
import { ENVIRONMENT_NAME } from '@snakecode/models'

import Env from '../constants/Env'

export class AmplifyAuthStack extends NestedStack {
readonly authenticatedRole: Role
readonly unauthenticatedRole: Role

// Outputs that will be used by other stacks
readonly region: string
readonly cognitoIdentityPoolId: string
readonly cognitoUserPoolId: string
readonly cognitoUserPoolClientId: string
readonly identityPoolId: string
readonly userPoolId: string
readonly userPoolClientId: string
readonly userPoolDomainUrl: string

constructor(scope: Construct, id: string, props: StackProps & { amplifyAuthConfiguration: AmplifyAuthConfiguration }) {
constructor(scope: Construct, id: string, props: StackProps & { amplifyAuthConfiguration: AmplifyAuthConfiguration; stage: string }) {
super(scope, id, props)

const cognitoUserPool = new UserPool(this, `${props.amplifyAuthConfiguration.userPoolName}`, {
userPoolName: `${props.amplifyAuthConfiguration.userPoolName}`,
// Create a User Pool with email and password login
const userPool = new UserPool(this, `${props.amplifyAuthConfiguration.userPoolName}-${props.stage}-${props.env!.region}`, {
userPoolName: `${props.amplifyAuthConfiguration.userPoolName}-${props.stage}-${props.env!.region}`,
selfSignUpEnabled: true,
userVerification: {
emailSubject: `Verify your email for ${APP_NAME}`,
Expand Down Expand Up @@ -57,17 +63,65 @@ export class AmplifyAuthStack extends NestedStack {
accountRecovery: AccountRecovery.EMAIL_ONLY
})

// This user pool client will
const cognitoUserPoolClient = new UserPoolClient(this, `${props.amplifyAuthConfiguration.userPoolClientName}`, {
userPool: cognitoUserPool
const uniquePrefix = `${ENVIRONMENT_NAME}-${props.stage}`.toLowerCase()
userPool.addDomain(`${props.amplifyAuthConfiguration.userPoolDomainName}-${props.stage}-${props.env!.region}`, {
cognitoDomain: {
domainPrefix: uniquePrefix
}
})

/* ======================================
* Federated Logins
====================================== */
new UserPoolIdentityProviderGoogle(this, `${props.amplifyAuthConfiguration.userPoolIdentityProviderGoogleName}-${props.stage}-${props.env!.region}`, {
userPool: userPool,
clientId: Env.GOOGLE_CLIENT_ID,
clientSecret: Env.GOOGLE_CLIENT_SECRET,
scopes: ['email'],
attributeMapping: {
email: ProviderAttribute.GOOGLE_EMAIL
}
})

new UserPoolIdentityProviderFacebook(this, `${props.amplifyAuthConfiguration.userPoolIdentityProviderFacebookName}-${props.stage}-${props.env!.region}`, {
userPool: userPool,
clientId: Env.FACEBOOK_CLIENT_ID,
clientSecret: Env.FACEBOOK_CLIENT_SECRET,
scopes: ['email'],
attributeMapping: {
email: ProviderAttribute.FACEBOOK_EMAIL
}
})

new UserPoolIdentityProviderApple(this, `${props.amplifyAuthConfiguration.userPoolIdentityProviderAppleName}-${props.stage}-${props.env!.region}`, {
userPool: userPool,
clientId: Env.APPLE_CLIENT_ID,
keyId: Env.APPLE_KEY_ID,
privateKey: Env.APPLE_PRIVATE_KEY,
teamId: Env.APPLE_TEAM_ID,
scopes: ['email'],
attributeMapping: {
email: ProviderAttribute.APPLE_EMAIL
}
})

// This user pool client will be used by the Amplify frontend
const cognitoUserPoolClient = new UserPoolClient(this, `${props.amplifyAuthConfiguration.userPoolClientName}-${props.stage}-${props.env!.region}`, {
userPool: userPool,
generateSecret: true,
supportedIdentityProviders: [UserPoolClientIdentityProvider.GOOGLE, UserPoolClientIdentityProvider.COGNITO],
oAuth: {
callbackUrls: [`${BASE_URL}`]
}
})

const cognitoIdentityPool = new CfnIdentityPool(this, `${props.amplifyAuthConfiguration.userPoolIdentityName}`, {
const cognitoIdentityPool = new CfnIdentityPool(this, `${props.amplifyAuthConfiguration.userPoolIdentityName}-${props.stage}-${props.env!.region}`, {
identityPoolName: `${props.amplifyAuthConfiguration.userPoolIdentityName}`,
allowUnauthenticatedIdentities: false
})

this.authenticatedRole = new Role(this, `${props.amplifyAuthConfiguration.authenticatedRoleName}`, {
// Creates the authenticated role which will be used with user pool identities
this.authenticatedRole = new Role(this, `${props.amplifyAuthConfiguration.authenticatedRoleName}-${props.stage}-${props.env!.region}`, {
roleName: `${props.amplifyAuthConfiguration.authenticatedRoleName}`,
description: 'IAM Role to be used as an Unauthenticated role for the Cognito user pool identities, used by Amplify',
assumedBy: new FederatedPrincipal(
Expand All @@ -85,7 +139,8 @@ export class AmplifyAuthStack extends NestedStack {
maxSessionDuration: Duration.hours(1)
})

this.unauthenticatedRole = new Role(this, `${props.amplifyAuthConfiguration.unauthenticatedRoleName}`, {
// Creates the unauthenticated role which will be used with user pool identities
this.unauthenticatedRole = new Role(this, `${props.amplifyAuthConfiguration.unauthenticatedRoleName}-${props.stage}-${props.env!.region}`, {
roleName: `${props.amplifyAuthConfiguration.unauthenticatedRoleName}`,
description: 'IAM Role to be used as an Authenticated role for the Cognito user pool identities, used by Amplify',
assumedBy: new FederatedPrincipal(
Expand All @@ -103,18 +158,18 @@ export class AmplifyAuthStack extends NestedStack {
maxSessionDuration: Duration.hours(1)
})

new CfnIdentityPoolRoleAttachment(this, `${props.amplifyAuthConfiguration.authenticatedRoleName}-attachment`, {
new CfnIdentityPoolRoleAttachment(this, `${props.amplifyAuthConfiguration.authenticatedRoleName}-attachment-${props.stage}-${props.env!.region}`, {
identityPoolId: cognitoIdentityPool.ref,
roles: {
unauthenticated: this.unauthenticatedRole.roleArn,
authenticated: this.authenticatedRole.roleArn
}
})

// Outputs that will be used by other stacks
this.region = props.env!.region!
this.cognitoIdentityPoolId = cognitoIdentityPool.ref
this.cognitoUserPoolId = cognitoUserPool.userPoolId
this.cognitoUserPoolClientId = cognitoUserPoolClient.userPoolClientId
this.identityPoolId = cognitoIdentityPool.ref
this.userPoolId = userPool.userPoolId
this.userPoolClientId = cognitoUserPoolClient.userPoolClientId
this.userPoolDomainUrl = `${uniquePrefix}.auth.${props.env!.region!}.amazoncognito.com`
}
}
23 changes: 16 additions & 7 deletions apps/snakecode-cdk/lib/stacks/AmplifyStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ export class AmplifyStack extends Stack {
super(scope, id, props)

// The website uses NextJS SSG which will be deployed by uploading the .zip file to Amplify.
const amplifyApp = new aws_amplify.CfnApp(this, `${props.amplifyStackConfiguration.amplifyAppName}`, {
const amplifyApp = new aws_amplify.CfnApp(this, `${props.amplifyStackConfiguration.amplifyAppName}-${props.stage}-${props.env!.region}`, {
name: `${props.amplifyStackConfiguration.amplifyAppName}`,
iamServiceRole: `${props.amplifyStackConfiguration.amplifyServiceRoleName}`,
description: `The ${APP_NAME} AWS Amplify Application`
})

const authStack = new AmplifyAuthStack(this, `${props.amplifyStackConfiguration.amplifyAuthConfiguration.stackName}`, {
// Create the nested stack which will contain all the resources related to authentication.
const authStack = new AmplifyAuthStack(this, `${props.amplifyStackConfiguration.amplifyAuthConfiguration.stackName}-${props.stage}-${props.env!.region}`, {
stackName: `${props.amplifyStackConfiguration.amplifyAuthConfiguration.stackName}`,
description: `This stack will contain all the ${APP_NAME} Auth related resources`,
amplifyAuthConfiguration: props.amplifyStackConfiguration.amplifyAuthConfiguration,
stage: props.stage,
env: props.env
})

Expand All @@ -49,19 +53,24 @@ export class AmplifyStack extends Stack {
value: authStack.region
})

new CfnOutput(this, Constants.AmplifyConstants.COGNITO_IDENTITY_POOL_ID, {
exportName: Constants.AmplifyConstants.COGNITO_IDENTITY_POOL_ID.replaceAll('_', '-'),
value: authStack.cognitoIdentityPoolId
new CfnOutput(this, Constants.AmplifyConstants.IDENTITY_POOL_ID, {
exportName: Constants.AmplifyConstants.IDENTITY_POOL_ID.replaceAll('_', '-'),
value: authStack.identityPoolId
})

new CfnOutput(this, Constants.AmplifyConstants.USER_POOLS_ID, {
exportName: Constants.AmplifyConstants.USER_POOLS_ID.replaceAll('_', '-'),
value: authStack.cognitoUserPoolId
value: authStack.userPoolId
})

new CfnOutput(this, Constants.AmplifyConstants.USER_POOLS_WEB_CLIENT_ID, {
exportName: Constants.AmplifyConstants.USER_POOLS_WEB_CLIENT_ID.replaceAll('_', '-'),
value: authStack.cognitoUserPoolClientId
value: authStack.userPoolClientId
})

new CfnOutput(this, Constants.AmplifyConstants.USER_POOLS_DOMAIN_URL, {
exportName: Constants.AmplifyConstants.USER_POOLS_DOMAIN_URL.replaceAll('_', '-'),
value: authStack.userPoolDomainUrl
})
}
}
4 changes: 3 additions & 1 deletion apps/snakecode-cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@
"@snakecode/tsconfig": "1.0.0",
"@types/jest": "^29.5.3",
"@types/node": "20.4.5",
"aws-cdk": "2.90.0",
"eslint-config-snakecode-base": "1.0.0",
"jest": "^29.6.2",
"ts-jest": "^29.1.1",
"aws-cdk": "2.90.0",
"ts-node": "^10.9.1",
"typescript": "~5.1.6"
},
"dependencies": {
"@snakecode/models": "1.0.0",
"aws-cdk-lib": "2.90.0",
"constructs": "^10.0.0",
"dotenv": "^16.3.1",
"envalid": "^8.0.0",
"source-map-support": "^0.5.21"
}
}
2 changes: 1 addition & 1 deletion apps/snakecode-webapp/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ module.exports = {
["^node:"],
["^next", "^react"],
["^@?\\w"],
["^@", "^@public", "^@root"],
["^@snakecode", "^@", "^@public", "^@root"],
["^\\."],
],
},
Expand Down
11 changes: 11 additions & 0 deletions apps/snakecode-webapp/exports/cdk-exports-dev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"snakecode-amplify-stack": {
"amplifyid": "d3thed3kk59i2n",
"userpoolsdomainurl": "snakecode-dev.auth.us-west-2.amazoncognito.com",
"identitypoolid": "us-west-2:db458b37-0e50-4284-beb1-75c1e9c72890",
"userpoolswebclientid": "3j97ggu8ccurvnnc4tkc3aua13",
"userpoolsid": "us-west-2_93GYjaMY6",
"region": "us-west-2",
"cognitoregion": "us-west-2"
}
}
3 changes: 3 additions & 0 deletions apps/snakecode-webapp/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module globalThis {
function loadPyodide(): any
}
3 changes: 3 additions & 0 deletions apps/snakecode-webapp/local-env-info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"envName": "dev"
}
5 changes: 4 additions & 1 deletion apps/snakecode-webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,19 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@aws-amplify/auth": "^5.6.5",
"@codemirror/lang-python": "^6.1.3",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@lezer/highlight": "^1.1.6",
"@mui/icons-material": "^5.14.8",
"@mui/material": "^5.14.8",
"@snakecode/models": "1.0.0",
"@uiw/codemirror-theme-dracula": "^4.21.13",
"@uiw/codemirror-themes": "^4.21.13",
"@uiw/react-codemirror": "^4.21.13",
"autoprefixer": "10.4.15",
"aws-amplify": "^5.3.11",
"gray-matter": "^4.0.3",
"next": "13.4.19",
"postcss": "8.4.29",
Expand Down Expand Up @@ -50,6 +53,7 @@
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-next": "13.4.19",
"eslint-config-prettier": "^9.0.0",
"eslint-config-snakecode-base": "1.0.0",
"eslint-plugin-cypress": "^2.14.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-jest": "^27.2.3",
Expand All @@ -60,7 +64,6 @@
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-config-snakecode-base": "1.0.0",
"eslint-plugin-storybook": "^0.6.13",
"eslint-plugin-tailwindcss": "^3.13.0",
"eslint-plugin-testing-library": "^6.0.1",
Expand Down
47 changes: 47 additions & 0 deletions apps/snakecode-webapp/public/images/logos/apple.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit da57a73

Please sign in to comment.