Tim Carlson
Winter 2025

<info 340/>

Firebase Authentication

View of the Day

  • FirebaseUI (code demo)

  • Managing Auth State (code demo)

  • Firebase Image Uploading (code demo)

Questions?

Firebase Demo 

Firebase Demo 

import React from 'react';
import ReactDOM from 'react-dom/client';
import { initializeApp } from "firebase/app";
import { BrowserRouter } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.css';
import './index.css';

import App from './components/App';

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "AIzaSyDb5rO7-hBq0PJ3emVEwv1Uwz8K3Dtz7kk",
  authDomain: "react-chat-wi23a.firebaseapp.com",
  projectId: "react-chat-wi23a",
  storageBucket: "react-chat-wi23a.appspot.com",
  messagingSenderId: "716444400015",
  appId: "1:716444400015:web:a3c6f14042da5bf100f3fa"
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

main.jsx

config isn't same as in our sample code

Firebase Demo 

iimport React, { useState, useEffect } from 'react';
import { Routes, Route, Outlet, Navigate, useNavigate } from 'react-router-dom';
import { HeaderBar } from './HeaderBar.jsx';
import ChatPage from './ChatPage.jsx';
import SignInPage from './SignInPage.jsx';
import ProfilePage from './ProfilePage.jsx';
import * as Static from './StaticPages.jsx';
import DEFAULT_USERS from '../data/users.json';

export default function App(props) {
  //state
  const [currentUser, setCurrentUser] = useState(DEFAULT_USERS[0]) //default to null user

  const navigateTo = useNavigate(); //navigation hook

  //effect to run when the component first loads
  useEffect(() => {
    //log in a default user
    // loginUser(DEFAULT_USERS[1])

  }, []) //array is list of variables that will cause this to rerun if changed

  const loginUser = (userObj) => {
    console.log("logging in as", userObj.userName);
    setCurrentUser(userObj);
    if(userObj.userId !== null){
      navigateTo('/chat/general'); //go to chat after login
    }
  }

 //... more App on next slide

App part 1

No longer initially logging in a user in App

Reminder this will be passed as a prop to  the Sign-in Component

Firebase Demo 

	//... App continued	
  return (
    <div className="container-fluid d-flex flex-column">
      <HeaderBar currentUser={currentUser} />

      <Routes>
        <Route index element={<Static.WelcomePage />} />
        <Route path="about" element={<Static.AboutPage />} />
        <Route path="*" element={<Static.ErrorPage />} />
        <Route path="signin" element={<SignInPage currentUser={currentUser} loginCallback={loginUser}/>} />

        {/* protected routes */}
        <Route element={<ProtectedPage currentUser={currentUser} />}>
          <Route path="chat/:channelName" element={<ChatPage currentUser={currentUser} />} />
          {/* redirect to general channel */}
          <Route path="chat" element={<Navigate to="/chat/general" />} />
          <Route path="profile" element={<ProfilePage currentUser={currentUser} />}/>
        </Route>
      </Routes>
    </div>
  );
}

function ProtectedPage(props) {
  if(props.currentUser.userId === null) { //if no user, send to sign in
    return <Navigate to="/signin" />
  }
  else { //otherwise, show the child route content
    return <Outlet />
  }
}

App part 2 

import React from 'react';
import { Link, NavLink } from 'react-router-dom';

export function HeaderBar(props) {
  const currentUser = props.currentUser;

  const handleSignOut = (event) => {
    console.log("signing out");
  }  
  
  return (
    <header className="text-light bg-primary px-1 d-flex justify-content-between">
      <h1>React Chat</h1>
      <ul className="nav nav-pills">
        <li className="nav-item">
          <NavLink className="nav-link" to="/">Home</NavLink>
        </li>
        <li className="nav-item">
          <NavLink className="nav-link" to="/chat">Chat</NavLink>
        </li>
        <li className="nav-item">
          <NavLink className="nav-link" to="/about">About</NavLink>
        </li>
        {currentUser.userId && 
          <>
            <li className="nav-item">
              <NavLink to="/profile" className="nav-link">Profile</NavLink>
            </li>
            <li className="nav-item">
              <button className="btn btn-secondary ms-2" onClick={handleSignOut}>Sign Out</button>
            </li>
          </>
        }
        {!currentUser.userId &&
          <li className="nav-item">
            <NavLink className="nav-link" to="/signin">
              <img src={currentUser.userImg} alt={currentUser.userName + " avatar"} />
            </NavLink>
          </li>
        }
      </ul>
    </header>
  )
}

Firebase Demo 

HeaderBar

If we have user, show the Profile Component

If no user, show Sign-in Component

Callback for the onClick on the SignOut Button

Firebase Demo 

export default function ChatPage(props) {
  const [chatMessages, setChatMessages] = useState([]);
...
  useEffect(() => {
    const db = getDatabase(); //"the database"
    const allMessageRef = ref(db, "allMessages");
    
  	const offFunction = onValue(allMessageRef, (snapshot) => {   
    const valueObj = snapshot.val();
    const objKeys = Object.keys(valueObj); 
    const objArray = objKeys.map((keyString) => {
    const theMessageObj = valueObj[keyString];
        theMessageObj.key = keyString;
        return theMessageObj;
      })
      setChatMessages(objArray);
    })

    function cleanup() {
      console.log("component is being removed");
    offFunction();     //when the component goes away, we turn off the listener
    }
    return cleanup;
  }, [])

  const addMessage = (messageText) => {
    const userObj = currentUser;
    const newMessage = {
      "userId": userObj.userId,
      "userName": userObj.userName,
      "userImg": userObj.userImg,
      "text": messageText,
      "timestamp": Date.now(),
      "channel": currentChannel
    }

    const db = getDatabase(); //"the database"
    const allMessageRef = ref(db, 'allMessages');
    firebasePush(allMessageRef, newMessage);
  }
//... More ChatPage below, but won't show on the next slide

ChatPage

The addMessage callback will get the reference to the allMessages key, and then push the new message to the database on Firebase

Use Effect hook to load the messages from firebase when component loads 1st time

Register 'onValue' listener for any change to the Messages on Firebase

If there is a change we get the new messages array and set state to cause re-render

Firebase Demo 

import React, { useState } from 'react';

export default function ProfilePage(props) {
  //convenience
  const displayName = props.currentUser.userName;

  const [imageFile, setImageFile] = useState(undefined)
  let initialURL = props.currentUser.userImg;
  const [imageUrl, setImageUrl] = useState(initialURL)
 
  const handleChange = (event) => {  //image uploading!
    if(event.target.files.length > 0 && event.target.files[0]) { 
      const imageFile = event.target.files[0]
      setImageFile(imageFile);
      setImageUrl(URL.createObjectURL(imageFile));
    }
  }

  const handleImageUpload = (event) => {
    console.log("Uploading", imageFile);
  }

  return (
    <div className="container">
      <h2>
        {props.currentUser.userName && displayName+"'s"} Profile
      </h2>

      <div className="mb-5 image-upload-form">
        <img src={imageUrl} alt="user avatar preview" className="mb-2"/>
        <label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
        <button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
        <input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
      </div>
    </div>
  )
}

Profile Component 

Firebase Auth

 

Set Up Firebase if you haven't already

  • Create new Firebase Project in Firebase.  

 

Include Firebase in your React app

  • Install (npm install firebase)
  • Import firebase into your app
  • Copy and Paste the config from Firebase Web Console into your code
  • Manage User Profiles
  • Handle Authentication Events (Sign In and Out)

 

 

Email Enum Protection

On Sep 15 2023 Google added additional security features to avoid login information being leaked from sign-in forms. This can potentially cause issues when doing frequent log-ins for Firebase.

As a workaround, when doing development, you can turn off this feature (just remember to turn it back on for deployment!)

Firebase Auth with Firebase UI Component

 

Include FirebaseUI in your React app

  • Install (npm install react-firebaseui)
  • Import StyledFirebaseAuth into your app
  • Copy and Paste the uiConfig from FirebaseUI website into your code
  • Manage the signout
  • Handle Authentication Events through effect hook

 

 

FirebaseUI

A library (provided by Firebase) that created a sign-in form for your page.

React 18 workarounds

The React bindings for FirebaseUI (firebaseui-web-react) has not been updated for React 18 yet, and appears to have been abandoned. See the open pull request from Gabriel Villenave.

# Install library (on command line)
npm install https://gitpkg.now.sh/gvillenave/firebaseui-web-react/dist 
--legacy-peer-deps

Until the pull request is accepted, one work around is to install the updated fork instead of the usual package. And even that version doesn't work with React 19, so you need to force the install.

Signin In with FirebaseUI

The FirebaseUI library provides pre-defined UI elements including a "login form" that supports the different authentication methods for the different providers

The <StyledFirebaseAuth> Component can be rendered like any other Component. It requires 2 props:

  • uiConfig - An option containing the configuration values for the login form
  • firebaseAuth - A reference to the "authenticator" service that handles logging in and out. You get access to this through the 'getAuth() function, similar to the 'getDatabase()' function.

We will install the fork instead

import React from 'react';
import { getAuth, EmailAuthProvider, GoogleAuthProvider } from 'firebase/auth';
import { StyledFirebaseAuth } from 'react-firebaseui';
...
export default function SignInPage(props) {
  const currentUser = props.currentUser;
  const loginFunction = props.loginCallback;
  
  const auth = getAuth(); //the authenticator

  const configObj = {
    signInOptions: [
      { 
        provider: EmailAuthProvider.PROVIDER_ID,
        requireDisplayName: true,
      },
      {
        provider: GoogleAuthProvider.PROVIDER_ID
      }
    ],
    signInFlow: 'popup',
    callbacks: {
      signInSuccessWithAuthResult: () => false //don't do anything special on signin
    },
    credentialHelper: 'none'
  }
...
  return (
    <div className="card bg-light">
      <div className="container card-body">
        <StyledFirebaseAuth firebaseAuth={auth} uiConfig={configObj} />
 
        {/* <p className="lead">Pick a user:</p>
        <Dropdown>
...
		</Dropdown> */}
      </div>
    </div>
  )
}

Add firebase config script config and initialize

import

Render component with 2 props

import

get the authenticator

signin

Creating users

Make it easy to test authentication by having consistent simple usernames and passwords

 

  • a@a.com - password: 'password'
  • b@b.com - password: 'password'
  • etc...

Authentication Events (sign in or out)

It is recommended to register an event listener to determine which user is logged in This event occurs when the page first loads and Firebase determines that a user has previously signed up (the “initial state” is set), or when a user logs out. You can register this listener by using the onAuthStateChanged method:

The firebase.auth() variable tracks which User is currently logged in. This info persists even after the browser is closed. Every time page is reloaded, the firebase.auth() function will perform the authentication and “re-login” the user

Firebase Demo 

import React, { useState, useEffect } from 'react';
import { Routes, Route, Outlet, Navigate, useNavigate } from 'react-router-dom';

import { getAuth, onAuthStateChanged } from 'firebase/auth';
...
import DEFAULT_USERS from '../data/users.json';

export default function App(props) {
  //state
  const [currentUser, setCurrentUser] = useState(DEFAULT_USERS[0]) //default to null user

  const navigateTo = useNavigate(); //navigation hook

   //effect to run when the component first loads
   useEffect(() => {
    //log in a default user
    // loginUser(DEFAULT_USERS[1])

    const auth = getAuth();
    //                 authenticator, a callback
    onAuthStateChanged(auth, (firebaseUser) => {
      if(firebaseUser) {
        console.log("signing in as", firebaseUser.displayName)
        console.log(firebaseUser);
        firebaseUser.userId = firebaseUser.uid;
        firebaseUser.userName = firebaseUser.displayName;
        firebaseUser.userImg = firebaseUser.photoURL || "/img/null.png";
        setCurrentUser(firebaseUser);
      }
      else { //no user
        console.log("signed out");
        setCurrentUser(DEFAULT_USERS[0]); //change the null user
      }
    })


  }, []) //array is list of variables that will cause this to rerun if changed
  
  ...

Get App to listen for authchanges and setState

In effect hook (runs 1st time component renders

import getAuth and onAuthStateChanged

getAuth object

Register onAuthStateChanged Listener

If there's a firebaseUser, add some additional fields for user object

Then setState for our currentUser state var

If there is no firebase user, we're signed out

Firebase Demo 

Signout


import { getAuth, signOut } from 'firebase/auth';

const handleSignOut = (event) => {
    //sign out here
    signOut(getAuth());
  }


//...

   <li className="nav-item">
            <button className="btn btn-secondary ms-2" onClick={handleSignOut}>Sign Out</button>
          </li>
...

  //little hacky
  if(currentUser.userId) { //if signed in
    return<Navigate to="/chat/general"/>
  }

  return (
    <div className="card bg-light">
      <div className="container card-body">
	      <StyledFirebaseAuth  uiConfig={configObj} firebaseAuth={auth} />
      </div>
    </div>
  )
}

Signout - in Header.jsx


import { getAuth, signOut } from 'firebase/auth';

const handleSignOut = (event) => {
    //sign out here
    signOut(getAuth());
  }
//...
   <li className="nav-item">
            <button className="btn btn-secondary ms-2" onClick={handleSignOut}>Sign Out</button>
          </li>

Start App with user Signed - in SignInPage.jsx

ProfilePage.jsx - initially before image upload

import React, { useState } from 'react';

export default function ProfilePage(props) {
 const { currentUser } = props;
  //convenience
  const displayName = currentUser.userName;

  const [imageFile, setImageFile] = useState(undefined)
  const [imageUrl, setImageUrl] = useState(currentUser.userImg) 

  //image uploading!
  const handleChange = (event) => {
    if(event.target.files.length > 0 && event.target.files[0]) {
      const imageFile = event.target.files[0]
      setImageFile(imageFile);
      setImageUrl(URL.createObjectURL(imageFile));
    }
  }
  
  const handleImageUpload = (event) => {
    console.log("Uploading", imageFile);
  }

  return (
    <div className="container">
      <h2>
        {props.currentUser.userName && displayName+"'s"} Profile
      </h2>
      <div className="mb-5 image-upload-form">
        <img src={imageUrl} alt="user avatar preview" className="mb-2"/>
        <label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
        <button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
        <input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
      </div>
    </div>
  )
}

Two state variables

On a file change, we get the file, setState on the file, and then create a url, and setState on the imageurl.

This is just a javascript function to create a url from a file

note display d-none

this is where we will  upload the file (slides 26 and 27)

ProfilePage.jsx - make file upload look better

import React, { useState } from 'react';

export default function ProfilePage(props) {
  const displayName = props.currentUser.userName;
  const [imageFile, setImageFile] = useState(undefined)
  let initialURL = props.currentUser.userImg;
  
  const [imageUrl, setImageUrl] = useState(initialURL)

  const handleChange = (event) => {	  //image uploading!
    if(event.target.files.length > 0 && event.target.files[0]) {
      const imageFile = event.target.files[0]
      setImageFile(imageFile);
      setImageUrl(URL.createObjectURL(imageFile));
    }
  }
  
  const handleImageUpload = (event) => {
    console.log("Uploading", imageFile);
  }

  return (
    <div className="container">
      <h2>
        {props.currentUser.userName && displayName+"'s"} Profile
      </h2>
      <div className="mb-5 image-upload-form">
        <img src={imageUrl} alt="user avatar preview" className="mb-2"/>
        <label htmlFor="imageUploadInput" className="btn btn-sm btn-secondary me-2">Choose Image</label>
        <button className="btn btn-sm btn-success" onClick={handleImageUpload}>Save to Profile</button>
        <input type="file" name="image" id="imageUploadInput" className="d-none" onChange={handleChange}/>
      </div>
    </div>
  )
}

label 'htmlFor' is a label for the 'file' input

file input is hidden

The label styled as button, and since its set with htmlFor to the file input it behaves as if clicking on the "choose file"

Firebase Storage

Allows us to store images, videos, other files

The real time database pretty much stores Strings. You can't store images there.

 

Firebase Storage is used to storing "large" files (e.g., images) that are not JSON compatible.

 

As of October 30 2024, Firebase only offers Storage on a "pay-as-you-go" plan. But you DO NOT need to provide credit card information for any service in this class. For workarounds, see the announcement on Ed Discussion.

Saving profile images: Profile.jsx part 1

import React, { useState } from 'react';
import { getApp } from "firebase/app";
import { getStorage, ref, uploadBytes, getDownloadURL} from 'firebase/storage';
import { updateProfile} from 'firebase/auth';
import { getDatabase, ref as dbRef, set as firebaseSet } from 'firebase/database';

export default function ProfilePage(props) {
  
  const currentUser = props.currentUser;
  const displayName = props.currentUser.userName;

  const [imageFile, setImageFile] = useState(undefined)
  let initialURL = props.currentUser.userImg;
  const [imageUrl, setImageUrl] = useState(initialURL)

  const handleChange = (event) => {
    if(event.target.files.length > 0 && event.target.files[0]) {
      const imageFile = event.target.files[0]
      setImageFile(imageFile);
      setImageUrl(URL.createObjectURL(imageFile));
    }
  }

...//more on next slide

imports to upload storage, update profile, get access to db

Saving profile images: Profile.jsx part 2

...//profile bottom half of file

const handleImageUpload = async (event) => {
    
	const storage = getStorage(getApp(), "gs://info340-storage.firebasestorage.app")
    const imageRef = ref(storage, "demo/userImages/"+currentUser.uid+".png");
    await uploadBytes(imageRef, imageFile)

    // get the url to this uploaded file so we can reference it from the web
    const downloadUrlString = await getDownloadURL(imageRef);
    console.log(downloadUrlString);

    
    await updateProfile(currentUser, { photoURL: downloadUrlString} );

    
    const db = getDatabase();
    const refString = "userData/"+currentUser.userId+"/imgUrl";
    console.log(refString);
    const userImgRef = dbRef(db, "userData/"+currentUser.userId+"/imgUrl")
    await firebaseSet(userImgRef, downloadUrlString);

  }
  return (
 ...
  )
}

asynchronous 

getStorage to shared class firebase storage db

get ref to storage location 

upload bytes there

get the url to saved image

put image in the userProfile db

store it in the real time db too so we can show it elsewhere easily

put image in the userProfile db

Action Items!

  • Complete task list for Week 10 (all items)

  • Review everything

  • Project due Monday 03/17!!!

 

Next time: Conclusions; work time?

 

If you are coming to Class Thursday. Here is a link to let me know  you are coming so I get the correct amount of pizza.

  const handleImageUpload = async (event) => {
    //upload the file to the storage db
    console.log("Uploading", imageFile);

// option 1 - where you use the shared class storage database. With this version you also have 
// to import 'getDatabase' from 'firebase/database'
    const storage = getStorage(getApp(), "gs://info340-storage.firebasestorage.app")
    const imageRef = ref(storage, "demo/userImages/" + currentUser.uid + ".png");
    
// // Option 3 - where you use your own storage database in your project
//     const storage = getStorage(); 
//     const imageRef = ref(storage, "userImages/"+currentUser.userId+".png");

    
    await uploadBytes(imageRef, imageFile)

    // get the url to this uploaded file so we can reference it from the web
    const downloadUrlString = await getDownloadURL(imageRef);
    console.log(downloadUrlString);

    //put in user profile
    await updateProfile(currentUser, { photoURL: downloadUrlString });

    //also put in real time database (for fun)
    const db = getDatabase();
    const refString = "userData/" + currentUser.userId + "/imgUrl";
    console.log(refString);
    const userImgRef = dbRef(db, "userData/" + currentUser.userId + "/imgUrl")
    await firebaseSet(userImgRef, downloadUrlString);

  }

Firebase storage Options 1 and 2

   // This is the code if you are using the real time database
  const handleImageUpload = async (event) => {
    //upload the file to the storage db
    console.log("Uploading", imageFile);
  //config: create a file reader and callback for what to do when the file is read
  const reader = new FileReader();
  reader.onload = function (e) {
    //get the read file (read as dataURL and get its url 
    const dataUrl = e.currentTarget.result

    const db = getDatabase();
    const userImgRef = dbRef(db, "userData/" + currentUser.userId + "/imgUrl")
    //you can put this data url into the firebase db 
    firebaseSet(userImgRef, dataUrl)
  }

  //call this function to read the file (and trigger the above callback)
  reader.readAsDataURL(imageFile) //initiate the reader
  }

Firebase storage Option 3

info340wi25-firebase-auth

By Tim Carlson

info340wi25-firebase-auth

  • 245