martes, 14 de enero de 2025

Nginx (V) Openresty Autenticación LDAP(IV). Manejo de sesiones y TSL (https)

1. Configurando openresty con TSL

Veamos el fichero de configuración de openresty (/usr/local/openresty/nginx/conf/nginx.conf)

Las líneas importantes son:

39: Le indicamos el puerto ssl 8449

46-47: Le damos la ruta del certificado de servidor

50-51: Protocolos

54-57: Propiedades del proxy para pasar información al servidor

62-101: Se indican las posibles rutas a utiizar y el tratamiento de las mismas. Para ello se hace includes de ficheros que están en las mismas carpetas. Estos ficheros ya se vieron en una entrada anterior.

  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
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
worker_processes  auto;

error_log  /var/log/openresty/error.log notice;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    error_log /usr/local/openresty/nginx/logs/error.log info;

    access_log  /var/log/openresty/access.log  main;

    sendfile        on;
    keepalive_timeout  65;
    
    # Enable Lua shared dictionary for sessions
    lua_shared_dict sessions 10m;
    lua_package_path '/usr/local/share/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/openresty/nginx/conf/?.lua;;';
    lua_package_cpath '/usr/local/lib/lua/5.1/?.so;;';

    # Creamos una bateria de servidores para softprop con un solo servidor
    upstream softpropsrv {
        server 0.0.0.0:5001;
        #server 127.0.0.1:5001;
        #server el_que_sea.com;
    }        
    

    server {
        #->listen       8001;
        listen       8449 ssl; #->New
        
        server_name  localhost;
        #server_name  edu.tavernes.es;
        
        #-> Begin new
        # SSL certificate configuration
        ssl_certificate     /usr/local/openresty/nginx/conf/wildcard2023Nginx.crt;
        ssl_certificate_key /usr/local/openresty/nginx/conf/wildcard2023Nginx.rsa;
    
        # Optional SSL settings for security and performance
        ssl_protocols       TLSv1.2 TLSv1.3;
        ssl_ciphers         HIGH:!aNULL:!MD5;

        # Common proxy settings (optional)
        proxy_set_header X-Real-IP        $remote_addr;
        proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header Host             $host;
        proxy_set_header X-Forwarded-Proto https;
    
    	
        
        # Location of static files
        location /static/ {
            root /home/eduard/MyPython/11.softprop-01;  # Path where static files are stored
            expires max;
            access_log off;
        }
        
        location /softprop {
            include nginx.conf.softprop;
        }

        # Redirect root to login page
        location = / {
            return 301 /login;
        }
        
        # Serve the login form
        location = /login {
            include nginx.conf.login;
        }
        
        location = /initial {
            include nginx.conf.initial;
        }
        
        # Protected proxy location
        location /protected {
            include nginx.conf.protected;
        }
        
        #location = /success {
        #    include nginx.conf.success;
        #}
        
        location = /error {
            include nginx.conf.error;
        }
        
        location = /logout {
            include nginx.conf.logout;
        }
    }
}


2. Manejo de sesiones en Python

En cualquier ruta, validamos la sesión con "xmsession.validate(tab_id=tab_id, req=req)"

veamos un ejemplo del tratamiento de un ruta

@app.get("/softprop/tree")
def geto2(tab_id:str | None= None, req=None):#request:Request): 
	''' Execute the main view that is tree menu'''
	
	# 1. Validem que haja tab_id i que l'usuari estiga en loa sessió de la BD
	my_response,myerror=xmsession.validate(tab_id=tab_id, req=req)
	if (len(myerror.strip())>1):
		print ("redirecting from /softprop/tree" )
		return my_response

Y el sistema te rebota a la pantalla de login si considera que no se ha validado convenientemente la sesión. 

Veamos que es lo que se valida, para ello veamos el código de xmsession.py y en concreto lo marcado en amarillo claro.

Primero se verifica que haya un tab_id y después que exista en la base de datos una sessión referenciada por el tab_id. SI no se cumplen las condiciones te envia a la pantalla de "login" para que te identifiques

El tab_id nos sirve para poder separar las sesiones que hay no solo entre diferentes navegadores o ventanas del navegador, sino también entre diferentes pestañas.

Hay una función "test_session" en rosa claro, que verifica que el tab_id se encuentra en una cookie y que además está guardada en la BD y que el navegador y la IP coinciden (esto puede dar lugar a fallos en dispositivos móviles si cambian la conexión).

from datetime import datetime, timedelta, timezone
import uuid
import orjson as json
from sqlalchemy.orm import Session
from fasthtml import common as fh
import os 

#------Imprescindible para poder importar de otras carpetas (de basicutils)
import sys
from pathlib import Path

path_root1 = Path(__file__).parents[1] # Damos un salto de directorio hacia arriba-> ximoutilsmod
sys.path.append(str(path_root1))
path_root0 = Path(__file__).parents[0] # El mismo directorio
sys.path.append(str(path_root0))
from basicutils import xmjwt, xmother, xmldap, xmdb
from session.xmsessionmodels import SessionStore, SessionStoreAbst, SessionStoreHst, SessionExistence
#from basicutils.xmdb import dbSingleton
# ------Fin imprescindible




def delete_old_sessions(db: Session, days: int=1):
	'''Delete old tokens from the db'''
	#1. Get the date to delete
	date_to_delete=datetime.now(timezone.utc) - timedelta(days=days)
	#2. Delete the old tokens
	db.query(SessionStore).filter(SessionStore.creation_date < date_to_delete).delete()
	db.commit()	

#-------------------------------------------------------------------	

def get_is_nginx(req)->bool:
	isnginx=False
	try:
		headers = req.headers
		isnginx = ("x-forwSessionarded-for" in headers or "x-forwarded-proto" in headers)
	except Exception: pass	
	return isnginx

def get_user_agent(req)->str:
	useragent=None	
	try:
		useragent=req.headers.get("User-Agent",'')
	except Exception: pass
	return useragent

def get_user_ip(req)->str:
	user_ip=None
	if get_is_nginx(req):
		x_forwarded_for = req.headers.get('X-Forwarded-For')
		if x_forwarded_for:
			# If there is more than one IP, the first is the client IP
			try:
				user_ip = x_forwarded_for.split(',')[0]
			except Exception:
				user_ip=req.headers.get("X-Real-IP",'') 
	else:
		# Fallback to the direct client IP
		user_ip = req.client.host
	return user_ip	

def get_req_info(req):
	'''
	Get client ip, user agent, is_nginx .... from request
	'''
	user_agent=user_ip=""
	is_nginx=False

	is_nginx=get_is_nginx(req)
	user_agent=get_user_agent(req)
	user_ip=get_user_ip(req)
	return user_ip, user_agent, is_nginx


def set_session(req, db: Session, user: str, tab_id:str)->tuple:
	''' creates the session in a DB after the login '''
	user_ip,user_agent,is_nginx=get_req_info(req)
	mysession=SessionStoreAbst(description=tab_id, user=user, user_agent=user_agent, user_ip=user_ip, is_nginx=is_nginx)	
	mysession=xmdb.merge_to_main_and_histo(db, mysession, SessionStore, SessionStoreHst)
	if not mysession: return "", "Cannot create session"
	return user,''

def test_session(req, db: Session, tab_id:str)->tuple:
	'''Tests if the session in the DB and the cookie (tab_id) has the same values'''
	# 1. Get the user from the tab_id cookie
	cookie_value= req.cookies.get(tab_id)
	if not cookie_value: return "","No tab_id cookie found"
	my_dict= json.loads(cookie_value)
	user=my_dict.get("username",'')
	user_ip,user_agent,is_nginx=get_req_info(req)

	# 2. Get the session from the DB
	mysession=db.query(SessionStore).filter_by(description=tab_id).first()
	
	# 3. If is nginx and has no session let's create a session
	if not mysession and is_nginx:
		mysession=SessionStoreAbst(description=tab_id, user=user, user_agent=user_agent, user_ip=user_ip, is_nginx=is_nginx)	
		mysession=xmdb.merge_to_main_and_histo(db, mysession, SessionStore, SessionStoreHst)

	# 4. If it has no session there is a problem !
	if not mysession: return "","Session not existing."
	
	# 3. Compare values.
	if mysession.user != user: return "","Users are different: "+user + "!=" + mysession.user

	if mysession.user_agent != user_agent: return "","Users agents are different: "+user_agent + "!=" + mysession.user_agent  

	if mysession.is_nginx != is_nginx: return "","Nginx problem: session is_nginx is different from is_nginx "

	if mysession.user_ip != user_ip: return "","User IPs are different: "+user_agent + "!=" + mysession.user_agent     
	
	return user,''

def validate(tab_id:str, req)->tuple:
	# 1. Si es la primera vegada, no tenim tab_id i envia un script per a arreplegar-lo
	if tab_id is None:
		redirection='/softprop/login'
		a=fh.RedirectResponse(redirection, status_code=303)
		print('enviem a  /softprop/login perque NO hi ha TAB_ID' )	
		return a, "no hi ha tab_id" 
		

	#2. Si tenim tab_id 
	with xmdb.session_local() as db:
		user, myerror=test_session(req, db, tab_id)
	
	if (len(user.strip())==0 or (len(myerror.strip())>0)):
		redirection='/softprop/login'
		a=fh.RedirectResponse(redirection, status_code=303)
		print('enviem a  /softprop/login perque:' + myerror)	
		return a, myerror  

	return '',''

def read_session_info(tab_id:str, db: Session)->dict:
	'''Get the field info from the table "x_session_store" with id=tab_id'''
	# 1. Get the session from the DB
	mysession=db.query(SessionStore).filter_by(description=tab_id).first()
	
	if not mysession : return {}
	
	info=mysession.info
	try:
		info_dict=json.loads(info)
	except json.JSONDecodeError:
		info_dict = {}
	return info_dict

def write_session_info(tab_id:str, db: Session, info:dict):
	'''Writes the field info from the table "x_session_store" with id=tab_id'''
	# 1. Get the session from the DB
	mysession=db.query(SessionStore).filter_by(description=tab_id).first()
	
	if mysession : 
		info_json=json.dumps(info).decode("utf-8")
		mysession.info=info_json
		mysession=xmdb.merge_to_main_and_histo(db, mysession, SessionStore, SessionStoreHst)
	
	





No hay comentarios :

Publicar un comentario