Accessing appsync via lambda produces unauthorized even with proper role - node.js

I'm trying to call appsync from a lambda function that I set up using aws amplify. I can tell that my lambda function has read/write permission for appsync, but when I make the POST request from lambda to appsync, I get a Unable to parse JWT token error. The weird thing is that when I look at the header, I don't see the authorization jwt that I see when I am requesting from the web application, so that could be why I'm seeing this error. Instead, I see an x-amz-security-token and a different type of authorization string that you can see in the image below.
My code is pulled from a blog I found from Adrian Hall:
const env = require('process').env
const fetch = require('node-fetch')
const URL = require('url')
const AWS = require('aws-sdk')
AWS.config.update({
region: env.AWS_REGION,
credentials: new AWS.Credentials(
env.AWS_ACCESS_KEY_ID,
env.AWS_SECRET_ACCESS_KEY,
env.AWS_SESSION_TOKEN
),
})
exports.handler = (event, context, callback) => {
const ListCourses = `query ListCourses(
$filter: ModelTodoFilterInput
$limit: Int
$nextToken: String
) {
listCourses(filter: $filter, limit: $limit, nextToken: $nextToken) {
items {
id
}
nextToken
}
}`
// const details = {
// userId: event.request.userAttributes.sub,
// userDetails: {
// name: event.request.userAttributes.name,
// },
// }
const post_body = {
query: ListCourses,
operationName: 'ListCourses',
variables: details,
}
console.log(env)
console.log(`Posting: ${JSON.stringify(post_body, null, 2)}`)
// POST the GraphQL mutation to AWS AppSync using a signed connection
const uri = URL.parse(env.API_GRAPHQLAPIENDPOINTOUTPUT)
const httpRequest = new AWS.HttpRequest(uri.href, env.REGION)
httpRequest.headers.host = uri.host
httpRequest.headers['Content-Type'] = 'application/json'
httpRequest.method = 'POST'
httpRequest.body = JSON.stringify(post_body)
AWS.config.credentials.get(err => {
const signer = new AWS.Signers.V4(httpRequest, 'appsync', true)
signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate())
const options = {
method: httpRequest.method,
body: httpRequest.body,
headers: httpRequest.headers,
}
console.log('here is the uri and options')
console.log(uri.href)
console.log(options)
fetch(uri.href, options)
.then(res => res.json())
.then(json => {
console.log(`JSON Response = ${JSON.stringify(json, null, 2)}`)
callback(null, event)
})
.catch(err => {
console.error(`FETCH ERROR: ${JSON.stringify(err, null, 2)}`)
callback(err)
})
})
}
Does anyone know why the credentials methods are authorizing the way that they are and how I can fix this UnauthorizedException error? Just to sanity check me, in amplify, I did select that I wanted this lambda function to have read/write access and I can see in the CF template that:
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"appsync:Create*",
"appsync:StartSchemaCreation",
"appsync:GraphQL",
"appsync:Get*",
"appsync:List*"
],
"Resource": [
{
"Fn::Join": [
"",
[
"arn:aws:appsync:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":apis/",
{
"Ref": "apiGraphQLAPIIdOutput"
},
"/*"
]
]
}
]
}
]
}

You would have to use AWS_IAM as the authorization mode for your API if you want to call it from the lambda. Based on the error, it seems your API is setup to use AMAZON_COGNITO_USER_POOLS as the authorization. If you want to mix the 2 in your API, you might want to look at the following blog:
https://aws.amazon.com/blogs/mobile/using-multiple-authorization-types-with-aws-appsync-graphql-apis/

Related

gcloud run deploy as an http request

I typically deploy new updates to my cloud run instances on GCP using the CLI:
gcloud run deploy CLOUD_RUN_INSTANCE --image gcr.io/ORGANIZATION/IMAGE --region us-east1 --platform managed --allow-unauthenticated --quiet
How would I run this same command as an http request using [axios][1] from my firebase functions?
The answers above solves the task of deploying Cloud Run by using a call to Cloud Build and then Cloud Build deploys the new revision, nevertheless the question is very specific:
gcloud run deploy CLOUD_RUN_INSTANCE --image gcr.io/ORGANIZATION/IMAGE --region us-east1 --platform managed --allow-unauthenticated --quiet
How would I run this same command as an http request using axios from my firebase functions?
So we need to use the replaceService method. The following code will do this by purely using axios and HTTP request. It's worth to mention that this is an snippet but can be adapted to different approaches like Firebase Functions, etc:
const {GoogleAuth} = require('google-auth-library');
const axios = require('axios');
const create_revision = async () => {
const auth = new GoogleAuth({
scopes: 'https://www.googleapis.com/auth/cloud-platform'
});
const token = await auth.getAccessToken();
//TODO: Replace as needed
region = 'REGION';
project_id = 'PROJECT_ID';
service_name = 'SERVICE_NAME';
image = 'gcr.io/PROJECT_ID/IMAGE';
//Get the current details of the service
try {
resp = await axios.get(
`https://${region}-run.googleapis.com/apis/serving.knative.dev/v1/namespaces/${project_id}/services/${service_name}`,
{headers: {'Authorization': `Bearer ${token}`}}
);
service = resp.data;
//Create the body to create a new revision
body = {
"apiVersion": service.apiVersion,
"kind": service.kind,
"metadata": {
"annotations": {
"client.knative.dev/user-image": image,
'run.googleapis.com/ingress': service.metadata.annotations['run.googleapis.com/ingress'],
'run.googleapis.com/ingress-status': service.metadata.annotations['run.googleapis.com/ingress-status']
},
"generation": service.metadata.generation,
"labels": (service.metadata.labels === undefined)? {} : service.metadata.labels,
"name": service.metadata.name,
},
"spec": {
"template": {
"metadata": {
"annotations": {
"autoscaling.knative.dev/maxScale": service.spec.template.metadata.annotations['autoscaling.knative.dev/maxScale'],
"client.knative.dev/user-image": image,
},
"labels": (service.spec.template.metadata.labels === undefined) ? {} : service.spec.template.metadata.labels,
},
"spec": {
"containerConcurrency": service.spec.template.spec.containerConcurrency,
"containers": [{
"image": image,
"ports": service.spec.template.spec.containers[0].ports,
"resources": {
"limits": service.spec.template.spec.containers[0].resources.limits
}
}],
"serviceAccountName": service.spec.template.spec.serviceAccountName,
"timeoutSeconds": service.spec.template.spec.timeoutSeconds
}
},
"traffic": service.spec.traffic[0]
}
}
//Make the request
create_service_response = await axios.put(
`https://${region}-run.googleapis.com/apis/serving.knative.dev/v1/namespaces/${project_id}/services/${service_name}`,
body,
{headers: {'Authorization': `Bearer ${token}`}}
);
console.log(create_service_response.status)
}catch (err) {
console.error(err.response.data);
}
};
This is the minimum body needed to create a new revision without modifying any previous configurations. As well to make any more customizations, the API docs can be helpful. The code was created by analyzing the output of the command by adding the --log-http flag.
Of course this is a little more complicated than using the Cloud Build approach, but this answers the question and can be helpful for others.
Got it working using the following
try {
async function updateTheme() {
let token: any
const jwtClient = new google.auth.JWT(firebaseClientEmail, '', firebaseKey, [
'https://www.googleapis.com/auth/cloud-platform',
])
await jwtClient.authorize(async function (err: any, _token: any) {
if (err) {
console.log('JWT ERROR: ', err)
return err
} else {
token = _token.access_token.split('.')
token = token[0] + '.' + token[1] + '.' + token[2]
const deploySteps = [
{
name: 'gcr.io/cloud-builders/gcloud',
args: [
'run',
'deploy',
`${name}`,
'--image',
`gcr.io/${googleCloudProject}/theme-${theme}`,
'--region',
'us-east1',
'--allow-unauthenticated',
'--platform',
'managed',
'--quiet',
],
},
]
const deployRevisions = async () => {
await axios({
method: 'post',
url: `https://cloudbuild.googleapis.com/v1/projects/${googleCloudProject}/builds`,
headers: {
Authorization: `Bearer ${token}`,
},
data: {
steps: deploySteps,
timeout: '1200s',
},
})
.catch(function (error: any) {
console.log('ERROR UPDATING THEME: ', error)
return
})
.then(function (response: any) {
console.log('SUCCESSFULLY DEPLOYED THEME UPDATE')
})
}
if (token) {
deployRevisions()
} else {
console.log('MISSING TOKEN')
}
}
})
}
await updateTheme()
} catch (e) {
console.log('tried updating theme but something went wrong')
return
}

How to set zapier cli authentication?

I am trying to build an app using zapier cli.
I am having a problem with authorizing the request .
I am using an api key and a client id.
When I try to use the same credentials in the UI
Its working perfect , however in the cli it gives an error code 403.
I have listed the code below . What could be the issue ?
//test
"use strict";
const should = require("should");
const zapier = require("zapier-platform-core");
const App = require("../index");
const appTester = zapier.createAppTester(App);
describe("custom authentication authentication", () => {
// Put your test TEST_USERNAME and TEST_PASSWORD in a .env file.
// The inject method will load them and make them available to use in your
// tests.
zapier.tools.env.inject();
it("should authenticate", (done) => {
const bundle = {
authData: {
api_key: process.env.API_KEY,
client_id: process.env.CLIENT_ID,
},
};
appTester(App.authentication, bundle)
.then((response) => {
should.exist(response);
done();
})
.catch(done);
});
});
//authentication.js
"use strict";
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = currentDate.getMonth() + 1;
const day = currentDate.getDate();
const authentication = (z, bundle) => {
const options = {
url: "url",
method: "GET",
headers: {
ContentType: "application/json",
Accept: "application/json",
"x-api-key": bundle.authData["api_key"],
client_id: bundle.authData["client_id"],
},
params: {
year: year,
month: month,
day: day,
page_size: "1000",
},
};
return z.request(options).then((response) => {
response.throwForStatus();
});
};
module.exports = authentication;
Do you have values in a .env file at the application root? You can also double check that console.log(process.env.API_KEY) prints a value when the test is running.
As an aside, you can use the zapier convert CLI command to copy your integration from the UI to the CLI without having to re-write it. If it works in the UI, it'll work in the CLI too.

Add new fields to context variable in strapi backend for user management

I am using strapi as backend and react in the front-end. So the use case is that the user will signup and that signup will be done using auth0. I have defined some roles for the users signing up as shown on auth0
Roles based on plan taken by user
const _ = require("lodash");
const axios = require("axios");
const jwt = require("../jwt");
module.exports = async (ctx, next) => {
let role;
if (ctx.state.user) {
// request is already authenticated in a different way
return next();
}
try {
const tokenInfo = await axios({ method: "post",
url: `${process.env.AUTH0_URL}/userinfo`,
headers: { Authorization: ctx.request.header.authorization,
},
});
let user_id = tokenInfo.data.sub;
var config = { method: "get",
url: `${process.env.AUTH0_URL}/api/v2/users/${user_id}/roles`,
headers: {Authorization: `Bearer ${jwt.jwtSecret}`,
},
};
axios(config).then(function (response) {
ctx.state.roles = response.data[0].name; // This part does not work in the next policy as ctx.state.role gives undefined in route specific policy
}).catch(function (error) {
console.log(error);
});
// console.log(tokenInfo.data, "tokeninfo");
if (tokenInfo && tokenInfo.data) {
return await next();
}
} catch (err) {
console.log(err.message);
return handleErrors(ctx, err, "unauthorized");
}
Currently these will be managed here only. Now I have a collection which has some research articles which can only be accessed depending upon the plan user has been assigned. In order to protect the route and strapi access I have installed user-permissions plugin in strapi and managing userinfo using a global policy as shown
Project Structure
. So here is the code through which I am checking the user info on every route
Now there are two ways in which I tried solving my problem. First I read the tokenInfo data from userInfo route but unfortunately auth0 is not returning roles assigned. It is only returning standard data like
"name": "ansh5#gmail.com",
"nickname": "ansh5",
"picture": "https://s.gravatar.com/avatar/6fdb83f10321dd7712ac2457b11ea34e?
s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fan.png",
"updated_at": "2021-07-19T08:03:50.461Z",
"user_id": "auth0|60ec0b3721224b0078ac95f4",
So in order to get user role I used the other API and configured it with my auth0 account.
${process.env.AUTH0_URL}/api/v2/users/${user_id}/roles
I am getting the correct response but when I am doing this assignment.
ctx.state.roles = response.data[0].name;
I am getting undefined in my ctx.state.roles in my route specific policy. Does anybody have idea how we manage strapi and auth0 together.
Yes, it's because the axios calls are asynchronous in nature. So as per your code, axios will try to get the user information over a network call, but strapi will not really wait for the response. Instead it will just move forward to the next policy, hence resulting in an undefined user role. To fix this, you need to await for the api response from axios. Try the code below:
const axios = require("axios");
const jwt = require("../jwt");
module.exports = async (ctx, next) => {
let role;
if (ctx.state.user) {
// request is already authenticated in a different way
return next();
}
try {
const tokenInfo = await axios({
method: "post",
url: `${process.env.AUTH0_URL}/userinfo`,
headers: {
Authorization: ctx.request.header.authorization,
},
});
let user_id = tokenInfo.data.sub;
var config = {
method: "get",
url: `${process.env.AUTH0_URL}/api/v2/users/${user_id}/roles`,
headers: {
Authorization: `Bearer ${jwt.jwtSecret}`,
},
};
const resp = await axios(config);
ctx.state.roles = response.data[0].name;
console.log(ctx.state.roles);
// console.log(tokenInfo.data, "tokeninfo");
if (tokenInfo && tokenInfo.data) {
return await next();
}
} catch (err) {
console.log(err.message);
return handleErrors(ctx, err, "unauthorized");
}
}

Alexa Smart Home Skill - Need help to make an http request

I'm trying to build a home automation system by myself. For this, I use the Blynk server to control my hardware. I can control my hardware by requesting the URL of the blynk server.
For example: When I make a request to https://139.59.206.133/myprivatekey/update/V1?value=1 and update a Virtual Pin to "1" and my light turns on.
I used this to make an Alexa custom skill which is able to turn my light on by making an HTTPS request to the Blynk server. For example when I say "Alexa ask my Room to turn the light on". The custom Skill is working how it should.
But a Custom skill is not really what I was looking for, so I decided to build an Alexa Smart Home Skill. So I set one up and simply tried to make an HTTPS request when "TurnON" or "TurnOFF" is called.
My problem is that every time I try to make an HTTPS request Alexa says that the device is not responding.
I tried a lot of things, but I couldn't solve my problem on my own.
Code:
The Code is from the node.js Alexa Smart Home Example.
exports.handler = function (request, context) {
if (request.directive.header.namespace === 'Alexa.Discovery' && request.directive.header.name === 'Discover') {
log("DEBUG:", "Discover request", JSON.stringify(request));
handleDiscovery(request, context, "");
}
else if (request.directive.header.namespace === 'Alexa.PowerController') {
if (request.directive.header.name === 'TurnOn' || request.directive.header.name === 'TurnOff') {
log("DEBUG:", "TurnOn or TurnOff Request", JSON.stringify(request));
handlePowerControl(request, context);
}
}
function handleDiscovery(request, context) {
var payload = {
"endpoints":
[
{
"endpointId": "demo_id",
"manufacturerName": "Smart Device Company",
"friendlyName": "Zimmerlicht",
"description": "Smart Device Switch",
"displayCategories": ["SWITCH"],
"cookie": {
"key1": "arbitrary key/value pairs for skill to reference this endpoint.",
"key2": "There can be multiple entries",
"key3": "but they should only be used for reference purposes.",
"key4": "This is not a suitable place to maintain current endpoint state."
},
"capabilities":
[
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
},
{
"interface": "Alexa.PowerController",
"version": "3",
"type": "AlexaInterface",
"properties": {
"supported": [{
"name": "powerState"
}],
"retrievable": true
}
}
]
}
]
};
var header = request.directive.header;
header.name = "Discover.Response";
log("DEBUG", "Discovery Response: ", JSON.stringify({ header: header, payload: payload }));
context.succeed({ event: { header: header, payload: payload } });
}
function log(message, message1, message2) {
console.log(message + message1 + message2);
}
function handlePowerControl(request, context) {
// get device ID passed in during discovery
var requestMethod = request.directive.header.name;
var responseHeader = request.directive.header;
responseHeader.namespace = "Alexa";
responseHeader.name = "Response";
responseHeader.messageId = responseHeader.messageId + "-R";
// get user token pass in request
var requestToken = request.directive.endpoint.scope.token;
var powerResult;
if (requestMethod === "TurnOn") {
const Http = new XMLHttpRequest();
const url='https://139.59.206.133/myprivatekey/update/V1?value=1';
Http.open("GET", url);
Http.send();
// Make the call to your device cloud for control
// powerResult = stubControlFunctionToYourCloud(endpointId, token, request);
powerResult = "ON";
}
else if (requestMethod === "TurnOff") {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", 'http://myprivatekey/update/V0?value=0', false ); // false for synchronous request
xmlHttp.send( null );
// Make the call to your device cloud for control and check for success
// powerResult = stubControlFunctionToYourCloud(endpointId, token, request);
powerResult = "OFF";
}
var contextResult = {
"properties": [{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": powerResult,
"timeOfSample": "2017-09-03T16:20:50.52Z", //retrieve from result.
"uncertaintyInMilliseconds": 50
}]
};
var response = {
context: contextResult,
event: {
header: responseHeader,
endpoint: {
scope: {
type: "BearerToken",
token: requestToken
},
endpointId: "demo_id"
},
payload: {}
}
};
log("DEBUG", "Alexa.PowerController ", JSON.stringify(response));
context.succeed(response);
}
};
My Requests:
if (requestMethod === "TurnOn") {
const Http = new XMLHttpRequest();
const url='https://139.59.206.133/myprivatekey/update/V1?value=1';
Http.open("GET", url);
Http.send();
// Make the call to your device cloud for control
// powerResult = stubControlFunctionToYourCloud(endpointId, token, request);
powerResult = "ON";
}
else if (requestMethod === "TurnOff") {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", 'http://myprivatekey/update/V0?value=0', false ); // false for synchronous request
xmlHttp.send( null );
// Make the call to your device cloud for control and check for success
// powerResult = stubControlFunctionToYourCloud(endpointId, token, request);
powerResult = "OFF";
}
Additional Info:
Programming language node.js
account linking, IAM role, and everything to create a smart home skill should be set up right
I am rather new to Alexa Skill building, and I'm also not very good at JavaScript.
If the request went through, I mean your code is being called, and light is being turned on/off, the problem is with the response.
My guess is that you are missing correlation id.
you need to add something like:
responseHeader.correlationToken = request.directive.header.correlationToken;
You should also implement the EndpointHealth: https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-endpointhealth.html

AWS - How to get username from token in a NodeJs Lamda?

I use a NodeJs Lambda after an API Gateway.
var AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'})
exports.handler = async (event) => {
const token = ...;
const username = ...;
const response = {
"username": "...",
firstname: "...",
name: "...",
email: "...",
groups: []
};
return response;
};
I add a cognito Authorizer. With token, the result is OK and whithout the result is 403. is it OK for me.
exemple:
https://123456789.execute-api.eu-west-1.amazonaws.com/v1/user/me
return:
{
"username": "foobar",
"firstname": "foo",
"name": "bar",
"email": "foobar#gmail.com",
"roles": [
"Admin"
]
}
* with foobar if foobar is in JWT and return toto if toto is in JWT
Now, I want return username of user (from JWT). My event is empty:
"event": {},
const claims = event.requestContext.authorizer.claims;
const username = claims['cognito:username'];
before use this, I need use proxyLambda
and add this for the CORS (CORB):
const response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*"
},
body: JSON.stringify(r)
};
return response;
or other solution: i do not use proxyLamba but you configure this:
after, you need decode JWT in Lambda.
const jwtToken = event.params.header.Authorization;
I don't think return is doing what you expect. Try using callback. Detail here
Node.js runtimes support the optional callback parameter. You can use
it to explicitly return information back to the caller.
callback(Error error, Object result); Both parameters are optional.
error is an optional parameter that you can use to provide results of
the failed Lambda function execution. When a Lambda function succeeds,
you can pass null as the first parameter.
result is an optional parameter that you can use to provide the result
of a successful function execution. The result provided must be
JSON.stringify compatible. If an error is provided, this parameter is
ignored.
If you don't use callback in your code, AWS Lambda will call it
implicitly and the return value is null. When the callback is called,
AWS Lambda continues the Lambda function invocation until the event
loop is empty.
var AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'})
exports.handler = function(event, context, callback) => {
const token = ...;
const username = ...;
const response = {
"username": "...",
firstname: "...",
name: "...",
email: "...",
groups: []
};
callback(null, response);
};

Resources