Source code for streamlit_authenticator.views.authentication_view

"""
Script description: This module renders the login, logout, register user, reset password,
forgot password, forgot username, and modify user details widgets.

Libraries imported:
-------------------
- json: Handles JSON documents.
- time: Implements sleep function.
- typing: Implements standard typing notations for Python functions.
- streamlit: Framework used to build pure Python web applications.
"""

import json
import time
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union

import streamlit as st

from ..controllers import AuthenticationController, CookieController
from .. import params
from ..utilities import (DeprecationError,
                         Encryptor,
                         Helpers,
                         LogoutError,
                         ResetError,
                         UpdateError,
                         Validator)


[docs] class Authenticate: """ This class renders login, logout, register user, reset password, forgot password, forgot username, and modify user details widgets. """ def __init__( self, credentials: Union[Dict[str, Any], str], cookie_name: str = 'some_cookie_name', cookie_key: str = 'some_key', cookie_expiry_days: float = 30.0, validator: Optional[Validator] = None, auto_hash: bool = True, api_key: Optional[str] = None, **kwargs: Optional[Dict[str, Any]] ) -> None: """ Initializes an instance of Authenticate. Parameters ---------- credentials : dict or str Dictionary of user credentials or path to a configuration file. cookie_name : str, default='some_cookie_name' Name of the re-authentication cookie stored in the client's browser. cookie_key : str, default='some_key' Secret key used for encrypting the re-authentication cookie. cookie_expiry_days : float, default=30.0 Expiry time for the re-authentication cookie in days. validator : Validator, optional Validator object for checking username, name, and email validity. auto_hash : bool, default=True If True, passwords will be automatically hashed. api_key : str, optional API key for sending password reset and authentication emails. **kwargs : dict, optional Additional keyword arguments. """ self.api_key = api_key self.attrs = kwargs self.secret_key = cookie_key if isinstance(validator, dict): raise DeprecationError(f"""Please note that the 'pre_authorized' parameter has been removed from the Authenticate class and added directly to the 'register_user' function. For further information please refer to {params.REGISTER_USER_LINK}.""") self.path = credentials if isinstance(credentials, str) else None self.cookie_controller = CookieController(cookie_name, cookie_key, cookie_expiry_days, self.path) self.authentication_controller = AuthenticationController(credentials, validator, auto_hash, self.path, self.api_key, self.secret_key, self.attrs.get('server_url')) self.encryptor = Encryptor(self.secret_key)
[docs] def forgot_password(self, location: Literal['main', 'sidebar'] = 'main', fields: Optional[Dict[str, str]] = None, captcha: bool = False, send_email: bool = False, two_factor_auth: bool = False, clear_on_submit: bool = False, key: str = 'Forgot password', callback: Optional[Callable] = None ) -> Tuple[Optional[str], Optional[str], Optional[str]]: """ Renders a forgot password widget. Parameters ---------- location : {'main', 'sidebar'}, default='main' Location of the forgot password widget. fields : dict, optional Custom labels for form fields and buttons. captcha : bool, default=False If True, requires captcha validation. send_email : bool, default=False If True, sends the new password to the user's email. two_factor_auth : bool, default=False If True, enables two-factor authentication. clear_on_submit : bool, default=False If True, clears input fields after form submission. key : str, default='Forgot password' Unique key for the widget to prevent duplicate WidgetID errors. callback : Callable, optional Function to be executed after form submission. Returns ------- tuple[str, str, str] or (None, None, None) - Username associated with the forgotten password. - Email associated with the forgotten password. - New plain-text password to be securely transferred to the user. """ if fields is None: fields = {'Form name':'Forgot password', 'Username':'Username', 'Captcha':'Captcha', 'Submit':'Submit', 'Dialog name':'Verification code', 'Code':'Code', 'Error':'Code is incorrect'} if location not in ['main', 'sidebar']: raise ValueError("Location must be one of 'main' or 'sidebar'") if location == 'main': forgot_password_form = st.form(key=key, clear_on_submit=clear_on_submit) elif location == 'sidebar': forgot_password_form = st.sidebar.form(key=key, clear_on_submit=clear_on_submit) forgot_password_form.subheader(fields.get('Form name', 'Forgot password')) username = forgot_password_form.text_input(fields.get('Username', 'Username'), autocomplete='off') entered_captcha = None if captcha: entered_captcha = forgot_password_form.text_input(fields.get('Captcha', 'Captcha'), autocomplete='off') forgot_password_form.image(Helpers.generate_captcha('forgot_password_captcha', self.secret_key)) result = (None, None, None) if forgot_password_form.form_submit_button(fields.get('Submit', 'Submit')): result = self.authentication_controller.forgot_password(username, callback, captcha, entered_captcha) if not two_factor_auth: if send_email: self.authentication_controller.send_password(result) return result self.__two_factor_auth(result[1], result, widget='forgot_password', fields=fields) if two_factor_auth and st.session_state.get('2FA_check_forgot_password'): decrypted = self.encryptor.decrypt(st.session_state['2FA_content_forgot_password']) result = json.loads(decrypted) if send_email: self.authentication_controller.send_password(result) del st.session_state['2FA_check_forgot_password'] return result return None, None, None
[docs] def forgot_username(self, location: Literal['main', 'sidebar'] = 'main', fields: Optional[Dict[str, str]] = None, captcha: bool = False, send_email: bool = False, two_factor_auth: bool = False, clear_on_submit: bool = False, key: str = 'Forgot username', callback: Optional[Callable]=None) -> Tuple[Optional[str], Optional[str]]: """ Renders a forgot username widget. Parameters ---------- location : {'main', 'sidebar'}, default='main' Location of the forgot username widget. fields : dict, optional Custom labels for form fields and buttons. captcha : bool, default=False If True, requires captcha validation. send_email : bool, default=False If True, sends the retrieved username to the user's email. two_factor_auth : bool, default=False If True, enables two-factor authentication. clear_on_submit : bool, default=False If True, clears input fields after form submission. key : str, default='Forgot username' Unique key for the widget to prevent duplicate WidgetID errors. callback : Callable, optional Function to be executed after form submission. Returns ------- tuple[str, str] or (None, str) - Username associated with the forgotten username. - Email associated with the forgotten username. """ if fields is None: fields = {'Form name':'Forgot username', 'Email':'Email', 'Captcha':'Captcha', 'Submit':'Submit', 'Dialog name':'Verification code', 'Code':'Code', 'Error':'Code is incorrect'} if location not in ['main', 'sidebar']: raise ValueError("Location must be one of 'main' or 'sidebar'") if location == 'main': forgot_username_form = st.form(key=key, clear_on_submit=clear_on_submit) elif location == 'sidebar': forgot_username_form = st.sidebar.form(key=key, clear_on_submit=clear_on_submit) forgot_username_form.subheader('Forgot username' if 'Form name' not in fields else fields['Form name']) email = forgot_username_form.text_input('Email' if 'Email' not in fields else fields['Email'], autocomplete='off') entered_captcha = None if captcha: entered_captcha = forgot_username_form.text_input('Captcha' if 'Captcha' not in fields else fields['Captcha'], autocomplete='off') forgot_username_form.image(Helpers.generate_captcha('forgot_username_captcha', self.secret_key)) if forgot_username_form.form_submit_button('Submit' if 'Submit' not in fields else fields['Submit']): result = self.authentication_controller.forgot_username(email, callback, captcha, entered_captcha) if not two_factor_auth: if send_email: self.authentication_controller.send_username(result) return result self.__two_factor_auth(email, result, widget='forgot_username', fields=fields) if two_factor_auth and st.session_state.get('2FA_check_forgot_username'): decrypted = self.encryptor.decrypt(st.session_state['2FA_content_forgot_username']) result = json.loads(decrypted) if send_email: self.authentication_controller.send_username(result) del st.session_state['2FA_check_forgot_username'] return result return None, email
[docs] def experimental_guest_login(self, button_name: str='Guest login', location: Literal['main', 'sidebar'] = 'main', provider: Literal['google', 'microsoft'] = 'google', oauth2: Optional[Dict[str, Any]] = None, max_concurrent_users: Optional[int]=None, single_session: bool=False, roles: Optional[List[str]]=None, use_container_width: bool=False, callback: Optional[Callable]=None) -> None: """ Renders a guest login button. Parameters ---------- button_name : str, default='Guest login' Display name for the guest login button. location : {'main', 'sidebar'}, default='main' Location where the guest login button is rendered. provider : {'google', 'microsoft'}, default='google' OAuth2 provider used for authentication. oauth2 : dict, optional Configuration parameters for OAuth2 authentication. max_concurrent_users : int, optional Maximum number of users allowed to log in concurrently. single_session : bool, default=False If True, prevents users from logging into multiple sessions simultaneously. roles : list of str, optional Roles assigned to guest users. use_container_width : bool, default=False If True, the button width matches the container. callback : Callable, optional Function to execute when the button is pressed. """ if location not in ['main', 'sidebar']: raise ValueError("Location must be one of 'main' or 'sidebar'") if provider not in ['google', 'microsoft']: raise ValueError("Provider must be one of 'google' or 'microsoft'") if not st.session_state.get('authentication_status'): token = self.cookie_controller.get_cookie() if token: self.authentication_controller.login(token=token) time.sleep(self.attrs.get('login_sleep_time', params.PRE_LOGIN_SLEEP_TIME)) if not st.session_state.get('authentication_status'): auth_endpoint = \ self.authentication_controller.guest_login(cookie_controller=\ self.cookie_controller, provider=provider, oauth2=oauth2, max_concurrent_users=\ max_concurrent_users, single_session=single_session, roles=roles, callback=callback) if location == 'main' and auth_endpoint: st.link_button(button_name, url=auth_endpoint, use_container_width=use_container_width) if location == 'sidebar' and auth_endpoint: st.sidebar.link_button(button_name, url=auth_endpoint, use_container_width=use_container_width)
[docs] def login(self, location: Literal['main', 'sidebar', 'unrendered'] = 'main', max_concurrent_users: Optional[int] = None, max_login_attempts: Optional[int] = None, fields: Optional[Dict[str, str]] = None, captcha: bool = False, single_session: bool=False, clear_on_submit: bool = False, key: str = 'Login', callback: Optional[Callable] = None ) -> Optional[Tuple[Optional[str], Optional[bool], Optional[str]]]: """ Renders a login widget. Parameters ---------- location : {'main', 'sidebar', 'unrendered'}, default='main' Location where the login widget is rendered. max_concurrent_users : int, optional Maximum number of users allowed to log in concurrently. max_login_attempts : int, optional Maximum number of failed login attempts allowed. fields : dict, optional Custom labels for form fields and buttons. captcha : bool, default=False If True, requires captcha validation. single_session : bool, default=False If True, prevents users from logging into multiple sessions simultaneously. clear_on_submit : bool, default=False If True, clears input fields after form submission. key : str, default='Login' Unique key for the widget to prevent duplicate WidgetID errors. callback : Callable, optional Function to execute when the form is submitted. Returns ------- tuple[str, bool, str] or None - If `location='unrendered'`, returns (user's name, authentication status, username). - Otherwise, returns None. """ if fields is None: fields = {'Form name':'Login', 'Username':'Username', 'Password':'Password', 'Login':'Login', 'Captcha':'Captcha'} if location not in ['main', 'sidebar', 'unrendered']: raise ValueError("Location must be one of 'main' or 'sidebar' or 'unrendered'") if not st.session_state.get('authentication_status'): token = self.cookie_controller.get_cookie() if token: self.authentication_controller.login(token=token) time.sleep(self.attrs.get('login_sleep_time', params.PRE_LOGIN_SLEEP_TIME)) if not st.session_state.get('authentication_status'): if location == 'main': login_form = st.form(key=key, clear_on_submit=clear_on_submit) elif location == 'sidebar': login_form = st.sidebar.form(key=key, clear_on_submit=clear_on_submit) elif location == 'unrendered': return (st.session_state['name'], st.session_state['authentication_status'], st.session_state['username']) login_form.subheader('Login' if 'Form name' not in fields else fields['Form name']) username = login_form.text_input('Username' if 'Username' not in fields else fields['Username'], autocomplete='off') if 'password_hint' in st.session_state: password = login_form.text_input('Password' if 'Password' not in fields else fields['Password'], type='password', help=st.session_state['password_hint'], autocomplete='off') else: password = login_form.text_input('Password' if 'Password' not in fields else fields['Password'], type='password', autocomplete='off') entered_captcha = None if captcha: entered_captcha = login_form.text_input('Captcha' if 'Captcha' not in fields else fields['Captcha'], autocomplete='off') login_form.image(Helpers.generate_captcha('login_captcha', self.secret_key)) if login_form.form_submit_button('Login' if 'Login' not in fields else fields['Login']): if self.authentication_controller.login(username, password, max_concurrent_users, max_login_attempts, single_session=single_session, callback=callback, captcha=captcha, entered_captcha=entered_captcha): self.cookie_controller.set_cookie() if self.path and self.cookie_controller.get_cookie(): st.rerun()
[docs] def logout(self, button_name: str = 'Logout', location: Literal['main', 'sidebar', 'unrendered'] = 'main', key: str = 'Logout', use_container_width: bool = False, callback: Optional[Callable] = None) -> None: """ Renders a logout button. Parameters ---------- button_name : str, default='Logout' Display name for the logout button. location : {'main', 'sidebar', 'unrendered'}, default='main' Location where the logout button is rendered. key : str, default='Logout' Unique key for the widget, useful in multi-page applications. use_container_width : bool, default=False If True, the button width matches the container. callback : Callable, optional Function to execute when the button is pressed. """ if not st.session_state.get('authentication_status'): raise LogoutError('User must be logged in to use the logout button') if location not in ['main', 'sidebar', 'unrendered']: raise ValueError("Location must be one of 'main' or 'sidebar' or 'unrendered'") if location == 'main': if st.button(button_name, key=key, use_container_width=use_container_width): self.authentication_controller.logout(callback) self.cookie_controller.delete_cookie() elif location == 'sidebar': if st.sidebar.button(button_name, key=key, use_container_width=use_container_width): self.authentication_controller.logout(callback) self.cookie_controller.delete_cookie() elif location == 'unrendered': if st.session_state.get('authentication_status'): self.authentication_controller.logout() self.cookie_controller.delete_cookie()
[docs] def register_user(self, location: Literal['main', 'sidebar'] = 'main', pre_authorized: Optional[List[str]] = None, domains: Optional[List[str]] = None, fields: Optional[Dict[str, str]] = None, captcha: bool = True, roles: Optional[List[str]] = None, merge_username_email: bool = False, password_hint: bool = True, two_factor_auth: bool = False, clear_on_submit: bool = False, key: str = 'Register user', callback: Optional[Callable] = None ) -> Tuple[Optional[str], Optional[str], Optional[str]]: """ Renders a register new user widget. Parameters ---------- location : {'main', 'sidebar'}, default='main' Location where the registration widget is rendered. pre_authorized : list of str, optional List of emails of unregistered users who are authorized to register. domains : list of str, optional List of allowed email domains (e.g., ['gmail.com', 'yahoo.com']). fields : dict, optional Custom labels for form fields and buttons. captcha : bool, default=True If True, requires captcha validation. roles : list of str, optional User roles for registered users. merge_username_email : bool, default=False If True, uses the email as the username. password_hint : bool, default=True If True, includes a password hint field. two_factor_auth : bool, default=False If True, enables two-factor authentication. clear_on_submit : bool, default=False If True, clears input fields after form submission. key : str, default='Register user' Unique key for the widget to prevent duplicate WidgetID errors. callback : Callable, optional Function to execute when the form is submitted. Returns ------- tuple[str, str, str] or (None, None, None) - Email associated with the new user. - Username associated with the new user. - Name associated with the new user. """ if isinstance(pre_authorized, bool) or isinstance(pre_authorized, dict): raise DeprecationError(f"""Please note that the 'pre_authorized' parameter now requires a list of pre-authorized emails. For further information please refer to {params.REGISTER_USER_LINK}.""") if fields is None: fields = {'Form name':'Register user', 'First name':'First name', 'Last name':'Last name', 'Email':'Email', 'Username':'Username', 'Password':'Password', 'Repeat password':'Repeat password', 'Password hint':'Password hint', 'Captcha':'Captcha', 'Register':'Register', 'Dialog name':'Verification code', 'Code':'Code', 'Submit':'Submit', 'Error':'Code is incorrect'} if location not in ['main', 'sidebar']: raise ValueError("Location must be one of 'main' or 'sidebar'") if location == 'main': register_user_form = st.form(key=key, clear_on_submit=clear_on_submit) elif location == 'sidebar': register_user_form = st.sidebar.form(key=key, clear_on_submit=clear_on_submit) register_user_form.subheader('Register user' if 'Form name' not in fields else fields['Form name']) col1_1, col2_1 = register_user_form.columns(2) new_first_name = col1_1.text_input('First name' if 'First name' not in fields else fields['First name'], autocomplete='off') new_last_name = col2_1.text_input('Last name' if 'Last name' not in fields else fields['Last name'], autocomplete='off') if merge_username_email: new_email = register_user_form.text_input('Email' if 'Email' not in fields else fields['Email'], autocomplete='off') new_username = new_email else: new_email = col1_1.text_input('Email' if 'Email' not in fields else fields['Email'], autocomplete='off') new_username = col2_1.text_input('Username' if 'Username' not in fields else fields['Username'], autocomplete='off') col1_2, col2_2 = register_user_form.columns(2) password_instructions = self.attrs.get('password_instructions', params.PASSWORD_INSTRUCTIONS) new_password = col1_2.text_input('Password' if 'Password' not in fields else fields['Password'], type='password', help=password_instructions, autocomplete='off') new_password_repeat = col2_2.text_input('Repeat password' if 'Repeat password' not in fields else fields['Repeat password'], type='password', autocomplete='off') if password_hint: password_hint = register_user_form.text_input('Password hint' if 'Password hint' not in fields else fields['Password hint'], autocomplete='off') entered_captcha = None if captcha: entered_captcha = register_user_form.text_input('Captcha' if 'Captcha' not in fields else fields['Captcha'], autocomplete='off').strip() register_user_form.image(Helpers.generate_captcha('register_user_captcha', self.secret_key)) if register_user_form.form_submit_button('Register' if 'Register' not in fields else fields['Register']): if two_factor_auth: self.__two_factor_auth(new_email, widget='register', fields=fields) else: return self.authentication_controller.register_user(new_first_name, new_last_name, new_email, new_username, new_password, new_password_repeat, password_hint, pre_authorized, domains, roles, callback, captcha, entered_captcha) if two_factor_auth and st.session_state.get('2FA_check_register'): del st.session_state['2FA_check_register'] return self.authentication_controller.register_user(new_first_name, new_last_name, new_email, new_username, new_password, new_password_repeat, password_hint, pre_authorized, domains, roles, callback, captcha, entered_captcha) return None, None, None
[docs] def reset_password(self, username: str, location: Literal['main', 'sidebar'] = 'main', fields: Optional[Dict[str, str]] = None, clear_on_submit: bool = False, key: str = 'Reset password', callback: Optional[Callable] = None ) -> Optional[bool]: """ Renders a password reset widget. Parameters ---------- username : str Username of the user whose password is being reset. location : {'main', 'sidebar'}, default='main' Location where the password reset widget is rendered. fields : dict, optional Custom labels for form fields and buttons. clear_on_submit : bool, default=False If True, clears input fields after form submission. key : str, default='Reset password' Unique key for the widget to prevent duplicate WidgetID errors. callback : Callable, optional Function to execute when the form is submitted. Returns ------- bool or None - True if the password reset was successful. - None if the reset failed or was not attempted. """ if not st.session_state.get('authentication_status'): raise ResetError('User must be logged in to use the reset password widget') if fields is None: fields = {'Form name':'Reset password', 'Current password':'Current password', 'New password':'New password','Repeat password':'Repeat password', 'Reset':'Reset'} if location not in ['main', 'sidebar']: raise ValueError("Location must be one of 'main' or 'sidebar'") if location == 'main': reset_password_form = st.form(key=key, clear_on_submit=clear_on_submit) elif location == 'sidebar': reset_password_form = st.sidebar.form(key=key, clear_on_submit=clear_on_submit) reset_password_form.subheader('Reset password' if 'Form name' not in fields else fields['Form name']) password = reset_password_form.text_input('Current password' if 'Current password' not in fields else fields['Current password'], type='password', autocomplete='off').strip() password_instructions = self.attrs.get('password_instructions', params.PASSWORD_INSTRUCTIONS) new_password = reset_password_form.text_input('New password' if 'New password' not in fields else fields['New password'], type='password', help=password_instructions, autocomplete='off').strip() new_password_repeat = reset_password_form.text_input('Repeat password' if 'Repeat password' not in fields else fields['Repeat password'], type='password', autocomplete='off').strip() if reset_password_form.form_submit_button('Reset' if 'Reset' not in fields else fields['Reset']): if self.authentication_controller.reset_password(username, password, new_password, new_password_repeat, callback): return True return None
def __two_factor_auth(self, email: str, content: Optional[Dict[str, Any]] = None, fields: Optional[Dict[str, str]] = None, widget: Optional[str] = None ) -> None: """ Renders a two-factor authentication widget. Parameters ---------- email : str Email address to which the two-factor authentication code is sent. content : dict, optional Optional content to save in session state. fields : dict, optional Custom labels for form fields and buttons. widget : str, optional Widget name used as a key in session state variables. """ self.authentication_controller.generate_two_factor_auth_code(email, widget) @st.dialog('Verification code' if 'Dialog name' not in fields else fields['Dialog name']) def two_factor_auth_form(): code = st.text_input('Code' if 'Code' not in fields else fields['Code'], help='Please enter the code sent to your email' if 'Instructions' not in fields else fields['Instructions'], autocomplete='off') if st.button('Submit' if 'Submit' not in fields else fields['Submit']): if self.authentication_controller.check_two_factor_auth_code(code, content, widget): st.rerun() else: st.error('Code is incorrect' if 'Error' not in fields else fields['Error']) two_factor_auth_form()
[docs] def update_user_details(self, username: str, location: Literal['main', 'sidebar'] = 'main', fields: Optional[Dict[str, str]] = None, clear_on_submit: bool = False, key: str = 'Update user details', callback: Optional[Callable] = None) -> bool: """ Renders an update user details widget. Parameters ---------- username : str Username of the user whose details are being updated. location : {'main', 'sidebar'}, default='main' Location where the update user details widget is rendered. fields : dict, optional Custom labels for form fields and buttons. clear_on_submit : bool, default=False If True, clears input fields after form submission. key : str, default='Update user details' Unique key for the widget to prevent duplicate WidgetID errors. callback : Callable, optional Function to execute when the form is submitted. Returns ------- bool or None - True if user details were successfully updated. - None if the update failed or was not attempted. """ if not st.session_state.get('authentication_status'): raise UpdateError('User must be logged in to use the update user details widget') if fields is None: fields = {'Form name':'Update user details', 'Field':'Field', 'First name':'First name', 'Last name':'Last name', 'Email':'Email', 'New value':'New value', 'Update':'Update'} if location not in ['main', 'sidebar']: raise ValueError("Location must be one of 'main' or 'sidebar'") if location == 'main': update_user_details_form = st.form(key=key, clear_on_submit=clear_on_submit) elif location == 'sidebar': update_user_details_form = st.sidebar.form(key=key, clear_on_submit=clear_on_submit) update_user_details_form.subheader('Update user details' if 'Form name' not in fields else fields['Form name']) update_user_details_form_fields = ['First name' if 'First name' not in fields else \ fields['First name'], 'Last name' if 'Last name' not in fields else \ fields['Last name'], 'Email' if 'Email' not in fields else fields['Email']] field = update_user_details_form.selectbox('Field' if 'Field' not in fields else fields['Field'], update_user_details_form_fields) new_value = update_user_details_form.text_input('New value' if 'New value' not in fields else fields['New value'], autocomplete='off').strip() if update_user_details_form_fields.index(field) == 0: field = 'first_name' elif update_user_details_form_fields.index(field) == 1: field = 'last_name' elif update_user_details_form_fields.index(field) == 2: field = 'email' if update_user_details_form.form_submit_button('Update' if 'Update' not in fields else fields['Update']): if self.authentication_controller.update_user_details(username, field, new_value, callback): # self.cookie_controller.set_cookie() return True