viernes, 22 de agosto de 2025

WEBPROPv2 (III). Validación de usuarios sin nginx (I). Log in. Visión general

 0. Introducción

Ya se vió en el apartado anterior que había una función "beforeware" que salvo los "endpoints" de recursos (imágenes, css, etc) verificaba que el usuario se había identificado correctamente, En caso contrario lo enviaba a una pantalla de "log in" con doble factor de identificación,

Por otra parte el tratamiento de los endpoints está definido en los siguientes módulos python:

  • routes00comp.py : Pruebas de acciones que fectan al departamento de informatica
  • routes01mnu.py: Acciones del menú y de "log in"
  • routes02form: Acciones del formulario
  • routes03grid:Acciones de la grid

1. Proceso de "Log in"

Para accedir a la pantalla de login de forma directa se introduce3 esta url:

https://edu.tavernes.es:5001/softprop/login

El tratamiento de este endpoint está en el módulo routes01mnu.py tal como se vió en la introducción 

La pantalla es esta:



Primero te pide usuario y contraseña y luego te pide el segundo factor de autenticación.

1.1 Primera pantalla del log in

Solamente nos pide usuario y contraseña. El endpoint tratado en routes01mnu.py tiene el siguiente código específico para esta pantalla, donde se hace una llamada a xmloginform.login_form()

@app.get("/softprop/login")
def get_login():
	''' Primero muestra el formulario de login sin campos de TOTP'''
	a=xmloginform.login_form() 
	return a 

Y el código específico para crear el formulario es:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def login_form():
	''' Simple login form '''
	# 1. js to save the tab:id in th session storage
	with open(f"{xmother.get_project_path()}/static/js/form_script.js", newline='') as f: myjs=f.read()
	
	# 2. Crete the random string of the tab_id 	
	tab_id = xmother.get_random_string()

	# 3. Fields in the form
	fields=[['usuari','text'],['paraula_de_pass','password'],['tab_id','hidden']]
	buttons=[
		{'name':'Identificar-se', 'type':'submit', 'cls':'btn btn-primary'},
		{'name':'Cancel·lar'    , 'type':'reset' , 'cls':'btn btn-secondary'}
	]	
	
	return fh.Div(
		fh.Script(code=myjs), # Este js guarda en la session store el tab_id com id_tab
		fh.H2("Identificar-se al sistema"),
		fh.Form(
			*[
				fh.Div(
					fh.Label(field[0].capitalize().replace("_"," ") , **{'cls':"form-label", 'for':field[0]}) if field[0] !='tab_id' else '',
					fh.Input(
						type=field[1], 
						value=tab_id if field[0]=='tab_id' else "",
						** {'name':field[0], 'id':field[0], 'cls':"form-control"}),
					cls="mb-3" 
				) for field in fields 
			],
			fh.Div(
				fh.Div(id="error-message", cls='text-danger text-center mb-3'),
				id="totp_div"
			),	
			fh.Div(
				*[fh.Button(button.get("name"), ** button) for button in buttons ],
				cls="d-flex justify-content-between"
			),
			id="my_form",
			# COMMENT:hx_post="/softprop/validate_login?tab_id="+tab_id, hx_target="#error-message", hx_swap="innerHTML"
			hx_post="/softprop/validate_login?tab_id="+tab_id, hx_target="#totp_div", hx_swap="innerHTML"

			
		),
		#fh.Form(id="logout-form", style="display-none", action="/logout", method="POST"),
		cls="card shadow-lg p-4", style="width: 350px;",
		
	)

Donde:
  • Se recoge el código javascript desde el fichero específico "form_script.js" para ser insertado mediante la sentencia fh.Script() (Líneas 4 y 17)
  • Se definen los campos (usuario y contraseña) y los tipos de controles que son y se insertan en el formulario (Líneas 10, 20-29)
  • Se definen los botones de "Identificarse" y "cancelar" (Líneas 11-14, 33-36)
  • Se define el campo de solo lectura de mensajes (Línea 29-32)
  • Se redirige al endpoint "/softprop/validate_login" donde se recoge el mensaje resultante de la ejecución y se crea dicho mensaje mediante adición de código html (hx_target) en el "div" cuyo id es "totp_div"

1.2 Endpoint /softprop/validate de routes01menu.py que valida usuario y contraseña y el segundo factor de autenticación

Veamos el código:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@app.post("/softprop/validate_login")
def post_login(req, login: xmclasses.Login):
	''' 1. Verifica usuario y contraseña
	 	2. Pregunta un doble factor de autenticación TOTP
		3. Añade una cookie httponly con la clave "tab_id" con valores:
		 	3.1 Usuario
			3.2 Duracion una hora (3600 segundos) 
	      
	'''
	# -------- COMPROBACION DE ERRORES -------
	# 1. Si no autenticamos al usuario con el login y la password
	#    Le devolvemos un error
	ldap_authenticated= xmldap.autheticate_by_login(login.usuari, login.paraula_de_pass)
	if not ldap_authenticated:
		print ("returning to first login page")
		return fh.Div("Error en l'usuari o la paraula de pas", cls="alert alert-danger")

	# 2. Comprobamos si tiene TOTP
	has_totp, totp, image_path=xmtotp.get_totp(login.usuari)
	
	# 3. Si no ha introducido el código TOTP mostramos la pantalla 
	#    para que lo ingrese
	no_digits=login.totp_digits is None
	if no_digits:
		print("Mostrando el input para introducir el TOTP")
		return xmother.get_totp_html(fh, has_totp, image_path)

	# 4. No ha introducido el TOTP correcto
	totp_correcto= xmtotp.validate_totp(totp, login.totp_digits, login.usuari)
	if not totp_correcto:
		print("Mostrando otra vez el input para introducir el TOTP")
		return xmother.get_totp_html(fh, has_totp, image_path, True)	
	
	#------ VAMOS A LA PANTALLA DE MENU ----------------
	#5. Si ha introducido el código TOTP correcto
	#   Mostramos el menu
	a_user=''
	with xmdb.session_local() as db:
		a_user, myerror=xmsession.set_session(req=req, db=db, user=login.usuari, tab_id=login.tab_id)
	
	if len(a_user.strip())==0: 
		print ("returning to the first login page")
		return fh.Div("Error al crear la sessió: "+myerror, cls="alert alert-danger")
	
	ck_dict={'httponly':True, 'max_age':3600}
	tab_id_dict={ "username": login.usuari }
	# COMMENT: sec_dict={'secure': True}
	cookies_dicts=[
		# COMMENT: {'key':"session_id", 'value':session_id }  | ck_dict,
		{'key':login.tab_id, 'value':json.dumps(tab_id_dict)} | ck_dict,
		# COMMENT: {'key':"username"  , 'value':login.usuari} | ck_dict 
	]	
	#COMMENT: redirection='/softprop/tree?tab_id='+urllib.parse.quote(login.tab_id)
	redirection='/softprop/tree?tab_id='+login.tab_id
	a=fh.RedirectResponse(redirection, status_code=303)
	for cookie in cookies_dicts:
		a.set_cookie(**cookie)
	print('passant per /softprop/validate_login i anem a /softprop/tree ja que ens hem validat')	
	return a

Donde:
  • Primeramente se consulta al "LDAP" para verificar el usuario y contraseña. (Línea 13)
  • En la primera pantalla solo se piede usuario y contraseña y no se tipde el código de 2º factor (TOTP). Es por ello que si despues de verificar el usuario y contraseña detecta que no se ha introducido el 2º factor, te añade el código HTML a la pantalla para pedir el 2º factor (Líneas 19-26)
  • Se verifica si el código TOTP (2º factor) és correcto. Si no es corre cto lo vuelve a pedir (Líneas  28-32)
  • Se guarda la sesión en BD. Si hubiera algún error al crearla, se reportaría (Líneas  37-44)
  • En las líneas 45 a 57, se crea una cookie httponly (que se guarda en el navegador y es solo accesible desde el servidor para dar mayor seguridad) cuya duración es de una hora (3600 segundos). En dicha cookie se guardan estos valores:
    • key: el tab_id generado
    • value.username: el nombre del usuario
    • httponly: true
    • max_age: 3600
  • Se redirige a la pantalla del menu tree (menú de arbol) con el código de status 303 que corresponde a la redirección








No hay comentarios :

Publicar un comentario