Autenticación de Servicios

Autenticación de Servicios Desplegados

DownToZero (DTZ) proporciona autenticación integrada para tus servicios desplegados, facilitando la construcción de aplicaciones seguras sin la necesidad de gestionar tu propia infraestructura de autenticación.

Visión General

Cuando despliegas un servicio en DTZ, la plataforma proporciona automáticamente credenciales de autenticación e información de contexto a través de variables de entorno. Tu servicio puede usar estas para:

  • Autenticar solicitudes a otros servicios DTZ (llamadas API, almacenamiento de objetos, etc.)
  • Verificar solicitudes entrantes de usuarios
  • Acceder a recursos específicos del contexto
  • Implementar comunicación segura entre servicios

Variables de Entorno

DTZ inyecta automáticamente las siguientes variables de entorno en los contenedores de tu servicio:

Variable Descripción Ejemplo
DTZ_ACCESS_TOKEN Un token JWT para acceder a servicios DTZ dentro de tu contexto. eyJhbGciOiJSUzI1NiI...
DTZ_CONTEXT_ID El identificador de tu contexto DTZ. context-3cd84429-64a4-4226-b868-c83feeff0f46
PORT El puerto en el que tu servicio debe escuchar. 80

Autenticación de Solicitudes Entrantes

Tu servicio desplegado puede autenticar solicitudes entrantes usando varios métodos:

Autenticación con Clave API

Los usuarios pueden autenticarse con tu servicio usando claves API de DTZ proporcionando la clave en el encabezado X-API-KEY.

curl -H "X-API-KEY: your-api-key" https://yourservice.dtz.rocks/api/endpoint

Autenticación con Token Bearer

Proporciona un token JWT en el encabezado Authorization para autenticar.

curl -H "Authorization: Bearer your-jwt-token" https://yourservice.dtz.rocks/api/endpoint

Autenticación Básica

También puedes pasar claves API usando autenticación básica.

curl -u apikey:your-api-key https://yourservice.dtz.rocks/api/endpoint

Autenticación basada en Cookies

Para aplicaciones web, el servicio de Identidad DTZ puede manejar la autenticación a través de cookies del navegador.

Flujo OAuth

DTZ provee autenticación OAuth automática para aplicaciones web. Cuando usuarios no autenticados acceden a tu servicio, son redirigidos automáticamente a la página de inicio de sesión de DTZ y luego redirigidos de vuelta tras un login exitoso.

Uso de la Autenticación DTZ en tu Servicio

Realizar Solicitudes Autenticadas a Servicios DTZ

Usa la variable de entorno DTZ_ACCESS_TOKEN para realizar llamadas autenticadas a otros servicios DTZ.

import os
import requests

# Obtener el token de acceso DTZ desde la variable de entorno
token = os.environ.get('DTZ_ACCESS_TOKEN')
context_id = os.environ.get('DTZ_CONTEXT_ID')

# Hacer una solicitud autenticada a la API DTZ
headers = {
    'Authorization': f'Bearer {token}',
    'Content-Type': 'application/json'
}

response = requests.get(
    'https://api.dtz.rocks/v1/containers/services',
    headers=headers
)
// Ejemplo en Node.js
const token = process.env.DTZ_ACCESS_TOKEN;
const contextId = process.env.DTZ_CONTEXT_ID;

const response = await fetch('https://api.dtz.rocks/v1/containers/services', {
    headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json'
    }
});
# Ejemplo en Bash
curl -H "Authorization: Bearer $DTZ_ACCESS_TOKEN" \
     -H "Content-Type: application/json" \
     https://api.dtz.rocks/v1/containers/services

Verificación de la Autenticación Entrante

DTZ maneja automáticamente la autenticación para solicitudes entrantes. Cuando un usuario realiza una solicitud autenticada a tu servicio, DTZ:

  1. Valida las credenciales de autenticación.
  2. Convierte las claves API en tokens JWT.
  3. Reenvía la solicitud con un encabezado Authorization: Bearer <token>.

Tu servicio recibe el token JWT y puede extraer información del usuario desde él.

import jwt
import os
from flask import Flask, request

app = Flask(__name__)

@app.route('/protected')
def protected_endpoint():
    auth_header = request.headers.get('Authorization')
    if not auth_header or not auth_header.startswith('Bearer '):
        return {'error': 'No se proporcionó autenticación'}, 401
    
    token = auth_header.split(' ')[1]
    
    try:
        # El token ya es verificado por DTZ, por lo que puedes 
        # extraer claims sin verificar la firma.
        payload = jwt.decode(token, options={"verify_signature": False})
        
        user_id = payload.get('sub')  # ID de identidad
        context_id = payload.get('scope')  # ID de contexto
        roles = payload.get('roles', [])  # Roles del usuario
        
        return {
            'user_id': user_id,
            'context_id': context_id,
            'roles': roles,
            'message': 'Acceso concedido'
        }
    except jwt.InvalidTokenError:
        return {'error': 'Token inválido'}, 401
// Ejemplo en Express.js
const express = require('express');
const jwt = require('jsonwebtoken');

const app = express();

app.get('/protected', (req, res) => {
    const authHeader = req.headers.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'No se proporcionó autenticación' });
    }
    
    const token = authHeader.split(' ')[1];
    
    try {
        // El token ya es verificado por DTZ.
        const payload = jwt.decode(token);
        
        const userId = payload.sub;      // ID de identidad
        const contextId = payload.scope; // ID de contexto
        const roles = payload.roles || [];    // Roles del usuario
        
        res.json({
            user_id: userId,
            context_id: contextId,
            roles: roles,
            message: 'Acceso concedido'
        });
    } catch (error) {
        res.status(401).json({ error: 'Token inválido' });
    }
});

Estructura del Token JWT

Los tokens JWT de DTZ contienen los siguientes claims:

Claim Descripción Ejemplo
iss Emisor (siempre “dtz.rocks”) "dtz.rocks"
sub Sujeto (ID de identidad del usuario) "identity-abc123..."
aud Audiencia (siempre “dtz.rocks”) "dtz.rocks"
scope ID del contexto "context-3cd84429..."
roles Roles/permisos del usuario ["https://dtz.rocks/context/admin/{context_id}"]
contexts Contextos disponibles ["context-3cd84429..."]
exp Tiempo de expiración 1640995200
iat Tiempo de emisión 1640908800

Control de Acceso Basado en Roles

DTZ usa control de acceso basado en roles con identificadores de roles basados en URI. Los patrones comunes de roles incluyen:

  • https://dtz.rocks/context/admin/{context_id} - Administrador de contexto
  • https://dtz.rocks/containers/admin/{context_id} - Administrador de servicio de contenedores
  • https://dtz.rocks/objectstore/admin/{context_id} - Administrador de almacenamiento de objetos

Puedes verificar roles en tu servicio:

def check_role(token, required_role_pattern):
    payload = jwt.decode(token, options={"verify_signature": False})
    roles = payload.get('roles', [])
    context_id = payload.get('scope')
    
    required_role = required_role_pattern.replace('{context_id}', context_id)
    return required_role in roles

# Ejemplo:
if check_role(token, 'https://dtz.rocks/containers/admin/{context_id}'):
    # El usuario tiene permisos de administrador de contenedores
    pass

Buenas Prácticas

Seguridad

  • Siempre valida que los tokens JWT contengan los claims esperados.
  • Verifica los roles de usuario antes de conceder acceso a operaciones sensibles.
  • Usa HTTPS para todas las comunicaciones.
  • No registres tokens de autenticación sensibles en logs.

Manejo de Errores

  • Devuelve códigos de estado HTTP adecuados (por ejemplo, 401 para no autorizado, 403 para prohibido).
  • Proporciona mensajes de error significativos sin exponer información sensible.

Rendimiento

  • Almacena en caché los resultados de la validación de tokens JWT cuando sea posible.
  • Usa pooling de conexiones para llamadas a la API DTZ.
  • Considera implementar limitación de tasa de solicitudes.

Ejemplo: Servicio Completo Autenticado

Aquí tienes un ejemplo completo de un servicio Python con Flask y autenticación DTZ:

import os
import jwt
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

DTZ_TOKEN = os.environ.get('DTZ_ACCESS_TOKEN')
DTZ_CONTEXT_ID = os.environ.get('DTZ_CONTEXT_ID')

def get_user_from_token(token):
    """Extrae información del usuario desde un token JWT DTZ."""
    try:
        payload = jwt.decode(token, options={"verify_signature": False})
        return {
            'user_id': payload.get('sub'),
            'context_id': payload.get('scope'),
            'roles': payload.get('roles', [])
        }
    except jwt.InvalidTokenError:
        return None

def require_auth(f):
    """Decorador para requerir autenticación."""
    def decorated(*args, **kwargs):
        auth_header = request.headers.get('Authorization')
        if not auth_header or not auth_header.startswith('Bearer '):
            return jsonify({'error': 'Autenticación requerida'}), 401
        
        token = auth_header.split(' ')[1]
        user = get_user_from_token(token)
        
        if not user:
            return jsonify({'error': 'Token inválido'}), 401
        
        request.user = user
        return f(*args, **kwargs)
    
    decorated.__name__ = f.__name__
    return decorated

@app.route('/health')
def health():
    """Endpoint público para verificar estado."""
    return jsonify({'status': 'healthy'})

@app.route('/profile')
@require_auth
def profile():
    """Endpoint protegido que devuelve el perfil del usuario."""
    return jsonify({
        'user_id': request.user['user_id'],
        'context_id': request.user['context_id'],
        'roles': request.user['roles']
    })

@app.route('/admin/users')
@require_auth
def admin_users():
    """Endpoint solo para administradores."""
    required_role = f'https://dtz.rocks/context/admin/{DTZ_CONTEXT_ID}'
    
    if required_role not in request.user['roles']:
        return jsonify({'error': 'Se requiere acceso de administrador'}), 403
    
    # Realizar una solicitud autenticada a la API DTZ
    headers = {'Authorization': f'Bearer {DTZ_TOKEN}'}
    response = requests.get(
        'https://identity.dtz.rocks/api/2021-02-21/users',
        headers=headers
    )
    
    return jsonify(response.json())

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 80))
    app.run(host='0.0.0.0', port=port)

Resolución de Problemas

Problemas Comunes

  • Token de autenticación no encontrado: Asegúrate de que tu servicio esté desplegado a través del servicio de contenedores DTZ y que las variables de entorno se lean correctamente.
  • Errores de token inválido: Verifica que extraes correctamente el token desde el encabezado Authorization y que el parsing JWT funciona.
  • Errores 403 Forbidden: Confirma que el usuario tiene los roles requeridos en su token JWT.
  • Falla en autenticación entre servicios: Asegúrate de usar la variable de entorno DTZ_ACCESS_TOKEN para solicitudes salientes.

Prueba de Autenticación

Puedes probar la autenticación de tu servicio usando curl:

# Prueba con una clave API
curl -H "X-API-KEY: your-api-key" https://yourservice.dtz.rocks/profile

# Prueba con un token bearer  
curl -H "Authorization: Bearer your-jwt-token" https://yourservice.dtz.rocks/profile

# Prueba sin autenticación (debería retornar 401)
curl https://yourservice.dtz.rocks/profile

Para más información detallada sobre la autenticación DTZ, consulta la documentación de autenticación.