Saltearse al contenido

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.

  1. 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.
  2. 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.
  3. 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:

1
import hashlib
2
3
def 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.

  1. 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.

  2. 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.

1
def verify_user(username, password):
2
for user in users:
3
if user["username"] == username and user["password"] == hash_string(password):
4
return user
5
return None
  1. 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.

  2. 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'])
2
def create_user_route():
3
data = request.json
4
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.

1
def 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.

1
from flask import Flask, request, jsonify, make_response
2
3
from cryptography.fernet import Fernet
4
import secrets
5
import hashlib
6
7
app = Flask(__name__)
8
9
# Clave secreta para encriptar el identificador de sesión
10
SECRET_KEY = b'clave_secreta'
11
12
# Base de datos de usuarios
13
users = [
14
{
15
'id': 1,
16
'username': 'admin',
17
'email': '
18
}
19
]
20
21
# Base de datos de sesiones
22
sessions = {}
23
24
# Función para generar un hash de una cadena
25
def hash_string(string):
26
return hashlib.sha256(string.encode()).hexdigest()
27
28
# Función para generar un identificador de sesión
29
def generate_session_id():
30
return secrets.token_hex(16)
31
32
# Función para encriptar un valor
33
def encrypt_value(value):
34
f = Fernet(SECRET_KEY)
35
return f.encrypt(value.encode()).decode()
36
37
# Función para desencriptar un valor
38
def 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 datos
43
def verify_user(username, password):
44
for user in users:
45
if user['username'] == username and user['password'] == hash_string(password):
46
return user
47
return None
48
49
# Función para verificar que el identificador de sesión sea válido
50
def verify_session(session_id):
51
return session_id in sessions
52
53
# Función para obtener el usuario a partir del identificador de sesión
54
def get_user(session_id):
55
return sessions[session_id]
56
57
# Función para crear un usuario
58
def 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 user
67
68
# Función para crear una sesión
69
def create_session(user):
70
session_id = generate_session_id()
71
sessions[session_id] = user
72
return session_id
73
74
# Función para eliminar una sesión
75
def delete_session(session_id):
76
del sessions[session_id]
77
78
# Función para obtener el identificador de sesión de una cookie
79
def 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 None
84
85
# Función para crear una cookie con el identificador de sesión
86
def 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 response
91
92
# Función para eliminar la cookie con el identificador de sesión
93
def delete_session_cookie():
94
response = make_response(jsonify({'message': 'Sesión cerrada'}))
95
response.set_cookie('session_id', '', expires=0)
96
return response
97
98
# Función para verificar que el usuario esté autenticado
99
def verify_authentication():
100
session_id = get_session_id_from_cookie()
101
if session_id and verify_session(session_id):
102
return True
103
return False
104
105
# Función para obtener el usuario autenticado
106
def 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 None
111
112
# Ruta para crear un usuario
113
@app.route('/users', methods=['POST'])
114
def 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ón
120
@app.route('/login', methods=['POST'])
121
def 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'}), 401
128
129
# Ruta para cerrar sesión
130
@app.route('/logout', methods=['POST'])
131
def 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'}), 401
137
138
# Ruta para obtener el usuario autenticado
139
@app.route('/me', methods=['GET'])
140
def 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'}), 401
145
146
if __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:

  1. Obtener las credenciales de la cabecera de la petición.
  2. Decodificar las credenciales de base64.
  3. 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.

1
from flask import Flask, request, jsonify, make_response
2
3
import base64
4
5
app = Flask(__name__)
6
7
# Función para decodificar las credenciales de acceso
8
def decode_credentials(credentials):
9
return base64.b64decode(credentials).decode().split(':')
10
11
# Función para verificar que las credenciales de acceso sean correctas
12
def verify_credentials(username, password):
13
return username == 'admin' and password == 'admin'
14
15
# Función para verificar que el usuario esté autenticado
16
def verify_authentication():
17
credentials = request.authorization
18
if credentials and credentials.type == 'Basic':
19
username, password = decode_credentials(credentials.username)
20
return verify_credentials(username, password)
21
return False
22
23
# Ruta para obtener el usuario autenticado
24
@app.route('/me', methods=['GET'])
25
def me_route():
26
if verify_authentication():
27
return jsonify({'username': 'admin'})
28
return jsonify({'message': 'No se ha iniciado sesión'}), 401
29
30
if __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:

  1. Obtener el token de acceso de la cabecera de la petición.
  2. Verificar que el token de acceso sea válido. Es decir, que exista en la base de datos y que no haya expirado.
  3. Obtener el usuario al que pertenece el token de acceso.
  4. 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.

1
from flask import Flask, request, jsonify
2
from flask_cors import CORS
3
import hashlib
4
import jwt
5
from datetime import datetime, timedelta, UTC
6
7
app = Flask(__name__)
8
CORS(app, supports_credentials=True)
9
10
# Clave secreta para firmar los tokens
11
SECRET_KEY = b"GPimJlIp7j1p-dsu9xvF2jhU8lL6cvzovhNH2CMRtmI="
12
13
# Base de datos de usuarios
14
users = {}
15
16
# JWT Tokens
17
token_store = {}
18
19
20
def hash_string(string):
21
return hashlib.sha256(string.encode()).hexdigest()
22
23
24
def verify_user(username, password):
25
for user in users.values():
26
if user["username"] == username and user["password"] == hash_string(password):
27
return user
28
return None
29
30
31
def 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"]] = user
39
return user
40
41
42
def 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
50
def verify_token(token):
51
try:
52
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
53
return payload
54
except jwt.ExpiredSignatureError:
55
return None
56
57
58
@app.route("/users", methods=["POST"])
59
def 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"])
66
def 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 tracking
72
return jsonify({"token": token})
73
return jsonify({"message": "Invalid credentials"}), 401
74
75
76
@app.route("/me", methods=["GET"])
77
def me_route():
78
auth = request.authorization
79
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"}), 401
93
94
95
if __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

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:

  1. 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.
  2. 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.
  3. El cliente solicita un token de acceso autenticándose con el servidor de autorización y presentando la concesión de autorización.
  4. 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.
  5. El cliente solicita un recurso protegido al servidor de recursos y autentica al servidor de recursos mediante la presentación del token de acceso.
  6. 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:

  1. Crear un proyecto en la Consola de Google Cloud.
  2. Habilitar la API de Google+ en el proyecto.
  3. Crear un ID de cliente OAuth 2.0 en el proyecto.
  4. Descargar el archivo JSON con las credenciales del ID de cliente.
  5. Instalar la librería authlib en el proyecto.
  6. Implementar el mecanismo de autenticación en Flask mediante los endpoints que consideremos necesarios.