How to set custom auth claims through Firebase and identify platform - node.js

I am following the firebase documentation here to set custom auth claims for users logging into my app for the first time using firebase auth + identify platform but it does not seem to be working.
When a user logs in for the first time, I want them to get the admin custom claim. I have created the following blocking function and have verified from the logs that it runs when I log in for the first time to my app using sign-in with google:
exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
return {
customClaims: {
admin: true,
},
};
});
I would expect this to create the admin custom claim in the user's token. However, when I get a list of claims using another cloud function the admin claim does not appear.
exports.getclaims = functions.https.onRequest(async (req, res) => {
const uid = req.query.uid as string;
if (uid) {
const user = await admin.auth().getUser(uid);
res.send(user.customClaims);
} else {
res.sendStatus(500);
}
});
If I set the claim using the admin SDK directly using the below cloud function, the admin claim does appear.
exports.setclaim = functions.https.onRequest(async (req, res) => {
const uid = req.query.uid as string;
if (uid) {
await admin.auth().setCustomUserClaims(uid, {admin: true});
res.sendStatus(200);
} else {
res.sendStatus(500);
}
});
What am I doing wrong in the beforeCreate function?

There's an open GitHub issue regarding that. See sessionClaims content not getting added to the decoded token. Also, there's a fix that has been recently merged regarding this issue.

From the snippet you provided, there does not appear to be anything wrong with beforeCreate as coded.
You may want to check you do not have a beforeSignIn that is overwriting the customClaims directly or via sessionClaims.
https://firebase.google.com/docs/auth/extend-with-blocking-functions#modifying_a_user

Try to use onCreate method instead of beforeCreate how it is shown on the official docs
functions.auth.user().onCreate(async (user) => {
try {
// Set custom user claims on this newly created user.
await getAuth().setCustomUserClaims(user.uid, {admin: true});
// Update real-time database to notify client to force refresh.
const metadataRef = getDatabase().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.
await metadataRef.set({refreshTime: new Date().getTime()});
} catch (error) {
console.log(error);
}
}
});
The main point here is that you need to create the user at first and then update claims and make the force update of the token at the client side:
firebase.auth().currentUser.getIdToken(true);

Related

How to get an idToken that is valid for development from firebase without having to spin up my frontend?

I am working on some firebase functions. This one will check if an user is logged in in firebase first. However this is a bit of a hassle in development. Now I need to login on the frontend first to get the id_token, pass it to my function url, then see the result.
The process I am following is described in the official docs: https://firebase.google.com/docs/auth/admin/verify-id-tokens
node.js
const admin = require('firebase-admin');
admin.initializeApp();
module.exports = function( request, response ) {
if( !request.query.id_token )
response.status(400).json({message: 'id token has not been provided'});
admin.auth()
.verifyIdToken( request.query.id_token )
.then( token => {
// TODO: redirect to payment portal
return response.status(200).json({message: 'Success'});
})
.catch( error => {
return response.status(401).json({message: 'You are currently not logged in as an authorised user'});
})
}
Is there a way to get an id_token that is valid from firebase without having to spin up my frontend? Good and simple alternatives solutions are welcome too.
NOTE: I am using the firebase emulators during development.
Since you're using the Firebase emulators you may create a fake user and retrieve an id token programmatically. The code below creates and logs in a user and returns an id_token that will be accepted by your function.
var firebase = require("firebase/app");
require("firebase/auth");
// Initialize Firebase and connect to the Authentication emulator
var firebaseConfig = {
// Insert Firebase config here
};
firebase.initializeApp(firebaseConfig);
firebase.auth().useEmulator('http://localhost:9099/');
// Create a fake user and get the token
firebase.auth().createUserWithEmailAndPassword("example#example.com", "password")
.then((userCredential) => {
console.log("User created")
});
firebase.auth().signInWithEmailAndPassword("example#example.com", "password")
.then((userCredential) => {
console.log("User logged in")
userCredential.user.getIdToken().then((idToken) => {
console.log(idToken)
});
});

Cloud function trigger on document update

I am trying to fire my cloud function if theres a document update on users/{userId}
I have a signIn method that's fired everytime a user logs in
signin: (email, password, setErrors) => {
firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(() => {
const isVerified = firebase.auth().currentUser.emailVerified
const userUid = firebase.auth().currentUser.uid
const db = firebase.firestore()
if (isVerified) {
db.collection('/users')
.doc(userUid)
.update({ isVerified: true })
}
})
.catch(err => {
setErrors(prev => [...prev, err.message])
})
},
Nothing fancy, aside from the basic log in stuff, it also checks if the user has verified their email, if it is verified it will update the collection for that user. Everything is working as intended here.
However I can't seem to get my cloud function to fire.
Basically, it's listening for changes on ther user collection. If users/{userId} document has isVerified and the user email address ends with xxxx it should grant them admin privledges.
exports.updateUser = functions.firestore.document('users/{userId}').onUpdate((change, context) => {
const after = change.after.data()
if (after.isVerified) {
firebase.auth().onAuthStateChanged(user => {
if (user.emailVerified && user.email.endsWith('#xxxx')) {
const customClaims = {
admin: true,
}
return admin
.auth()
.setCustomUserClaims(user.uid, customClaims)
.then(() => {
console.log('Cloud function fired')
const metadataRef = admin.database().ref('metadata/' + user.uid)
return metadataRef.set({ refreshTime: new Date().getTime() })
})
.catch(error => {
console.log(error)
})
}
})
}
})
Right now the function is not firing, any ideas?
Code in Cloud Functions runs as an administrative account, which cannot be retrieved with the regular Firebase Authentication SDK. In fact, you should only be using Firebase Admin SDKs in your Cloud Functions code, in which the onAuthStateChanged method doesn't exist.
It's not entirely clear what you want this code to do with the user, but if you want to check whether the user whose uid is in the path has a verified email address, you can load that user by their UID with the Admin SDK.
To ensure that the uid in the path is the real UID of the user who performed the operation you can use security rules, as shown in the documentation on securing user data.
I support what Frank said.
You can fetch users with admin in this way:
if (after.isVerified) {
admin.auth().getUser(userId)
.then((userData) => { ...

Node.js Express Spotify API save in session

Question appeared while integrating Spotify API into Nodejs Express web application using spotify-web-api-node. How multiple simultaneous user requests should be handled? After passing the authentication step, user receives access_token, which is different for each user. Each request can have a session, for example using express-session since access_token is unique for each authenticated user. The weird thing is that I can't find an example with proper session usage in the description and samples https://www.npmjs.com/package/spotify-web-api-node where spotify-web-api-node is used. How is that possible to use global variable without session? Would it make full mess among separate user requests or I'm missing something? I guess that the access_token would be always replaced with latest authenticated user. Another usage example is here https://github.com/thelinmichael/spotify-web-api-node, though it also suggests to use one global instance.
the solution is to store the access_token and refresh_token after successful authentication in the session storage, than before calling Spotify API endpoints set both tokens for the current user from the present session:
saving tokens in the session after successful authentication:
app.get('/login', (req,res) => {
var scopes = [ ... ]
var authUrl = spotifyApi.createAuthorizeURL(scopes)
res.redirect(authUrl+"&show_dialog=true")
})
app.get('/callback', async (req, res) => {
const { code } = req.query
try {
var data = await spotifyApi.authorizationCodeGrant(code)
const { access_token, refresh_token } = data.body
spotifyApi.setAccessToken(access_token)
spotifyApi.setRefreshToken(refresh_token)
req.session.spotifyAccount = { access_token, refresh_token }
res.redirect('...')
} catch(err) {
res.send(`error ${err}`)
}
});
app.get('/userinfo', async (req,res) => {
try {
spotifyApi.setAccessToken(req.session.spotifyAccount["access_token"])
spotifyApi.setRefreshToken(req.session.spotifyAccount["refresh_token"])
var result = await spotifyApi.getMe()
console.log(result.body);
res.status(200).send(result.body)
} catch (err) {
res.status(400).send(err)
}
});
since access_token is only identification key which identifies any API request, that ensures that API endpoints are called for the current user. This technique prevents mess and confusion, so that each user can see and manipulate his data only.

Firebase - setting custom user claims in admin is not working [duplicate]

This question already has answers here:
Firebase Auth Custom claims not propagating to client
(3 answers)
Closed 2 years ago.
I have a Firebase cloud function to set custom claims on a Firebase user, found from given email.
This is the function code:
export const addAdmin = functions.https.onRequest(async (request, response) => {
// Setting policies
response.set("Access-Control-Allow-Origin", "*");
response.set("Access-Control-Allow-Methods", "POST");
response.set("Access-Control-Allow-Headers", "Content-Type");
try {
let details = JSON.parse(request.body);
let user = await admin.auth().getUserByEmail(details.email);
if (details.master !== "************") throw Error("Wrong Password");
admin
.auth()
.setCustomUserClaims(user.uid, { admin: true })
.then(() => {
response.send(
`User with uid ${user.uid} was set with claims - ${JSON.stringify(
user.customClaims
)}`
);
})
.catch(error => {
console.error(error);
response.status(400).send(error);
});
} catch (error) {
console.error(error);
response.status(400).send(error);
}
});
When i call this function from the client, I get the expected result:
User with uid JiT*******v0Pp7U2 was set with claims - {"admin":true}
but it doesn't seem to have set the claims. The security rules I set in Firestore is not recognizing the claims.
This is how my security rule is configured:
allow read: if request.auth.uid == userId || request.auth.token.admin == true;
I even tried logging the token.claims object on client side but that's also undefined
const token = await user?.getIdTokenResult();
console.log(token?.claims["admin"]);
What's going wrong here?
Just refreshing the web page or re-running a query isn't enough to see a change to custom claims. In order for a user in a client app to see changes to their custom claims, the user has to either sign out and back in again, or force refresh their ID token with user.getIdToken(true).

do something 'before' login in loopback

I am pretty new to loopback and here is what I am doing:
I am using standard login route provided by the loopback to log in the users - extended base Users to my own model say orgadmin.
With prebuilt route /api/orgadmin/login, I can easily login.
Now, I have a flag in orgadmins say 'status' which can be either 'active' or 'inactive' based on which I have to defer user login.
I was thinking something with remote hooks like beforeRemote as below but it doesn't work:
//this file is in the boot directory
module.exports = function(orgadmin) {
orgadmin.beforeRemote('login', function(context, user, next) {
console.log(user)
// context.args.data.date = Date.now();
// context.args.data.publisherId = context.req.accessToken.userId;
next();
});
};
So what is the best way to accomplish this?
The user attribute will only be available if the request is coming with a valid access token. The attribute is unused for unauthenticated requests, which login is.
Here's a possible alternative:
module.exports = (OrgAdmin) => {
OrgAdmin.on('dataSourceAttached', () => {
const { login } = OrgAdmin;
OrgAdmin.login = async (credentials, include) => {
const accessToken = await login.call(OrgAdmin, credentials, include);
const orgAdmin = await OrgAdmin.findById(accessToken.userId);
if (orgAdmin.status !== 'active') {
OrgAdmin.logout(accessToken);
const err = new Error('Your account has not been activated');
err.code = 'NOT_ACTIVE_USER';
err.statusCode = 403;
throw err
}
return accessToken;
};
});
};
The above code overrides the login method and does the following:
Login the user, using loopback's built-in login
Take the response of login, which is an access token, and use it to get the user.
If the user is active, return the access token, satisfying the expected successful response of login.
If the user is not active, remove the access token that was created (which is what logout does), and throw an error.

Resources