Checkout page signs out the user - javascript

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.

Related

Send verification email before logging in

This is the code that i'm practicing in to create a new user. I can receive the email verification and confirm it however, the site will still logged me in even if I have not yet confirmed my email yet.
try{
const { user } = await auth.createUserWithEmailAndPassword(email,password);
await user.sendEmailVerification();
await handleUserProfile(user, { displayName});
this.setState({
...initialSate
});
}catch(err){
console.log(err);
}
}
This is the handleUserProfile in another js file.
export const handleUserProfile = async (userAuth, additionalData) => {
if (!userAuth) return;
const {uid} = userAuth;
const userRef = firestore.doc(`users/${uid}`);
//create new user
const snapshot = await userRef.get();
if (!snapshot.exists){
const { displayName, email} = userAuth;
const timestamp = new Date();
//if the user exist does not exist
try{
await userRef.set({
displayName,
email,
createdDate: timestamp,
...additionalData
});
}catch(err){
console.log(err);
}
}
return userRef;
};
Everything is explained in the firebase documentation.
There you have the corresponding code snippets to try.
You would need to narrow down your question with some of this trials.
Even you have the chance to check if user opens the link from a differenc device from which waas signed up.
I think this is the snippet you might need:
// Confirm the link is a sign-in with email link.
if (firebase.auth().isSignInWithEmailLink(window.location.href)) {
// Additional state parameters can also be passed via URL.
// This can be used to continue the user's intended action before triggering
// the sign-in operation.
// Get the email if available. This should be available if the user completes
// the flow on the same device where they started it.
var email = window.localStorage.getItem('emailForSignIn');
if (!email) {
// User opened the link on a different device. To prevent session fixation
// attacks, ask the user to provide the associated email again. For example:
email = window.prompt('Please provide your email for confirmation');
}
// The client SDK will parse the code from the link for you.
firebase.auth().signInWithEmailLink(email, window.location.href)
.then((result) => {
// Clear email from storage.
window.localStorage.removeItem('emailForSignIn');
// You can access the new user via result.user
// Additional user info profile not available via:
// result.additionalUserInfo.profile == null
// You can check if the user is new or existing:
// result.additionalUserInfo.isNewUser
})
.catch((error) => {
// Some error occurred, you can inspect the code: error.code
// Common errors could be invalid email and invalid or expired OTPs.
});
}
The site will still logged me in even if I have not yet confirmed my
email yet.
Yes this is how it is implemented in Firebase: there is nothing, out of the box, that prevents a user with a non-verified email to authenticate to your app.
You should manage that yourself, by:
Checking the email is verified in the back-end security rules (Firestore, Cloud Storage, etc..). For example with a function like:
function isVerifiedEmailUser() {
return request.auth.token.email_verified == true;
}
Possibly redirect and logout the user from your app if his/her email is not verified. For example, right after signing-up, as follows:
try {
const { user } = await auth.createUserWithEmailAndPassword(email,password);
await user.sendEmailVerification();
if (user.emailVerified) {
// display the content, redirect to another page, etc...
} else {
auth.signOut(); // Maybe call that after showing an error message
}
} catch(err){
console.log(err);
}
}
plus, potentially, something similar with signInWithEmailAndPassword() and onAuthStateChanged().

Firebase Auth: managing users with the Admin SDK

I have to write a firebase function that receives a JSON with a list of users and has to manage them with the following rules. For each user in the received list:
If the user is already registered (email/password) in firebase, I update it.
If the user is not registered yet, I create it
If a user is registered in firebase but it's not present in the received list, I disable it.
Now, I came up with the following solution: I iterate for each user in the received list. I call admin.auth().createUser() method so that if the user is not registered it will be created, otherwise the method throws an error and in the catch() block I call admin.auth().updateUser().
For the second part, I retrieve all the users registered with admin.auth().listUsers() and for each of them I check whether it's present in the received list: if don't so, I disable it.
For some reason, the correctness of this solution is uncertain: sometimes it doesn't work at all, other times when I call the function once it doesn't work but the second time a call the function it works, idk why is that.
This only happens when I send to the function a lot of users (about 400). If I send only few users it works fine.
Could anyone suggest to me maybe a better solution? Thanks a lot for your answer.
This is the function:
exports.addClients = functions.https.onRequest(async (req, res) => {
// fetch recevied list from payload
var receivedClients = req.body.clients;
// create or update user
receivedClients.forEach(client => {
admin.auth().createUser({
uid: client.id,
email: client.email,
emailVerified: true,
password: client.password,
})
.catch(err => {
// update user
admin.auth().updateUser(client.id, {
email: client.email
}).catch(err => {
// error updating user
log("Error updating user: " + err);
});
})
});
// disabling users not present in the received list
listUsers = await admin.auth().listUsers();
userRecords = listUsers.users;
userRecords.forEach(record => {
if (!receivedClients.some(client => client.id === record.uid)) {
// disable user
admin.auth().updateUser(record.uid, {
disabled: true
})
.catch(err => {
// error disabling user
log("Error disaling user: " + err);
});
}
});
// send response
res.sendStatus(200);
});

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

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.

How to linkWithCredential after you delete current user?

In my app, I'm trying following the documentation for how to link multiple auths in Firebase, but when I follow the example for linking to a pre-existing account linkWithCredential fails with an error of No user currently signed in.
// Get reference to the currently signed-in user
let prevUser = firebase.auth().currentUser;
// Sign in user with another account
firebase.auth().signInWithCredential(_credential).then((user) => {
let currentUser = user.user;
// Merge prevUser and currentUser data stored in Firebase.
// Note: How you handle this is specific to your application
// After data is migrated delete the duplicate user
return currentUser.delete().then(() => {
// Link the OAuth Credential to original account
console.log("!!DELETE FACEBOOK")
return prevUser.linkWithCredential(_credential).then(() => {
console.log("!!LINKING-FACEBOOK")
}).catch((error) => {
console.log("!!LINKING ERROR", error)
});
}).then(() => {
console.log("!!SIGNIN-LINKED")
// Sign in with the newly linked credential
return firebase.auth().signInWithCredential(_credential)
});
}).catch((error) => {
console.log("Sign In Error", error);
});
What I'm confused about is how can I can re-sign in to my previous account in order to be able to link the newly deleted account credentials to it?

Firebase & Backbone: Trouble using uid as a key

I created logins and unique todo lists per user using Firebase and TodoMVC as proof of concept for another project. I'm using Firebase and Google to log users in and when things are working, they get a unique persistent todo list.
Everything works (I think) when the user is already logged into Google via their browser.
The problem happens when they aren't. Instead of their todo list, or a blank one under their user id, they see the todo list of an undefined user until they hit refresh, then things work again. The Firebase url doesn't see their uid until they hit refresh. If you're logged in to Google, you can replicate the error by opening an incognito window.
You can see the errors in my code at http://lacyjpr.github.io/todo-backbone, and my repo at https://github.com/lacyjpr/todo-backbone
This is my authentication code:
// Authenticate with Google
var ref = new Firebase(<firebase url>);
ref.onAuth(function(authData) {
if (authData) {
console.log("Authenticated successfully");
} else {
// Try to authenticate with Google via OAuth redirection
ref.authWithOAuthRedirect("google", function(error, authData) {
if (error) {
console.log("Login Failed!", error);
}
});
}
})
// Create a callback which logs the current auth state
function authDataCallback(authData) {
if (authData) {
console.log("User " + authData.uid + " is logged in with " + authData.provider);
uid = authData.uid;
} else {
console.log("User is logged out");
}
}
This is the code that gets the UID to use as a firebase key:
// Get the uid of the user so we can save data on a per user basis
var ref = new Firebase(<firebase url>);
var authData = ref.getAuth();
if (authData) {
var uid = authData.uid;
console.log(uid);
}
// The collection of todos is backed by firebase instead of localstorage
var TodoList = Backbone.Firebase.Collection.extend({
// Reference to this collection's model.
model: app.Todo,
// Save all of the todos to firebase
url: <firebase url> + uid,
Thanks in advance for any advice you can offer!
You're calling .getAuth() before a user is authenticated.
Your app heavily relies on the uid to work properly. So in your case you would want to kick off the Backbone portion of the app once user has successfully authenticated.
You could modify your app.js to only kick off if the user is authenticated.
// js/app.js
var app = app || {};
var ENTER_KEY = 13;
$(function() {
var ref = new Firebase(<firebase url>);
var authData = ref.getAuth();
if (authData) {
ref.authWithOAuthRedirect("google", function(error, authData) {
if (error) {
console.log("Login Failed!", error);
} else {
// kick off app
new app.AppView();
}
});
} else {
new app.AppView();
}
});
While this will work, it isn't the ideal solution. But there is no other option since you don't have a login screen.
Ideally, you'd want to provide the user a place to login, and then you'd have access the .getAuth() value.
Also, don't worry about storing the uid on the window. .getAuth() is the cached user, so there's no network call to get the data.

Categories

Resources