Skip to content

Instantly share code, notes, and snippets.

@ianchen06
Forked from cquangc/app.py
Created March 24, 2024 19:30
Show Gist options
  • Save ianchen06/2258dc3153c9e1ddeeb75884cbb9e442 to your computer and use it in GitHub Desktop.
Save ianchen06/2258dc3153c9e1ddeeb75884cbb9e442 to your computer and use it in GitHub Desktop.

Revisions

  1. @vovavili vovavili revised this gist Feb 26, 2023. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -536,12 +536,12 @@ def main() -> None:
    if not cookie_is_valid(cookie_manager, cookie_name) and not_logged_in(
    cookie_manager, cookie_name, preauthorized="gmail.com"
    ):
    return
    return None

    with st.sidebar:
    login_panel(cookie_manager, cookie_name)

    app()
    return app()


    # Run the Streamlit app
  2. @vovavili vovavili revised this gist Feb 26, 2023. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -290,7 +290,7 @@ def token_encode(exp_date: datetime) -> str:
    )


    def check_cookie_is_valid(cookie_manager: stx.CookieManager, cookie_name: str) -> bool:
    def cookie_is_valid(cookie_manager: stx.CookieManager, cookie_name: str) -> bool:
    """Check if the reauthentication cookie is valid and, if it is, update the session state.
    Parameters
    @@ -533,7 +533,7 @@ def main() -> None:
    pretty_title(TITLE)
    cookie_manager, cookie_name = stx.CookieManager(), "login_cookie"

    if not check_cookie_is_valid(cookie_manager, cookie_name) and not_logged_in(
    if not cookie_is_valid(cookie_manager, cookie_name) and not_logged_in(
    cookie_manager, cookie_name, preauthorized="gmail.com"
    ):
    return
  3. @vovavili vovavili revised this gist Feb 26, 2023. 1 changed file with 7 additions and 7 deletions.
    14 changes: 7 additions & 7 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -204,13 +204,13 @@ def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    url = f"{POST_REQUEST_URL_BASE}sendOobCode?key={st.secrets['FIREBASE_API_KEY']}"
    payload = {"requestType": "VERIFY_EMAIL", "idToken": token}
    response = post_request(url, json=payload)
    if response.status_code == 200:
    success(
    "Your account has been created successfully. To complete the registration process, "
    "please verify your email address by clicking on the link we have sent to your inbox."
    )
    return st.balloons()
    return error(f"Error sending verification email: {parse_error_message(response)}")
    if response.status_code != 200:
    return error(f"Error sending verification email: {parse_error_message(response)}")
    success(
    "Your account has been created successfully. To complete the registration process, "
    "please verify your email address by clicking on the link we have sent to your inbox."
    )
    return st.balloons()


    def update_password_form() -> None:
  4. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 11 additions and 26 deletions.
    37 changes: 11 additions & 26 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -85,31 +85,6 @@ def parse_error_message(response: requests.Response) -> str:
    )


    def send_verification_email(id_token: str) -> None:
    """
    Sends a verification email to the user associated with the given Firebase ID token though
    the Firebase Authentication REST API.
    Parameters:
    id_token (str): A Firebase ID token that identifies the authenticated user.
    Raises:
    requests.exceptions.RequestException: If there was an error while sending the
    verification email.
    """

    url = f"{POST_REQUEST_URL_BASE}sendOobCode?key={st.secrets['FIREBASE_API_KEY']}"
    payload = {"requestType": "VERIFY_EMAIL", "idToken": id_token}
    response = post_request(url, json=payload)
    if response.status_code == 200:
    success(
    "Your account has been created successfully. To complete the registration process, "
    "please verify your email address by clicking on the link we have sent to your inbox."
    )
    return st.balloons()
    return error(f"Error sending verification email: {parse_error_message(response)}")


    def authenticate_user(
    email: str, password: str, require_email_verification: bool = True
    ) -> Optional[Dict[str, Union[str, bool, int]]]:
    @@ -222,10 +197,20 @@ def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    auth.create_user(
    email=email, password=password, display_name=name, email_verified=False
    )
    # Having registered the user, send them a verification e-mail
    token = authenticate_user(email, password, require_email_verification=False)[
    "idToken"
    ]
    return send_verification_email(token)
    url = f"{POST_REQUEST_URL_BASE}sendOobCode?key={st.secrets['FIREBASE_API_KEY']}"
    payload = {"requestType": "VERIFY_EMAIL", "idToken": token}
    response = post_request(url, json=payload)
    if response.status_code == 200:
    success(
    "Your account has been created successfully. To complete the registration process, "
    "please verify your email address by clicking on the link we have sent to your inbox."
    )
    return st.balloons()
    return error(f"Error sending verification email: {parse_error_message(response)}")


    def update_password_form() -> None:
  5. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 3 additions and 24 deletions.
    27 changes: 3 additions & 24 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -21,7 +21,7 @@
    from contextlib import suppress
    from datetime import datetime, timedelta
    from functools import partial
    from typing import Any, Dict, Final, Optional, Sequence, Union
    from typing import Dict, Final, Optional, Sequence, Union

    import extra_streamlit_components as stx
    import firebase_admin
    @@ -274,28 +274,6 @@ def update_display_name_form(
    return success("Successfully updated user display name.")


    def token_decode(token: str) -> Optional[Dict[str, Any]]:
    """Decodes a JSON Web Token (JWT) and returns the resulting dictionary.
    The function decodes the provided JWT using the secret key specified in the
    application's secrets. The decoded dictionary contains information required for
    passwordless reauthentication.
    Parameters
    ----------
    token : str
    The JWT to decode.
    Returns
    -------
    Dict[str, Any] or None
    The decoded dictionary, or None if an error occurred during decoding.
    """

    with suppress(Exception):
    return jwt.decode(token, st.secrets["COOKIE_KEY"], algorithms=["HS256"])


    def token_encode(exp_date: datetime) -> str:
    """Encodes a JSON Web Token (JWT) containing user session data for passwordless
    reauthentication.
    @@ -354,7 +332,8 @@ def check_cookie_is_valid(cookie_manager: stx.CookieManager, cookie_name: str) -
    token = cookie_manager.get(cookie_name)
    if token is None:
    return False
    token = token_decode(token)
    with suppress(Exception):
    token = jwt.decode(token, st.secrets["COOKIE_KEY"], algorithms=["HS256"])
    if (
    token
    and not st.session_state["logout"]
  6. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion app.py
    Original file line number Diff line number Diff line change
    @@ -212,7 +212,8 @@ def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    # I multiply this number by 1.5 to display password strength with st.progress
    # For an explanation, read this -
    # https://en.wikipedia.org/wiki/Password_strength#Entropy_as_a_measure_of_password_strength
    strength = int(len(password) * math.log2(len(set(password))) * 1.5)
    alphabet_chars = len(set(password))
    strength = int(len(password) * math.log2(alphabet_chars) * 1.5)
    if strength < 100:
    st.progress(strength)
    return st.warning(
  7. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 5 additions and 3 deletions.
    8 changes: 5 additions & 3 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -16,6 +16,7 @@
    )
    """

    import math
    import time
    from contextlib import suppress
    from datetime import datetime, timedelta
    @@ -29,7 +30,6 @@
    import streamlit as st
    from email_validator import EmailNotValidError, validate_email
    from firebase_admin import auth
    from password_strength import PasswordPolicy

    TITLE: Final = "Example app"

    @@ -209,8 +209,10 @@ def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    return error(e)

    # Need a password that has minimum 66 entropy bits (the power of its alphabet)
    # I multiply this number by 150 (100 * 1.5) to display password strength with st.progress
    strength = round(PasswordPolicy().password(password).strength() * 150)
    # I multiply this number by 1.5 to display password strength with st.progress
    # For an explanation, read this -
    # https://en.wikipedia.org/wiki/Password_strength#Entropy_as_a_measure_of_password_strength
    strength = int(len(password) * math.log2(len(set(password))) * 1.5)
    if strength < 100:
    st.progress(strength)
    return st.warning(
  8. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 19 additions and 28 deletions.
    47 changes: 19 additions & 28 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -39,12 +39,13 @@
    headers={"content-type": "application/json; charset=UTF-8"},
    timeout=10,
    )
    success = partial(st.success, icon="✅")
    error = partial(st.error, icon="🚨")


    def pretty_title(title: str) -> None:
    """Make a centered title, and give it a red line. Adapted from
    'streamlit_extras.colored_headers' package.
    Parameters:
    -----------
    title : str
    @@ -101,16 +102,12 @@ def send_verification_email(id_token: str) -> None:
    payload = {"requestType": "VERIFY_EMAIL", "idToken": id_token}
    response = post_request(url, json=payload)
    if response.status_code == 200:
    st.success(
    success(
    "Your account has been created successfully. To complete the registration process, "
    "please verify your email address by clicking on the link we have sent to your inbox.",
    icon="✅",
    "please verify your email address by clicking on the link we have sent to your inbox."
    )
    return st.balloons()
    return st.error(
    f"Error sending verification email: {parse_error_message(response)}",
    icon="🚨",
    )
    return error(f"Error sending verification email: {parse_error_message(response)}")


    def authenticate_user(
    @@ -143,14 +140,11 @@ def authenticate_user(
    }
    response = post_request(url, json=payload)
    if response.status_code != 200:
    st.error(
    f"Authentication failed: {parse_error_message(response)}",
    icon="🚨",
    )
    error(f"Authentication failed: {parse_error_message(response)}")
    return None
    response = response.json()
    if require_email_verification and "idToken" not in response:
    st.error("Invalid e-mail or password.", icon="🚨")
    error("Invalid e-mail or password.")
    return None
    return response

    @@ -175,11 +169,8 @@ def forgot_password_form(preauthorized: Union[str, Sequence[str], None]) -> None
    payload = {"requestType": "PASSWORD_RESET", "email": email}
    response = post_request(url, json=payload)
    if response.status_code == 200:
    return st.success(f"Password reset link has been sent to {email}", icon="✅")
    return st.error(
    f"Error sending password reset email: {parse_error_message(response)}",
    icon="🚨",
    )
    return success(f"Password reset link has been sent to {email}")
    return error(f"Error sending password reset email: {parse_error_message(response)}")


    def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    @@ -205,17 +196,17 @@ def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    return None
    # Below are some checks to ensure proper and secure registration
    if password != confirm_password:
    return st.error("Passwords do not match", icon="🚨")
    return error("Passwords do not match")
    if not name:
    return st.error("Please enter your name", icon="🚨")
    return error("Please enter your name")
    if "@" not in email and isinstance(preauthorized, str):
    email = f"{email}@{preauthorized}"
    if preauthorized and not email.endswith(preauthorized):
    return st.error("Domain not allowed", icon="🚨")
    return error("Domain not allowed")
    try:
    validate_email(email, check_deliverability=True)
    except EmailNotValidError as e:
    return st.error(e, icon="🚨")
    return error(e)

    # Need a password that has minimum 66 entropy bits (the power of its alphabet)
    # I multiply this number by 150 (100 * 1.5) to display password strength with st.progress
    @@ -244,7 +235,7 @@ def update_password_form() -> None:
    return None
    user = auth.get_user_by_email(st.session_state["username"])
    auth.update_user(user.uid, password=new_password)
    return st.success("Successfully updated user password.", icon="✅")
    return success("Successfully updated user password.")


    def update_display_name_form(
    @@ -277,7 +268,7 @@ def update_display_name_form(
    token_encode(exp_date),
    expires_at=exp_date,
    )
    return st.success("Successfully updated user display name.", icon="✅")
    return success("Successfully updated user display name.")


    def token_decode(token: str) -> Optional[Dict[str, Any]]:
    @@ -419,7 +410,7 @@ def login_form(
    decoded_token = auth.verify_id_token(login_response["idToken"])
    user = auth.get_user(decoded_token["uid"])
    if not user.email_verified:
    return st.error("Please verify your e-mail address.", icon="🚨")
    return error("Please verify your e-mail address.")
    # At last, authenticate the user
    st.session_state["name"] = user.display_name
    st.session_state["username"] = user.email
    @@ -431,7 +422,7 @@ def login_form(
    expires_at=exp_date,
    )
    except Exception as e:
    st.error(e)
    error(e)
    return None


    @@ -523,7 +514,7 @@ def not_logged_in(

    auth_status = st.session_state["authentication_status"]
    if auth_status is False:
    st.error("Username/password is incorrect", icon="🚨")
    error("Username/password is incorrect")
    return early_return
    if auth_status is None:
    return early_return
    @@ -588,4 +579,4 @@ def main() -> None:

    # Run the Streamlit app
    if __name__ == "__main__":
    main()
    main()
  9. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 5 additions and 6 deletions.
    11 changes: 5 additions & 6 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -1,20 +1,17 @@
    """Example Streamlit apps that uses Firebase for authentication.
    """Module for handling authentication, interactions with Firebase and JWT cookies.
    This solution is refactored from the ‘streamlit_authenticator’ package . It leverages JSON
    Web Token (JWT) cookies to maintain the user’s login state across browser sessions. For the
    backend, It uses Google’s Firebase Admin Python SDK. This solution ensures that the content
    of the page and user settings panel are only displayed if the user is authenticated. Similarly,
    the login page can only be accessed if the user is not authenticated. Upon registration, the
    user is sent a verification link to their e-mail address.
    Important - to make this app run, put the following variables in your secrets.toml file:
    COOKIE_KEY - a random string key for your passwordless reauthentication
    FIREBASE_API_KEY - Key for your Firebase API (how to find it -
    https://firebase.google.com/docs/projects/api-keys#find-api-keys
    )
    And make sure that your Firebase login token JSON is stored in “firebase_auth_token.json” in
    root directory of your file (how to create one -
    firebase_auth_token - Information extracted from Firebase login token JSON (how to get one -
    https://firebase.google.com/docs/admin/setup#initialize_the_sdk_in_non-google_environments
    )
    """
    @@ -571,7 +568,9 @@ def main() -> None:

    # noinspection PyProtectedMember
    if not firebase_admin._apps:
    cred = firebase_admin.credentials.Certificate("firebase_auth_token.json")
    cred = firebase_admin.credentials.Certificate(
    dict(st.secrets["firebase_auth_token"])
    )
    firebase_admin.initialize_app(cred)
    pretty_title(TITLE)
    cookie_manager, cookie_name = stx.CookieManager(), "login_cookie"
  10. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 0 additions and 1 deletion.
    1 change: 0 additions & 1 deletion app.py
    Original file line number Diff line number Diff line change
    @@ -556,7 +556,6 @@ def main() -> None:

    st.set_page_config(
    page_title=TITLE,
    page_icon="📖",
    layout="wide",
    initial_sidebar_state="collapsed",
    )
  11. @vovavili vovavili revised this gist Feb 25, 2023. No changes.
  12. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion app.py
    Original file line number Diff line number Diff line change
    @@ -578,7 +578,7 @@ def main() -> None:
    cookie_manager, cookie_name = stx.CookieManager(), "login_cookie"

    if not check_cookie_is_valid(cookie_manager, cookie_name) and not_logged_in(
    cookie_manager, cookie_name, preauthorized="loxsolution.com"
    cookie_manager, cookie_name, preauthorized="gmail.com"
    ):
    return

  13. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 4 additions and 3 deletions.
    7 changes: 4 additions & 3 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -577,9 +577,10 @@ def main() -> None:
    pretty_title(TITLE)
    cookie_manager, cookie_name = stx.CookieManager(), "login_cookie"

    if not check_cookie_is_valid(cookie_manager, cookie_name):
    if not_logged_in(cookie_manager, cookie_name, preauthorized="gmail.com"):
    return
    if not check_cookie_is_valid(cookie_manager, cookie_name) and not_logged_in(
    cookie_manager, cookie_name, preauthorized="loxsolution.com"
    ):
    return

    with st.sidebar:
    login_panel(cookie_manager, cookie_name)
  14. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 14 additions and 9 deletions.
    23 changes: 14 additions & 9 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -1,17 +1,22 @@
    """Example Streamlit apps that uses Firebase for authentication.
    This solution is refactored from the ‘streamlit_authenticator’ package . It leverages JSON Web Token (JWT) cookies
    to maintain the user’s login state across browser sessions. For the backend, It uses Google’s Firebase Admin Python SDK.
    This solution ensures that the content of the page and user settings panel are only displayed if the user is
    authenticated. Similarly, the login page can only be accessed if the user is not authenticated. Upon registration,
    the user is sent a verification link to their e-mail address.
    This solution is refactored from the ‘streamlit_authenticator’ package . It leverages JSON
    Web Token (JWT) cookies to maintain the user’s login state across browser sessions. For the
    backend, It uses Google’s Firebase Admin Python SDK. This solution ensures that the content
    of the page and user settings panel are only displayed if the user is authenticated. Similarly,
    the login page can only be accessed if the user is not authenticated. Upon registration, the
    user is sent a verification link to their e-mail address.
    Important - to make this app run, put the following variables in your secrets.toml file:
    COOKIE_KEY - a random string key for your passwordless reauthentication
    FIREBASE_API_KEY - Key for your Firebase API (how to find it - https://firebase.google.com/docs/projects/api-keys#find-api-keys)
    And make sure that your Firebase login token JSON is stored in “firebase_auth_token.json” in root directory of
    your file (how to create one - https://firebase.google.com/docs/admin/setup#initialize_the_sdk_in_non-google_environments).
    FIREBASE_API_KEY - Key for your Firebase API (how to find it -
    https://firebase.google.com/docs/projects/api-keys#find-api-keys
    )
    And make sure that your Firebase login token JSON is stored in “firebase_auth_token.json” in
    root directory of your file (how to create one -
    https://firebase.google.com/docs/admin/setup#initialize_the_sdk_in_non-google_environments
    )
    """

    import time
    @@ -584,4 +589,4 @@ def main() -> None:

    # Run the Streamlit app
    if __name__ == "__main__":
    main()
    main()
  15. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 15 additions and 1 deletion.
    16 changes: 15 additions & 1 deletion app.py
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,18 @@
    """Module for handling authentication, interactions with Firebase and JWT cookies.."""
    """Example Streamlit apps that uses Firebase for authentication.
    This solution is refactored from the ‘streamlit_authenticator’ package . It leverages JSON Web Token (JWT) cookies
    to maintain the user’s login state across browser sessions. For the backend, It uses Google’s Firebase Admin Python SDK.
    This solution ensures that the content of the page and user settings panel are only displayed if the user is
    authenticated. Similarly, the login page can only be accessed if the user is not authenticated. Upon registration,
    the user is sent a verification link to their e-mail address.
    Important - to make this app run, put the following variables in your secrets.toml file:
    COOKIE_KEY - a random string key for your passwordless reauthentication
    FIREBASE_API_KEY - Key for your Firebase API (how to find it - https://firebase.google.com/docs/projects/api-keys#find-api-keys)
    And make sure that your Firebase login token JSON is stored in “firebase_auth_token.json” in root directory of
    your file (how to create one - https://firebase.google.com/docs/admin/setup#initialize_the_sdk_in_non-google_environments).
    """

    import time
    from contextlib import suppress
  16. @vovavili vovavili revised this gist Feb 25, 2023. 1 changed file with 270 additions and 136 deletions.
    406 changes: 270 additions & 136 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,9 @@
    """Module for handling authentication, interactions with Firebase and JWT cookies.."""

    import time
    import json
    from contextlib import suppress
    from datetime import datetime, timedelta
    from functools import partial
    from typing import Any, Dict, Final, Optional, Sequence, Union

    import extra_streamlit_components as stx
    @@ -13,14 +15,20 @@
    from firebase_admin import auth
    from password_strength import PasswordPolicy

    TITLE: Final = "Example app"

    TITLE: Final = "Example page"
    POST_REQUEST_URL_BASE: Final = "https://identitytoolkit.googleapis.com/v1/accounts:"
    post_request = partial(
    requests.post,
    headers={"content-type": "application/json; charset=UTF-8"},
    timeout=10,
    )


    def pretty_title(title: str) -> None:
    """
    Make a centered title, and give it a red line. Adapted from 'streamlit_extras.colored_headers'
    package.
    """Make a centered title, and give it a red line. Adapted from
    'streamlit_extras.colored_headers' package.
    Parameters:
    -----------
    title : str
    @@ -31,42 +39,140 @@ def pretty_title(title: str) -> None:
    unsafe_allow_html=True,
    )
    st.markdown(
    '<hr style="background-color: #ff4b4b; margin-top: 0;'
    ' margin-bottom: 0; height: 3px; border: none; border-radius: 3px;">',
    (
    '<hr style="background-color: #ff4b4b; margin-top: 0;'
    ' margin-bottom: 0; height: 3px; border: none; border-radius: 3px;">'
    ),
    unsafe_allow_html=True,
    )


    def forgot_password_form() -> None:
    """Creates a Streamlit widget to reset a user's password.
    def parse_error_message(response: requests.Response) -> str:
    """
    Parses an error message from a requests.Response object and makes it look better.
    Parameters:
    response (requests.Response): The response object to parse.
    Returns:
    str: Prettified error message.
    The function prompts the user to enter their email address, and
    generates a password reset link for the corresponding user. If the
    user has not verified their email address, a new verification link
    is generated and sent to the user.
    Raises:
    KeyError: If the 'error' key is not present in the response JSON.
    """
    return (
    response.json()["error"]["message"]
    .casefold()
    .replace("_", " ")
    .replace("email", "e-mail")
    )

    # Get the email and password from the user
    email = st.text_input("Email", key="email_password")
    # Attempt to log the user in
    if not st.button("Reset password"):

    def send_verification_email(id_token: str) -> None:
    """
    Sends a verification email to the user associated with the given Firebase ID token though
    the Firebase Authentication REST API.
    Parameters:
    id_token (str): A Firebase ID token that identifies the authenticated user.
    Raises:
    requests.exceptions.RequestException: If there was an error while sending the
    verification email.
    """

    url = f"{POST_REQUEST_URL_BASE}sendOobCode?key={st.secrets['FIREBASE_API_KEY']}"
    payload = {"requestType": "VERIFY_EMAIL", "idToken": id_token}
    response = post_request(url, json=payload)
    if response.status_code == 200:
    st.success(
    "Your account has been created successfully. To complete the registration process, "
    "please verify your email address by clicking on the link we have sent to your inbox.",
    icon="✅",
    )
    return st.balloons()
    return st.error(
    f"Error sending verification email: {parse_error_message(response)}",
    icon="🚨",
    )


    def authenticate_user(
    email: str, password: str, require_email_verification: bool = True
    ) -> Optional[Dict[str, Union[str, bool, int]]]:
    """
    Authenticates a user with the given email and password using the Firebase Authentication
    REST API.
    Parameters:
    email (str): The email address of the user to authenticate.
    password (str): The password of the user to authenticate.
    require_email_verification (bool): Specify whether a user has to be e-mail verified to
    be authenticated
    Returns:
    dict or None: A dictionary containing the authenticated user's ID token, refresh token,
    and other information, if authentication was successful. Otherwise, None.
    Raises:
    requests.exceptions.RequestException: If there was an error while authenticating the user.
    """

    url = f"{POST_REQUEST_URL_BASE}signInWithPassword?key={st.secrets['FIREBASE_API_KEY']}"
    payload = {
    "email": email,
    "password": password,
    "returnSecureToken": True,
    "emailVerified": require_email_verification,
    }
    response = post_request(url, json=payload)
    if response.status_code != 200:
    st.error(
    f"Authentication failed: {parse_error_message(response)}",
    icon="🚨",
    )
    return None
    response = response.json()
    if require_email_verification and "idToken" not in response:
    st.error("Invalid e-mail or password.", icon="🚨")
    return None
    user = auth.get_user_by_email(email)
    auth.generate_password_reset_link(user.email)
    if user.email_verified:
    return response


    def forgot_password_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    """Creates a Streamlit widget to reset a user's password. Authentication uses
    the Firebase Authentication REST API.
    Parameters:
    preauthorized (Union[str, Sequence[str], None]): An optional domain or a list of
    domains which are authorized to register.
    """

    with st.form("Forgot password"):
    email = st.text_input("E-mail", key="forgot_password")
    if not st.form_submit_button("Reset password"):
    return None
    if "@" not in email and isinstance(preauthorized, str):
    email = f"{email}@{preauthorized}"

    url = f"{POST_REQUEST_URL_BASE}sendOobCode?key={st.secrets['FIREBASE_API_KEY']}"
    payload = {"requestType": "PASSWORD_RESET", "email": email}
    response = post_request(url, json=payload)
    if response.status_code == 200:
    return st.success(f"Password reset link has been sent to {email}", icon="✅")
    st.warning(f"{email} is not verified. Resending the verification link.")
    return auth.generate_email_verification_link(user.email)
    return st.error(
    f"Error sending password reset email: {parse_error_message(response)}",
    icon="🚨",
    )


    def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    """Creates a Streamlit widget for user registration.
    The function prompts user to enter all the required information for registration through
    Firebase. Password strength is validated using entropy bits (the power of the password
    alphabet). Upon registration, a validation link is sent to the user's email address.
    Password strength is validated using entropy bits (the power of the password alphabet).
    Upon registration, a validation link is sent to the user's email address.
    Arguments:
    Parameters:
    preauthorized (Union[str, Sequence[str], None]): An optional domain or a list of
    domains which are authorized to register.
    """
    @@ -86,14 +192,13 @@ def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    return st.error("Passwords do not match", icon="🚨")
    if not name:
    return st.error("Please enter your name", icon="🚨")
    if "@" not in email and isinstance(preauthorized, str):
    email = f"{email}@{preauthorized}"
    if preauthorized and not email.endswith(preauthorized):
    return st.error("Domain not allowed", icon="🚨")
    try:
    # Check that the email address is valid.
    validate_email(email, check_deliverability=True)
    except EmailNotValidError as e:
    # Email is not valid.
    # The exception message is human-readable.
    return st.error(e, icon="🚨")

    # Need a password that has minimum 66 entropy bits (the power of its alphabet)
    @@ -104,21 +209,17 @@ def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    return st.warning(
    "Password is too weak. Please choose a stronger password.", icon="⚠️"
    )
    auth.create_user(email=email, password=password)
    st.success(
    "Your account has been created successfully. To complete the registration process, "
    "please verify your email address by clicking on the link we have sent to your inbox.",
    icon="✅",
    auth.create_user(
    email=email, password=password, display_name=name, email_verified=False
    )
    return st.balloons()
    token = authenticate_user(email, password, require_email_verification=False)[
    "idToken"
    ]
    return send_verification_email(token)


    def update_password_form() -> None:
    """Creates a Streamlit widget to update a user's password.
    The function prompts the user to enter a new password, and updates
    the corresponding user's password with the new value.
    """
    """Creates a Streamlit widget to update a user's password."""

    # Get the email and password from the user
    new_password = st.text_input("New password", key="new_password")
    @@ -130,11 +231,19 @@ def update_password_form() -> None:
    return st.success("Successfully updated user password.", icon="✅")


    def update_display_name_form() -> None:
    def update_display_name_form(
    cookie_manager: stx.CookieManager, cookie_name: str, cookie_expiry_days: int = 30
    ) -> None:
    """Creates a Streamlit widget to update a user's display name.
    The function prompts the user to enter a new display name, and
    updates the corresponding user's display name with the new value.
    Parameters
    ----------
    - cookie_manager : stx.CookieManager
    A JWT cookie manager instance for Streamlit
    - cookie_name : str
    The name of the reauthentication cookie.
    - cookie_expiry_days: (optional) str
    An integer representing the number of days until the cookie expires
    """

    # Get the email and password from the user
    @@ -143,7 +252,15 @@ def update_display_name_form() -> None:
    if not st.button("Update name"):
    return None
    user = auth.get_user_by_email(st.session_state["username"])
    auth.update_user(user.display_name, display_name=new_name)
    auth.update_user(user.uid, display_name=new_name)
    st.session_state["name"] = new_name
    # Update the cookie as well
    exp_date = datetime.utcnow() + timedelta(days=cookie_expiry_days)
    cookie_manager.set(
    cookie_name,
    token_encode(exp_date),
    expires_at=exp_date,
    )
    return st.success("Successfully updated user display name.", icon="✅")


    @@ -201,15 +318,16 @@ def token_encode(exp_date: datetime) -> str:


    def check_cookie_is_valid(cookie_manager: stx.CookieManager, cookie_name: str) -> bool:
    """Check if the reauthentication cookie is valid and update the session state if it
    is valid.
    """Check if the reauthentication cookie is valid and, if it is, update the session state.
    Parameters
    ----------
    cookie_manager : stx.CookieManager
    - cookie_manager : stx.CookieManager
    A JWT cookie manager instance for Streamlit
    cookie_name : str
    - cookie_name : str
    The name of the reauthentication cookie.
    - cookie_expiry_days: (optional) str
    An integer representing the number of days until the cookie expires
    Returns
    -------
    @@ -218,24 +336,20 @@ def check_cookie_is_valid(cookie_manager: stx.CookieManager, cookie_name: str) -
    Notes
    -----
    This function checks if the reauthentication cookie specified by `cookie_name` is present in
    the cookies stored by `cookie_manager`, and if it is valid, meaning that it is not expired, and
    that it contains the required fields "name" and "username". If the cookie is valid, this
    function updates the session state of the Streamlit app, setting the "name", "username", and
    "authentication_status" keys accordingly. If the cookie is not valid or cannot be decoded, the
    function does not modify the session state and the user does not get authenticated.
    This function checks if the specified reauthentication cookie is present in the cookies stored by
    the cookie manager, and if it is valid. If the cookie is valid, this function updates the session
    state of the Streamlit app and authenticates the user.
    """

    token = cookie_manager.get(cookie_name)
    if token is None:
    return False
    token = token_decode(token)
    if (
    token is not False
    token
    and not st.session_state["logout"]
    and token["exp_date"] > datetime.utcnow().timestamp()
    and "name" in token
    and "username" in token
    and {"name", "username"}.issubset(set(token))
    ):
    st.session_state["name"] = token["name"]
    st.session_state["username"] = token["username"]
    @@ -245,16 +359,21 @@ def check_cookie_is_valid(cookie_manager: stx.CookieManager, cookie_name: str) -


    def login_form(
    cookie_manager: stx.CookieManager, cookie_name: str, cookie_expiry_days: int = 30
    cookie_manager: stx.CookieManager,
    cookie_name: str,
    preauthorized: Union[str, Sequence[str], None],
    cookie_expiry_days: int = 30,
    ) -> None:
    """Creates a login widget using Firebase REST API and a cookie manager.
    Arguments
    ---------
    - cookie_manager: a JWT cookie manager instance for Streamlit
    - cookie_name: a name of the reauthentication cookie
    - cookie_expiry_days: (optional) an integer representing the number of days until the
    cookie expires
    Parameters
    ----------
    - cookie_manager : stx.CookieManager
    A JWT cookie manager instance for Streamlit
    - cookie_name : str
    The name of the reauthentication cookie.
    - cookie_expiry_days: (optional) str
    An integer representing the number of days until the cookie expires
    Notes
    -----
    @@ -263,63 +382,57 @@ def login_form(
    a login form which prompts the user to enter their email and password. If the login credentials
    are valid and the user's email address has been verified, the user is authenticated and a
    reauthentication cookie is created with the specified expiration date.
    If the user's email address has not been verified or if there is an error with the
    authentication process, the user is not authenticated.
    """

    if st.session_state["authentication_status"]:
    return None
    if check_cookie_is_valid(cookie_manager, cookie_name):
    return None
    with st.form("Login"):
    email = st.text_input("Email")
    email = st.text_input("E-mail")
    if "@" not in email and isinstance(preauthorized, str):
    email = f"{email}@{preauthorized}"
    st.session_state["username"] = email
    password = st.text_input("Password", type="password")
    if not st.form_submit_button("Login"):
    return None

    # Authenticate the user with Firebase REST API
    url = (
    f"https://identitytoolkit.googleapis.com/v1/"
    f"accounts:signInWithPassword?key={st.secrets['FIREBASE_API_KEY']}"
    )
    payload = {
    "email": email,
    "password": password,
    "returnSecureToken": True,
    "emailVerified": True, # Require email verification
    }
    response = requests.post(url, data=json.dumps(payload), timeout=10).json()
    if "idToken" not in response:
    return st.error("Invalid e-mail or password.", icon="🚨")
    login_response = authenticate_user(email, password)
    if not login_response:
    return None
    try:
    decoded_token = auth.verify_id_token(response["idToken"])
    decoded_token = auth.verify_id_token(login_response["idToken"])
    user = auth.get_user(decoded_token["uid"])
    if not user.email_verified:
    return st.error("Please verify your e-mail address.", icon="🚨")
    # At last, authenticate the user
    st.session_state["name"] = user.display_name
    st.session_state["username"] = user.email
    st.session_state["authentication_status"] = True
    exp_date = datetime.utcnow() + timedelta(days=cookie_expiry_days)
    cookie_manager.set(
    cookie_name,
    token_encode(exp_date),
    expires_at=exp_date,
    )
    st.session_state["authentication_status"] = True
    except Exception as e:
    st.error(e)
    return None


    def login_panel(cookie_manager: stx.CookieManager, cookie_name: str) -> None:
    """Creates a side panel for logged in users, preventing the login menu from
    def login_panel(
    cookie_manager: stx.CookieManager, cookie_name: str, cookie_expiry_days: int = 30
    ) -> None:
    """Creates a side panel for logged-in users, preventing the login menu from
    appearing.
    Arguments
    ---------
    - cookie_manager: a JWT cookie manager instance for Streamlit
    - cookie_name: a name of the reauthentication cookie
    Parameters
    ----------
    - cookie_manager : stx.CookieManager
    A JWT cookie manager instance for Streamlit
    - cookie_name : str
    The name of the reauthentication cookie.
    - cookie_expiry_days: (optional) str
    An integer representing the number of days until the cookie expires
    Notes
    -----
    @@ -338,71 +451,73 @@ def login_panel(cookie_manager: stx.CookieManager, cookie_name: str) -> None:
    st.session_state["authentication_status"] = None
    return None
    st.write(f"Welcome, *{st.session_state['name']}*!")
    tab1, tab2 = st.tabs(["Reset password", "Update user details"])
    try:
    with tab1:
    update_password_form()
    with tab2:
    update_display_name_form()
    except Exception as e:
    st.error(e)
    user_tab1, user_tab2 = st.tabs(["Reset password", "Update user details"])
    with user_tab1:
    update_password_form()
    with user_tab2:
    update_display_name_form(cookie_manager, cookie_name, cookie_expiry_days)
    return None


    def not_logged_in(preauthorized: Union[str, Sequence[str], None]) -> bool:
    def not_logged_in(
    cookie_manager, cookie_name, preauthorized: Union[str, Sequence[str], None] = None
    ) -> bool:
    """Creates a tab panel for unauthenticated, preventing the user control sidebar and
    the rest of the script from appearing until the user logs in.
    Arguments
    ---
    'preauthorized' - an optional domain or a list of domains which are authorized to register
    Parameters
    ----------
    - cookie_manager : stx.CookieManager
    A JWT cookie manager instance for Streamlit
    - cookie_name : str
    The name of the reauthentication cookie.
    - cookie_expiry_days: (optional) str
    An integer representing the number of days until the cookie expires
    Returns
    -------
    Authentication status boolean.
    Notes
    -----
    The function first sets the authentication status flag to True and pre-populates missing
    session state arguments for the first run. If the user is already authenticated, the login
    panel function is called to create a side panel for logged-in users. If the function call
    does not update the authentication status because the username/password does not exist in
    the Firebase database, the functions halts. If the user is not logged in, tab panel created
    for the Login, Register, and Forgot password forms, and the rest of the script does not get
    executed until the user logs in.
    If the user is already authenticated, the login panel function is called to create a side
    panel for logged-in users. If the function call does not update the authentication status
    because the username/password does not exist in the Firebase database, the rest of the script
    does not get executed until the user logs in.
    """

    early_return = True
    # In case of a first run, pre-populate missing session state arguments
    cookie_manager, cookie_name = stx.CookieManager(), "login_cookie"
    for key in {"name", "authentication_status", "username", "logout"}.difference(
    set(st.session_state)
    ):
    st.session_state[key] = None

    # Give a loading message when the website page restarts
    with st.spinner("Loading..."):
    while "authentication_status" not in st.session_state:
    time.sleep(0.1)
    login_tabs = st.empty()
    with login_tabs:
    login_tab1, login_tab2, login_tab3 = st.tabs(
    ["Login", "Register", "Forgot password"]
    )
    with login_tab1:
    login_form(cookie_manager, cookie_name, preauthorized)
    with login_tab2:
    register_user_form(preauthorized)
    with login_tab3:
    forgot_password_form(preauthorized)

    if st.session_state["authentication_status"]:
    with st.sidebar:
    login_panel(cookie_manager, cookie_name)
    return not early_return
    if st.session_state["authentication_status"] is False:
    auth_status = st.session_state["authentication_status"]
    if auth_status is False:
    st.error("Username/password is incorrect", icon="🚨")

    tab1, tab2, tab3 = st.tabs(["Login", "Register", "Forgot password"])
    with tab1:
    login_form(cookie_manager, cookie_name)
    try:
    with tab2:
    register_user_form(preauthorized)
    with tab3:
    forgot_password_form()
    except Exception as e:
    st.error(e)
    return early_return
    return early_return
    if auth_status is None:
    return early_return
    login_tabs.empty()
    # A workaround for a bug in Streamlit -
    # https://playground.streamlit.app/?q=empty-doesnt-work
    # TLDR: element.empty() doesn't actually seem to work with a multi-element container
    # unless you add a sleep after it.
    time.sleep(0.01)
    return not early_return


    def app() -> None:
    @@ -420,16 +535,35 @@ def main() -> None:
    is not logged in, no content other than the login form gets shown.
    """

    st.set_page_config(page_title=TITLE, layout="wide")
    st.set_page_config(
    page_title=TITLE,
    page_icon="📖",
    layout="wide",
    initial_sidebar_state="collapsed",
    )
    # Hides 'Made with Streamlit'
    st.markdown(
    """
    <style>
    footer {visibility: hidden;}
    </style>
    """,
    unsafe_allow_html=True,
    )

    # noinspection PyProtectedMember
    if not firebase_admin._apps:
    cred = firebase_admin.credentials.Certificate("firebase_auth_token.json")
    firebase_admin.initialize_app(cred)

    pretty_title(TITLE)
    cookie_manager, cookie_name = stx.CookieManager(), "login_cookie"

    if not check_cookie_is_valid(cookie_manager, cookie_name):
    if not_logged_in(cookie_manager, cookie_name, preauthorized="gmail.com"):
    return

    if not_logged_in(preauthorized="gmail.com"):
    return
    with st.sidebar:
    login_panel(cookie_manager, cookie_name)

    app()

  17. @vovavili vovavili revised this gist Feb 22, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion app.py
    Original file line number Diff line number Diff line change
    @@ -405,7 +405,7 @@ def not_logged_in(preauthorized: Union[str, Sequence[str], None]) -> bool:
    return early_return


    def app():
    def app() -> None:
    """This is a part of a Streamlit app which is only visible if the user is logged in."""
    st.subheader("Yay!!!")
    st.write("You are logged in!")
  18. @vovavili vovavili revised this gist Feb 22, 2023. 1 changed file with 61 additions and 65 deletions.
    126 changes: 61 additions & 65 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -13,14 +13,14 @@
    from firebase_admin import auth
    from password_strength import PasswordPolicy


    TITLE: Final = "Example page"


    def pretty_title(title: str) -> None:
    """
    Make a centered title, and give it a red line. Adapted from 'streamlit_extras.colored_headers'
    package.
    Parameters:
    -----------
    title : str
    @@ -31,19 +31,19 @@ def pretty_title(title: str) -> None:
    unsafe_allow_html=True,
    )
    st.markdown(
    f'<hr style="background-color: #ff4b4b; margin-top: 0;'
    '<hr style="background-color: #ff4b4b; margin-top: 0;'
    ' margin-bottom: 0; height: 3px; border: none; border-radius: 3px;">',
    unsafe_allow_html=True,
    )


    def forgot_password_form() -> None:
    """
    Creates a Streamlit widget to reset a user's password.
    """Creates a Streamlit widget to reset a user's password.
    The function prompts the user to enter their email address, and generates a password reset link for the
    corresponding user. If the user has not verified their email address, a new verification link is generated
    and sent to the user.
    The function prompts the user to enter their email address, and
    generates a password reset link for the corresponding user. If the
    user has not verified their email address, a new verification link
    is generated and sent to the user.
    """

    # Get the email and password from the user
    @@ -60,16 +60,15 @@ def forgot_password_form() -> None:


    def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    """
    Creates a Streamlit widget for user registration.
    """Creates a Streamlit widget for user registration.
    The function prompts user to enter all the required information for registration through Firebase.
    Password strength is validated using entropy bits (the power of the password alphabet). Upon registration,
    a validation link is sent to the user's email address.
    The function prompts user to enter all the required information for registration through
    Firebase. Password strength is validated using entropy bits (the power of the password
    alphabet). Upon registration, a validation link is sent to the user's email address.
    Arguments:
    preauthorized (Union[str, Sequence[str], None]): An optional domain or a list of domains which are authorized
    to register.
    preauthorized (Union[str, Sequence[str], None]): An optional domain or a list of
    domains which are authorized to register.
    """

    with st.form(key="register_form"):
    @@ -115,11 +114,10 @@ def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:


    def update_password_form() -> None:
    """
    Creates a Streamlit widget to update a user's password.
    """Creates a Streamlit widget to update a user's password.
    The function prompts the user to enter a new password, and updates the corresponding user's password with the
    new value.
    The function prompts the user to enter a new password, and updates
    the corresponding user's password with the new value.
    """

    # Get the email and password from the user
    @@ -133,11 +131,10 @@ def update_password_form() -> None:


    def update_display_name_form() -> None:
    """
    Creates a Streamlit widget to update a user's display name.
    """Creates a Streamlit widget to update a user's display name.
    The function prompts the user to enter a new display name, and updates the corresponding
    user's display name with the new value.
    The function prompts the user to enter a new display name, and
    updates the corresponding user's display name with the new value.
    """

    # Get the email and password from the user
    @@ -151,11 +148,11 @@ def update_display_name_form() -> None:


    def token_decode(token: str) -> Optional[Dict[str, Any]]:
    """
    Decodes a JSON Web Token (JWT) and returns the resulting dictionary.
    """Decodes a JSON Web Token (JWT) and returns the resulting dictionary.
    The function decodes the provided JWT using the secret key specified in the application's secrets. The decoded
    dictionary contains information required for passwordless reauthentication.
    The function decodes the provided JWT using the secret key specified in the
    application's secrets. The decoded dictionary contains information required for
    passwordless reauthentication.
    Parameters
    ----------
    @@ -173,8 +170,8 @@ def token_decode(token: str) -> Optional[Dict[str, Any]]:


    def token_encode(exp_date: datetime) -> str:
    """
    Encodes a JSON Web Token (JWT) containing user session data for passwordless reauthentication.
    """Encodes a JSON Web Token (JWT) containing user session data for passwordless
    reauthentication.
    Parameters
    ----------
    @@ -188,9 +185,9 @@ def token_encode(exp_date: datetime) -> str:
    Notes
    -----
    The JWT contains the user's name, username, and the expiration date of the JWT in timestamp format.
    The `st.secrets["COOKIE_KEY"]` value is used to sign the JWT with the HS256 algorithm.
    The JWT contains the user's name, username, and the expiration date of the JWT in
    timestamp format. The `st.secrets["COOKIE_KEY"]` value is used to sign the JWT with
    the HS256 algorithm.
    """
    return jwt.encode(
    {
    @@ -204,8 +201,8 @@ def token_encode(exp_date: datetime) -> str:


    def check_cookie_is_valid(cookie_manager: stx.CookieManager, cookie_name: str) -> bool:
    """
    Check if the reauthentication cookie is valid and update the session state if it is valid.
    """Check if the reauthentication cookie is valid and update the session state if it
    is valid.
    Parameters
    ----------
    @@ -221,12 +218,12 @@ def check_cookie_is_valid(cookie_manager: stx.CookieManager, cookie_name: str) -
    Notes
    -----
    This function checks if the reauthentication cookie specified by `cookie_name` is present in the cookies stored by
    `cookie_manager`, and if it is valid, meaning that it is not expired, and that it contains the required fields
    "name" and "username". If the cookie is valid, this function updates the session state of the Streamlit app,
    setting the "name", "username", and "authentication_status" keys accordingly. If the cookie is not valid or
    cannot be decoded, the function does not modify the session state and the user does not get authenticated.
    This function checks if the reauthentication cookie specified by `cookie_name` is present in
    the cookies stored by `cookie_manager`, and if it is valid, meaning that it is not expired, and
    that it contains the required fields "name" and "username". If the cookie is valid, this
    function updates the session state of the Streamlit app, setting the "name", "username", and
    "authentication_status" keys accordingly. If the cookie is not valid or cannot be decoded, the
    function does not modify the session state and the user does not get authenticated.
    """

    token = cookie_manager.get(cookie_name)
    @@ -250,25 +247,25 @@ def check_cookie_is_valid(cookie_manager: stx.CookieManager, cookie_name: str) -
    def login_form(
    cookie_manager: stx.CookieManager, cookie_name: str, cookie_expiry_days: int = 30
    ) -> None:
    """
    Creates a login widget using Firebase REST API and a cookie manager.
    """Creates a login widget using Firebase REST API and a cookie manager.
    Arguments
    ---------
    - cookie_manager: a JWT cookie manager instance for Streamlit
    - cookie_name: a name of the reauthentication cookie
    - cookie_expiry_days: (optional) an integer representing the number of days until the cookie expires
    - cookie_expiry_days: (optional) an integer representing the number of days until the
    cookie expires
    Notes
    -----
    If the user has already been authenticated, this function does nothing. Otherwise, it displays a login form
    which prompts the user to enter their email and password. If the login credentials are valid and the user's email
    address has been verified, the user is authenticated and a reauthentication cookie is created with the specified
    expiration date.
    If the user has already been authenticated, this function does nothing. Otherwise, it displays
    a login form which prompts the user to enter their email and password. If the login credentials
    are valid and the user's email address has been verified, the user is authenticated and a
    reauthentication cookie is created with the specified expiration date.
    If the user's email address has not been verified or if there is an error with the authentication process,
    the user is not authenticated.
    If the user's email address has not been verified or if there is an error with the
    authentication process, the user is not authenticated.
    """

    if st.session_state["authentication_status"]:
    @@ -316,8 +313,8 @@ def login_form(


    def login_panel(cookie_manager: stx.CookieManager, cookie_name: str) -> None:
    """
    Creates a side panel for logged in users, preventing the login menu from appearing.
    """Creates a side panel for logged in users, preventing the login menu from
    appearing.
    Arguments
    ---------
    @@ -326,8 +323,8 @@ def login_panel(cookie_manager: stx.CookieManager, cookie_name: str) -> None:
    Notes
    -----
    If the user is logged in, this function displays two tabs for resetting the user's password and updating
    their display name.
    If the user is logged in, this function displays two tabs for resetting the user's password
    and updating their display name.
    If the user clicks the "Logout" button, the reauthentication cookie and user-related information
    from the session state is deleted, and the user is logged out.
    @@ -353,8 +350,7 @@ def login_panel(cookie_manager: stx.CookieManager, cookie_name: str) -> None:


    def not_logged_in(preauthorized: Union[str, Sequence[str], None]) -> bool:
    """
    Creates a tab panel for unauthenticated, preventing the user control sidebar and
    """Creates a tab panel for unauthenticated, preventing the user control sidebar and
    the rest of the script from appearing until the user logs in.
    Arguments
    @@ -367,12 +363,13 @@ def not_logged_in(preauthorized: Union[str, Sequence[str], None]) -> bool:
    Notes
    -----
    The function first sets the authentication status flag to True and pre-populates missing session state arguments
    for the first run. If the user is already authenticated, the login panel function is called to create a
    side panel for logged-in users. If the function call does not update the authentication status because
    the username/password does not exist in the Firebase database, the functions halts. If the user is not
    logged in, tab panel created for the Login, Register, and Forgot password forms, and the rest of the script
    does not get executed until the user logs in.
    The function first sets the authentication status flag to True and pre-populates missing
    session state arguments for the first run. If the user is already authenticated, the login
    panel function is called to create a side panel for logged-in users. If the function call
    does not update the authentication status because the username/password does not exist in
    the Firebase database, the functions halts. If the user is not logged in, tab panel created
    for the Login, Register, and Forgot password forms, and the rest of the script does not get
    executed until the user logs in.
    """

    early_return = True
    @@ -417,11 +414,10 @@ def app():

    def main() -> None:
    """Launches a Streamlit example interface.
    The interface supports authentication through Firebase REST API and JSON Web Token (JWT) cookies.
    To use the portal, the user must be registered, optionally only with a preauthorized e-mail domain. The Firebase
    REST API and JWT cookies are used for authentication. If the user is not logged in, no content other than the
    login form gets shown.
    The interface supports authentication through Firebase REST API and JSON Web Token (JWT)
    cookies. To use the portal, the user must be registered, optionally only with a preauthorized
    e-mail domain. The Firebase REST API and JWT cookies are used for authentication. If the user
    is not logged in, no content other than the login form gets shown.
    """

    st.set_page_config(page_title=TITLE, layout="wide")
  19. @vovavili vovavili revised this gist Feb 22, 2023. 1 changed file with 8 additions and 7 deletions.
    15 changes: 8 additions & 7 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -50,7 +50,7 @@ def forgot_password_form() -> None:
    email = st.text_input("Email", key="email_password")
    # Attempt to log the user in
    if not st.button("Reset password"):
    return
    return None
    user = auth.get_user_by_email(email)
    auth.generate_password_reset_link(user.email)
    if user.email_verified:
    @@ -81,7 +81,7 @@ def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    st.form_submit_button(label="Submit"),
    )
    if not register_button:
    return
    return None
    # Below are some checks to ensure proper and secure registration
    if password != confirm_password:
    return st.error("Passwords do not match", icon="🚨")
    @@ -126,7 +126,7 @@ def update_password_form() -> None:
    new_password = st.text_input("New password", key="new_password")
    # Attempt to log the user in
    if not st.button("Update password"):
    return
    return None
    user = auth.get_user_by_email(st.session_state["username"])
    auth.update_user(user.uid, password=new_password)
    return st.success("Successfully updated user password.", icon="✅")
    @@ -144,7 +144,7 @@ def update_display_name_form() -> None:
    new_name = st.text_input("New name", key="new name")
    # Attempt to log the user in
    if not st.button("Update name"):
    return
    return None
    user = auth.get_user_by_email(st.session_state["username"])
    auth.update_user(user.display_name, display_name=new_name)
    return st.success("Successfully updated user display name.", icon="✅")
    @@ -293,14 +293,14 @@ def login_form(
    "returnSecureToken": True,
    "emailVerified": True, # Require email verification
    }
    response = requests.post(url, data=json.dumps(payload)).json()
    response = requests.post(url, data=json.dumps(payload), timeout=10).json()
    if "idToken" not in response:
    return st.error("Invalid e-mail or password.", icon="🚨")
    try:
    decoded_token = auth.verify_id_token(response["idToken"])
    user = auth.get_user(decoded_token["uid"])
    if not user.email_verified:
    st.error("Please verify your e-mail address.", icon="🚨")
    return st.error("Please verify your e-mail address.", icon="🚨")
    # At last, authenticate the user
    st.session_state["name"] = user.display_name
    exp_date = datetime.utcnow() + timedelta(days=cookie_expiry_days)
    @@ -312,6 +312,7 @@ def login_form(
    st.session_state["authentication_status"] = True
    except Exception as e:
    st.error(e)
    return None


    def login_panel(cookie_manager: stx.CookieManager, cookie_name: str) -> None:
    @@ -391,7 +392,7 @@ def not_logged_in(preauthorized: Union[str, Sequence[str], None]) -> bool:
    with st.sidebar:
    login_panel(cookie_manager, cookie_name)
    return not early_return
    elif st.session_state["authentication_status"] is False:
    if st.session_state["authentication_status"] is False:
    st.error("Username/password is incorrect", icon="🚨")

    tab1, tab2, tab3 = st.tabs(["Login", "Register", "Forgot password"])
  20. @vovavili vovavili created this gist Feb 22, 2023.
    442 changes: 442 additions & 0 deletions app.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,442 @@
    import time
    import json
    from contextlib import suppress
    from datetime import datetime, timedelta
    from typing import Any, Dict, Final, Optional, Sequence, Union

    import extra_streamlit_components as stx
    import firebase_admin
    import jwt
    import requests
    import streamlit as st
    from email_validator import EmailNotValidError, validate_email
    from firebase_admin import auth
    from password_strength import PasswordPolicy

    TITLE: Final = "Example page"


    def pretty_title(title: str) -> None:
    """
    Make a centered title, and give it a red line. Adapted from 'streamlit_extras.colored_headers'
    package.
    Parameters:
    -----------
    title : str
    The title of your page.
    """
    st.markdown(
    f"<h2 style='text-align: center'>{title}</h2>",
    unsafe_allow_html=True,
    )
    st.markdown(
    f'<hr style="background-color: #ff4b4b; margin-top: 0;'
    ' margin-bottom: 0; height: 3px; border: none; border-radius: 3px;">',
    unsafe_allow_html=True,
    )


    def forgot_password_form() -> None:
    """
    Creates a Streamlit widget to reset a user's password.
    The function prompts the user to enter their email address, and generates a password reset link for the
    corresponding user. If the user has not verified their email address, a new verification link is generated
    and sent to the user.
    """

    # Get the email and password from the user
    email = st.text_input("Email", key="email_password")
    # Attempt to log the user in
    if not st.button("Reset password"):
    return
    user = auth.get_user_by_email(email)
    auth.generate_password_reset_link(user.email)
    if user.email_verified:
    return st.success(f"Password reset link has been sent to {email}", icon="✅")
    st.warning(f"{email} is not verified. Resending the verification link.")
    return auth.generate_email_verification_link(user.email)


    def register_user_form(preauthorized: Union[str, Sequence[str], None]) -> None:
    """
    Creates a Streamlit widget for user registration.
    The function prompts user to enter all the required information for registration through Firebase.
    Password strength is validated using entropy bits (the power of the password alphabet). Upon registration,
    a validation link is sent to the user's email address.
    Arguments:
    preauthorized (Union[str, Sequence[str], None]): An optional domain or a list of domains which are authorized
    to register.
    """

    with st.form(key="register_form"):
    email, name, password, confirm_password, register_button = (
    st.text_input("E-mail"),
    st.text_input("Name"),
    st.text_input("Password", type="password"),
    st.text_input("Confirm password", type="password"),
    st.form_submit_button(label="Submit"),
    )
    if not register_button:
    return
    # Below are some checks to ensure proper and secure registration
    if password != confirm_password:
    return st.error("Passwords do not match", icon="🚨")
    if not name:
    return st.error("Please enter your name", icon="🚨")
    if preauthorized and not email.endswith(preauthorized):
    return st.error("Domain not allowed", icon="🚨")
    try:
    # Check that the email address is valid.
    validate_email(email, check_deliverability=True)
    except EmailNotValidError as e:
    # Email is not valid.
    # The exception message is human-readable.
    return st.error(e, icon="🚨")

    # Need a password that has minimum 66 entropy bits (the power of its alphabet)
    # I multiply this number by 150 (100 * 1.5) to display password strength with st.progress
    strength = round(PasswordPolicy().password(password).strength() * 150)
    if strength < 100:
    st.progress(strength)
    return st.warning(
    "Password is too weak. Please choose a stronger password.", icon="⚠️"
    )
    auth.create_user(email=email, password=password)
    st.success(
    "Your account has been created successfully. To complete the registration process, "
    "please verify your email address by clicking on the link we have sent to your inbox.",
    icon="✅",
    )
    return st.balloons()


    def update_password_form() -> None:
    """
    Creates a Streamlit widget to update a user's password.
    The function prompts the user to enter a new password, and updates the corresponding user's password with the
    new value.
    """

    # Get the email and password from the user
    new_password = st.text_input("New password", key="new_password")
    # Attempt to log the user in
    if not st.button("Update password"):
    return
    user = auth.get_user_by_email(st.session_state["username"])
    auth.update_user(user.uid, password=new_password)
    return st.success("Successfully updated user password.", icon="✅")


    def update_display_name_form() -> None:
    """
    Creates a Streamlit widget to update a user's display name.
    The function prompts the user to enter a new display name, and updates the corresponding
    user's display name with the new value.
    """

    # Get the email and password from the user
    new_name = st.text_input("New name", key="new name")
    # Attempt to log the user in
    if not st.button("Update name"):
    return
    user = auth.get_user_by_email(st.session_state["username"])
    auth.update_user(user.display_name, display_name=new_name)
    return st.success("Successfully updated user display name.", icon="✅")


    def token_decode(token: str) -> Optional[Dict[str, Any]]:
    """
    Decodes a JSON Web Token (JWT) and returns the resulting dictionary.
    The function decodes the provided JWT using the secret key specified in the application's secrets. The decoded
    dictionary contains information required for passwordless reauthentication.
    Parameters
    ----------
    token : str
    The JWT to decode.
    Returns
    -------
    Dict[str, Any] or None
    The decoded dictionary, or None if an error occurred during decoding.
    """

    with suppress(Exception):
    return jwt.decode(token, st.secrets["COOKIE_KEY"], algorithms=["HS256"])


    def token_encode(exp_date: datetime) -> str:
    """
    Encodes a JSON Web Token (JWT) containing user session data for passwordless reauthentication.
    Parameters
    ----------
    exp_date : datetime
    The expiration date of the JWT.
    Returns
    -------
    str
    The encoded JWT cookie string for reauthentication.
    Notes
    -----
    The JWT contains the user's name, username, and the expiration date of the JWT in timestamp format.
    The `st.secrets["COOKIE_KEY"]` value is used to sign the JWT with the HS256 algorithm.
    """
    return jwt.encode(
    {
    "name": st.session_state["name"],
    "username": st.session_state["username"],
    "exp_date": exp_date.timestamp(),
    },
    st.secrets["COOKIE_KEY"],
    algorithm="HS256",
    )


    def check_cookie_is_valid(cookie_manager: stx.CookieManager, cookie_name: str) -> bool:
    """
    Check if the reauthentication cookie is valid and update the session state if it is valid.
    Parameters
    ----------
    cookie_manager : stx.CookieManager
    A JWT cookie manager instance for Streamlit
    cookie_name : str
    The name of the reauthentication cookie.
    Returns
    -------
    bool
    True if the cookie is valid and the session state is updated successfully; False otherwise.
    Notes
    -----
    This function checks if the reauthentication cookie specified by `cookie_name` is present in the cookies stored by
    `cookie_manager`, and if it is valid, meaning that it is not expired, and that it contains the required fields
    "name" and "username". If the cookie is valid, this function updates the session state of the Streamlit app,
    setting the "name", "username", and "authentication_status" keys accordingly. If the cookie is not valid or
    cannot be decoded, the function does not modify the session state and the user does not get authenticated.
    """

    token = cookie_manager.get(cookie_name)
    if token is None:
    return False
    token = token_decode(token)
    if (
    token is not False
    and not st.session_state["logout"]
    and token["exp_date"] > datetime.utcnow().timestamp()
    and "name" in token
    and "username" in token
    ):
    st.session_state["name"] = token["name"]
    st.session_state["username"] = token["username"]
    st.session_state["authentication_status"] = True
    return True
    return False


    def login_form(
    cookie_manager: stx.CookieManager, cookie_name: str, cookie_expiry_days: int = 30
    ) -> None:
    """
    Creates a login widget using Firebase REST API and a cookie manager.
    Arguments
    ---------
    - cookie_manager: a JWT cookie manager instance for Streamlit
    - cookie_name: a name of the reauthentication cookie
    - cookie_expiry_days: (optional) an integer representing the number of days until the cookie expires
    Notes
    -----
    If the user has already been authenticated, this function does nothing. Otherwise, it displays a login form
    which prompts the user to enter their email and password. If the login credentials are valid and the user's email
    address has been verified, the user is authenticated and a reauthentication cookie is created with the specified
    expiration date.
    If the user's email address has not been verified or if there is an error with the authentication process,
    the user is not authenticated.
    """

    if st.session_state["authentication_status"]:
    return None
    if check_cookie_is_valid(cookie_manager, cookie_name):
    return None
    with st.form("Login"):
    email = st.text_input("Email")
    st.session_state["username"] = email
    password = st.text_input("Password", type="password")
    if not st.form_submit_button("Login"):
    return None

    # Authenticate the user with Firebase REST API
    url = (
    f"https://identitytoolkit.googleapis.com/v1/"
    f"accounts:signInWithPassword?key={st.secrets['FIREBASE_API_KEY']}"
    )
    payload = {
    "email": email,
    "password": password,
    "returnSecureToken": True,
    "emailVerified": True, # Require email verification
    }
    response = requests.post(url, data=json.dumps(payload)).json()
    if "idToken" not in response:
    return st.error("Invalid e-mail or password.", icon="🚨")
    try:
    decoded_token = auth.verify_id_token(response["idToken"])
    user = auth.get_user(decoded_token["uid"])
    if not user.email_verified:
    st.error("Please verify your e-mail address.", icon="🚨")
    # At last, authenticate the user
    st.session_state["name"] = user.display_name
    exp_date = datetime.utcnow() + timedelta(days=cookie_expiry_days)
    cookie_manager.set(
    cookie_name,
    token_encode(exp_date),
    expires_at=exp_date,
    )
    st.session_state["authentication_status"] = True
    except Exception as e:
    st.error(e)


    def login_panel(cookie_manager: stx.CookieManager, cookie_name: str) -> None:
    """
    Creates a side panel for logged in users, preventing the login menu from appearing.
    Arguments
    ---------
    - cookie_manager: a JWT cookie manager instance for Streamlit
    - cookie_name: a name of the reauthentication cookie
    Notes
    -----
    If the user is logged in, this function displays two tabs for resetting the user's password and updating
    their display name.
    If the user clicks the "Logout" button, the reauthentication cookie and user-related information
    from the session state is deleted, and the user is logged out.
    """

    if st.button("Logout"):
    cookie_manager.delete(cookie_name)
    st.session_state["logout"] = True
    st.session_state["name"] = None
    st.session_state["username"] = None
    st.session_state["authentication_status"] = None
    return None
    st.write(f"Welcome, *{st.session_state['name']}*!")
    tab1, tab2 = st.tabs(["Reset password", "Update user details"])
    try:
    with tab1:
    update_password_form()
    with tab2:
    update_display_name_form()
    except Exception as e:
    st.error(e)
    return None


    def not_logged_in(preauthorized: Union[str, Sequence[str], None]) -> bool:
    """
    Creates a tab panel for unauthenticated, preventing the user control sidebar and
    the rest of the script from appearing until the user logs in.
    Arguments
    ---
    'preauthorized' - an optional domain or a list of domains which are authorized to register
    Returns
    -------
    Authentication status boolean.
    Notes
    -----
    The function first sets the authentication status flag to True and pre-populates missing session state arguments
    for the first run. If the user is already authenticated, the login panel function is called to create a
    side panel for logged-in users. If the function call does not update the authentication status because
    the username/password does not exist in the Firebase database, the functions halts. If the user is not
    logged in, tab panel created for the Login, Register, and Forgot password forms, and the rest of the script
    does not get executed until the user logs in.
    """

    early_return = True
    # In case of a first run, pre-populate missing session state arguments
    cookie_manager, cookie_name = stx.CookieManager(), "login_cookie"
    for key in {"name", "authentication_status", "username", "logout"}.difference(
    set(st.session_state)
    ):
    st.session_state[key] = None

    # Give a loading message when the website page restarts
    with st.spinner("Loading..."):
    while "authentication_status" not in st.session_state:
    time.sleep(0.1)

    if st.session_state["authentication_status"]:
    with st.sidebar:
    login_panel(cookie_manager, cookie_name)
    return not early_return
    elif st.session_state["authentication_status"] is False:
    st.error("Username/password is incorrect", icon="🚨")

    tab1, tab2, tab3 = st.tabs(["Login", "Register", "Forgot password"])
    with tab1:
    login_form(cookie_manager, cookie_name)
    try:
    with tab2:
    register_user_form(preauthorized)
    with tab3:
    forgot_password_form()
    except Exception as e:
    st.error(e)
    return early_return


    def app():
    """This is a part of a Streamlit app which is only visible if the user is logged in."""
    st.subheader("Yay!!!")
    st.write("You are logged in!")
    st.write("Hello :sunglasses:")


    def main() -> None:
    """Launches a Streamlit example interface.
    The interface supports authentication through Firebase REST API and JSON Web Token (JWT) cookies.
    To use the portal, the user must be registered, optionally only with a preauthorized e-mail domain. The Firebase
    REST API and JWT cookies are used for authentication. If the user is not logged in, no content other than the
    login form gets shown.
    """

    st.set_page_config(page_title=TITLE, layout="wide")
    # noinspection PyProtectedMember
    if not firebase_admin._apps:
    cred = firebase_admin.credentials.Certificate("firebase_auth_token.json")
    firebase_admin.initialize_app(cred)

    pretty_title(TITLE)

    if not_logged_in(preauthorized="gmail.com"):
    return

    app()


    # Run the Streamlit app
    if __name__ == "__main__":
    main()