๐Ÿ”Adding Authentication

Setting up Firebase Authentication in Databutton

Databutton app includes a basic setup for authentication. Here's how to set it up and allowing users to sign in with email/password and Google.

Authentication Wrapper with Databutton Apps

The AuthWrapper template is included in the "components" section of any new app by default. Alternatively, one can create this wrapper just pasting the code below.

Auth Wrapper Template Code

Ensure to name the component as AuthWrapper

import React, { useState, useEffect } from "react";
import { initializeApp, FirebaseError } from "firebase/app";
import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut as firebaseSignOut, User, signInWithPopup, GoogleAuthProvider } from "firebase/auth";
import { Brain } from "../brain/Brain"
import { API_PATH } from "../constants"
import { Button, Input, FormControl, FormLabel, Heading, Divider, AbsoluteCenter, Box, FormErrorMessage, useToast, Spinner } from "@chakra-ui/react"
import { useForm, SubmitHandler } from 'react-hook-form'

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: "",
  measurementId: ""
};

const provider = new GoogleAuthProvider();
provider.addScope("https://www.googleapis.com/auth/userinfo.profile");

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

const signInWithGoogle = async (): Promise<User | null> => {
  try {
    const result = await signInWithPopup(auth, provider);
    return result.user;
  } catch (error: unknown) {
    // Ignore errors when user closes login window
    const shouldIgnoreError =
      error instanceof FirebaseError &&
      ["auth/popup-closed-by-user", "auth/cancelled-popup-request"].includes(
        error.code,
      );

    if (shouldIgnoreError) {
      return null;
    }

    throw error;
  }
};

export interface Props {
  children: React.ReactNode;
  title?: string;
  google?: boolean;
  email?: boolean;
}

interface FormData {
  email: string;
  password: string;
}

export const AuthWrapper = ({ children, google = true, email = true, title = "Welcome" }: Props) => {
  const user = useLoggedInUser()
  const toast = useToast()

  const { register, handleSubmit, formState: { errors }, reset } = useForm<FormData>({
    defaultValues: {
      email: '',
      password: ''
    }
  })
  const [isSigningIn, setIsSigningIn] = useState(false)

  const showDivider = google && email

  const onSubmit: SubmitHandler<FormData> = async (data) => {
    setIsSigningIn(true)
    try {
      await signInWithEmailAndPassword(auth, data.email, data.password);
      reset()
    } catch (err: unknown) {
      try {
        await createUserWithEmailAndPassword(auth, data.email, data.password);
        reset()
      } catch (err: unknown) {
        console.error(err)
        toast({
          status: 'error',
          title: 'Could not sign in. Please try again.'
        })
      }

    } finally {
      setIsSigningIn(false)
    }

  }

  if (user) {
    return <>{children}</>
  }

  return (
    <div className="flex w-full justify-center mt-20">
      <div className="flex flex-col gap-10">
        <Heading className="text-center">{title}</Heading>

        <div className="flex flex-col gap-10" style={{ width: "600px" }}>
          {email && (
            <form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
              <FormControl isInvalid={!!errors.email?.message}>
                <FormLabel>Email</FormLabel>
                <Input {...register('email', {
                  validate: {
                    isValidEmail: it => {
                      if (/^\S+@\S+\.\S+$/i.test(it)) {
                        return true
                      }

                      return 'Please enter a valid email address'
                    },
                    required: it => it.length > 0 ? true : 'This field is required.'
                  }
                })} placeholder="email@example.com" type="email" />
                {errors.email?.message && (
                  <FormErrorMessage>{errors.email.message}</FormErrorMessage>
                )}
              </FormControl>

              <FormControl isInvalid={!!errors.password?.message}>
                <FormLabel>Password</FormLabel>
                <Input {...register('password', {
                  validate: {
                    isOkPassword: it => {
                      if (it.length >= 8) {
                        return true
                      }

                      return 'Minimum length is 8 characters.'
                    },
                    required: it => it.length > 0 ? true : 'This field is required.'
                  }
                })} placeholder="********" type="password" />
                {errors.password?.message && (
                  <FormErrorMessage>{errors.password.message}</FormErrorMessage>
                )}
              </FormControl>

              <Button className="flex gap-2" type="submit" disabled={isSigningIn}>{isSigningIn ? <>Signing in... <Spinner size="sm" speed='0.65s' emptyColor='gray.200' /></> : 'Continue'}</Button>
            </form>
          )}

          {showDivider && (
            <Box position='relative'>
              <Divider />
              <AbsoluteCenter bg='white' px='4'>
                Or
              </AbsoluteCenter>
            </Box>
          )
          }

          {google && (
            <Button type="button" onClick={signInWithGoogle}>
              Continue with Google
            </Button>
          )}
        </div>
      </div>
    </div>
  );
};

export const useLoggedInUser = (): User | null => {
  const [user, setUser] = useState<User | null>(null)

  useEffect(() => {
    const unsubscribe = auth.onAuthStateChanged((user: User | null) => {
      setUser(user)
    })

    return () => {
      unsubscribe()
    }
  }, [])

  return user
}

export const signOut = () => firebaseSignOut(auth)

const isLocalhost = /localhost:\d{4}/i.test(window.location.origin);

const baseUrl = isLocalhost ? `${window.location.origin}${API_PATH}` : `https://api.databutton.com${API_PATH}`;

export const authedBrain = new Brain({
  baseUrl,
  baseApiParams: {
    credentials: "include",
    secure: true,
  },
  securityWorker: async () => {
    const idToken = await auth.currentUser.getIdToken()
    return {
      headers: {
        "x-authorization": `Bearer ${idToken}`,
      },
    }
  },
});

Setting up Firebase Authentication

  1. Getting started with the Firebase Console ( You can easily Log in with your Google account )

  1. Firebase configuration Setup - After registering the app, Firebase will display a snippet of code.

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_AUTH_DOMAIN",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_STORAGE_BUCKET",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID",
  measurementId: "YOUR_MEASUREMENT_ID"
};

Typically the config keys and values has this format. Take note of the firebaseConfig as you will need it later. You can also find it under Project Overview in Firebase later.

  1. Authentication Settings - Once the project is set up, navigate to the โ€œAuthenticationโ€ section in the Firebase console side menu.

  2. Configuring Sign-In Methods - In the Authentication panel, go to the โ€œSign-in methodโ€ tab. Configure sign-in providersโ€Šโ€”โ€ŠEmail/Password, Google.

  3. Adding Authorised Domains in Firebase

Detailed Steps

To ensure application securely manages where login attempts can originate from, follow these steps to add authorized domains to your Firebase project:

  1. Click on โ€œAuthenticationโ€ in the side menu.

  2. Select the โ€œSettingsโ€ tab.

  3. Scroll down to the โ€œAuthorized domainsโ€ section.

  4. Click on the โ€œAdd domainโ€ button.

  5. Enter databutton.com and confirm by clicking "Add"

  6. If your app is deployed with a custom domain, you must also authorize this domain to handle user logins.

  7. For example, if your app URL is https://avra.databutton.app/demo-authentication, add avra.databutton.app as an authorized domain.

  8. Repeat the process: Click โ€œAdd domainโ€, enter your custom domain like YOUR_USER_NAME.databutton.app, and then confirm by clicking "Add".

  1. Configuring your Databutton app - The final step involves adding the Firebase project ID to the Databutton secrets. This project ID is essential for the backend operations of your app. This project ID is same as in the config file which we obtain earlierโ€Š.

    We can do a manual Entryโ€Šโ€”โ€Š

    • Navigate to the โ€˜Configโ€™ tab in the Databutton.

    • Manually add the project ID as a secret with the name FIREBASE_PROJECT_NAME.

    Alternatively, we can ask the AI agent to that.

    Example Prompt

    I would need you to add my FIREBASE_PROJECT_NAME in the secrets. Can you help me doing that?

Implementing Firebase Authentication over the app

Protecting UI Pages

Now, once we have the firebase authentication set up done, we can start protecting our UI pages. Example prompt -

I would like to add the component #AuthWrapper and protect my page.
Code snippet from the example app

Here's an example of using AuthWrapper. Further you can also make changes by adding "Logout" button as shown in the code snippet below.

import React from "react";
import { AuthWrapper } from "components/AuthWrapper";
import { signOut } from "components/AuthWrapper";
import { ChakraProvider, extendTheme } from "@chakra-ui/react";
import { ChatInput } from "components/ChatInput";
import { Button } from '@chakra-ui/react'
// Extend the default theme to include a dark mode configuration
const theme = extendTheme({
  config: {
    initialColorMode: "light",
    useSystemColorMode: false,
  },
});

const App: React.FC = () => {
  return (
    <AuthWrapper>
      <ChakraProvider theme={theme}>
        <div className="min-h-screen bg-white">
          <div className="min-h-screen bg-white">
            <div className="container mx-auto px-4 py-8">
              <Button colorScheme="blue" onClick={signOut}>
                Logout
              </Button>
              <div className="my-4"></div>
              <ChatInput />
            </div>
          </div>
        </div>
      </ChakraProvider>
    </AuthWrapper>
  );
};

export default App;

Protecting the Capabilities (FastAPI routers)

Itโ€™s important to secure our router calls as well. This helps prevent unauthorised access to the backend services.

Here's an example prompt to do thatโ€Š-

Hey! I would like to protect this capability so that only authenticated users can use this. Can you do that for me?
Code snippet from the example app ( Comparing protected vs non protected capabilities )
  1. Non-protected Capabilities

from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import databutton as db
from openai import OpenAI

router = APIRouter()

class StartupIdeaRequest(BaseModel):
    startup_topic: str

class StartupIdeaResponse(BaseModel):
    startup_ideas: list[str]

@router.post("/generate-startup-ideas", tags=["stream"])
def generate_startup_ideas(body: StartupIdeaRequest):
    OPENAI_API_KEY = db.secrets.get("OPENAI_API_KEY")
    client = OpenAI(api_key=OPENAI_API_KEY)
    prompt = f"Generate 3 innovative startup ideas in the field of {body.startup_topic}, focusing on non-coder tech co-founders. Keep the ideas concise."
    def idea_generator():
        response = client.chat.completions.create(
            model="gpt-4-0125-preview",
            messages=[
                {"role": "system", "content": "You are an assistant that generates startup ideas."},
                {"role": "user", "content": prompt}
            ],
            stream=True
        )
        for chunk in response:
            if chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content
    return StreamingResponse(idea_generator(), media_type="text/plain")
  1. Protected Capabilities

from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import databutton as db
from openai import OpenAI
from databutton.experimental.auth.get_user import FirebaseUser
from fastapi import Depends

router = APIRouter()

class StartupIdeaRequest(BaseModel):
    startup_topic: str

class StartupIdeaResponse(BaseModel):
    startup_ideas: list[str]

@router.post("/generate-startup-ideas", tags=["stream"])
def generate_startup_ideas(body: StartupIdeaRequest, user: FirebaseUser = Depends(db.experimental.auth.get_firebase_user)):
    OPENAI_API_KEY = db.secrets.get("OPENAI_API_KEY")
    client = OpenAI(api_key=OPENAI_API_KEY)
    prompt = f"Generate 3 innovative startup ideas in the field of {body.startup_topic}, focusing on non-coder tech co-founders. Keep the ideas concise."
    def idea_generator():
        response = client.chat.completions.create(
            model="gpt-4-0125-preview",
            messages=[
                {"role": "system", "content": "You are an assistant that generates startup ideas."},
                {"role": "user", "content": prompt}
            ],
            stream=True
        )
        for chunk in response:
            if chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content
    return StreamingResponse(idea_generator(), media_type="text/plain")

Note the usage of defined arguments - def generate_startup_ideas(body: StartupIdeaRequest, user: FirebaseUser = Depends(db.experimental.auth.get_firebase_user)) Also, the usage of additional dependencies.

from databutton.experimental.auth.get_user import FirebaseUser
from fastapi import Depends

Under the hood, Databutton takes care of all these.

Updating Component with the protected capability

Untill now we have implemented a layer of proetction over our capability. But we have not updated the component where this capability ( FastAPI endpoint) has been used. Therefore, in our case we have the enpoint used over the ChatInput UI component and thus we will ask the agent to make the necessary changes / updates. Here's how it can be prompted -

My #AI Chatbot is updated with aunthentication. Can you read and update the changes here as well.
Code snippet from the example app
import React from "react";
import ReactMarkdown from "react-markdown";
import { authedBrain } from "components/AuthWrapper"; // Import authedBrain for authenticated requests
import {
  Box,
  Button,
  Input,
  useColorModeValue,
  VStack,
  Heading,
} from "@chakra-ui/react";
import { StartupIdeaRequest } from "types"; // Import the type for request

const ChatInput = () => {
  const bgColor = useColorModeValue("gray.800", "gray.800");
  const color = useColorModeValue("#000000", "#000000");

  const [startupIdeas, setStartupIdeas] = React.useState<string[]>([]);

  const [ideaKey, setIdeaKey] = React.useState(0);

  const [startupIdeasText, setStartupIdeasText] = React.useState(""); // Maintain streamed content as a single string

  const [inputValue, setInputValue] = React.useState("");
  const [isLoading, setIsLoading] = React.useState(false);

  const handleInputChange = (e) => setInputValue(e.target.value);

  const handleSubmit = async () => {
    setIsLoading(true);
    try {
      setStartupIdeas([]); // Reset startup ideas before streaming new ones
      setStartupIdeasText(""); // Reset startup ideas text before streaming new ones
      const requestPayload: StartupIdeaRequest = { startup_topic: inputValue };
      for await (const chunk of authedBrain.generate_startup_ideas(
        requestPayload,
      )) {
        setStartupIdeasText((prevText) => `${prevText}${chunk}`);
      }
    } catch (error) {
      console.error(error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <VStack spacing={4} bg="#FFFFFF" p={5} borderRadius="md">
      <Heading color={color}>Generate Startup Ideas</Heading>
      <Input
        placeholder="Enter your idea..."
        value={inputValue}
        onChange={handleInputChange}
        color="#000000"
        borderColor="gray.300"
      />
      <Button colorScheme="blue" onClick={handleSubmit} isLoading={isLoading}>
        Get Ideas
      </Button>
      <Box className="prose" p={5} bg="#FFFFFF" color={color} borderRadius="md">
        <ReactMarkdown>{startupIdeasText}</ReactMarkdown>
      </Box>
    </VStack>
  );
};

export { ChatInput };

It's important to note that once the capability is protected, the Agent uses -

authedBrain

instead of ,

brain
for await (const chunk of authedBrain.generate_startup_ideas(
        requestPayload,
      )) {
        setStartupIdeasText((prevText) => `${prevText}${chunk}`);
      }

Databutton takes care of all these implementation under-the-hood again.

Advanced cases to acess profile picture or user names

All the additional changes required is in the UI page. Otheriwse, everything from the backend and UI components remains same as it is.

Code Snippet
import React from "react";
import { AuthWrapper, useLoggedInUser, signOut } from "components/AuthWrapper";
import {
  ChakraProvider,
  extendTheme,
  Flex,
  Box,
  Button,
  Heading,
  Avatar,
} from "@chakra-ui/react";
import { ChatInput } from "components/ChatInput";
// Extend the default theme to include a dark mode configuration
const theme = extendTheme({
  config: {
    initialColorMode: "light",
    useSystemColorMode: false,
  },
});

const Test: React.FC = () => {
  const user = useLoggedInUser(); // Get the logged in user

  return (
    <AuthWrapper>
      <ChakraProvider theme={theme}>
        <Flex
          className="w-full justify-between items-center flex-row gap-10 mt-20 px-10"
          direction="row"
          align="center"
          justify="space-between"
        >
          <Box className="flex gap-5">
            {user?.photoURL && <Avatar src={user.photoURL} />}
            <Heading size="md">{user?.displayName}</Heading>
          </Box>
          <Button colorScheme="blue" onClick={signOut}>
            Logout
          </Button>
        </Flex>
        <Box className="min-h-screen bg-white">
          <Box className="container mx-auto px-4 py-8">
            <ChatInput />
          </Box>
        </Box>
      </ChakraProvider>
    </AuthWrapper>
  );
};

export default Test;

Last updated