Add Google authentication to Firebase Real Time Database - javascript

I'm using the Firebase Real Time Database and I need to add user authentication to it. Users can only login with Google as a provider.
Current database mode:
{
"rules": {
".read": true,
".write": true
}
}
New mode should be like this:
// These rules grant access to a node matching the authenticated
// user's ID from the Firebase auth token
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
What should I use to authenticate in my case? userID, Google providerID or a token like described here?
This is the function without authentication to store data:
createMeetup ({commit, getters}, payload) {
console.log('index.js -> createMeetup')
const meetup = {
title: payload.title,
}
let imageUrl
let key
firebase.database().ref('meetups').push(meetup)
.then((data) => {
key = data.key
return key
})
.then(() => {
commit('createMeetup', {
...meetup,
imageUrl: imageUrl,
id: key
})
})
.catch((error) => {
console.log(error)
})
},

For your use case it seems like you need to sort out a few steps. I'm guessing your application can already connect/use Firebase, but these are essentially it:
Step 1 - Connecting
Connect to Firebase using your API key/config as per usual, should look something like the following.
firebase.initializeApp(config)
See also: https://firebase.google.com/docs/web/setup
You probably already have this somewhere. This does not change, but if you would apply the rules as described your users would not be able to use Firebase after just connecting.
Step 2 - Authenticating
This is basically telling Firebase who is connected. This must be done with a token/method Firebase can verify. Using a Google ID is the most common method.
With an existing Google ID / user login
// Initialize a generate OAuth provider with a `google.com` providerId.
var provider = new firebase.auth.OAuthProvider('google.com');
var credential = provider.credential(googleUser.getAuthResponse().id_token);
firebase.auth().signInWithCredential(credential)
See also: https://firebase.google.com/docs/reference/js/firebase.auth.OAuthProvider#credential
Or make Firebase SDK do the login flow
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then(function(result) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
})
See also: https://firebase.google.com/docs/auth/web/google-signin
This last option is preferred / suggested by the documentation you referenced.
If, as you described, users can already login with Google to your app for other functionalities then you should already have a login flow somewhere. Depending on your situation it might be advisable to let the Firebase SDK / library take over this process for simplicity in your application.
Step 3 - Using the database
Lastly, after authenticating users and applying the rules you suggested you will need to also make sure the paths you write to are within those accessible by the current user. You can put this in a simple function to make it easier.
const getUserRef = (ref) => {
const user = firebase.auth().currentUser;
return firebase.database().ref(`/users/${user.uid}/${ref}/`);
}
You should of course not be retrieving the current user every time you want to get a database reference, but I think this clearly illustrates the steps that need to be taken.

You can allow users to login/auth using multiple methods. Then you can merge them together to a single account as described here:
https://firebase.google.com/docs/auth/web/account-linking
So really it boils down to two options:
Allow users to login with multiple methods such as Facebook, Google, Github, basic username/password, etc.
Or allow only a single login method such as Google only.
Whichever options you pick will help you decide which ID to use.

The auth rules in your question are only stating that the users can read/write their own (presumably) user data.
I assume that you are rather looking for a solution to authorize the user to create meetup data and you should crerate rules similar to this:
These rules allow any user that is logged in to create meetups
{
"rules": {
"meetups": {
"$meetupId": {
".read": "auth.uid != null",
".write": "auth.uid != null"
}
}
}
}
Your code-snippet that pushes new meetup data to the database will automatically try and succeed or fail depending on whether the user was logged in or not. You don't need to specifically tell Firebase in which way the user was logged in. Firebase SDK will take care of the authentication for you.
But if you do want to provide different mechanisms depending on which login type that the user is authenticated with, you can check it in the rules. For example if you want to make sure that the user is not just "anonymously" logged in.
See the documentation: https://firebase.google.com/docs/database/security/user-security#section-variable

the documentation has you covered there: Authenticate Using Google Sign-In with JavaScript.
You can let your users authenticate with Firebase using their Google Accounts by integrating Google Sign-In into your app. You can integrate Google Sign-In either by using the Firebase SDK to carry out the sign-in flow, or by carrying out the Google Sign-In flow manually and passing the resulting ID token to Firebase.
Before you begin:
Add Firebase to your JavaScript project.
Enable Google Sign-In in the Firebase console:
In the Firebase console, open the Auth section.
On the Sign in method tab, enable the Google sign-in method and click Save.
Handle the sign-in flow with the Firebase SDK
If you are building a web app, the easiest way to authenticate your users with
Firebase using their Google Accounts is to handle the sign-in flow with the
Firebase JavaScript SDK. (If you want to authenticate a user in Node.js or
other non-browser environment, you must handle the sign-in flow manually.)
To handle the sign-in flow with the Firebase JavaScript SDK, follow these steps:
Create an instance of the Google provider object:
var provider = new firebase.auth.GoogleAuthProvider();
Optional: Specify additional OAuth 2.0 scopes that you want to request from the authentication provider. To add a scope, call addScope().
For example:
provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
See the authentication provider documentation.
Optional: To localize the provider's OAuth flow to the user's preferred language without explicitly passing the relevant custom OAuth parameters, update the language code on the Auth instance before starting the OAuth flow.
For example:
firebase.auth().languageCode = 'pt';
// To apply the default browser preference instead of explicitly setting it.
// firebase.auth().useDeviceLanguage();
Optional: Specify additional custom OAuth provider parameters that you want to send with the OAuth request. To add a custom parameter, call setCustomParameters on the initialized provider with an object containing the key as specified by the OAuth provider documentation and the corresponding value.
For example:
provider.setCustomParameters({
'login_hint': 'user#example.com'
});
Reserved required OAuth parameters are not allowed and will be ignored. See the authentication provider reference for more details.
Authenticate with Firebase using the Google provider object. You can prompt your users to sign in with their Google Accounts either by opening a pop-up window or by redirecting to the sign-in page. The redirect method is preferred on mobile devices.
To sign in with a pop-up window, call signInWithPopup:
firebase.auth().signInWithPopup(provider).then(function(result) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
Also notice that you can retrieve the Google provider's OAuth token which can be used to fetch additional data using the Google APIs.
This is also where you can catch and handle errors. For a list of error codes have a look at the Auth Reference Docs.
To sign in by redirecting to the sign-in page, call signInWithRedirect:
firebase.auth().signInWithRedirect(provider);
Then, you can also retrieve the Google provider's OAuth token by calling getRedirectResult() when your page loads:
firebase.auth().getRedirectResult().then(function(result) {
if (result.credential) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// ...
}
// The signed-in user info.
var user = result.user;
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});

Related

how to work with google calender API in Nodes js and React

my organization is creating a hotel booking website for another company. Apart from the website we are helping this company to create, They also have their rooms or spaces listed on the external website, Airbnb precisely. so they have the same spaces and hotels listed on the website we created for them and also in Airbnb. so we need to implement a way that won't allow their clients to book the same space or room at the same range of time. I.E if a client books a room at 9:45 am in the morning and will lodge only for 2 days, that room should no longer be available for the dates range the other user has booked it. So we decided to use the google calendar API as a middle man between our own website and AirbnB. if a user books any room or space in our website, on payment successful on the client at our ends here, The days the client wanna lodge should be added to the google calendar API to be exported to Airbnb and this should avoid bringing that consent screen google normally brings out to authenticate as we don't need to show anything to the client, we just want everything to be done underground. Most importantly I wanna do this using React.
I followed one node js video tutorial and he was able to create and read events successfully. he used the googleOauth2 playground API for authenticating. The API gave us a client Id, client Secret and most importantly a refresh token that would be used mainly to authenticate. This is the code below
// Require google from googleapis package.
const { google } = require('googleapis')
// Require oAuth2 from our google instance.
const { OAuth2 } = google.auth
// Create a new instance of oAuth and set our Client ID & Client Secret.
const oAuth2Client = new OAuth2(
'YOUR CLIENT ID GOES HERE',
'YOUR CLIENT SECRET GOES HERE'
)
// Call the setCredentials method on our oAuth2Client instance and set our refresh token.
oAuth2Client.setCredentials({
refresh_token: 'YOUR REFRESH TOKEN GOES HERE',
})
// Create a new calender instance.
const calendar = google.calendar({ version: 'v3', auth: oAuth2Client })
I didn't post everything because the remaining codes is just creating events and the likes. So I was following this format to make the events work here in react but I was unable to pass the authenticate block in react.
According to the documentation for javascript
It states that I have to add the script src file below to make gapi works for javascript
<script async defer type='text/Javascript' src="https://apis.google.com/js/api.js"></script>
But the problem now is how to implement the authentication using the refresh token available that was downloaded as json in react. The below code is how this was implemented using client and api key always showing windows for users to login before they can create events which in my case, I dont want
// Client ID and API key from the Developer Console
var CLIENT_ID = '<YOUR_CLIENT_ID>';
var API_KEY = '<YOUR_API_KEY>';
// Array of API discovery doc URLs for APIs used by the quickstart
var DISCOVERY_DOCS = ["https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest"];
// Authorization scopes required by the API; multiple scopes can be
// included, separated by spaces.
var SCOPES = "https://www.googleapis.com/auth/calendar.readonly";
/**
* Called when the signed in status changes, to update the UI
* appropriately. After a sign-in, the API is called.
*/
function updateSigninStatus(isSignedIn) {
if (isSignedIn) {
authorizeButton.style.display = 'none';
signoutButton.style.display = 'block';
listUpcomingEvents();
} else {
authorizeButton.style.display = 'block';
signoutButton.style.display = 'none';
}
}
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
// Listen for sign-in state changes.
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
// Handle the initial sign-in state.
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
authorizeButton.onclick = handleAuthClick;
signoutButton.onclick = handleSignoutClick;
}, function(error) {
appendPre(JSON.stringify(error, null, 2));
});
}
So the below is what have done but am stuck as I dont know what to else from here. I dont even know if what am doing is right or wrong
useEffect(()=>{
gapi.load("client:auth2", () => {
gapi.client
.init({
clientId: "myClientId",
clientKey: "myClientKey",
})
.setToken({
access_token:
"myRefreshToken",
});
});
},[])
I need to get pass authentication to be able to create, read events in google calendar in REACT

Redirecting user to specific URL after Firebase Authentication

I have web portal single page login form where on user account I want to redirect users to different URLs, for example. I have enabled email/password in firebase authentication: Now
if user A login then redirect him abc.com
if user B login then redirect him to xyz.com
if user c login then redirect him to abc123.com
I mean I want to allow or restrict different domains to different users. one way is to right js code with if-else and reidrect user to a specific domain. But this is not safe as the code exists in JS.
Edit (Dharmaraj Answer):
I have received Dharmaraj answer but there is a confusion or a clarification required.
Currently I am not allowing publically to register any user for security reason. I am just creating user on firebase console. So only the sign in required. Therefore the code snippet is slightly changed like this (Please correct me if it is secure).
firebase.auth().signInWithEmailAndPassword(username, password)
.then((userCredential) =>
{
var user = userCredential.user;
console.log(userCredential);
console.log(user);
//This code is not working
/*const userDocRef = firebase.firestore().collection("users").doc(userCredential.uid);
const domain = userDocRef.get().data();
//const domain = (await userDocRef.get()).data()
if(domain)
{
console.log(domain + " domain");
}*/
console.log(userCredential.uid + " uid");
console.log(userCredential.user.uid + " uid");
//getting user specic domain! Is this safe
firebase.firestore().collection("users").where('email','==', username).get().then((snapshot) =>{
console.log("user specifc data");
console.log(snapshot);
snapshot.docs.forEach(doc => {
console.log(doc.data());
window.location.href = doc.data(),domain;
break;
});
})
//unsecure way to redirect users to specific page
// window.location = "TrenoxPilot/index.html"; //redirect user to specific path
})
Further, the given security rule is not allowing me to get the records from firestore so i have changed like this but it is not save.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
Edit 2: Securtiy Rule Issue
One way is to right JS code with if-else and redirect user to a specific domain. But this is not safe as the code exists in JS.
That sounds like you don't want to hardcode all the domains in your code and allow users to check their domain only. In such cases, you should store user-specific data in a database or you can even use Custom Claims if it's just a single URL. You would have to use a Cloud function to set that claim whenever user sets their domain.
exports.setDomain = functions.https.onCall((data, context) => {
const {domain} = data;
const {uid} = context.auth
return admin
.auth()
.setCustomUserClaims(uid, { domain })
.then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
return {data: "Domain updated"}
});
});
Now whenever a user signs in, just check for this claim and redirect them.
firebase.auth().createUserWithEmailAndPassword(email, password)
.then(async ({user}) => {
// Signed in
const {claims} = await user.getIdTokenResult()
if (claims.domain) {
console.log("Domain present")
window.location.href = claims.domain
} else {
console.log("Domain is not present")
}
})
Do note that you can also store user's domain in a database like Firestore and read the value from it instead of custom claims. However, I'd prefer using custom claims if it's just a single URL to save some Firestore reads.
If you do not wish to use Cloud function then you can use Firestore (or realtime db). Whenever a user sets their domain, update it in their documents. Then you can read the domain when the user logs in.
firebase.auth().createUserWithEmailAndPassword(email, password)
.then(async ({user}) => {
// Signed in
const userDocRef = firebase.firestore().collection("users").doc(user.uid)
const snap = await userDocRef.get()
if (snap.exists) {
console.log("User Doc present")
const {domain} = snap.data()
if (domain) {
window.location.href = domain
} else {
console.log("Domain not present")
}
} else {
console.log("User doc is not present")
}
})
Make sure your security rules allow users to read their own document only by setting the following rule:
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
Edit:
The code in updated question does not work as intended since you are not handling promises correct here: const domain = userDocRef.get().data();
Try changing the security rules as in my answer above. Users will be able to read and write their own documentation only so I don't think there's any security risk involved.
It's OK to redirect with JS as long as the destination does a server-side check to make sure that user is allowed on the domain. (Similar to how you repeat server-side form validation even if you have client-side form validation.) So the flow would be:
User submits login form on example.com.
Firebase handles user auth and redirects to example.com/authRedirectHandler/
Then example.com/authRedirectHandler/ redirects again to the final destination (abc.com, xyc.com, ...) based on the user.
The destination domain then verifies that user is allowed on the domain. If not, you can return 403 not authorized instead of the protected content. (Or redirect back to the login page.)
I think you have to handle the Firebase auth redirect response from client-side JS (the access token is returned after a # to prevent it from reaching the server), but after getting the token, you can just do the redirect from the server.
Some notes:
It may not even be possible to have Firebase auth redirect directly to a different domain from the login form domain. I think the purpose of the 'authorized domains' setting is to limit the domains the login form can be called from.
Even if it is possible, the main use case is to use the same domain for the login form and the redirect URL.
If the domains are different you will probably run into some problems:
The Firebase authentication is stored in a browser cookie, which is probably tied to a single domain, the one with the signup form. You will have to figure out how to marshal this authentication cookie to the other domain.
It is bad UX to switch domains after submitting a login form. The user may get confused and/or not trust the site.

How can I check or verify that a user is signed in with AWS Cognito Javascript?

I am creating a React web app where the user sign in/up and other authentication related processes are being handled by AWS Cognito and the accompanying Javascript SDK.
My app has some 'public' routes/pages that everybody, signed in or not, can view, such as /documentation/ and /sign-in/. There also exist various private routes which you can only see when you are logged in, such as /my-documents/.
At the moment, I have a working sign in page, where a user is signed in with code very similar to use case #4 (Cognito Docs).
My question now is: as soon as a user goes to /my-documents/, how do I check whether the user is signed in and actually has the rights to see this page?
I am not using AWS Amplify for the authentication in my app. I only use the NPM package 'amazon-cognito-identity-js'.
This is the code I currently use to check if the session is valid, in other words if the user is successfully signed in. This however, seems like a cumbersome way to check such a simple status.
const isAuthenticated = () => {
const cognitoUser = userPool.getCurrentUser();
let isSessionValid = false;
if (cognitoUser) {
cognitoUser.getSession((err: Error, result: CognitoUserSession) => {
if (!err) {
isSessionValid = result.isValid();
}
});
}
return isSessionValid;
};
isSessionValid is returned before the callback in getSession is executed.

Administrator Views for a Firebase Web Application: How To

My Firebase web app requires administrator access, i.e., the UI should show a few things only for admins (an 'administrator' section). I came up with the below as a means to authorize the UI to display the admin section for valid admins only. My question is, good or bad? Is this a sound means of authorizing? ...so many ways to do this. This particular way requires me to configure admins in the security rules (vs in a node/tree in a db/firestore)
My idea is that if the .get() fails due to unauthorized access, I tell my app logic the user is not an admin, if the .get() succeeds my logic shows the 'admin' sections. Of course, the 'sections' are just HTML skeletons/empty elements populated by the database so even if the end user hacks the JS/logic, no real data will be there - only the empty 'admin section' framework.
function isAdmin(){
return new Promise(function(resolve, reject){
var docRef = firebase.firestore().collection("authorize").doc("admin");
docRef.get().then(function(result) {
if (result) {
resolve (true);
}
}).catch(function(error) {
resolve (false);
});
});
}
The firestore rule specifies the 'admins' by UID.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid == "9mB3UxxxxxxxxxxxxxxxxxxCk1";
}
}
}
You're storing the role of each user in the database, and then looking it up in the client to update its UI. This used to be the idiomatic way for a long time on realtime database, and it still works on Firestore.
The only thing I'd change is to have the rules also read from /authorize/admin, instead of hard-coding the UID in them. That way you only have the UID in one place, instead of having it in both the rules and the document.
But you may also want to consider an alternative: set a custom claim on your admin user, that you can then read in both the server-side security rules (to enforce authorized access) and the front-end (to optimize the UI).
To set a custom claim you use the Firebase Admin SDK. You can do this on a custom server, in Cloud Functions, but in your scenario it may be simpler to just run it from your development machine.
Detailed How To: Firebase has what's called Custom Claims for this functionality as detailed in their Control Access with Custom Claims and Security Rules. Basically, you stand up a separate node server, install the Firebase AdminSDK:
npm install firebase-admin --save
Generate/Download a Private Key from the Service Accounts tab in the Firebase Console and put that on your node server. Then simply create a bare bones node app to assign Custom Claims against each UID (user) that you wish. Something like below worked for me:
var admin = require('firebase-admin');
var serviceAccount = require("./the-key-you-generated-and-downloaded.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://xxxxxxxxxxxxxxxxxxxx.firebaseio.com"
});
admin.auth().setCustomUserClaims("whatever-uid-you-want-to-assign-claim-to", {admin: true}).then(() => {
console.log("Custom Claim Added to UID. You can stop this app now.");
});
That's it. You can now verify if the custom claim is applied by logging out of your app (if you were previously logged in) and logging back in after you update your web app's .onAuthStateChanged method:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
firebase.auth().currentUser.getIdToken()
.then((idToken) => {
// Parse the ID token.
const payload = JSON.parse(window.atob(idToken.split('.')[1]));
// Confirm the user is an Admin.
if (!!payload['admin']) {
//showAdminUI();
console.log("we ARE an admin");
}
else {
console.log("we ARE NOT an admin");
}
})
.catch((error) => {
console.log(error);
});
}
else {
//USER IS NOT SIGNED IN
}
});

how to retrieve and handle Redirect errors using firebase Auth?

I'm developing mobile first app, using Firebase Auth. Firebase recommends redirect instead of popup. However, I can't seem to find any example of retrieving errors on using Oauth providers (facebook ,Google). Firebase has an example of handling error in SignwithPopup , but fore redirect it only states:
This error is handled in a similar way in the redirect mode, with the
difference that the pending credential has to be cached between page
redirects (for example, using session storage).
We show where to do error handling for redirect operation in the previous section of the same doc: Just search for "firebase.auth().getRedirectResult()" in this page specifically in the catch here:
firebase.auth().getRedirectResult().then(function(result) {
if (result.credential) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// ...
}
// The signed-in user info.
var user = result.user;
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
By the way, adding multiple auth providers and handling linking accounts perfectly is actually quite tricky because there are many sub-flows to take into account (e.g. what if the user wants to link but then signs-in an account where the emails do not match...). I recommend you use Firebase UI which provides a configurable UI component that will handle all these tricky flows for you.

Categories