Firebase: how to get getAdditionalUserInfo in onCreate Firebase Cloud function - node.js

I'm trying to get some additional data from a SAML login provider. I can see this data in the client but I fail to see how to get this in the back end (firebase functions).
I'm using these in the FE
"firebase": "^9.8.2",
"firebase-functions": "^3.14.1",
And this in the BE
"firebase-admin": "^10.2.0",
"firebase-functions": "^3.21.2",
This is the data and how I get it in the client:
async myproviderSignIn() {
const provider = new SAMLAuthProvider('saml.myprovider');
const auth = getAuth();
const userCredential = await signInWithPopup(auth, provider);
const credential = SAMLAuthProvider.credentialFromResult(userCredential);
if (!environment.production) {
console.log('User:', userCredential, credential);
console.log(
'getAdditionalUserInfo:',
getAdditionalUserInfo(userCredential)
);
}
}
This is what I'm after and logged by getAdditionalUserInfo in the client:
{
"isNewUser": false,
"providerId": "saml.myprovider",
"profile": {
"urn:schac:attribute-def:schacPersonalUniqueCode": "urn:schac:personalUniqueCode:nl:local:diy.myproviderconext.nl:studentid:123456",
"urn:oid:1.3.6.1.4.1.25178.1.2.9": "diy.myproviderconext.nl",
"urn:oid:1.3.6.1.4.1.25178.1.2.14": "urn:schac:personalUniqueCode:nl:local:diy.myproviderconext.nl:studentid:123456",
"urn:oid:0.9.2342.19200300.100.1.3": "student1#diy.myproviderconext.nl",
"urn:oid:1.3.6.1.4.1.5923.1.1.1.1": [
"student",
"employee",
"staff",
"member"
],
"urn:mace:dir:attribute-def:eduPersonAffiliation": [
"student",
"employee",
"staff",
"member"
],
"urn:mace:dir:attribute-def:sn": "One",
"urn:mace:dir:attribute-def:givenName": "Student",
"urn:oid:2.5.4.42": "Student",
"urn:mace:dir:attribute-def:mail": "student1#diy.myproviderconext.nl",
"urn:oid:2.5.4.4": "One",
"urn:mace:terena.org:attribute-def:schacHomeOrganization": "diy.myproviderconext.nl"
}
}
Finally this is my BE on user create trigger. It creates a DB record of the user when a new user is created in Firebase auth. I'd wish to map some of the properties shown above here to the user record in the DB.
export const onCreate = functions.auth
.user()
.onCreate((user: UserRecord, context: EventContext) => {
const timestamp = serverTimestamp();
const dbUser: DbUser = {
uid: user.uid,
name: user.displayName || '',
firstName: user.displayName || '',
lastName: '',
email: user.email,
photoURL: user.photoURL,
emailVerified: user.emailVerified,
createdDate: timestamp,
lastSeen: timestamp,
providerData: JSON.parse(JSON.stringify(user.providerData)),
userDump: JSON.parse(JSON.stringify(user)),
contextDump: JSON.parse(JSON.stringify(context)),
};
// Get additional user data from the UserCredential
// const additionalUserInfo = getAdditionalUserInfo(user); ???
const result = getFirestore()
.collection(constants.dbCollections.users)
.doc(user.uid)
.set(dbUser);
return result;
});
How do I access these additional properties in my cloud function without relying on the client?

the problem you try to solve is something explained in here:
https://firebase.google.com/docs/auth/admin/custom-claims#node.js
some of the data you described below is stored within the auth.user
const dbUser: DbUser = {
uid: user.uid,
name: user.displayName || '',
firstName: user.displayName || '',
lastName: '',
email: user.email,
photoURL: user.photoURL,
emailVerified: user.emailVerified,
createdDate: timestamp,<-----------------------------------------------here
lastSeen: timestamp,<--------------------------------------------------and here
providerData: JSON.parse(JSON.stringify(user.providerData)),
userDump: JSON.parse(JSON.stringify(user)),
contextDump: JSON.parse(JSON.stringify(context)),
};
but the data i marked as "here" needs to be created via "custom user claims" but its under the FireBaseAnalytics tab neither auth. nor firestore if you insist to retrive this data from Firebase.
But for this data :
providerData: JSON.parse(JSON.stringify(user.providerData)),
userDump: JSON.parse(JSON.stringify(user)),
contextDump: JSON.parse(JSON.stringify(context)),
you should use FireStore or FirebaseRealTimeDatabase i repeat if you insist of getting those data from FireBase because of the 1000byte policy of Custom Claims.
i hope i do understand your question well. But im sure if you add those properties with this
// Set admin privilege on the user corresponding to uid.
getAuth()
.setCustomUserClaims(uid, { admin: true })
.then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
you can retrive it with .auth token

Related

How to make NextAuth update the User on a role change?

I'm using NextAuth with the Prisma adapter and AWS Cognito and it works perfectly, but my problem is that my User model doesn't get updated if I change the groups on Cognito. This is how I configured NextAuth:
// I copied the original and changed some of the fields
export type CognitoProfile = {
email: string;
sub: string;
preferred_username: string;
"cognito:groups": string[];
};
const CognitoProvider = (
options: OAuthUserConfig<CognitoProfile>
): OAuthConfig<CognitoProfile> => {
return {
id: "cognito",
name: "Cognito",
type: "oauth",
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
idToken: true,
profile: (profile) => {
return {
id: profile.sub,
name: profile.preferred_username,
email: profile.email,
image: "",
roles: profile["cognito:groups"],
};
},
options,
};
};
export const authOptions: NextAuthOptions = {
// Include user.id on session
callbacks: {
session: ({ session, user }) => {
console.log(`User: ${JSON.stringify(user)}`);
if (session.user) {
session.user.id = user.id;
}
return session;
},
},
adapter: PrismaAdapter(prisma),
providers: [
CognitoProvider({
clientId: process.env.COGNITO_CLIENT_ID!,
clientSecret: process.env.COGNITO_CLIENT_SECRET!,
issuer: process.env.COGNITO_ISSUER,
}),
],
};
This works perfectly when a new user logs in (their groups are saved properly).
The problem is that the database is not updated when I log out and log back in after I add/remove group(s) to a Cognito user. This problem is not Cognito-specific it would be the same with things like Keycloak.
I checked the NextAuth docs, but I didn't find a solution for this. What's the recommended way of keeping the User model up to date? I don't want to reinvent the wheel 😅

Node JS Sequelize: running the same query again is retrieving the previous result

I am building a Web API using Node JS. For database operation, I am using the Sequalize package. In my test when I run the same query that gets a record after updating the record again, it is giving me the old/previous result.
Here is my code:
let testUser = await User.findOne({
where: {
email: {
[Op.eq]: testHelper.testUser.email
}
}
})
let now = new Date();
//genrate the token in the database before making the request.
let verificationToken = await VerificationToken.create({
userId: testUser.id,
verificationToken: "testing",
expiresAt: date.addSeconds(now, 40000)
})
let requestBody = {
email: testUser.email,
password: "Newpassword123",
confirm_password: "Newpassword123",
token: "testing"
}
const res = await request(app).put("/api/auth/reset-password").send(requestBody);
expect(res.statusCode).toBe(200);
expect(res.body.error).toBe(false);
//refresh the user instance by fetching the new record from the database.
let user = await User.findOne({
where: {
email: {
[Op.eq]: testHelper.testUser.email
}
}
})
// here user.password and testUser.password are the same even the record is actually updated
As you can see in the comment, the password of user object is still the same as the password of testUser object even though the password is updated in the database. How can I fix it to get the latest record?

How to add AWS Cognito sub UUID to Sequelize table as primary key?

I've been getting a lot of problems trying to add my Cognito user sub UUID into my Sequelize table. I have seen solutions for Sequelize to auto-generate their own UUID but not for custom ones? I am pushing the AWS sub UUID from my client-side as a payload in express.
Model file:
function User(sequelize) {
const User = sequelize.define(
"User",
{
id: {
type: DataTypes.UUID,
primaryKey: true,
allowNull: false,
},
Client Side
console.log('result: ', user);
console.log('result: ', user.userSub, typeof user.userSub);
console.log('uuid result: ', uuidParse(user.userSub), typeof uuidParse(user.userSub));
let payload = {
id: user.userSub,
username: username,
email: email,
birthday: birthdate
}
const request = {
url: `/api/user`,
method: 'POST',
payload,
};
query(request);
Error:
"name":"SequelizeValidationError","errors":[{"message":"User.id cannot be null","type":"notNull Violation","path":"id","value":null,"origin":"CORE"
Thank you
Just figured it out, it seems I forget to add id in my parameter in my createUser method in my model. Everything is working fine
User.createUser = async function ({ **id**, username, email, bio, birthday, gender }) {
return await this.create({
**id**,
username,
email,
bio,
birthday,
gender
});
};

Undefined error on simple firebase auth function

Strange issue, I have a function that trigger by Firebase newUser auth
if the user uses Google or Facebook provider the function works but if it email/password signup the function returns an undefined error.
what I'm missing here?
The function code:
exports.newUser = functions.auth.user().onCreate((user) => {
const docRef = admin.firestore().collection('Users').doc(user.uid)
return docRef.set({
email: user.email,
name: '',
photo: '',
signupDate: admin.firestore.FieldValue.serverTimestamp()
},{merge: true});
});
The documentation for the email property of UserRecord suggests that it is not always available. So, you should check for that in your code.
user.email field appears under provider and not like in the Firebase Example directly under user.
here how it should look like:
var email = user.email; // The email of the user.
if (email == undefined) {
for (var provider of user.providerData) {
if (provider.email) {
email = provider.email;
break;
}
}
}
return docRef.set({
email: email,
name: '',
photo: '',
signupDate: admin.firestore.FieldValue.serverTimestamp()
},{merge: true});
});

Configuring and testing Stripe Accounts Create for Stripe custom

I am trying to configure stripe.accounts.create({}) for Stripe custom. My goal here is to create everything in one form so the user fulfills all of the information requirements for their stripe account to transact after the form is compete. When testing the current code using the credit card number Stripe recommended, I am getting the error that is displayed after the following code block. I am wondering if there is a tokenization process that I am missing that isn't referenced in the stripe create account docs. This is my current post method
var knex = require("../models/knex"),
express = require('express'),
middleware = require("../middleware/index"),
stripe = require("stripe")("sk_test_VALUEOFMYTESTKEY"),
router = express.Router({mergeParams:true});
router.post("/formuser",function(req,res){
console.log(req.user[0].user_id);
knex("users.user").select("*").where("user_id",req.user[0].user_id)
.then((user) => {
var today = new Date(Date.now()).toLocaleString();
var accountType = String(req.body.accountType).toLowerCase();
var checkIfCard = accountType=="card";
console.log(req.body.accountType,checkIfCard,String(req.body.cardNumber));
var ip = req.headers['x-forwarded-for'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
req.connection.socket.remoteAddress;
console.log(ip);
if(!checkIfCard){
stripe.accounts.create({
email: user.email,
country: "US",
type: "custom",
//Required fields for Custom via... https://stripe.com/docs/connect/required-verification-information
metadata: {
"external_account": {
"object": "bank_account",
"exp_month": req.body.cardExpirationMonth,
"exp_year": req.body.cardExpirationYear,// : null,
"number": req.body.bankNumber,// : null,
}, //external account info... https://stripe.com/docs/api#account_create_bank_account
"city": req.body.city,
"legal_entity.adress.line1": req.body.streetAddress,
"legal_entity.address.postal_code": req.body.zipCode,
"legal_entity.address.state": req.body.state,
"legal_entity.dob.day": req.body.birthDay,
"legal_entity.dob.month": req.body.birthMonth,
"legal_entity.dob.year": req.body.birthYear,
"legal_entity.first_name": req.body.firstName,
"legal_entity.last_name": req.body.lastName,
"legal_entity.ssn_last_4": req.body.ssn_last_4,
"tos_acceptance.date": today,
"tos_acceptance.ip": ip,
}
}).then((acct) => {
res.redirect("/");
})
.catch((e) => {
console.log(e);
});
} else {
stripe.accounts.create({
email: user.email,
country: "US",
type: "custom",
//Required fields for Custom via... https://stripe.com/docs/connect/required-verification-information
metadata: {
"external_account": {
"object": "card", //bank account or cc or dc...
"card": req.body.cardNumber.toString(),
"cvc" : req.body.cvc.toString(),
"currency" : "usd",// : null
}, //external account info... https://stripe.com/docs/api#account_create_bank_account
"city": req.body.city,
"legal_entity.adress.line1": req.body.streetAddress,
"legal_entity.address.postal_code": req.body.zipCode,
"legal_entity.address.state": req.body.state,
"legal_entity.dob.day": req.body.birthDay,
"legal_entity.dob.month": req.body.birthMonth,
"legal_entity.dob.year": req.body.birthYear,
"legal_entity.first_name": req.body.firstName,
"legal_entity.last_name": req.body.lastName,
"legal_entity.ssn_last_4": req.body.ssn_last_4,
"tos_acceptance.date": today,
"tos_acceptance.ip": ip,
}
}).then((acct) => {
res.redirect("/");
})
.catch((e) => {
console.log(e);
});
}});
});
When I enter in the credit card information that Stripe recommends to test, I get the following error
{ [Error: Invalid val: {"object"=>"card", "card"=>"4242 4242 4242 4242", "cvc"=>"111", "currency"=>"usd"} must be a string under 500 characters]
type: 'StripeInvalidRequestError',
stack: 'Error: Invalid val: {"object"=>"card", "card"=>"4242 4242 4242 4242", "cvc"=>"111", "currency"=>"usd"} must be a string under 500 character
when I expected a user to be created.
EDIT: I removed some of the knex database code in this post to shorten it's length as it is not relevant to the current error. The current error is specifically from Stripe's promise.
Your code is trying to pass bank account details in external_account but also passing card data at the same time. This is unlikely to be what you want.
On top of this, you should not be passing this information server-side at all as it's sensitive. Instead, you should be creating a token client-side. For card data, you would use Elements and for bank account data you would build your own form and tokenize with Stripe.js. Once this is done, you get a card token tok_123 or a bank account token btok_123 and can then use this server-side in the external_account parameter.
Then, you should also pass the data as nested hashes. This means that you would not pass "legal_entity.adress.line1" but instead legal_entity[address][line1]. Your code should instead look something like this:
stripe.accounts.create(
{
type: 'custom',
country: 'US',
legal_entity : {
first_name : 'john',
last_name : 'doe',
type : 'individual',
address: {
line1: 'line1',
city: 'city',
state: 'state',
postal_code: '90210',
country: 'US'
}
},
external_account: 'tok_visa_debit',
}).then((acct) => {
console.log('account: ', JSON.stringify(acct));
}).catch((e) => {
console.log(e);
});

Resources