Métodos de Autenticación en APIs
En la materia anterior, Programación 2, hemos visto el concepto de API y cómo implementar una Web API en Python, mediante el uso de Flask. Además, vimos como consumir una API desde JavaScript, mediante el uso de fetch
. En esta ocasión, veremos como integrar una API al momento de desarrollar nuestros componentes en React.
Teniendo siempre presente que, una API permite la comunicación entre clientes y servidores, en el contexto de React, podemos decir que:
- Cliente: es la aplicación web desarrollada con React.
- Servidor: es la aplicación web que implementa la API.
Hasta el momento, nos hemos limitado a trabajar y desarrollar APIs públicas. Sin embargo, en la mayoría de los casos (sobre todo en el ámbito empresarial), las APIs son privadas y requieren de un mecanismo de autenticación para poder ser consumidas. En este tema aprenderemos a trabajar con APIs privadas, analizando diferentes mecanismos de autenticación que podemos utilizar:
Autenticación basada en sesiones
La autenticación basada en sesiones es el mecanismo más simple que podemos utilizar para autenticar a los usuarios de nuestra aplicación.
- Este mecanismo consiste en registrar información de autenticación del usuario en el servidor. Por ejemplo, podemos registrar el nombre de usuario y la contraseña del usuario en la base de datos del servidor.
- Luego, cuando el usuario desee acceder a la aplicación, deberá proporcionar sus credenciales de acceso (usuario y contraseña). Si las credenciales de acceso son correctas, el servidor generará un identificador de sesión y lo almacenará en la base de datos o en la memoria del servidor.
- Finalmente, el servidor devolverá el identificador de sesión al cliente, mediante una cookie. De esta forma, el cliente enviará el identificador de sesión en cada petición que realice al servidor y, el servidor, verificará que el identificador de sesión sea válido.
En palabras más simples, este mecanismo consiste en almacenar una clave secreta en el servidor y enviar dicha clave secreta al cliente, mediante una cookie. Mientras el cliente cuenta con la clave secreta, el servidor lo considerará autenticado.
Si recordamos, en la materia anterior, abordamos brevemente este mecanismo de autenticación, cuando implementamos la API de usuarios en Flask. Para implementar este mecanismo de autenticación, solicitabamos como credenciales de acceso el nombre de usuario (o email) y la contraseña. Luego, verificabamos que el usuario existiera en la base de datos y que la contraseña ingresada fuera correcta. Si la verificación era exitosa, generabamos un identificador de sesión (en su momento mediante el uso del objeto session
de Flask). Finalmente, devolviamos el identificador de sesión al cliente, mediante una cookie.
En su momento pasamos por alto ciertos detalles que son importantes tener en cuenta al momento de implementar este mecanismo de autenticación. Así que veremos un ejemplo de implementación de este mecanismo de autenticación en Flask, teniendo en cuenta estas reglas de seguridad.
Registrar usuarios
Al registrar un usuario, y dependiendo del sistema para el cual estemos desarrollando la API, podemos solicitar diferentes datos. Pero en general, veremos que hay dos datos que nunca pueden faltar: un identificador único y una contraseña.
El identificador corresponderse con el nombre de usuario, el email o cualquier otro dato, siempre y cuando exista uno y solo uno por usuario.
Por otro lado, la contraseña es un dato sensible que debemos tratar de manera especial, y existe una regla implícita que debemos cumplir para garantizar la seguridad de los usuarios:
Un hash es un valor que se obtiene a partir de una cadena, en este caso la contraseña, mediante un algoritmo que transforma la cadena de entrada en un valor de longitud fija. Además, esta función debe ser determinística, es decir, que siempre que se le pase la misma cadena de entrada, debe devolver el mismo valor de salida.
Por último, y aún más importante, esta función debe ser unidireccional. Es decir, no se puede obtener la cadena de entrada a partir del valor de salida. De esta forma, si un atacante logra acceder a la base de datos, no podrá obtener las contraseñas de los usuarios. En su lugar, solo podrá obtener el hash asociado a cada contraseña.
Una manera de hacer esto en Python puede ser empleando la librería hashlib
y la función sha256
, con la podemos definir una función que reciba una cadena y devuelva el hash de dicha cadena:
1import hashlib2
3def hash_string(string):4 return hashlib.sha256(string.encode()).hexdigest()
Luego solo debemos almacenar el hash de la contraseña en la base de datos, en lugar de la contraseña en sí. Por supuesto, también almacenaremos el identificador único del usuario y cualquier otro dato que necesitemos.
Iniciar sesión
Una vez registrado, el usuario podrá iniciar sesión en la aplicación. Para ello, deberá proporcionar sus credenciales de acceso (identificador único y contraseña). Luego, el servidor deberá verificar que las credenciales de acceso sean correctas.
-
Para verificar que la contraseña ingresada por el usuario sea correcta, debemos aplicar la función de hash a la contraseña ingresada y comparar el resultado con el hash almacenado en la base de datos. Si ambos “hashes” coinciden, entonces la contraseña es correcta.
-
A continuación, vemos un ejemplo de implementación de esta verificación en Flask. Recordemos que, estamos abstrayendo el acceso a la base de datos mediante el uso de listas y diccionarios en memoria.
1def verify_user(username, password):2 for user in users:3 if user["username"] == username and user["password"] == hash_string(password):4 return user5 return None
-
Luego, el identificador de sesión debe ser un valor aleatorio. Existen diferentes formas de generar valores aleatorios en Python, por ejemplo, Flask provee la función
secrets.token_hex
para generar valores aleatorios en hexadecimal. -
Por último, si almacenamos el identificador de sesión en una cookie, debemos tener en cuenta que las cookies pueden ser robadas. Por lo tanto, debemos almacenar el identificador de sesión encriptado, de forma que, si un atacante logra obtener el valor de la cookie, no pueda obtener el identificador de sesión. Una vez más, existen diferentes formas de encriptar valores en Python, por ejemplo, podemos usar el módulo
cryptography
para encriptar valores mediante el uso de una clave secreta (clave que debe tener cualquier aplicación web que utilice este mecanismo de autenticación).
Podemos plantear la implementación de este mecanismo de autenticación en cualquier framework web, teniendo en cuenta estas reglas de seguridad. A continuación, veremos un ejemplo de implementación de este mecanismo de autenticación en Flask, teniendo en cuenta estas reglas de seguridad. Además, abstraremos la implementación de la base de datos a través de estructuras de datos como listas o diccionarios en memoria, para simplificar el ejemplo.
Para comenzar veremos el registro de usuarios. Para esto supondremos que la API REST que estamos desarrollando tiene el siguiente endpoint para crear usuarios:
1@app.route('/users', methods=['POST'])2def create_user_route():3 data = request.json4 user = create_user(data['username'], data['email'], data['password'])5 return jsonify(user)
Este endpoint recibe un objeto JSON con los datos del usuario a crear e invoca a otra función (o método, dependiendo del contexto) para crear el usuario. En este caso, la función create_user
recibe como parámetros el nombre de usuario, el email y la contraseña del usuario a crear, y devuelve un diccionario con los datos del usuario creado.
1def create_user(username, email, password):2 user = {3 'id': len(users) + 1,4 'username': username,5 'email': email,6 'password': hash_string(password)7 }8 users.append(user)9 return user
En este caso, el diccionario representa un usuario en memoria, pero podríamos tener un modelo User
que represente a un usuario en la base de datos y que tenga los atributos id
, username
, email
y password
. Siendo ese el caso, podríamos reemplazar el diccionario por una instancia de la clase User
. Así mismo, vemos que este usuario se agregará a una lista de usuarios en memoria, pero podríamos tener una base de datos con una tabla users
y agregarlo a ella.
1from flask import Flask, request, jsonify, make_response2
3from cryptography.fernet import Fernet4import secrets5import hashlib6
7app = Flask(__name__)8
9# Clave secreta para encriptar el identificador de sesión10SECRET_KEY = b'clave_secreta'11
12# Base de datos de usuarios13users = [14 {15 'id': 1,16 'username': 'admin',17 'email': '18 }19]20
21# Base de datos de sesiones22sessions = {}23
24# Función para generar un hash de una cadena25def hash_string(string):26 return hashlib.sha256(string.encode()).hexdigest()27
28# Función para generar un identificador de sesión29def generate_session_id():30 return secrets.token_hex(16)31
32# Función para encriptar un valor33def encrypt_value(value):34 f = Fernet(SECRET_KEY)35 return f.encrypt(value.encode()).decode()36
37# Función para desencriptar un valor38def decrypt_value(value):39 f = Fernet(SECRET_KEY)40 return f.decrypt(value.encode()).decode()41
42# Función para verificar que el usuario exista en la base de datos43def verify_user(username, password):44 for user in users:45 if user['username'] == username and user['password'] == hash_string(password):46 return user47 return None48
49# Función para verificar que el identificador de sesión sea válido50def verify_session(session_id):51 return session_id in sessions52
53# Función para obtener el usuario a partir del identificador de sesión54def get_user(session_id):55 return sessions[session_id]56
57# Función para crear un usuario58def create_user(username, email, password):59 user = {60 'id': len(users) + 1,61 'username': username,62 'email': email,63 'password': hash_string(password)64 }65 users.append(user)66 return user67
68# Función para crear una sesión69def create_session(user):70 session_id = generate_session_id()71 sessions[session_id] = user72 return session_id73
74# Función para eliminar una sesión75def delete_session(session_id):76 del sessions[session_id]77
78# Función para obtener el identificador de sesión de una cookie79def get_session_id_from_cookie():80 session_id = request.cookies.get('session_id')81 if session_id:82 return decrypt_value(session_id)83 return None84
85# Función para crear una cookie con el identificador de sesión86def create_session_cookie(session_id):87 session_id = encrypt_value(session_id)88 response = make_response(jsonify({'message': 'Sesión iniciada'}))89 response.set_cookie('session_id', session_id)90 return response91
92# Función para eliminar la cookie con el identificador de sesión93def delete_session_cookie():94 response = make_response(jsonify({'message': 'Sesión cerrada'}))95 response.set_cookie('session_id', '', expires=0)96 return response97
98# Función para verificar que el usuario esté autenticado99def verify_authentication():100 session_id = get_session_id_from_cookie()101 if session_id and verify_session(session_id):102 return True103 return False104
105# Función para obtener el usuario autenticado106def get_authenticated_user():107 session_id = get_session_id_from_cookie()108 if session_id and verify_session(session_id):109 return get_user(session_id)110 return None111
112# Ruta para crear un usuario113@app.route('/users', methods=['POST'])114def create_user_route():115 data = request.get_json()116 user = create_user(data['username'], data['email'], data['password'])117 return jsonify(user)118
119# Ruta para iniciar sesión120@app.route('/login', methods=['POST'])121def login_route():122 data = request.get_json()123 user = verify_user(data['username'], data['password'])124 if user:125 session_id = create_session(user)126 return create_session_cookie(session_id)127 return jsonify({'message': 'Usuario o contraseña incorrectos'}), 401128
129# Ruta para cerrar sesión130@app.route('/logout', methods=['POST'])131def logout_route():132 session_id = get_session_id_from_cookie()133 if session_id:134 delete_session(session_id)135 return delete_session_cookie()136 return jsonify({'message': 'No se ha iniciado sesión'}), 401137
138# Ruta para obtener el usuario autenticado139@app.route('/me', methods=['GET'])140def me_route():141 if verify_authentication():142 user = get_authenticated_user()143 return jsonify(user)144 return jsonify({'message': 'No se ha iniciado sesión'}), 401145
146if __name__ == '__main__':147 app.run(debug=True)
Autenticación Basic
El mecanismo de autenticación más simple que podemos utilizar es el de Basic Authentication. Este mecanismo consiste en enviar las credenciales de acceso (usuario y contraseña) en cada petición que realicemos a la API. Para ello, debemos enviar un header Authorization
con el valor Basic <credenciales>
, donde <credenciales>
es un string que contiene el usuario y la contraseña, separados por dos puntos (:
) y codificados en base64.
A diferencia de la autenticación basada en sesiones, en este caso, el servidor no almacena ninguna información de autenticación del usuario. Por lo tanto, en cada petición que realice el usuario, las credenciales de acceso deben ser enviadas al servidor. Además, el servidor debe verificar que las credenciales de acceso sean correctas, en cada petición que reciba.
Teniendo en cuenta esto, los pasos a seguir para implementar este mecanismo de autenticación desde el lado del servidor son los siguientes:
- Obtener las credenciales de la cabecera de la petición.
- Decodificar las credenciales de base64.
- Verificar que las credenciales sean correctas.
En el caso de Flask, podemos obtener las credenciales de la cabecera de la petición mediante el objeto request
y el atributo authorization
. Este atributo es un objeto de la clase Authorization
que contiene dos atributos: type
y username
. El atributo type
contiene el tipo de autenticación que se está utilizando, en este caso, Basic
. El atributo username
contiene las credenciales de acceso, codificadas en base64.
1from flask import Flask, request, jsonify, make_response2
3import base644
5app = Flask(__name__)6
7# Función para decodificar las credenciales de acceso8def decode_credentials(credentials):9 return base64.b64decode(credentials).decode().split(':')10
11# Función para verificar que las credenciales de acceso sean correctas12def verify_credentials(username, password):13 return username == 'admin' and password == 'admin'14
15# Función para verificar que el usuario esté autenticado16def verify_authentication():17 credentials = request.authorization18 if credentials and credentials.type == 'Basic':19 username, password = decode_credentials(credentials.username)20 return verify_credentials(username, password)21 return False22
23# Ruta para obtener el usuario autenticado24@app.route('/me', methods=['GET'])25def me_route():26 if verify_authentication():27 return jsonify({'username': 'admin'})28 return jsonify({'message': 'No se ha iniciado sesión'}), 40129
30if __name__ == '__main__':31 app.run(debug=True)
Autenticación basada en tokens
El mecanismo de autenticación más utilizado en la actualidad para el uso de API es el de Token Authentication. Este mecanismo consiste en enviar un token de acceso en cada petición que realicemos a la API. Para ello, debemos enviar un header Authorization
con el valor Bearer <token>
o Token <token>
, donde <token>
es un string que contiene el token de acceso.
Al igual que la autenticación Basic, el servidor no almacena ninguna información de autenticación del usuario. Por lo tanto, en cada petición que realice el usuario, el token de acceso debe ser enviado al servidor. Además, el servidor debe verificar que el token de acceso sea válido, en cada petición que reciba.
Convencionalmente, los tokens de acceso son generados por el servidor y tienen una fecha de expiración, aunque pueden generarse tokens de acceso de duración indefinida. En el caso de los tokens de acceso con fecha de expiración, el servidor debe solicitar la generación de un nuevo token de acceso, antes de que el token de acceso actual expire.
Por estos motivos, los tokens de acceso suelen tener su propio modelo en la base de datos. En este modelo, se almacena el token de acceso, la fecha de expiración y el usuario al que pertenece el token de acceso. Además, el servidor debe implementar un mecanismo para generar y validar los tokens de acceso. Por ejemplo, podemos utilizar el módulo cryptography
para generar tokens de acceso mediante el uso de una clave secreta (clave que debe tener cualquier aplicación web que utilice este mecanismo de autenticación). Por supuesto, cada framework tiene su propia implementación de este mecanismo de autenticación.
Teniendo en cuenta esto, los pasos a seguir para implementar este mecanismo de autenticación desde el lado del servidor son los siguientes:
- Obtener el token de acceso de la cabecera de la petición.
- Verificar que el token de acceso sea válido. Es decir, que exista en la base de datos y que no haya expirado.
- Obtener el usuario al que pertenece el token de acceso.
- Si el usuario existe, entonces el usuario pasa a estar autenticado.
En el caso de Flask, podemos obtener el token de acceso de la cabecera de la petición mediante el objeto request
y el atributo authorization
. Este atributo es un objeto de la clase Authorization
que contiene dos atributos: type
y token
. El atributo type
contiene el tipo de autenticación que se está utilizando, en este caso, Bearer
o Token
. El atributo token
contiene el token de acceso.
1from flask import Flask, request, jsonify2from flask_cors import CORS3import hashlib4import jwt5from datetime import datetime, timedelta, UTC6
7app = Flask(__name__)8CORS(app, supports_credentials=True)9
10# Clave secreta para firmar los tokens11SECRET_KEY = b"GPimJlIp7j1p-dsu9xvF2jhU8lL6cvzovhNH2CMRtmI="12
13# Base de datos de usuarios14users = {}15
16# JWT Tokens17token_store = {}18
19
20def hash_string(string):21 return hashlib.sha256(string.encode()).hexdigest()22
23
24def verify_user(username, password):25 for user in users.values():26 if user["username"] == username and user["password"] == hash_string(password):27 return user28 return None29
30
31def create_user(username, email, password):32 user = {33 "id": len(users) + 1,34 "username": username,35 "email": email,36 "password": hash_string(password),37 }38 users[user["id"]] = user39 return user40
41
42def create_token(user):43 payload = {44 "user_id": user["id"],45 "exp": datetime.now(UTC) + timedelta(hours=1),46 }47 return jwt.encode(payload, SECRET_KEY, algorithm="HS256")48
49
50def verify_token(token):51 try:52 payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])53 return payload54 except jwt.ExpiredSignatureError:55 return None56
57
58@app.route("/users", methods=["POST"])59def create_user_route():60 data = request.get_json()61 user = create_user(data["username"], data["email"], data["password"])62 return jsonify(user)63
64
65@app.route("/login", methods=["POST"])66def login_route():67 data = request.get_json()68 user = verify_user(data["username"], data["password"])69 if user:70 token = create_token(user)71 token_store[token] = user # Optional: store token for tracking72 return jsonify({"token": token})73 return jsonify({"message": "Invalid credentials"}), 40174
75
76@app.route("/me", methods=["GET"])77def me_route():78 auth = request.authorization79 if auth and (auth.type == "bearer" or auth.type == "token"):80 payload = verify_token(auth.token)81 if payload:82 user_id = payload["user_id"]83 user = users[user_id]84 if user:85 return jsonify(86 {87 "id": user["id"],88 "username": user["username"],89 "email": user["email"],90 }91 )92 return jsonify({"message": "Unauthorized"}), 40193
94
95if __name__ == "__main__":96 app.run(debug=True)
Podemos ver una clara similitud entre la autenticación Basic y la autenticación basada en tokens. La principal diferencia radica en la forma en que se envían las credenciales de acceso al servidor. En el caso de la autenticación Basic, las credenciales de acceso se envían en cada petición (encodeadas en base64 por supuesto), mientras que en el caso de la autenticación basada en tokens, se envía un token de acceso en cada petición. Es decir, que las credenciales de acceso se envían una sola vez, al momento de iniciar sesión, y el servidor devuelve un token de acceso que se envía en cada petición subsiguiente.
OAuth 2.0
Por último, el mecanismo de autenticación más utilizado en la actualidad es el de OAuth 2.0. Este mecanismo consiste en delegar la autenticación del usuario a un tercero, el cual será una aplicación web que siga el protocolo OAuth 2.0 (como Google, Facebook, Twitter, entre otros).
En los métodos de autenticación tradicionales del modelo cliente-servidor, como la autenticación basada en sesiones, la autenticación Basic y la autenticación basada en tokens, el cliente realiza una petición a un recurso protegido en el servidor, para lo cual será necesario autenticarse mediante credenciales de acceso. En cambio, en el modelo OAuth 2.0, el cliente delega la autenticación del usuario a una aplicación web de terceros, la cual será la encargada de autenticar al usuario y devolver un token de acceso al cliente.
Este mecanismo trae consigo ciertos problemas y limitaciones a tener en cuenta si desea implementarse en una aplicación:
- Las aplicaciones de terceros deben almacenar las credenciales del propietario del recurso para su uso futuro, en el peor delos casos una contraseña en texto plano.
- Los servidores deben admitir la autenticación de contraseñas, a pesar de las debilidades de seguridad inherentes a las contraseñas.
- Las aplicaciones de terceros obtienen un acceso excesivamente amplio a los recursos protegidos del propietario del recurso, dejando a los propietarios de los recursos sin la posibilidad de restringir la duración o el acceso a un subconjunto limitado de recursos.
- Los propietarios de los recursos no pueden revocar el acceso a un tercero individual sin revocar el acceso a todos los terceros, y deben hacerlo cambiando la contraseña del tercero.
- El compromiso de cualquier aplicación de terceros resulta en el compromiso de la contraseña del usuario final y de todos los datos protegidos por esa contraseña.
Teniendo en cuenta esto, si las ventajas de utilizar OAuth 2.0 son mayores que las desventajas antes mencionadas, entonces podemos implementar este mecanismo de autenticación en nuestra aplicación. Por supuesto, los usuarios deben ser informados de toda la información que se comparte con la aplicación de terceros y deben dar su consentimiento para que la aplicación de terceros acceda a sus datos.
Roles en OAuth 2.0
OAuth define cuatro roles involucrados en el proceso de autenticación:
- Propietario del recurso: es una entidad capaz de otorgar acceso a un recurso protegido. Cuando el propietario del recurso es una persona, se le llama usuario final.
- Servidor de recursos: es el servidor que aloja los recursos protegidos, capaz de aceptar y responder a las solicitudes de recursos protegidos utilizando tokens de acceso.
- Cliente: es una aplicación que realiza solicitudes de recursos protegidos en nombre del propietario del recurso y con su autorización. El término “cliente” no implica ninguna característica de implementación en particular (por ejemplo, si la aplicación se ejecuta en un servidor, un escritorio u otros dispositivos).
- Servidor de autorización: es el servidor que emite tokens de acceso al cliente después de autenticar con éxito al propietario del recurso y obtener autorización. También se le conoce como proveedor.
Flujo del Protocolo OAuth 2.0
El flujo abstracto de OAuth 2.0 ilustrado en la figura describe la interacción entre los cuatro roles e incluye los siguientes pasos:
- El cliente solicita autorización al propietario del recurso. La solicitud de autorización puede hacerse directamente al propietario del recurso (como se muestra), o preferiblemente indirectamente a través del servidor de autorización como intermediario.
- El cliente recibe un concesión de autorización, que es una credencial que representa la autorización del propietario del recurso, expresada mediante uno de los cuatro tipos de concesión definidos en esta especificación o mediante el uso de un tipo de concesión de extensión. El tipo de concesión de autorización depende del método utilizado por el cliente para solicitar autorización y los tipos admitidos por el servidor de autorización.
- El cliente solicita un token de acceso autenticándose con el servidor de autorización y presentando la concesión de autorización.
- El servidor de autorización autentica al cliente y valida la concesión de autorización, y si es válida, emite un token de acceso.
- El cliente solicita un recurso protegido al servidor de recursos y autentica al servidor de recursos mediante la presentación del token de acceso.
- El servidor de recursos valida el token de acceso y, si es válido, sirve el recurso protegido al cliente.
En el caso de Flask, podemos utilizar la librería authlib
para implementar este mecanismo de autenticación y emplearemos a Google como proveedor de autenticación. Para ello, debemos seguir los siguientes pasos:
- Crear un proyecto en la Consola de Google Cloud.
- Habilitar la API de Google+ en el proyecto.
- Crear un ID de cliente OAuth 2.0 en el proyecto.
- Descargar el archivo JSON con las credenciales del ID de cliente.
- Instalar la librería
authlib
en el proyecto. - Implementar el mecanismo de autenticación en Flask mediante los endpoints que consideremos necesarios.