I am using Prisma and Nextjs with the following data structure, with authentication using Next-Auth.
user
|-->profile
|-->log
|-->sublog
Right now the CRUD is sent to the database via API routes on Nextjs. And I want to write to sublog securely via the API.
So when I write this, it is open-ended:
const sublog = await prisma.sublog.create({
data: {
name: req.body.name,
content: req.body.content,
log: {
connect: {
id: req.body.logId,
}
}
}
})
I have access to the user session from the frontend and backend in order to get the userID. But I am not sure how to make the form submission secure that only if the user who owns the log can they be allowed to submit a sublog.
Any ideas on how to securely submit something securely while it is deeply nested?
P.S. Note that I can turn on and off any component that edit/delete data at the frontend - but that's only on the frontend, I want to secure it on the API so that even if the client somehow is able to access a form within the log that doesn't belong to them, it would still push an error from the API since the client don't belong there.
You'd need to make a prisma query that checks who owns the log before allowing the prisma.sublog.create to be executed. Prisma is agnostic to the concept of ownership - You need to add and check that logic yourself.
const fullLog = await prisma.log.findUnique({
select: { // don't know what your model looks like, just guessing
id: true,
profile: {
select: {
userId: true
}
}
},
where: {
id: req.body.logId
}
});
// currentUserId = however you get the current user's id
if (fullLog && fullLog.profile.userId !== currentUserId) {
// throw an error
}
Related
I'm exploring the possibilities of using the Google Forms API to create Forms dynamically from my Node Express service.
After some trail and error I'm able to do a basic conversion of assessments from another system into Google forms.
But now I want to manually change the created forms and I don't know how I can access these. It's created using a service account. Can I give permissions to email addresses or something?
Accessing the Form
If you are not using Google Workspace you would need to manually share the file with the Gmail account.
Depending on which Version of Drive API you are using. You would need to use:
permissions:create
or
permissions:insert
This will allow you to create an option to directly share the file owned by the service account to another user with access to the Drive UI.
There is also a guide on how to utilize the "permission" for Drive V3 with a sample code for Node, it might give you an insight on how to follow it up:
/**
* Batch permission modification
* #param{string} fileId file ID
* #param{string} targetUserEmail username
* #param{string} targetDomainName domain
* #return{list} permission id
* */
async function shareFile(fileId, targetUserEmail, targetDomainName) {
const {GoogleAuth} = require('google-auth-library');
const {google} = require('googleapis');
// Get credentials and build service
// TODO (developer) - Use appropriate auth mechanism for your app
const auth = new GoogleAuth({
scopes: 'https://www.googleapis.com/auth/drive',
});
const service = google.drive({version: 'v3', auth});
const permissionIds = [];
const permissions = [
{
type: 'user',
role: 'writer',
emailAddress: targetUserEmail, // 'user#partner.com',
},
{
type: 'domain',
role: 'writer',
domain: targetDomainName, // 'example.com',
},
];
// Note: Client library does not currently support HTTP batch
// requests. When possible, use batched requests when inserting
// multiple permissions on the same item. For this sample,
// permissions are inserted serially.
for (const permission of permissions) {
try {
const result = await service.permissions.create({
resource: permission,
fileId: fileId,
fields: 'id',
});
permissionIds.push(result.data.id);
console.log(`Inserted permission id: ${result.data.id}`);
} catch (err) {
// TODO(developer): Handle failed permissions
console.error(err);
}
}
return permissionIds;
}
Reference
https://developers.google.com/drive/api/v2/reference/permissions/insert
https://developers.google.com/drive/api/v3/reference/permissions/create
Guide on how to manage sharing files: https://developers.google.com/drive/api/guides/manage-sharing#node.js
TLDR;
Setup firestore security rules based on custom claim.
Cloud firestore user is created by phone auth.
Cloud function triggers on create user and adds a custom claim role- admin. An entry is updated in realtime database to indicate the claim update.
Listen to updates to realtime database in the client and call user.getIdToken(true); after custom claim is updated.
Able to see the added custom claim in the code.
Unable to read a document in firestore due to missing permission(custom claim).
Refresh the browser page, able to read the doc now.
I have a cloud function that adds a custom claim role - admin on user create.
exports.processSignUp = functions.auth.user().onCreate((user) => {
const customClaims = {
role: 'admin',
};
// Set custom user claims on this newly created user.
return admin.auth().setCustomUserClaims(user.uid, customClaims)
.then(() => {
// Update real-time database to notify client to force refresh.
const metadataRef = admin.database().ref("metadata/" + user.uid);
// Set the refresh time to the current UTC timestamp.
// This will be captured on the client to force a token refresh.
return metadataRef.set({refreshTime: new Date().getTime()});
})
.catch(error => {
console.log(error);
});
});
I listen to change events in realtime database to detect updates to custom claim for a user.
let callback = null;
let metadataRef = null;
firebase.auth().onAuthStateChanged(user => {
// Remove previous listener.
if (callback) {
metadataRef.off('value', callback);
}
// On user login add new listener.
if (user) {
// Check if refresh is required.
metadataRef = firebase.database().ref('metadata/' + user.uid + '/refreshTime');
callback = (snapshot) => {
// Force refresh to pick up the latest custom claims changes.
// Note this is always triggered on first call. Further optimization could be
// added to avoid the initial trigger when the token is issued and already contains
// the latest claims.
user.getIdToken(true);
};
// Subscribe new listener to changes on that node.
metadataRef.on('value', callback);
}
});
I have the following security rules on cloud firestore.
service cloud.firestore {
match /databases/{database}/documents {
match /{role}/{document=**} {
allow read: if request.auth != null &&
request.auth.token.role == role;
}
}
}
After the user is created, my cloud function triggers and adds the custom claim role = admin.
As a result of user.getIdToken(true); the token is refreshed on my client and I am able to see the set custom claim.
When I try to get a document that the user should be able to read, I get a permission denied by cloud firestore security rules.
When I refresh the browser page, I am able to read the documents in the path.
I am expecting that to be able to access the firebase doc without having to refresh the browser. Is this a possibility?
Can someone please tell me what is wrong with my approach/expectation?
It can take longer than you think for the token to be propagated. I use custom claims extensively - what I have found to work is to setup .onIdTokenChanged() to track uid & token updates, and then explicitly call .getIdTokenResult(true) to update my local token. Only after both are complete can you make customClaim secured calls to Firestore and/or RTDB.
I am still a nuxt beginner, so please excuse any faults.
I am using the "official" firebase module for nuxt https://firebase.nuxtjs.org/ to access firebase services such as auth signIn and singOut.
This works.
However, I am using nuxt in universal mode and I cannot access this inside my page fetch function. So my solution is to save this info in the vuex store and update it as it changes.
So, once a user is logged in or the firebase auth state changes, a state change needs to happen in the vuex store.
Currently, when a user logs in or the firebase auth state changes, if the user is still logged in, I save the state to my store like so :
const actions = {
async onAuthStateChangedAction(state, { authUser, claims }) {
if (!authUser) {
// claims = null
// TODO: perform logout operations
} else {
// Do something with the authUser and the claims object...
const { uid, email } = authUser
const token = await authUser.getIdToken()
commit('SET_USER', { uid, email, token })
}
}
}
I also have a mutation where the state is set, a getter to get the state and the actual state object as well to store the initial state:
const mutations = {
SET_USER(state, user) {
state.user = user
}
}
const state = () => ({
user: null
})
const getters = {
getUser(state) {
return state.user
}
}
My problem is, on many of my pages, I use the fetch method to fetch data from an API and then I store this data in my vuex store.
This fetch method uses axios to make the api call, like so:
async fetch({ store }) {
const token = store.getters['getUser'] //This is null for a few seconds
const tempData = await axios
.post(
my_api_url,
{
my_post_body
},
{
headers: {
'Content-Type': 'application/json',
Authorization: token
}
}
)
.then((res) => {
return res.data
})
.catch((err) => {
return {
error: err
}
console.log('error', err)
})
store.commit('my_model/setData', tempData)
}
Axios needs my firebase user id token as part of the headers sent to the API for authorization.
When the fetch method runs, the state has not always changed or updated yet, and thus the state of the user is still null until the state has changed, which is usually about a second later, which is a problem for me since I need that token from the store to make my api call.
How can I wait for the store.user state to finish updating / not be null, before making my axios api call inside my fetch method ?
I have considered using cookies to store this information when a user logs in. Then, when inside the fetch method, I can use a cookie to get the token instead of having to wait for the state to change. The problem I have with this approach is that the cookie also needs to wait for a state change before it updates it's token, which means it will use an old token upon the initial page load. I might still opt for this solution, it just feels like it's the wrong way to approach this. Is there any better way to handle this type of conundrum ?
Also, when inside fetch, the first load will be made from the server, so I can grab the token from the cookie, however the next load will be from the client, so how do I retrieve the token then if the store value will still be null while loading ?
EDIT:
I have opted for SPA mode. After thinking long and hard about it, I don't really need the nuxt server and SPA mode has "server-like" behaviour, where you could still use asyncdata and fetch to fetch data before pages render, middleware still works similar and authentication actually works where you dont have to keep the client and server in sync with access tokens etc. I would still like to see a better solution for this in the future, but for now SPA mode works fine.
I came across this question looking for a solution to a similar problem. I had a similar solution in mind as mentioned in the other answer before coming to this question, what I was looking for was the implementation details.
I use nuxt.js, the first approach that came to my mind was make a layout component and render the <Nuxt/> directive only when the user is authenticated, but with that approach, I can have only one layout file, and if I do have more than one layout file I will have to implement the same pre-auth mechanism across every layout, although this is do-able as now I am not implementing it in every page but implementing in every layout which should be considerably less.
I found an even better solution, which was to use middlewares in nuxt, you can return a promise or use async-await with the middleware to stop the application mounting process until that promise is resolved. Here is the sample code:
// middleware/auth.js
export default async function ({ store, redirect, $axios, app }) {
if (!store.state.auth) { // if use is not authenticated
if (!localStorage.getItem("token")) // if token is not set then just redirect the user to login page
return redirect(app.localePath('/login'))
try {
const token = localStorage.getItem("token");
const res = await $axios.$get("/auth/validate", { // you can use your firebase auth mechanism code here
headers: {
'Authorization': `Bearer ${token}`
}
});
store.commit('login', { token, user: res.user }); // now just dispatch a login action to vuex store
}
catch (err) {
store.commit('logout'); // preauth failed, so dispatch logout action which will clear localStorage and our Store
return redirect(app.localePath('/login'))
}
}
}
Now you can use this middleware in your page/layout component, like so:
<template>
...
</template>
<script>
export default {
middleware: "auth",
...
}
</script>
One way of fixing this is to do the firebase login before mounting the app.
Get the token from firebase, save it in vuex and only after that mount the app.
This will ensure that by the time the pages load you have the firebase token saved in the store.
Add checks on the routes for the pages that you don't want to be accessible without login to look in the store for the token (firebase one or another) and redirect to another route if none is present.
i have a meteor app where i'm using nginx with an internal SSO service to authenticate. I'm able to do this successfully and retrieve user details in the nginx set http headers on the server Meteor.onConnection method.
At this point, i'm not sure what the best approach is to get access to the user details on the client side. I feel like i should use the built in Meteor Accounts but i'm not sure how to initiate the login process from the client since the user will not actually be logging in through the Meteor client but through a redirect that happens through nginx. I feel like i need a way to automatically initiate the login process on the meteor side to set up the meteor.users collection appropriately, but i can't figure out a way to do that.
Checkout the answers here. You can pass the userId (or in whatever you want to pass the user) through nginx to the server then onto the client to login. You can generate and insert the token in a Webapp.connectHandler.
import { Inject } from 'meteor/meteorhacks:inject-initial';
// server/main.js
Meteor.startup(() => {
WebApp.connectHandlers.use("/login",function(req, res, next) {
Fiber(function() {
var userId = req.headers["user-id"]
if (userId){
var stampedLoginToken = Accounts._generateStampedLoginToken();
//check if user exists
Accounts._insertLoginToken(userId, stampedLoginToken);
Inject.obj('auth', {
'loginToken':stampedLoginToken
},res);
return next()
}
}).run()
})
}
Now you can login on the client side with the help of the meteor-inject-initial package
import { Inject } from 'meteor/meteorhacks:inject-initial';
// iron router
Router.route('/login', {
action: function() {
if (!Meteor.userId()){
Meteor.loginWithToken(Inject.getObj('auth').loginToken.token,
function(err,res){
if (err){
console.log(err)
}
}
)
} else {
Router.go('/home')
}
},
});
I have two meteor app using database for frontend and backend. Due to some bulk operation. frontend app calls backend server. works fine in many methods. in few method I should check authentication.
frontend
let remote = DDP.connect(<url>);
remote.call('methodName',argument, function(err,res){
});
backend
Meteor.methods({
methodName: function(argument) {
Meteor.user() // null value
}
});
How secure suppose I send userId as parameter?
You have to login in a way or another.
You can do something like this:
var remote = DDP.connect(url);
result = remote.call('login', {
user: user,
password: {digest: SHA256(password), algorithm: 'sha-256' }
});
Sources:
https://forums.meteor.com/t/different-interfaces-based-on-devices/264
You can get user data on server-side by:
var userData = Meteor.users.findOne(Meteor.userId());