Russell Seymour
Russell Seymour

Reputation: 1423

How can I setup a whitelist of users for access to a React website?

I have created a website for my son's school class. I am using React with Firebase and have got all the authentication through Social Media sorted.

However I want to have a table in Firebase of permitted users, which will check the social media login and see if that person is able to access the website. I have the table called permitted (which is just a list of usernames) and I created the following function to check to see if that user is authorized to access the site:

    const isWhitelisted = async (username: string) => {
        let result: boolean = false;

        if (username)
        {
            // check the user against the whitelist of approved people
            projectFirestore.collection("permitted")
                .where("username", "==", username)
                .get()
                .then((snapshot) => {
                    snapshot.forEach((doc) => {
                        if (doc.data().enabled)
                        {
                            result = true;
                        }
                    })
                })
        }

        return result;
    }  

The problem I have is that I am not sure when to call this function after authentication.

I have an auth module which exports SignIn:

import firebase from 'firebase';
import {auth} from '../../config/firebase';

const SignIn = (provider: firebase.auth.AuthProvider) => 
    new Promise<firebase.auth.UserCredential>((resolve, reject) => {
        auth.signInWithPopup(provider)
            .then(result => resolve(result))
            .catch(error => reject(error));
    });

export {SignIn};

And a LoginPage.tsx that allows people to log into the site:

import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import firebase from "firebase";

import IPageProps from "../../interfaces/page.interface";
import { SignIn } from "../../modules/auth";
import { Providers } from "../../config/firebase";

import Title from '../../comps/Title';
import SiteNavbar from "../../comps/Navbar";
import { projectFirestore } from "../../config/firebase";

const LoginPage: React.FC<IPageProps> = props => {
    
    const [authenticating, setAuthenticating] = useState<boolean>(false);
    const [error, setError] = useState<string>('');
    const history = useHistory();

    const signIn = (provider: firebase.auth.AuthProvider) => {
        if (error !== "") setError("");

        setAuthenticating(true);

        SignIn(provider)
            .then(result => {
                history.push("/photos");
            })
            .catch(error => {
                setAuthenticating(false);
                setError(error.message);
            })
    }

    return (
        <div>
        <SiteNavbar />
        <div className="AuthLogin">
            
            <div className="auth-main-container">
                <div>
                    <h1>Welcome to Website</h1>
                </div>
                <div className="auth-btn-wrapper">
                    <button
                        disabled={authenticating}
                        onClick={() => signIn(Providers.google)}
                    >
                        Login in with Google
                    </button>
                </div>
            </div>
        </div>
        </div>
    )
}

export default LoginPage;

And then I have a UserPrivider.tsx that handles the authentication state change:

import React, { Component, createContext } from "react";
import firebase from "firebase/app";
import { auth, generateUserDocument } from "../config/firebase";

const UserContext = createContext<firebase.User | null>(null);

class UserProvider extends Component {

    state = {
        user: null
    };

    componentDidMount = async () => {
        auth.onAuthStateChanged(async userAuth => {
            const user = await generateUserDocument(userAuth);
            this.setState({
                user
            })
        })
    }

    render() {
        const { user } = this.state;
        return (
            <UserContext.Provider value={user}>
                {this.props.children}
            </UserContext.Provider>
        )
    }
}

export { UserProvider, UserContext };

I thought the best place to put the isWhitelist would be after the then(result) function in the LoginPage.tsx but then I have bumped into asynchronous issues.

Have I taken the correct approach here is is there a better more recognosed way of dealing with authorization that I have completely missed?

Upvotes: 1

Views: 1525

Answers (1)

Dharmaraj
Dharmaraj

Reputation: 50830

You cannot prevent anyone from logging in but you can restrict their access to your Firestore using security rules. You can check if the user is present in your permitted collection as shown below.

service cloud.firestore {
  match /databases/{database}/documents {
    match /collection/{doc} {
      allow create: if request.auth != null && exists(/databases/$(database)/documents/permitted/$(request.auth.uid))
    }
  }
}

This rule will allow user to create a document in the collection only if a document with user's UID as the key in "permitted" collection.

However you cannot check this in storage rules or realtime databases so I'd recommend using Custom Claims. You can access these in security rules by using request.auth.token.<claim_name>.

All this is necessary as any frontend redirects or conditional rendering can be bypassed. On the frontend, the best you can do is when the user logs in, check if their UID is present in your permitted collection or if they have the permitted custom claim which can be done like this:

firebase.auth().onAuthStateChanged(async (user) => {
  if (user) {
    // User is signed in, 
    // 1] Checking Custom Claims
    // const {claims} = await user.getIdTokenResult()
    // if (!claims.permitted) { // redirect and force logout }

    // 2] checking in permitted collection
    const permittedRef = firebase.firestore().collection("permitted").doc(user.uid)
    if (!(await permittedRef.get()).exists) { // Redirect and force logout }
  } else {
    // User is signed out
    // Redirect to login page
  }
});

You can use the get() method in Firestore security rules to read document and validate the data.

allow read: get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true;

If you want to read data from the document that you are reading, then you can access the data like this:

match /collection/{docID} {
  allow read: if resource.data.field == 'value';
}

Upvotes: 1

Related Questions