Skip to content

Commit

Permalink
fix(aws): Merge pull request #438 from boostercloud/enable-cors-in-th…
Browse files Browse the repository at this point in the history
…e-auth-endpoints

Enable CORS in the auth endpoints
  • Loading branch information
Javier Toledo authored Oct 7, 2020
2 parents a74d6a9 + 2f4ef78 commit 0785768
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('With the auth API', () => {
mockCartId = random.uuid()
})

context('an internet rando', () => {
context('an internet random', () => {
let client: DisconnectableApolloClient

before(async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { authClientID, createPassword, signInURL, signOutURL, signUpURL } from '../utils'
import { internet } from 'faker'
import fetch from 'cross-fetch'
import { expect } from '@boostercloud/framework-provider-aws/test/expect'

describe('Given the Authentication API', () => {
let clientId: string
const username = internet.email()
const password = createPassword()
const role = 'SuperUserNoConfirmation'
before(async () => {
clientId = await authClientID()
})

context('When /auth/sign-up', () => {
let signUpUrl: string
let validAuthBody: string
const invalidAuthBody = JSON.stringify({})
const methodsToCheck = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH']
const preflightOptions = generatePreflightOptionsList(methodsToCheck)

before(async () => {
signUpUrl = await signUpURL()
validAuthBody = JSON.stringify({
clientId,
username,
password,
userAttributes: {
role,
},
})
})

context('OPTIONS', () => {
it('should allow all the headers and methods regardless the requests values', async () => {
const responses = await Promise.all(preflightOptions.map(performPreflightRequest(signUpUrl)))

responses.forEach(assertResponseContainsPreflightHeaders)
})
})

context('POST', () => {
it('should return the Access-Control-Allow-Origin header for 200 responses', async () => {
const response = await fetch(signUpUrl, {
method: 'POST',
headers: {
'Content-Type': 'Application/json',
},
body: validAuthBody,
})

await verifyResponseAndAllowedOriginHeader(response, 200, '*', await response.json())
})

it('should return the Access-Control-Allow-Origin header for 400 responses', async () => {
const response = await fetch(signUpUrl, {
method: 'POST',
headers: {
'Content-Type': 'Application/json',
},
body: invalidAuthBody,
})

await verifyResponseAndAllowedOriginHeader(response, 400, '*', await response.json())
})

it('should return the Access-Control-Allow-Origin header for 500 responses')
})

context('And then /auth/sign-in', () => {
let signInUrl: string
let accessToken: string

before(async () => {
signInUrl = await signInURL()
})

context('OPTIONS', () => {
it('should allow all the headers and methods regardless the requests values', async () => {
const responses = await Promise.all(preflightOptions.map(performPreflightRequest(signInUrl)))

responses.forEach(assertResponseContainsPreflightHeaders)
})
})

context('POST', () => {

it('should return the Access-Control-Allow-Origin header for 200 responses', async () => {
const response = await fetch(signInUrl, {
method: 'POST',
headers: { 'Content-Type': 'Application/json' },
body: validAuthBody,
})
const jsonBody = await response.json()
accessToken = jsonBody['accessToken']

await verifyResponseAndAllowedOriginHeader(response, 200, '*', jsonBody)
})
it('should return the Access-Control-Allow-Origin header for 400 responses', async () => {
const response = await fetch(signInUrl, {
method: 'POST',
headers: { 'Content-Type': 'Application/json' },
body: invalidAuthBody,
})

await verifyResponseAndAllowedOriginHeader(response, 400, '*', await response.json())
})
it('should return the Access-Control-Allow-Origin header for 500 responses')
})

context('And then /auth/sign-out', () => {
let signOutUrl: string

before(async () => {
signOutUrl = await signOutURL()
})

context('OPTIONS', () => {
it('should allow all the headers and methods regardless the requests values', async () => {
const responses = await Promise.all(preflightOptions.map(performPreflightRequest(signOutUrl)))

responses.forEach(assertResponseContainsPreflightHeaders)
})
})

context('POST', () => {
it('should return the Access-Control-Allow-Origin header for 200 responses', async () => {
const response = await fetch(signOutUrl, {
method: 'POST',
headers: { 'Content-Type': 'Application/json' },
body: JSON.stringify({
accessToken: accessToken,
}),
})

await verifyResponseAndAllowedOriginHeader(response, 200, '*', await response.json())
})
it('should return the Access-Control-Allow-Origin header for 400 responses', async () => {
const response = await fetch(signOutUrl, {
method: 'POST',
headers: { 'Content-Type': 'Application/json' },
body: invalidAuthBody,
})

await verifyResponseAndAllowedOriginHeader(response, 400, '*', await response.json())
})
it('should return the Access-Control-Allow-Origin header for 500 responses')
})
})
})

function generatePreflightOptionsList(desiredHttpMethods: string[]): RequestInit[] {
// For more info about preflight requests see: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
return desiredHttpMethods.map(
(method: string): RequestInit => ({
method: 'OPTIONS',
headers: {
'Access-Control-Request-Method': method,
'Access-Control-Request-Headers': 'X-any-header',
Origin: internet.url(),
},
})
)
}

function performPreflightRequest(url: string) {
return (options: RequestInit) => fetch(url, options)
}

function assertResponseContainsPreflightHeaders(response: Response): void {
expect(response.status).to.be.eq(204)
expect(response.headers.get('Access-Control-Allow-Origin')).to.be.eq('*')
expect(response.headers.get('Access-Control-Allow-Headers')).to.be.eq('*')
expect(response.headers.get('Access-Control-Allow-Methods'))
.to.include('OPTIONS')
.and.to.include('POST')
}

async function verifyResponseAndAllowedOriginHeader(
response: Response,
expectedHttpStatus: number,
expectedAllowedOrigin: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
jsonBody: any
): Promise<void> {
expect(response.status).to.be.eq(expectedHttpStatus, `Response body was: ${JSON.stringify(jsonBody)}`)
expect(response.headers.get('Access-Control-Allow-Origin')).to.be.eq(expectedAllowedOrigin)
}
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ export async function signInURL(): Promise<string> {
return new URL('auth/sign-in', await baseHTTPURL()).href
}

export async function signOutURL(): Promise<string> {
return new URL('auth/sign-out', await baseHTTPURL()).href
}

export async function refreshTokenURL(): Promise<string> {
return new URL('auth/refresh-token', await baseHTTPURL()).href
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Code, Function } from '@aws-cdk/aws-lambda'
import * as params from '../params'
import { APIs } from '../params'
import { Effect, IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'
import { AwsIntegration, PassthroughBehavior } from '@aws-cdk/aws-apigateway'
import { AwsIntegration, Cors, CorsOptions, MethodOptions, PassthroughBehavior } from '@aws-cdk/aws-apigateway'
import { CognitoTemplates } from './api-stack-velocity-templates'

export class AuthStack {
Expand Down Expand Up @@ -100,32 +100,43 @@ export class AuthStack {
const cognitoIntegrationRole = this.buildCognitoIntegrationRole(userPool)

const authResource = this.apis.restAPI.root.addResource('auth')
const methodOptions = {
const allowedOriginHeaderForCors = {
'method.response.header.Access-Control-Allow-Origin': true,
}
const methodOptions: MethodOptions = {
methodResponses: [
{
statusCode: '200',
responseParameters: allowedOriginHeaderForCors,
},
{
statusCode: '400',
responseParameters: allowedOriginHeaderForCors,
},
{
statusCode: '500',
responseParameters: allowedOriginHeaderForCors,
},
],
}
const signUpResource = authResource.addResource('sign-up')
const defaultCorsPreflightOptions: CorsOptions = {
allowHeaders: ['*'],
allowOrigins: Cors.ALL_ORIGINS,
allowMethods: ['POST', 'OPTIONS'],
}
const signUpResource = authResource.addResource('sign-up', { defaultCorsPreflightOptions })
signUpResource.addMethod('POST', this.buildSignUpIntegration(cognitoIntegrationRole), methodOptions)
signUpResource
.addResource('confirm')
.addMethod('POST', this.buildConfirmSignUpIntegration(cognitoIntegrationRole), methodOptions)
authResource
.addResource('sign-in')
.addResource('sign-in', { defaultCorsPreflightOptions })
.addMethod('POST', this.buildSignInIntegration(cognitoIntegrationRole), methodOptions)
authResource
.addResource('refresh-token')
.addMethod('POST', this.buildRefreshTokenIntegration(cognitoIntegrationRole), methodOptions)
authResource
.addResource('sign-out')
.addResource('sign-out', { defaultCorsPreflightOptions })
.addMethod('POST', this.buildSignOutIntegration(cognitoIntegrationRole), methodOptions)
}

Expand Down Expand Up @@ -205,6 +216,9 @@ export class AuthStack {
withRole: IRole,
templates: { requestTemplate: string; responseTemplate: string }
): AwsIntegration {
const responseParameters = {
['method.response.header.Access-Control-Allow-Origin']: "'*'",
}
return new AwsIntegration({
service: 'cognito-idp',
action: forAction,
Expand All @@ -216,14 +230,17 @@ export class AuthStack {
{
selectionPattern: '5\\d\\d',
statusCode: '500',
responseParameters,
},
{
selectionPattern: '4\\d\\d',
statusCode: '400',
responseParameters,
},
{
selectionPattern: '2\\d\\d',
statusCode: '200',
responseParameters,
responseTemplates: {
'application/json': templates.responseTemplate,
},
Expand Down

0 comments on commit 0785768

Please sign in to comment.