MongoDB Stitch REST API - Payload Signature Verification - mongodb

I am working on a SANDBOX Cluster & a new app created by me in MongoDB Stitch.
I need to understand "Payload Signature Verification" in MongoDB Stitch App. Lets say, I need to make a REST GET API, which will fetch me a list of products, but this API call must be authenticated ie. only registered/authenticated users will be able to make this call. MongoDB Stitch suggests below to do that:
https://docs.mongodb.com/stitch/services/webhook-requests-and-responses/#webhook-verify-payload-signature
But, i need to understand:
(1) Where to add this BODY & SECRET ? As per my knowledge, it must be kept in the stitch app, as you must not expose any of your secret keys in client side javascript.
(2) { "message":"MESSAGE" } is this configurable? if yes, what value should we add here?
This function must be coded in MongoDB Stitch App. That is clear. This function returns "hash" based on the "body" & "secret" you pass in earlier step.
And now, you must pass this hash in your API Request:
Now, the question is:
You can easily see any request which is being passed to server in developer tools, anybody can easily copy it & pass it same through POSTMAN. So:
-> How do i secure my requests? (FYI: I have also added "RULES", saying this request must execute only if the domain name contains lets say, www.mysite.com. But i am able to execute the request successfully from localhost.)
-> If, anybody can copy & paste my request in POSTMAN & run it. SO, what is the use of generating that HASH ?
-> How do i keep my request(s) tokens alive/valid for limited period of time, lets say request is valid only for next 5 minutes ? (i mean how do i do this in Stitch APP ? Where is that Option ?)
-> How do i get the refresh token ? & even if i get it somehow, how do i re-pass it to the request ?
All such queries are UN_ANSWERED in MongoDB Stich Documentation : https://docs.mongodb.com/stitch/
Basically i want to understand the full life-cycle of any GET/POST/PUT/PATCH/DELETE request of MongoDB Stitch App / Stitch REST APIs.
If anybody have used MongoDB Stich, please explain me.

I don't know your specific use-case, though I also had issues with creating an Authenticated HTTP REST API. My idea was: I already have all security rules and schemas defined in Stitch, now I want to access the data over HTTP still using the logic defined in Stitch and not rewriting everything.
I wasn't able to create such API with Stitch functions and Webhooks, though I created an API server in (literally) 1 hour with NodeJS Koa (express or any other framework would do) and Stitch server SDK:
// app.js
const Koa = require('koa')
const app = module.exports = new Koa()
const auth = require('./auth')
const router = require('./router')
app.use(auth())
app.use(router.routes())
app.use(router.allowedMethods())
// listen
if (!module.parent) {
app.listen(3000)
}
// auth.js
const { loginWithApiKey } = require('./stitch')
function auth () {
return async function auth (ctx, next) {
const apiKey = ctx.query.api_key
try {
await loginWithApiKey(apiKey)
} catch (e) {
ctx.throw(401, 'Not Authorized')
}
await next()
}
}
module.exports = auth
// router.js
const router = require('koa-router')()
const { BSON } = require('mongodb-stitch-server-sdk')
const { db } = require('./stitch')
router.get('/', async (ctx) => {
ctx.body = { message: 'Nothing to see, but you\'re good!' }
})
const COLLECTIONS_WHITELIST = [
'activities',
'expenses',
'projects',
'resources'
]
// List
router.get('/:collection', async (ctx) => {
const collection = ctx.params.collection
isValidCollection(ctx, collection)
ctx.body = await db
.collection(collection)
.find()
.toArray()
})
function isValidCollection (ctx, collection) {
// check if the collection is allowed in the API
if (!COLLECTIONS_WHITELIST.includes(collection)) {
ctx.throw(404, `Unknown API entity ${collection}`)
}
}
module.exports = router
I hope it helps

Related

Could a callable cloud function that delete users be abused so it can delete users by only id?

I have a callable cloud function on the frontend, that gets a sub-user id from front-end pass it to the cloud function, and then the cloud function delete that user and also deletes his doc from the collection...
my question is could someone get the id of some user and use that function and start popping requests using this function to delete users left and right ?
it make sense that this could function won't follow any rules, so I consider this to be a major security risk if implemented in the wrong way any idea how to improve security on this and guard against any abuse attempts.
Front end callable function
const functions = getFunctions();
const deleteClient = httpsCallable(functions, 'deleteClient');
deleteClient({ uid: 'clientId' })
.then((result: any) => {
// Read result of the Cloud Function.
/** #type {any} */
// const data = result.data;
// const sanitizedMessage = data.text;
console.log(result);
})
.catch((err: any) => {
alert(err);
});
Cloud Function
export const deleteClient = functions.https.onCall((data, context) => {
admin
.auth()
.deleteUser(data.uid)
.then(() => {
console.log('Successfully deleted user');
})
.catch((error: any) => {
console.log('Error deleting user:', error);
});
db.collection('ClientsData').doc(data.uid).delete();
});
It indeed sounds like you created a security risk, and is also precisely why Firebase Authentication only allows deleting the currently signed-in user in its client-side SDKs.
You'll have to implement some sort of authorization scheme in your Cloud Functions code. This takes a two step process:
Pass the identity of the signed-in user making the call from the client to the server, and use it there to establish who is making the call. Since you're using Callable Cloud Functions, this is already done for you and the user is available in the context.auth variable in your Cloud Functions code.
Determine whether the user is authorized to perform the operation. This is typically done by having a list of authorized users, and then checking of the context.auth.uid who made the call is in that list. The list could be stored in your database too of course, so that you can update it without making changes to the code.

Proper way to cleanup a mongo db() reference?

I'm making a multi tenant app using mongo db and would like to know what the proper procedure between switching between databases is. I know I can get a new reference to a database using the db() command:
const client = await MongoClient.connect(url);
client.mainDb = client.db('main');
app.set('mongoClient', client);
On bootup I get and store a reference to my main for all my global app data. Then each request also passes in a tenant id. I'm using Feathersjs which provides me with a hook for every request before and after.
In my before hook, I get a reference to the clients data and store it to be used during that singular request:
app.hooks({
before: {
all: [(context) => {
// Run before all API requests
const tenant = context.params?.query?.$tenant;
const tenantDbName = ... // some logic to query the tenant db name
const client = context.app.get('mongoClient');
context.params.tenantDb = client.db(tenantDbName);
}]
}
}
After the request, I'm unclear on if I should do anything to cleanup the connection. Do I just let the garbage collector clean it up since its request that was made which has ended? Or is there a function in Mongo to clean it up?
app.hooks({
after: {
all: [(context) => {
// Cleanup DB or reset connection?
context.params.tenantDb = null;
}]
}
}
I just need to ensure that the next request doesn't use a previous requests database as this could serve them other users data.

strapi get related objects of User

I'm using strapi community edition v3.6.8. I have two different models ,User and CarModel. The User Model is strapi's integrated user model. The relation User: CarModel is 1:n
So I've got a profile page in which I want to fetch the User and their related CarModels. I can't get my head around how to achieve this.
I've read several answers that include creating a service which then fetches the related CarModelobjects but I can't figure out what to put into the service.
So the conclusion I've reached so far is that it is probably best if I just create a custom endpoint which fetches the current user and related objects.
How do I go on about this? This is the code I currently have:
axios.get(`http://localhost:1337/users/currentUser`, {
headers: {
Authorization: `Bearer ${token}`
}
})
In extensions/users-permissions/config I've created a routes.json with this content:
"method": "GET",
"path": "/users/currentUser",
"handler": "User.currentUser",
"config": {
"policies": ["policies.isAuthenticated"]
}
}
in config/policies I've created a is-authenticated.js - File with the following content:
module.exports = async (ctx, next) => {
if (ctx.state.user) {
return await next();
}
ctx.unauthorized(`You're not logged in!`);
};
And lastly in extensions/users-permissions/controllers I've created a User.js file with the following content:
const { sanitizeEntity } = require('strapi-utils');
const sanitizeUser = user =>
sanitizeEntity(user, {
model: strapi.query('user', 'users-permissions').model,
});
module.exports = {
currentUser: async (ctx, next) => {
strapi.query('user').find({id: ctx.id}, ['car-model']);
await next();
}
};
So now my questions would be:
1st: Something is wrong because when trying to GET /users/currentUser I get a 403. What exactly am I doing wrong?
2nd: Is this approach even valid in the first place?
And 3rd: What would be the correct approach to solving this problem? Because somewhere else I've read another approach which included writing a custom service which handles resolving the relation, but this looked very complicated imho, considering I'm simply trying to resolve a relation that already exists in the database.
I've also tried manipulating the users/me endpoint which didn't yield any results (and is probably also discouraged).
Interestingly: when the user logs in, I get the user object and all foreign key relations returned. Only when I query /users/me I get only the user data without relations. So I've read that this is a security feature, but what endpoint is used then, when posting to /auth/local and why does this endpoint return the user and related objects?
Could I use this endpoint instead of /users/me?
Any help to this problem would be greatly appreciated, best regards,
deM
So for anyone else looking for a solution, I figured it out. I added a custom route to currentUser as described above then I added a controller for this route in which I put the following code:
currentUser: async (ctx, next) => {
let carModelsOfUser = await (strapi.query('user', 'users-permissions').findOne({id: ctx.state.user.id}, ['carModels', 'carModels.images', 'carModels.ratings.rating']));
return carModelsOfUser;
}
CAUTION!
This also returns the user's hashed password and other potentially sensitive information.
Strapi offers the sanitizeEntity function to remove sensitive information, but as of now I haven't figured out how to use this in that context, as I'm not using the "raw" user here but instead joining some fields.

How to trigger Google Composer Airflow dag using appscript?

I want to trigger a Google Composer airflow dag using Appscript. Is there any way to do it via rest API or another way.
If it is possible please suggest the solution.
Airflow has an endpoint that allows to trigger a DAG through its REST API, however it’s not possible to access it directly, since within the Cloud Composer architecture, the Airflow web server is located under an App Engine flexible environment. By default, the Airflow web server is integrated with Identity-Aware Proxy (IAP) and authentication is required.
Based on that, I found an example in the Cloud Composer documentation, that guides you to trigger a DAG using Cloud Functions, although the code is in JavaScript I don’t think it’s possible to execute it by Google App Script.
On the other hand, a workaround is to follow the Triggering DAGs guide changing some settings as follows.
In the creation of the function instead of setting the trigger type as Cloud Storage set it as HTTP, and check the “Allow unauthenticated invocations” for test purpose. An URL will be displayed, the goal is that every time that URL is accessed the DAG is executed.
Modify the first part of the index.js file, since no data would be passed as parameters and also the makeIapPostRequest function to return the response of the API call.
exports.triggerDag = async (req, res) => { // Modification
// Fill in your Composer environment information here.
// The project that holds your function
const PROJECT_ID = 'your-project-id';
// Navigate to your webserver's login page and get this from the URL
const CLIENT_ID = 'your-iap-client-id';
// This should be part of your webserver's URL:
// {tenant-project-id}.appspot.com
const WEBSERVER_ID = 'your-tenant-project-id';
// The name of the DAG you wish to trigger
const DAG_NAME = 'composer_sample_trigger_response_dag';
// Other constants
const WEBSERVER_URL = `https://${WEBSERVER_ID}.appspot.com/api/experimental/dags/${DAG_NAME}/dag_runs`;
const USER_AGENT = 'gcf-event-trigger';
const BODY = {conf: ‘’}; // Modification
// Make the request
try {
const iap = await authorizeIap(CLIENT_ID, PROJECT_ID, USER_AGENT);
const apiReponse = await makeIapPostRequest(WEBSERVER_URL, BODY, iap.idToken, USER_AGENT); // Modification
res.status(200).send('DAG_running!'); // Modification
} catch (err) {
console.error('Error authorizing IAP:', err.message);
throw new Error(err);
}
};
const makeIapPostRequest = async (url, body, idToken, userAgent) => {
const res = await fetch(url, {
method: 'POST',
headers: {
'User-Agent': userAgent,
Authorization: `Bearer ${idToken}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.text();
console.error('Error making IAP post request:', err.message);
throw new Error(err);
}
return {
apiRes: res.ok, // Modification
};
};
At this point, anything else has to be changed, so in your Script file execute the next instructions in order to trigger the DAG.
function myFunction() {
var response = UrlFetchApp.fetch("Cloud-function-URL");
Logger.log(response.getAllHeaders());
}
Finally, verify in the Airflow web interface if the DAG was triggered.

Why am I getting this 'undefined' error?

I'm working on a Meteor project, and for some reason this profile template refuses to work.
I'm using the following code, as well as the accounts-password and accounts-entry packages for user management:
this.route('profile', {
path: '/profile/:username',
data: function() {
var userDoc = Meteor.users.findOne({"username": this.params.username});
var bookCursor = Books.find({owner: userDoc._id});
return {
theUser: userDoc,
theBooks: bookCursor
};
}
});
When I try to go to the profile URL for my test accounts ('misutowolf', and 'test2', respectively), I am given the following error in Chrome's dev console: Exception from Deps recompute function: TypeError: Cannot read property '_id' of undefined, pointing to the use of userDoc._id in the call to Books.find().
This makes no sense whatsoever, as I was able to find a user document with the names in question using meteor mongo with both usernames, in the form db.users.find({username: "misutowolf"}) and db.users.find({username: "test2"}).
I am very confused, not sure what is causing this issue at all.
By default Meteor only publish the currently logged in user info via an automatically setup publication.
What you need to do is push to the client the user info (username) you're trying to use, because if you don't do that, the user you're accessing is not published to the client and you get an undefined error when accessing its _id.
First, setup a dedicated publication (on the server) :
Meteor.publish("userByUsername",function(username){
return Meteor.users.find({
username:username
});
});
Then waitOn this publication in your route :
waitOn:function(){
return this.subscribe("userByUsername",this.params.username);
}
Finally, guard against accessing the user document until it is pushed to the client because even if you are waiting on the subscription, the data method might actually get called even if the subscription is not ready yet.
data: function() {
var userDoc = Meteor.users.findOne({"username": this.params.username});
if(!userDoc){
return;
}
// ...
}