Edit User's Custom Claims from Firebase - node.js

I am using firebase to generate JWT tokens to authorize access to a hasura graphql server.
I want an end user to have a callable firebase function that they can call from the app so they can change the x-hasura-role in their claims without changing other parts of their claims. I am guessing the best way to do this is to export the old custom user claims and set a new role inputted by the user.
PseudoCode:
exports.changeUserType = functions.https.onCall( async (data, context) => {
var userType = data.usertype;
// get the old user claims somehow
// check if user should be able to change their userType via a graphql query
...
// edit the user claims
return admin.auth().setCustomUserClaims(userType, {
'https://hasura.io/jwt/claims': {
'x-hasura-role': userType,
'x-hasura-default-role': 'orgdriver',
'x-hasura-allowed-roles': ['orgauditor', 'orgdriver', 'orgmanager', 'orgadmin', 'orgdirector'],
'x-hasura-user-id': user.uid // <-- from the old claims so user can't edit
}
});
If there is a better way to do this, maybe by grabbing a user's id from the auth database by checking who ran the function please tell me. Thank you in advance.

When a Firebase Authenticated user hits a Firebase Function, their uid is passed in through context. I would ensure they are authenticated first:
if (context.auth == undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The user must be authenticated.',
);
}
Then I would grab their uid:
const uuid = context?.auth?.uid as string;
Then you can get their user using the firebase-admin library's getAuth():
// get user
const user = await getAuth().getUser(uuid);
Now finally you can set your new custom claim property:
// set the hasura role
return await getAuth().setCustomUserClaims(uuid, {
...user.customClaims,
'x-hasura-role': userType,
});
Be sure to import:
import { getAuth } from 'firebase-admin/auth';
In this way you can safely know the user is authenticated and a uid exists, then you can simply grab the user and all their existing claims, then when you go to update destructure all existing claims values, and update the one value you want.
In this way get all the user's old claims, ensure they are authenticated, retain all old claim properties, and update the one thing you want to update.
I hope that helps out!

Related

NestJS: Authorization based on instances property best practice

I need authorization in NestJS based on instances property.
Ex. user can update only his own articles.
Is there another way despite defining the logic in each services? ( I know it is possible using CASL )
Not having a global guard will facility errors, and everything is authorized by default unless add logic on the service.
What about creating a function that takes the request, the model and the name of the proprety and use it wherever you want ?
const verifAuthorization = (
req: Request,
propName: string,
model: any
): void => {
const sender: User = req.user;
if (!sender) {
throw new BadRequestException("there is no user in the token");
}
if (!sender._id.equals(model[propName])) {
throw new UnauthorizedException();
}
};
Yes ! you will call it in every service you want to check the authorization in, but it will save you a lot of time and code

Are fcmTokens and ID Tokens the same and how to verify them with Node.js as a cloud function?

My app uses fcmTokens assigned to a user and stored in a Firestore document to keep track of app installations and logins. When a user logs out of the app I delete the fcmToken from the Firestore document and run InstanceID.instanceID().deleteID.
However when the user has bad internet 'InstanceID.instanceID().deleteID' is run again when the app starts the next time. The fcmToken in the Firestore document is not deleted in this case.
Theoretically I could also run a query in the app and search for this token in all of the Firestore user documents and delete it there but I rather would like to use cloud functions to check if the fcmTokens of a user are still valid. If not I want to delete them. I started writing the following cloud function but I am getting an error saying
Decoding Firebase ID token failed. Make sure you passed the entire string JWT which represents an ID token. See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
I assume I am using the wrong function and fcmTokens are not the same as ID Tokens?
Is there a way to check for the validity of the fcmToken similar to how I check here for the (non existent) ID Token.
Or should I somehow use ID Tokens in general for managing device specific login? ( I'm using a Snapshot listener that listens for fcmToken changes and I am logging the user out when a specific fcmToken is deleted.)
Here is my cloud function:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
var userA_UID = ""
exports.checkFcmToken = functions.firestore.document('registeredUsers/{userA_UID}').onUpdate(async (snapshot, context) => {
userA_UID = context.params.userA_UID;
const userInfo = await admin.firestore().collection('registeredUsers').doc(userA_UID).get();
const fcmTokens = userInfo.data()['fcmTokens'];
if (fcmTokens !== undefined) {
if (fcmTokens.length > 0) {
for (let fcmToken of fcmTokens) {
checkToken(fcmToken)
}
}
}
function checkToken(fcmToken) {
//will delete token from document array if invalid
admin.auth().verifyIdToken(fcmToken)
.then((decodedToken) => {
let uid = decodedToken.uid;
console.log(uid)
throw new Error('Error!')
}).catch((error) => {
console.log(error)
});
}
})
FCM tokens and ID tokens are quite different, and cannot be used interchangeably.
Firebase Authentication ID tokens identify a user. This means that if the same user is signed in on two different devices, they have ID tokens that identify the same user.
FCM tokens (also called Instance ID tokens) identify an application installation. If you have two tokens from two different devices, there is nothing that is shared between those tokens.
FCM tokens are opaque strings, and cannot be be verified without calling the FCM API.
When you send a message to an outdated token, the FCM API responds with a clear error message. The idiomatic way to keep your list of tokens clean is to handle this error message and remove the outdated token, as shown in this example from the Cloud Functions repo.

Storing firebase authenticated user info (such as FirstName, LastName, Gender and etc.) to our own database

I use the user registration and login through the firebase authentication, The moment the user registers / login , I want to store the additional user information (such as FirstName, LastName, Gender and etc.) in my database.
Here is what I am doing to do so but what happens when a rest call to store the new user information fails. The second time he logins he is not a new user
loginWithFacebook() {
const provider = new firebase.auth.FacebookAuthProvider();
provider.addScope('user_birthday');
provider.addScope('user_friends');
provider.addScope('user_gender');
return new Promise<any>((resolve, reject) => {
this.afAuth.auth
.signInWithPopup(provider) // a call made to sign up via fb
.then(res => {
if (res) {
resolve(res);
if (res.additionalUserInfo.isNewUser) { // creatin profile only if he is a new user
this.createProfile(res); // a call to store the response in the db
}
this.setTokenSession(res.credential.accessToken);
}
}, err => {
console.log(err);
reject(err);
})
})
}
How to always ensure that the new user information is stored in my own db?
I don't know angular, but I think that you need to use the user uid as a key in the firebase database. I think that this uid is generated when a user first logs into your app, and is kept through the entire life of your app.. So when a user logs in, you can get the uid, and check in the Firebase DB, if there is an entry with the uid..if not, it means that you have a new user, and you should create an entry in the DB.
More details here: https://firebase.google.com/docs/auth/web/manage-users.
If you want to see how to find UID : Is there any way to get Firebase Auth User UID?

Firebase acces and id tokens

I'd like to know how to get both access and id tokens in Node.js SDK Firebase.
When I print user object after signUpWithEmailAndPassword, I see that accessToken is one the properties there, but then when i use method on user object called getIdToken, I get the same token I saw in users object. Why then it is not called getAccessToken???
What I want is return to the client object containing access, id, refresh tokens and expiration time.
P.S. I can't just say user.stsTokenManager.accessToken as it tells me that there is no already such property.
This is only an internal name. This "accessToken" is really the Firebase ID token. You should rely on the officially supported getIdToken to get that Firebase ID token. Firebase also recently added getIdTokenResult which provides the ID token and additional information like expiration time and other token related information without you having to parse it from the ID token. You can also get the refreshToken from the user via firebase.auth().currentUser.refreshToken.
const result = await getRedirectResult(auth);
if (result) {
const provider = new FacebookAuthProvider();
// This is the signed-in user
const user = result.user;
// This gives you a Access Token.
const credential = provider.credentialFromResult(auth, result);
const accessToken = credential.accessToken;
// this gives you the id token
const idToken = user.getIdToken();
}

Spotify node web api - trouble with multiple users

I am working on an app that uses Spotify Node web API and having trouble when multiple users login into my application. I am successfully able to go through authentication flow and get the tokens and user ID after a user logs in. I am using the Authorization Code to authorize user (since I would like to get refresh tokens after expiration). However, the current problem is that getUserPlaylists function described here (FYI, if the first argument is undefined, it will return the playlists of the authenticated user) returns playlists of the most recently authenticated user instead of the user currently using the app.
Example 1: if user A logins in to the application, it will get its playlists fine. If user B logins in to the application, it also sees its own playlists. BUT, if user A refreshes the page, user A sees the playlists of the user B (instead of its own, user A playlists).
Example 2: user A logs in, user B can see user A's playlists just by going to the app/myplaylists route.
My guess is, the problem is with this section of the code
spotifyApi.setAccessToken(access_token);
spotifyApi.setRefreshToken(refresh_token);
The latest user tokens override whatever user was before it and hence the previous user is losing grants to do actions such as viewing its own playlists.
Expected behavior: user A sees own playlists after user B logs in event after refreshing the page.
Actual behavior: user A sees user B's playlists after user B logged in and user A refreshes the page.
I am aware that I could use the tokens without using the Spotify Node API
and just use the tokens to make requests and it should probably be fine, however, it would be great to still be able to use the Node API and to handle multiple users.
Here is the portion of code that most likely has problems:
export const createAuthorizeURL = (
scopes = SCOPE_LIST,
state = 'spotify-auth'
) => {
const authUrl = spotifyApi.createAuthorizeURL(scopes, state);
return {
authUrl,
...arguments
};
};
export async function authorizationCodeGrant(code) {
let params = {
clientAppURL: `${APP_CLIENT_URL || DEV_HOST}/app`
};
try {
const payload = await spotifyApi.authorizationCodeGrant(code);
const { body: { expires_in, access_token, refresh_token } } = payload;
spotifyApi.setAccessToken(access_token);
spotifyApi.setRefreshToken(refresh_token);
params['accessToken'] = access_token;
params['refreshToken'] = refresh_token;
return params;
} catch (error) {
return error;
}
return params;
}
export async function getMyPlaylists(options = {}) {
try {
// if undefined, should return currently authenticated user
return await spotifyApi.getUserPlaylists(undefined, options);
} catch (error) {
return error;
}
}
Would appreciate any help on this. I am really excited about what I am making so it would mean a LOT if someone could help me find the issue...
You're on the right track. When you set your access token and refresh token, though, you're setting it for your entire application, and all users who call your server will use it. Not ideal.
Here's a working example of the Authorization Code Flow in Node: https://glitch.com/edit/#!/spotify-authorization-code
As you can see, it uses a general instance of SpotifyWebApi to handle authentication, but it instantiates a new loggedInSpotifyApi for every request to user data, so you get the data for the user who's asking for it.
If you want to use the above example, you can just start editing to "remix" and create your own copy of the project.
Happy hacking!

Resources