Where and how to change session user object after signing in? - javascript

I have weird problem and I don't know where the problem is. I use next-auth library to make authentication system in my Next.js app.
Everything is OK - I can sign in by checking if there is account in google firebase with submitted credentials, session is being created properly, but when the authorize callback is initialized I pass data received from google firestore after correct sign in. User object contains whole data and it's passed forward.
Then, when I want to read data from session, some data I passed before is missing.
Code:
/pages/api/auth/[...nextauth.js]
export default (req, res) =>
NextAuth(req, res, {
providers: [
Providers.Credentials({
name: 'Credentials',
credentials: {
phone: { label: "Phone number", type: "text" },
password: { label: "Password", type: "password" }
},
authorize: async (loginData) => {
const { csrfToken, phone, password } = loginData;
// checking if there is account with these credentials
let res = await login({
phone,
password: sha1(md5(password)).toString()
})
// 200 = OK
if(res.status == 200){
// collect account data
const user = {
phone,
...res.data.info
}
// user object is created correctly, whole data is stored there
console.log('received account data from firestore', user)
return Promise.resolve(user);
}
else {
// wrong credentials
return Promise.resolve(null);
}
}
})
],
callbacks: {
session: async (session, user) => {
console.log('data passed to object when signed in', user)
// user object there doesn't have all data passed before
return Promise.resolve(session)
}
},
debug: false
})
Console logged objects:
received account data from firestore
{
phone: '123123123',
id: 'w2zh88BZzSv5BJeXZeZX',
email: 'jan#gmail.com',
name: 'Jan',
surname: 'Kowalski'
}
data passed to object when signed in
{
name: 'Jan',
email: 'jan#gmail.com',
iat: 1603900133,
exp: 1606492133
}
The best thing is, the object (above) always has the same properties. I can pass any object in authorize callback, but in session, user object always has "name, email, iat, exp" ALWAYS. The only thing that changes are values of these two properties in object (name, email). (rest properties - "phone, id, surname" - are missing).
Below there is console logged session object in any react component:
import {
signIn,
signOut,
useSession
} from 'next-auth/client'
const [ session, loading ] = useSession();
console.log(session)
Photo of console logged session object
What can I do? Do I have to receive data from firestore separately in session callback? Is the server-side rendering of Next.js causing the problem?

I resolved that problem by myself.
This issue thread helped me a lot!
https://github.com/nextauthjs/next-auth/issues/764
Below is the explanation:
callbacks: {
jwt: async (token, user, account, profile, isNewUser) => {
// "user" parameter is the object received from "authorize"
// "token" is being send below to "session" callback...
// ...so we set "user" param of "token" to object from "authorize"...
// ...and return it...
user && (token.user = user);
return Promise.resolve(token) // ...here
},
session: async (session, user, sessionToken) => {
// "session" is current session object
// below we set "user" param of "session" to value received from "jwt" callback
session.user = user.user;
return Promise.resolve(session)
}
}
EDIT: Due to NextAuth update to v4
Version 4 of NextAuth brings some changes to callbacks shown above. Now there is only one argument assigned to jwt and session functions. However, you can destructure it to separate variables. Rest of the code stays the same as before.
https://next-auth.js.org/configuration/callbacks#jwt-callback
https://next-auth.js.org/configuration/callbacks#session-callback
// api/auth/[...nextauth].js
...
callbacks: {
jwt: async ({ token, user }) => {
user && (token.user = user)
return token
},
session: async ({ session, token }) => {
session.user = token.user
return session
}
}
...

I had this problem too, and it turned out to be the functions in the Adapter that caused the problem. In my case, I wanted to use v4 of next-auth, and I wanted to use DynamoDB. As there is no official v4 adapter for Dynamo, I had to write my own, basing it on the publicly available v3 adapter.
One of the functions you have to provide when creating your own adapter is:
async updateUser(user) {
// use ddb client to update the user record
// client returns `data`
return { ...user, ...data.Attributes }
}
It turns out that the data in data.Attributes is what gets passed to your jwt() callback as user. This seems to be different to the v3 implementation.
Therefore in my case I had to structure the dynamodb adapter's updateUser() function to instruct the client to return ALL_NEW and not merely UPDATED_NEW (which is what the v3 adapter does).
My complete updateUser() function is as follows - bear in mind that at this point, I used the recommended dynamodb table structure (which I don't necessarily agree with, especially in my use case, but that's another story)
async updateUser(user) {
const now = new Date()
const data = await client.update({
TableName: tableName,
Key: {
pk: `USER#${user.id}`,
sk: `USER#${user.id}`,
},
UpdateExpression:
"set #emailVerified = :emailVerified, #updatedAt = :updatedAt",
ExpressionAttributeNames: {
"#emailVerified": "emailVerified",
"#updatedAt": "updatedAt",
},
ExpressionAttributeValues: {
":emailVerified": user.emailVerified?.toISOString() ?? null,
":updatedAt": now.toISOString(),
},
ReturnValues: "ALL_NEW",
}).promise()
return { ...user, ...data.Attributes }
},
As a result, I see user fully populated in the jwt() callback:
callbacks: {
async jwt({ token, user, account, profile, isNewUser }) {
console.debug("callback jwt user:", user)
user && (token.user = user)
return token
},
The debug output is the complete record for the user from DynamoDB, and can be used as described in #MateuszWawrzynski's answer.

Related

NextAuth DB Query in Session Callback Not Working

I'm attempting to add data to a user's session from Firestore in a NextAuth callback, however, any form of DB query I seem to use, no matter the way I do it, will return a null session.
If I adjust the session data with a pre-defined value, everything works, but that's isn't what I would like to work.
This is my code:
callbacks: {
session: async (session, user) => {
db.collection("users").where("email", "==", session.user.email).get().then(query => {
query.docs.map(item => { session.session.user.myCustomData = item.data() })
})
// session.session.user.myCustomData = "Hello";
return session;
},
},
Thanks,

access a variable from callback function NodeJS

Here is a simplified code of my payment service in node.js:
async function pay({ package, via }) {
const options = {
api: 'test',
factorNumber: '123456789',
description: `package-${package}`,
redirect: 'http://localhost:4000/accounts/pay/callback', // this will hit router.get('/pay/callback', payCallback);
}
const response = await axios.post('https://pay.ir/pg/send', options, {
headers: {'content-type': 'text/json'}
});
return { redirect: `https://pay.ir/pg/${response.data.token}` };
}
// this function is executed when we redirect to http://localhost:4000/accounts/pay/callback as I explained above
async function payCallback(req, res) {
// how can I access 'via' here
}
As you see in the first function named pay I have access to via variable which is the email or phone number of the user who wants to pay, ok?
The payment API I'm using just allow the options in the pay function to be accessible from the payCallback (this one is a function which fires at successful payment).
But I need to know who paid and check the database to insert the new payment for the user right?
So I need to access via inside the payCallback...
How can I access via inside payCallback function?
I assume the required data would be sent by your payment API in the payCallback's request body. Check their documentation.
If that isn't actually the case, you could insert the pending payment into a database with a unique ID, then add that unique ID to your redirect url:
async function pay({ package, via }) {
const paymentId = insertIntoDatabase(...);
const options = {
api: 'test',
factorNumber: '123456789',
description: `package-${package}`,
redirect: `http://localhost:4000/accounts/pay/callback?paymentId=${paymentId}`,
}
const response = await axios.post('https://pay.ir/pg/send', options, {
headers: { 'content-type': 'text/json' }
});
return { redirect: `https://pay.ir/pg/${response.data.token}` };
}
async function payCallback(req, res) {
const { paymentId } = req.query;
if (!paymentId) {
// Unexpected, log an error or so. Tell customer to contact customer service
return;
}
const paymentInfo = getFromDatabase(paymentId);
if (!paymentInfo) {
// Also unexpected, so again log the error and tell the customer to contact you
return;
}
// Do whatever with paymentInfo
}
If strictly speaking you only need via, you could add that as a query parameter instead of working with a database. But when it comes to payments, having some logging is a good idea anyway.

Checkout page signs out the user

The only way I check if user if logged in to my web app is using the following on the frontend
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
However, I have a cloud-function that I use in order to process a payment, it looks like the following
app.post("/payment", function (req, res) {
const orderId = new Date().getTime();
mollieClient.payments
.create({
amount: {
value: req.body.amount.amount,
currency: req.body.amount.currency,
},
description: req.body.description,
//redirectUrl: `http://localhost:5500/project/order.html?id=${orderId}`,
redirectUrl: `https://......web.app/order.html?id=${orderId}`,
webhookUrl: .....
})
.then((payment) => {
res.send({
redirectUrl: payment.getCheckoutUrl(),
});
return payment.getCheckoutUrl();
})
My problem is regarding the redirect URL, the redirect to order page is supposed to display information about that order, but it does not display anything because I set that only logged in users can see it. My question is why does it log out the user. I tried both to redirect to 'localhost' and 'URL-of-deployed-firebase-app' and in both cases it logs out the user. I thought by intuition that Firebase stores auth information to local storage because I can enter to any page I want without have to login. But I think this is not the case here. I am not using any let token = result.credential.accessToken tokens to keep track of auth status. What is the problem here and how should I proceed?
Here is Order page
function getOrderDetails() {
const url = new URL(window.location.href);
let orderId = url.searchParams.get("id");
firebase.auth().onAuthStateChanged(function (user) {
if (user) {
console.log("User logged in " + user);
firebase
.firestore()
.doc(`orders/${orderId}`)
.get()
.then((doc) => {
let orderSelected = {
category: doc.data().category,
delivery: doc.data().delivery,
description: doc.data().description,
price: doc.data().price,
title: doc.data().title,
quantity: doc.data().quantity,
};
// set textContent of divs
} else {
console.log("User not logged in " + user);
}
});
}
getOrderDetails();
On a new page it takes time to restore the user credentials, as it may require a call to the server. So it is normal that your onAuthStateChanged() listener first is called with null and only after that with the actual user. So you will have to handle the flow in a way that deals with the initial null, for example by waiting a few seconds before assuming that the user session isn't restored.
Alternatively, you can store a marker value in local storage yourself to indicate the was signed in recently, and then on the next page use that to assume the restore will succeed.

How can I differentiate between authenticated users?

I am building an app that has both merchants and clients. Merchants offer their services and clients can book services from the merchants.
They BOTH are authenticated with Firebase and are on the Authentication list you can find on the Firebase Console.
On sign up, merchants' info go to a collection called 'businesses'. Clients go on a collection called 'users'.
This is how I create a 'user' document
async createUserProfileDocument(user, additionalData) {
if (!user) return
const userRef = this.firestore.doc(`users/${user.uid}`)
const snapshot = await userRef.get()
if (!snapshot.exists) {
const { displayName, email, photoURL, providerData } = user
const createdAt = moment().format('MMMM Do YYYY, h:mm:ss a')
try {
await userRef.set({
displayName,
email,
photoURL,
createdAt,
providerData: providerData[0].providerId, //provider: 'google.com', 'password'
...additionalData,
})
} catch (error) {
console.error('error creating user: ', error)
}
}
return this.getUserDocument(user.uid)
}
async getUserDocument(uid) {
if (!uid) return null
try {
const userDocument = await this.firestore.collection('users').doc(uid).get()
return { uid, ...userDocument.data() }
} catch (error) {
console.error('error getting user document: ', error)
}
}
This is how 'users' sign up
export const Register = () => {
const history = useHistory()
async function writeToFirebase(email, password, values) { //function is called below
try {
const { user } = await firebaseService.auth.createUserWithEmailAndPassword(email, password)
firebaseService.createUserProfileDocument(user, values)
} catch (error) {
console.error('error: ', error)
}
}
//Formik's onSubmit to submit a form
function onSubmit(values, { setSubmitting }) {
values.displayName = values.user.name
writeToFirebase(values.user.email, values.user.password, values) //function call
}
This is how a 'merchant' registers. They sign up with email + password and their info from a form go to a collection called 'businesses'
firebaseService.auth.createUserWithEmailAndPassword(values.user.email, values.user.password)
await firebaseService.firestore.collection('businesses').add(values) //values from a form
Here is where I would like to be able to differentiate between 'users' and 'merchants', so that I can write some logic with the 'merchant' data. so far it only works with 'users'
useEffect(() => {
firebaseService.auth.onAuthStateChanged(async function (userAuth) {
if (userAuth) {
//**how can I find out if this userAuth is a 'merchant' (business) or 'user' (client)
const user = await firebaseService.createUserProfileDocument(userAuth)
setUsername(user.displayName)
//if (userAuth IS A MERCHANT) setUserIsMerchant(true) **what I'd like to be able to do
} else {
console.log('no one signed in')
}
})
}, [])
The recommended way for implementing a role-based access control system is to use Custom Claims.
You will combine Custom Claims (and Firebase Authentication) together with Firebase Security Rules. As explained in the doc referred to above:
The Firebase Admin SDK supports defining custom attributes on user
accounts. This provides the ability to implement various access
control strategies, including role-based access control, in Firebase
apps. These custom attributes can give users different levels of
access (roles), which are enforced in an application's security rules.
Once you'll have assigned to your users a Custom Claim corresponding to their user role (e.g. a merchant or client Claim), you will be able to:
Adapt your Security Rules according to the claims;
Get the Claim in your front-end and act accordingly (e.g. route to specific app screens/pages, display specific UI elements, etc...)
More precisely, as explained in the doc, you could do something like:
useEffect(() => {
firebaseService.auth.onAuthStateChanged(userAuth => {
if (userAuth) {
userAuth.getIdTokenResult()
.then((idTokenResult) => {
// Confirm the user is a Merchant or a Client
if (!!idTokenResult.claims.merchant) {
// Do what needs to be done for merchants
} else if (!!idTokenResult.claims.client) {
// Do what needs to be done for clients
}
} else {
console.log('no one signed in')
}
})
}, [])
You may be interested by this article which presents "How to create an Admin module for managing users access and roles" (disclaimer, I'm the author).

Cloud Functions for Firebase HTTP timeout

I'm so close with this one.
I have written a Cloud Function that takes information sent from an Azure token to custom mint a Firebase token and send this token back to the client.
The token is created correctly, but isn't returned on my HTTP-request.
Unfortunately my Firebase app causes a timeout.
Function execution took 60002 ms, finished with status: 'timeout'
I can't really wrap my head around why that is, hence this post. Is there something wrong with my code, or is it me that's calling the HTTP-request wrong?
Here is the log I get from the Firebase Functions console.
Here's my code
// Create a Firebase token from any UID
exports.createFirebaseToken = functions.https.onRequest((req, res) => {
// The UID and other things we'll assign to the user.
const uid = req.body.uid;
const additionalClaims = {
name: req.body.name,
email: req.body.email
};
// Create or update the user account.
const userCreationTask = admin.auth().updateUser(uid, additionalClaims).catch(error => {
// If user does not exists we create it.
if (error.code === 'auth/user-not-found') {
console.log(`Created user with UID:${uid}, Name: ${additionalClaims.name} and e-mail: ${additionalClaims.email}`);
return admin.auth().createUser({
uid: uid,
displayName: displayName,
email: email,
});
}
throw error;
console.log('Error!');
});
// Wait for all async tasks to complete, then generate and return a custom auth token.
return Promise.all([userCreationTask]).then(() => {
console.log('Function create token triggered');
// Create a Firebase custom auth token.
return admin.auth().createCustomToken(uid, additionalClaims).then((token) => {
console.log('Created Custom token for UID "', uid, '" Token:', token);
return token;
});
});
});
When I'm making this HTTP-request, all i'm sending in is a JSON that looks like this:
parameters = [
"uid" : id,
"email" : mail,
"name" : name
]
Cloud Functions triggered by HTTP requests need to be terminated by ending them with a send(), redirect(), or end(), otherwise they will continue running and reach the timeout.
From the terminate HTTP functions section of the documentation on HTTP triggers:
Always end an HTTP function with send(), redirect(), or end(). Otherwise, your function might to continue to run and be forcibly terminated by the system. See also Sync, Async and Promises.
After retrieving and formatting the server time using the Node.js moment module, the date() function concludes by sending the result in the HTTP response:
const formattedDate = moment().format(format);
console.log('Sending Formatted date:', formattedDate);
res.status(200).send(formattedDate);
So, within your code, you could send the token back in the response with send(), for example:
// ...
// Create a Firebase custom auth token.
return admin.auth().createCustomToken(uid, additionalClaims).then((token) => {
console.log('Created Custom token for UID "', uid, '" Token:', token);
res.status(200).send(token);
return token;
});
// ...

Categories

Resources