InfoThis documentation is just for PropelAuth Components, an optional library for those who want deeper design control over their UIs.Click here to view our standard documentation

Login and Signup API Guide

Logging or signing up a user takes more than just designing a login page. There are many edge cases to consider, such as enforcing MFA, if the user is missing required User Properties, or if the user needs to confirm their email address. This guide will help you understand how to use the APIs provided by PropelAuth to handle these cases.

Building Required Pages

The first step is to build the necessary pages to handle the full login and signup flow. This library provides all the APIs you'll need to get started, but for your convenience, here are some common pages you may need to build and the APIs to help you do so.

Page TypeAPIs
Login PageemailPasswordLogin, passwordlessLogin, loginViaSamlForOrg, loginWithSocialProvider
Signup Pagesignup, passwordlessLogin, loginViaSamlForOrg, loginWithSocialProvider
Request Reset Password PagesendForgotPasswordEmail
Update Password PageupdatePassword
Confirm Email PageresendEmailConfirmation
Update User Properties PageupdateUserMetadata
Join Org PagefetchJoinableOrgs, joinOrg
Enroll In 2FA PagefetchMfaStatusWithNewSecret, enableMfa
Verify 2FA PageverifyMfaForLogin, verifyMfaBackupCodeForLogin

Once you have built these pages, how do you know which page to show the user? This is where the concept of Login State comes in.

Login State

Our Login and Signup APIs were built from the ground up around the concept of a Login State. This state is a way for you to know what the user needs to do next to complete the login process. For example, if the user needs to log in, you can show them a login form. If the user needs to confirm their email, you can show them a message to check their email.

But how do you know what the user's Login State is? You can use the fetchLoginState API which will return a LoginState enum that you can use to determine what page to show the user.

Here is each possible Login State the user can be in:

  • LOGIN_REQUIRED - The user needs to log in.
  • TWO_FACTOR_REQUIRED- The user has enrolled in 2FA but needs to enter their code.
  • TWO_FACTOR_ENROLLMENT_REQUIRED - The user needs to enroll in 2FA because an organization they are in requires it
  • EMAIL_NOT_CONFIRMED_YET - A confirmation email has been sent to the user and they need to accept it.
  • USER_MISSING_REQUIRED_PROPERTIES - The user is missing required User Properties such as username, first name, last name, etc.
  • USER_MUST_BE_IN_AT_LEAST_ONE_ORG - You have the "User must be in at least one org" option enabled and the user does not belong to an organization. They can either join an organization or create a new one.
  • UPDATE_PASSWORD_REQUIRED - The user needs to update their password.
  • LOGGED_IN - The user has completed the login process.

Retrieving and Managing Login State

So far we've covered what Login State is and how to retrieve it. But how can we keep the Login State up to date while effectively redirecting your user from page to page? There are a few ways to do this, but we recommend using React Context to pass the Login State easily throughout your application.

Here is an example of how you can use React Context to pass the Login State to your components:

src/contexts/LoginContext.tsx

import { useAuthFrontendApis } from '@propelauth/frontend-apis-react'
import { LoginState } from '@propelauth/frontend-apis'
import { createContext, useCallback, useEffect, useState } from 'react'

type Loading = {
    isLoading: true
    error: never
    loginState: never
}

type Error = {
    isLoading: false
    error: string
    loginState: never
}

type Success = {
    isLoading: false
    error: never
    loginState: LoginState
}

export type LoginStateContext = {
    getLoginState: () => Promise<void>
    setLoginState: (loginState: LoginState) => void
} & (Loading | Error | Success)

export const LoginContext = createContext<LoginStateContext | undefined>(undefined)

export const LoginContextProvider = ({ children }: { children: React.ReactNode }) => {
    const [loginState, setLoginState] = useState<LoginState>()
    const [isLoading, setIsLoading] = useState(true)
    const [error, setError] = useState<string>()

    const { fetchLoginState } = useAuthFrontendApis()

    const getLoginState = useCallback(async () => {
        setIsLoading(true)
        setError(undefined)
        const response = await fetchLoginState()
        response.handle({
            success({ login_state: loginState }) {
                setLoginState(loginState)
            },
            unexpectedOrUnhandled(error) {
                console.error('Unexpected or unhandled', error.user_facing_error)
                setError(error.user_facing_error)
            },
        })
        setIsLoading(false)
    }, [])

    // On initial render, fetch the login state.
    useEffect(() => {
        getLoginState()
    }, [getLoginState])

    let value: LoginStateContext
    if (isLoading) {
        value = {
            isLoading: true,
            error: undefined as never,
            loginState: undefined as never,
            getLoginState: getLoginState,
            setLoginState: setLoginState,
        }
    } else if (error || !loginState) {
        value = {
            isLoading: false,
            error: error ?? 'An error occurred when fetching the login state.',
            loginState: undefined as never,
            getLoginState: getLoginState,
            setLoginState: setLoginState,
        }
    } else {
        value = {
            isLoading: false,
            error: undefined as never,
            loginState,
            getLoginState: getLoginState,
            setLoginState: setLoginState,
        }
    }

    return <LoginContext.Provider value={value}>{children}</LoginContext.Provider>
}

You can then create a React hook to refetch the Login State whenever you need it:

src/hooks/useLoginContext.ts

import { useContext } from 'react'
import { LoginContext, LoginStateContext } from '@/contexts/LoginContext'

export const useLoginContext = (): LoginStateContext => {
    const context = useContext(LoginContext)
    if (!context) {
        throw new Error('useLoginContext must be used within a LoginContextProvider')
    }
    return context
}

This hook will allow us to keep the Login State up to date. For example, if the user successfully logs in using emailPasswordLogin, we can refetch the Login State to see if they need to complete any additional steps.

src/pages/LoginPage.tsx

import { useAuthFrontendApis } from '@propelauth/frontend-apis-react'
import { useLoginContext } from '@/hooks/useLoginContext'

const LoginPage = () => {
    const { emailPasswordLogin } = useAuthFrontendApis()
    const { setLoginState } = useLoginContext()

    const handleLogin = async () => {
        const response = await emailPasswordLogin({
            email: 'test@example.com',
            password: 'password',
        })
        response.handle({
            async success(data) {
                setLoginState(data.login_state)
            },
            // handle error cases
            badRequest(error) {
                // setFormErrors(error.user_facing_errors)
            }
        })
    }

    // render login form
}

Redirecting Users Based on Login State

Now that we have our React Context built and a way to keep the Login State up to date, let's build a state manager that will check the Login State and redirect the user accordingly.

src/components/LoginStateManager.tsx

import { LoginState } from '@propelauth/frontend-apis'
import { useLoginContext } from '@/hooks/useLoginContext'
import { LoginContextProvider } from '@/contexts/LoginContext'

const LoginElementByState = () => {
    const { loginState, isLoading, error } = useLoginContext()

    if (isLoading) {
        return <div>Loading...</div>
    } else if (error || !loginState) {
        return <div>{error ?? "An unexpected error has occurred"}</div>
    }

    switch (loginState) {
        case LoginState.LOGIN_REQUIRED:
            return // login or signup page
        case LoginState.USER_MISSING_REQUIRED_PROPERTIES:
            return // update user properties page
        case LoginState.EMAIL_NOT_CONFIRMED_YET:
            return // email confirmation page
        case LoginState.UPDATE_PASSWORD_REQUIRED:
            return // update password page
        case LoginState.USER_MUST_BE_IN_AT_LEAST_ONE_ORG:
            return // create or join org page
        case LoginState.TWO_FACTOR_ENROLLMENT_REQUIRED:
            return // enable 2FA page
        case LoginState.TWO_FACTOR_REQUIRED:
            return // 2FA verification page
        case LoginState.LOGGED_IN:
            window.location.reload()
            return null
    }
}

const LoginStateManager = () => {
    return <LoginContextProvider>
        <LoginElementByState />
    </LoginContextProvider>
}

export default LoginStateManager

Now you can use the LoginStateManager component to render a Component representing the next step the user take based on their Login State. Below, we'll use RequiredAuthProvider's displayIfLoggedOut property to render this component if the user is not logged in.

src/main.tsx

import LoginStateManager from '@/components/LoginStateManager'
import { RequiredAuthProvider } from '@propelauth/react'

createRoot(document.getElementById('root')!).render(
    <RequiredAuthProvider
        authUrl={AUTH_URL}
        displayIfLoggedOut={<LoginStateManager />}
    >
        <App />
    </RequiredAuthProvider>,
)

If the user has a Login State of anything besides LOGGED_IN, the component within the displayIfLoggedOut property will rendered. If the user has logged in successfully and has a Login State of LOGGED_IN, the <App /> will be rendered instead.

Signup vs Login

There are cases where you may want to redirect users directly to your signup page instead of your login page. One example of this is when users are invited to join an organization in your app - they'll receive an email which accepts the invite and redirects them to signup. But based on what we've covered so far, how can we do so?

One option is to create /login and /signup routes, but in this example let's instead use query parameters. When a user navigates to your app and is unauthenticated, a query parameter of signup=true will render the signup page over the login page. Let's create a component called LoginAndSignup to handle this for us.

src/components/LoginAndSignup.tsx

export default function LoginAndSignup() {
    const params = new URLSearchParams(window.location.search)
    const defaultToSignup = params.get('signup') === 'true' 
    const [displaySignup, setDisplaySignup] = useState(defaultToSignup)

    function switchPage(page: 'signup' | 'login') {
        const params = new URLSearchParams(window.location.search)
        params.set('signup', page === 'signup' ? 'true' : 'null')
        window.history.replaceState({}, '', `${window.location.pathname}${params.toString() ? '?' + params : ''}`)
        setDisplaySignup(page === 'signup')
    }

    if (displaySignup) {
        return (
            <>
                <h2>Signup</h2>
                <EmailPasswordSignup />
                <button onClick={() => switchPage('login')}>Login</button>
            </>
        )
    } else {
        return (
            <>
                <h2>Login</h2>
                <EmailPasswordLogin />
                <button onClick={() => switchPage('signup')}>Signup</button>
            </>
        )
    }
}

Now that we redirect users based on the signup query parameter, let's update the Disable Hosted Pages menu to ensure users are redirected to the signup page from the transactional emails.

tet

Next Steps

Have any feedback or questions? Feel free to reach out to us at support@propelauth.com