Serve Swagger docs via Lambda - amazon-web-services

I'm trying to publish the documentation for an API I've developed by means of swagger-ui. Since the API is hosted on AWS API Gateway, I have developed the below Lambda to handle the /swagger-simpler API endpoint. I've confirmed that I'm successfully retrieving the docs, but when I go to the /swagger-simpler endpoint, I get the error: Uncaught SyntaxError: expected expression, got '<' in swagger-ui-bundle.js. When I pull up swagger-ui-bundle.js, it's the exact same HTML as I get when I pull up the /swagger-simpler endpoint.
What am I doing wrong?
Swagger Lambda:
/** #format */
import 'source-map-support/register'
import express from 'express'
import serverless from 'serverless-http'
import swaggerUi from 'swagger-ui-express'
import { Handler } from 'aws-lambda'
import { APIGatewayClient, GetExportCommand } from '#aws-sdk/client-api-gateway'
const app = express()
const apiGateway = new APIGatewayClient({})
export const handler: Handler = async (event, context) => {
const apiId = event.requestContext.apiId
const stage = event.requestContext.stage
console.debug('From request context', { apiId, stage })
let swaggerJson: swaggerUi.JsonObject
try {
swaggerJson = await getSwaggerJson(apiId, stage)
} catch (e) {
console.error('Failed to retreive Swagger JSON', e)
throw new Error('Failed to retreive Swagger JSON')
}
console.debug('Got Swagger doc object', { swaggerJson })
app.use('/swagger-simpler', swaggerUi.serve, swaggerUi.setup(swaggerJson))
console.debug('here')
const handler = serverless(app)
console.debug('got handler', { handler })
const ret = await handler(event, context)
console.debug('handler returned', { ret })
return ret
}
const getSwaggerJson = async (
restApiId: string,
stageName: string
): Promise<swaggerUi.JsonObject> => {
const params = {
exportType: 'oas30',
restApiId,
stageName,
accepts: 'application/json',
}
const res = await apiGateway.send(new GetExportCommand(params))
console.debug('GetExportCommand successful', { res })
let swaggerJson: string
if (res.body) {
swaggerJson = Buffer.from(res.body).toString()
} else {
throw new Error('Empty response body from GetExportCommand')
}
console.debug('Got Swagger JSON', { swaggerJson })
return JSON.parse(swaggerJson)
}

Use S3 instead of Lambda to host the auto generated Swagger docs from API Gateway. Within S3 you can host a static website with a clicks.
Just make sure this meets your security needs.

It turned out to be a CDK issue. See the article I wrote about it for details, but basically I needed to install swagger-ui-express and express via node_modules rather than having those modules bundled into the generated JS.

Related

How to block external requests to a NextJS Api route

I am using NextJS API routes to basically just proxy a custom API built with Python and Django that has not yet been made completely public, I used the tutorial on Vercel to add cors as a middleware to the route however it hasn't provided the exact functionality I wanted.
I do not want to allow any person to make a request to the route, this sort of defeats the purpose for but it still at least hides my API key.
Question
Is there a better way of properly stopping requests made to the route from external sources?
Any answer is appreciated!
// Api Route
import axios from "axios";
import Cors from 'cors'
// Initializing the cors middleware
const cors = Cors({
methods: ['GET', 'HEAD'],
allowedHeaders: ['Content-Type', 'Authorization','Origin'],
origin: ["https://squadkitresearch.net", 'http://localhost:3000'],
optionsSuccessStatus: 200,
})
function runMiddleware(req, res, fn) {
return new Promise((resolve, reject) => {
fn(req, res, (res) => {
if (res instanceof Error) {
return reject(res)
}
return resolve(res)
})
})
}
async function getApi(req, res) {
try {
await runMiddleware(req, res, cors)
const {
query: { url },
} = req;
const URL = `https://xxx/api/${url}`;
const response = await axios.get(URL, {
headers: {
Authorization: `Api-Key xxxx`,
Accept: "application/json",
}
});
if (response.status === 200) {
res.status(200).send(response.data)
}
console.log('Server Side response.data -->', response.data)
} catch (error) {
console.log('Error -->', error)
res.status(500).send({ error: 'Server Error' });
}
}
export default getApi
Sorry for this late answer,
I just think that this is the default behaviour of NextJS. You are already set, don't worry. There is in contrast, a little bit customization to make if you want to allow external sources fetching your Next API

Access to fetch at 'API_Gateway_URL' from origin 'S3_host_url' has been blocked by CORS policy

I know this question has been asked previously but I couldn't find any answer that solves my problem, so please forgive me if it is repetitive.
I have created a Lambda function that reads data from a DynamoDB table. I created an API gateway for this Lambda function.
When I directly hit the url in my browser, I get the expected result. But when I fetch the URL in my react app, I'm getting the below error(I have hosted my react app on S3 bucket with static website hosting)
Access to fetch at 'API_gateway_url'
from origin 'S3_static_website_endpoint' has been blocked
by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
On searching the web, I found out that I need to set the 'Access-Control-Allow-Origin' header in my Lambda and I have done it, but still I'm getting the same issue.
PS: I'm posting this question after 1 whole day of trial-error and looking at different answers, so if you know the answer please help me!
Lambda function:
console.log('function starts');
const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();
exports.handler = (event, context, callback) => {
function formatResponse(data, code) {
return {
statusCode: code,
headers: {
'Access-Control-Allow-Origin': '*',
"Access-Control-Allow-Credentials" : true,
"Access-Control-Allow-Headers":"X-Api-Key"
},
body: JSON.stringify(data)
}
}
let param = {
TableName: 'tableName',
Limit: 100 //maximum result of 100 items
};
//Will scan your entire table in dynamoDB and return results.
dynamoDB.scan(param, function(err,data){
if(err){
return formatResponse(data, 400);
}else{
return formatResponse(data, 200);
}
});
}
React app:
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
dataSource: {}
};
}
async componentDidMount() {
try {
const response = await fetch('API_gateway_url');
let responseJson = await response.json();
this.setState(
{
isLoading: false,
dataSource: responseJson
},
function () { }
);
} catch (error) {
console.error(error);
}
}
render() {
let { dataSource } = this.state;
if (this.state.isLoading) {
return <div>Loading...</div>;
} else {
return (
<div>
{dataSource.Items.map(item => (
<div key={item.PlayerId}>
<h1>{item.PlayerId}</h1>
<li>{item.PlayerName}</li>
<li>{item.PlayerPosition}</li>
<li>{item.PlayerNationality}</li>
</div>
))}
</div>
);
}
}
}
export default App;
I suspect that your Lambda is not run for OPTIONS requests (i.e. a "preflight"). You can configure CORS in your API Gateway which should resolve the problem. See Enabling CORS for a REST API resource.
This was resolved by using the cors package.
Implementation can be found here:
https://epsagon.com/blog/aws-lambda-express-getting-started-guide/

Cannot find module aws-amplify, lambda function

I'm try to make an API post request in my lambda function but in the aws website, using nodejs I cannot import API ? Here is what I am trying
console.log('Loading function');
const AWS = require('aws-sdk');
const translate = new AWS.Translate({ apiVersion: '2017-07-01' });
var API = require('aws-amplify');
exports.handler = async (event, context) => {
try {
const params = {
SourceLanguageCode: 'en', /* required */
TargetLanguageCode: 'es', /* required */
Text: 'Hello World', /* required */
};
const data = await translate.translateText(params).promise();
createSite(data.TranslatedText);
} catch (err) {
console.log(err, err.stack);
}
function createSite(site) {
return API.post("sites", "/sites", {
body: site
});
}
};
I have also tried import...
I think you may be looking at front-end browser based JavaScript examples, which aren't always going to work in a back-end AWS Lambda NodeJS runtime environment. It appears you are trying to use this library, which states it is "a JavaScript library for frontend and mobile developers", which probably isn't what you want to use on AWS Lambda. It appears you also did not include that library in your AWS Lambda function's deployment.
I suggest using the AWS Amplify client in the AWS SDK for NodeJS which is automatically included in your Lambda function's runtime environment. You would create an Amplify client like so:
var amplify = new AWS.Amplify();

AWS Cognito services APIs? Amplify => Javascript SDK Angular app

I used Amplify for authentication and it seems to work fine. Now I want to setup an admin app for CRUD with the user pool. It seems that I have to leave Amplify and use the JavaScript SDK to use the appropriate api's.
How does this work? I've failed at figuring out how to get the tokens I receive in Amplify into AWS.config or wherever they are supposed to go.
What a struggle this had been. It seems that the code in the docs is dated and what little advice is online is worse. I suspect that is because an Amplify object contains the config options and I have to bring those to the AWS.config object. I've tried below and failed. Any idea what I need to do? I'm sure answers here will be useful for many AWS newbies.
I have this code in my Angular app but thinking about Lambda. I have an EC2 server with Node.js as another option.
This is for dev on my MBP but I'm integrating it with AWS.
With the code below I get an error message that contains in part:
Error in getCognitoUsers: Error: Missing credentials in config
at credError (config.js:345)
at getStaticCredentials (config.js:366)
at Config.getCredentials (config.js:375)
The JWT I inserted into the object below is the AccessKeyID that is in my browser storage and I used for authentication.
In console.log cognitoidentityserviceprovider I have this object, in part:
config: Config
apiVersion: "2016-04-18"
credentialProvider: null
credentials: "eyJraWQiOiJwaUdRSnc4TWtVSlR...
endpoint: "cognito-idp.us-west-2.amazonaws.com"
region: "us-west-2"
endpoint: Endpoint
host: "cognito-idp.us-west-2.amazonaws.com"
hostname: "cognito-idp.us-west-2.amazonaws.com"
href: "https://cognito-idp.us-west-2.amazonaws.com/"
These functions flow down as a sequence. I left some vars in the bodies in case someone wants to know how to get this data from the user object. I used them in various attempts build objects but most aren't needed here, maybe. The all produce the correct results from the Amplify user object.
import { AmplifyService } from 'aws-amplify-angular';
import Amplify, { Auth } from 'aws-amplify';
import { CognitoIdentityServiceProvider } from 'aws-sdk';
import * as AWS from 'aws-sdk';
#Injectable()
export class CognitoApisService {
private cognitoConfig = Amplify.Auth.configure(); // Data from main.ts
private cognitoIdPoolID = this.cognitoConfig.identityPoolId;
private cognitoUserPoolClient = this.cognitoConfig.userPoolWebClientId;
private cognitoIdPoolRegion = this.cognitoConfig.region;
private cognitoUserPoolID = this.cognitoConfig.userPoolId;
...
constructor(
private amplifyService: AmplifyService,
) { }
public getAccessToken() {
return this.amplifyService
.auth() // Calls class that includes currentAuthenticaedUser.
.currentAuthenticatedUser() // Sets up a promise and gets user session info.
.then(user => {
console.log('user: ', user);
this.accessKeyId = user.signInUserSession.accessToken.jwtToken;
this.buildAWSConfig();
return true;
})
.catch(err => {
console.log('getAccessToken err: ', err);
});
}
public buildAWSConfig() {
// Constructor for the global config.
this.AWSconfig = new AWS.Config({
apiVersion: '2016-04-18',
credentials: this.accessKeyId,
region: this.cognitoIdPoolRegion
});
this.cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider(this.AWSconfig);
/* This doesn't get creds, probably because of Amplify.
this.cognitoidentityserviceprovider.config.getCredentials(function(err) {
if (err) console.log('No creds: ', err); // Error: Missing credentials
else console.log("Access Key:", AWS.config.credentials.accessKeyId);
});
*/
console.log('cognitoidentityserviceprovider: ', this.cognitoidentityserviceprovider);
this.getCognitoUsers();
}
public getCognitoUsers() {
// Used for listUsers() below.
const params = {
UserPoolId: this.cognitoUserPoolID,
AttributesToGet: [
'username',
'given_name',
'family_name',
],
Filter: '',
Limit: 10,
PaginationToken: '',
};
this.cognitoidentityserviceprovider.listUsers(params, function (err, data) {
if
(err) console.log('Error in getCognitoUsers: ', err); // an error occurred
else
console.log('all users in service: ', data);
});
}
The one problem was that the credentials needs the whole user object from Amplify, not just the access token as I show above. By the way, I have the Cognito settings in main.ts. They can also go in environment.ts. A better security option is to migrate this to the server side. Not sure how to do that yet.
// Constructor for the global config.
this.AWSconfig = new AWS.Config({
apiVersion: '2016-04-18',
credentials: this.accessKeyId, // Won't work.
region: this.cognitoIdPoolRegion
});
My complete code is simpler and now an observable. Notice another major issue I had to figure out. Import the AWS object from Amplify, not the SDK. See below.
Yeah, this goes against current docs and tutorials. If you want more background on how this has recently changed, even while I was working on it, see the bottom of this Github issue. Amplify is mostly for authentication and the JavaScript SDK is for the service APIs.
import { AmplifyService } from 'aws-amplify-angular';
// Import the config object from main.ts but must match Cognito config in AWS console.
import Amplify, { Auth } from 'aws-amplify';
import { AWS } from '#aws-amplify/core';
import { CognitoIdentityServiceProvider } from 'aws-sdk';
// import * as AWS from 'aws-sdk'; // Don't do this.
#Injectable()
export class CognitoApisService {
private cognitoConfig = Amplify.Auth.configure(); // Data from main.ts
private cognitoIdPoolRegion = this.cognitoConfig.region;
private cognitoUserPoolID = this.cognitoConfig.userPoolId;
private cognitoGroup;
private AWSconfig;
// Used in listUsers() below.
private params = {
AttributesToGet: [
'given_name',
'family_name',
'locale',
'email',
'phone_number'
],
// Filter: '',
UserPoolId: this.cognitoUserPoolID
};
constructor(
private amplifyService: AmplifyService,
) { }
public getCognitoUsers() {
const getUsers$ = new Observable(observer => {
Auth
.currentCredentials()
.then(user => {
// Constructor for the global config.
this.AWSconfig = new AWS.Config({
apiVersion: '2016-04-18',
credentials: user, // The whole user object goes in the config.credentials field! Key issue.
region: this.cognitoIdPoolRegion
});
const cognitoidentityserviceprovider = new CognitoIdentityServiceProvider(this.AWSconfig);
cognitoidentityserviceprovider.listUsers(this.params, function (err, userData) {
if (err) {
console.log('Error in getCognitoUsers: ', err);
} else {
observer.next(userData);
}
});
});
});
return getUsers$;
}
Let's call this service from a component. I'm putting the JS object parsing in the component but for now, I left the console.log here for you to get started and see if the code works for your app.
// Called from button on html component.
public getAllCognitoUsers() {
this.cognitoApisService.getCognitoUsers()
.subscribe(userData => {
console.log('data in cognito component: ', userData);
})
}

How do I get response headers for AWS JavaScript SDK calls?

I have a simple AWS Lambda function which makes an S3.getObject() call as follows:
const AWS = require('aws-sdk');
AWS.config.logger = console;
const s3 = new AWS.S3();
exports.handler = async (event) => {
return await getObject({
Bucket: "<MY-BUCKET>",
Key: "<MY-KEY>"
}).then( (res) => {
console.log('Retrieved object from S3');
console.log(res);
return res.Body.toString('ascii');
})
};
async function getObject(params){
return await s3.getObject(params).promise();
}
I've enabled logging SDK calls as per this document.
How do I get response headers for the s3.getObject() SDK call that was made? I am basically trying to retrieve the S3 request ID and extended request ID.
The in-built logger added via the "AWS.config.logger = console;" line does not seem to log response headers. How else do I get response headers for AWS JavaScript SDK calls?
P.S: Bonus points if you can let me know whether or not I need two await keywords in the code above.
Listen to httpHeaders event.
var requestObject = s3.getObject(params);
requestObject.on('httpHeaders', (statusCode, headers, response, statusMessage) => {
// your code here.
});
requestObject.promise()
.then(response => { ... })