T! Turinga! Connect

Turinga! Connect

Turinga! Connect permite que aplicaciones externas identifiquen usuarios mediante su cuenta de Turinga! sin recibir ni conocer su contraseña.

OAuth 2.0 Authorization Code PKCE S256 obligatorio Refresh Token Rotation Token Revocation
Actualmente Turinga! Connect ofrece autenticación OAuth 2.0 y un endpoint UserInfo asociado al scope openid. Todavía no debe considerarse una implementación completa de OpenID Connect: ID Tokens, Discovery y JWKS se incorporarán posteriormente.

Estado actual de la plataforma

Las siguientes funciones forman parte de la implementación actual de Turinga! Connect:

Authorization Code Código de autorización de vida corta y uso único.
PKCE S256 Obligatorio en todas las autorizaciones.
State Obligatorio para proteger el flujo de autorización.
Access Tokens Tokens Bearer con expiración y revocación.
Refresh Token Rotation Cada renovación emite un nuevo Refresh Token.
Replay Detection La reutilización de un Refresh Token consumido puede invalidar toda su familia.
UserInfo Identidad y datos permitidos por scopes.
Token Revocation Revocación explícita de Access Tokens y Refresh Tokens.
Los flujos de Authorization Code, UserInfo, Refresh Token Rotation, detección de replay y revocación han sido probados en la implementación actual.

Flujo de autenticación

1. Aplicación Genera state + PKCE
2. Connect Solicita autorización
3. Callback Recibe un code
4. API Intercambia el code
5. UserInfo Obtiene identidad

La contraseña del usuario nunca abandona Turinga!. Tu aplicación solamente recibe credenciales OAuth y los datos correspondientes a los scopes autorizados.

Registrar una aplicación

Registra tu aplicación desde el portal de desarrolladores:

https://connect.turinga.net/developers/

Tu aplicación obtiene:

  • client_id: identificador público de la aplicación.
  • client_secret: credencial privada de aplicaciones confidenciales.
El Client Secret se muestra únicamente al crearlo o regenerarlo. Turinga! Connect almacena solamente su hash y no puede recuperar el secreto original.

Las aplicaciones nuevas pueden permanecer en estado pending hasta completar su revisión.

PKCE S256

Turinga! Connect requiere PKCE con S256 para todas las autorizaciones.

1. Generar code_verifier

$verifier = rtrim(
    strtr(base64_encode(random_bytes(64)), '+/', '-_'),
    '='
);

2. Generar code_challenge

$challenge = rtrim(
    strtr(
        base64_encode(hash('sha256', $verifier, true)),
        '+/',
        '-_'
    ),
    '='
);

El code_verifier debe mantenerse temporalmente del lado servidor de tu aplicación y utilizarse cuando intercambies el Authorization Code.

Authorization Endpoint

GET https://connect.turinga.net/oauth/authorize.php
Parámetro Requerido Descripción
response_type Sí Debe ser code.
client_id Sí Client ID registrado.
redirect_uri Sí Debe coincidir exactamente con una Redirect URI registrada.
scope Sí Scopes separados por espacios.
state Sí Valor aleatorio generado por la aplicación.
code_challenge Sí Challenge derivado del code_verifier.
code_challenge_method Sí Debe ser S256.
nonce No Valor opcional reservado para mecanismos de identidad.

Ejemplo

https://connect.turinga.net/oauth/authorize.php
?response_type=code
&client_id=tur_xxxxx
&redirect_uri=https%3A%2F%2Fejemplo.com%2Fauth%2Fcallback.php
&scope=openid%20profile
&state=STATE_ALEATORIO
&code_challenge=CHALLENGE
&code_challenge_method=S256

Respuesta correcta

https://ejemplo.com/auth/callback.php
?code=tac_xxxxx
&state=STATE_ALEATORIO
La aplicación debe validar siempre state antes de utilizar el Authorization Code.

Token Endpoint

POST https://api.turinga.net/oauth/token.php

Intercambia el Authorization Code por Access Token y Refresh Token.

grant_type=authorization_code
client_id=tur_xxxxx
client_secret=tcs_xxxxx
code=tac_xxxxx
redirect_uri=https://ejemplo.com/auth/callback.php
code_verifier=VERIFIER_ORIGINAL

Content-Type:

application/x-www-form-urlencoded

Respuesta

{
  "access_token": "tka_xxxxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tkr_xxxxx",
  "scope": "openid profile"
}
El Authorization Code tiene una vida corta y solamente puede utilizarse una vez.

Refresh Token Rotation

Cuando el Access Token expire, utiliza el Refresh Token para obtener una nueva pareja de tokens.

POST https://api.turinga.net/oauth/token.php
grant_type=refresh_token
client_id=tur_xxxxx
client_secret=tcs_xxxxx
refresh_token=tkr_xxxxx

Respuesta

{
  "access_token": "tka_NUEVO",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tkr_NUEVO",
  "scope": "openid profile"
}
El Refresh Token anterior queda consumido después de una renovación correcta. La aplicación debe sustituirlo inmediatamente por el nuevo token recibido.

Detección de reutilización

Si un Refresh Token que ya fue utilizado aparece nuevamente, Turinga! Connect puede interpretar la situación como una posible filtración de credenciales.

En ese caso puede:

  • marcar la reutilización del token;
  • revocar toda la familia de Refresh Tokens;
  • revocar Access Tokens relacionados con esa aplicación y usuario.
Una aplicación nunca debe intentar reutilizar voluntariamente un Refresh Token después de haber recibido su reemplazo.

UserInfo

GET https://api.turinga.net/v1/userinfo.php

Envía el Access Token mediante el header:

Authorization: Bearer tka_xxxxx
Accept: application/json

Ejemplo de respuesta

{
  "sub": "1",
  "id": 1,
  "nick": "Usuario",
  "profile_url": "https://turinga.net/perfil/Usuario",
  "avatar": "https://...",
  "role": "Desarrollador"
}

Los datos devueltos dependen de los scopes otorgados.

No envíes Access Tokens mediante parámetros GET, fragmentos de URL ni Query Strings.

Revocación de tokens

POST https://api.turinga.net/oauth/revoke.php

Una aplicación puede invalidar voluntariamente un Access Token o un Refresh Token.

Revocar Access Token

client_id=tur_xxxxx
client_secret=tcs_xxxxx
token=tka_xxxxx
token_type_hint=access_token

Revocar Refresh Token

client_id=tur_xxxxx
client_secret=tcs_xxxxx
token=tkr_xxxxx
token_type_hint=refresh_token

Los valores reconocidos de token_type_hint son:

  • access_token
  • refresh_token

Comportamiento

Al revocar un Access Token solamente se invalida ese token.

Al revocar un Refresh Token, Turinga! Connect puede revocar:

  • la familia completa de Refresh Tokens;
  • los Access Tokens activos pertenecientes a esa aplicación y usuario.
Por diseño, el endpoint de revocación puede responder HTTP 200 incluso cuando el token proporcionado no existe, tiene un formato desconocido o no pertenece a la aplicación. Esto evita convertir el endpoint en un mecanismo para descubrir tokens válidos.

Respuesta

Una revocación procesada correctamente responde:

HTTP/1.1 200 OK

La respuesta puede no contener cuerpo.

Scopes

Scope Información Sensible
openid Identificador, nick y URL pública del perfil. No
profile Avatar y rol/rango visible. No
email Correo electrónico de la cuenta. Sí

Una aplicación solamente puede solicitar scopes previamente habilitados para ella.

Rate limits

Turinga! Connect aplica límites de solicitudes en endpoints sensibles para reducir abuso, enumeración de tokens y ataques automatizados.

UserInfo

Límite Valor actual
Solicitudes generales por IP 120 por 60 segundos
Intentos con tokens inválidos por IP 30 por 60 segundos

Token Revocation

Límite Valor actual
Solicitudes por IP 60 por 60 segundos
Solicitudes por Client ID 30 por 60 segundos
Estos límites pueden ajustarse en el futuro según tráfico, seguridad y necesidades operativas. Las aplicaciones no deben depender de que los valores permanezcan constantes indefinidamente.

HTTP 429

Cuando una aplicación supera un límite puede recibir:

HTTP/1.1 429 Too Many Requests
Retry-After: 42

y una respuesta similar a:

{
  "error": "slow_down",
  "error_description": "Demasiadas solicitudes."
}
Respeta siempre el header Retry-After. No intentes solucionar un 429 aumentando inmediatamente la frecuencia de solicitudes.

Errores OAuth

Los endpoints pueden responder errores estructurados:

{
  "error": "invalid_grant",
  "error_description": "Authorization Code inválido o expirado."
}
Error Significado
invalid_request Falta un parámetro obligatorio o su formato no es válido.
invalid_client Las credenciales de la aplicación no son válidas.
invalid_grant Authorization Code, PKCE o Refresh Token inválido, utilizado, revocado o expirado.
invalid_scope La aplicación solicitó un scope que no tiene permitido.
access_denied El usuario rechazó la autorización.
invalid_token Access Token inválido, expirado o revocado.
insufficient_scope El Access Token no posee el scope requerido.
slow_down Se alcanzó un límite de solicitudes.
server_error Ocurrió un error interno temporal.

Recomendaciones de seguridad

  • Utiliza HTTPS.
  • Genera state criptográficamente aleatorio para cada autorización.
  • Valida siempre state en el callback.
  • Utiliza PKCE S256.
  • No reutilices Authorization Codes.
  • Guarda el Client Secret únicamente del lado servidor.
  • Nunca incluyas el Client Secret en JavaScript público.
  • No envíes Access Tokens ni Refresh Tokens mediante URLs.
  • No escribas tokens completos en logs.
  • Guarda Refresh Tokens en almacenamiento protegido.
  • Sustituye inmediatamente un Refresh Token después de rotarlo.
  • Respeta HTTP 429 y Retry-After.
  • No utilices el Access Token como cookie de sesión de tu propia aplicación.
  • Después de obtener UserInfo, crea tu propia sesión segura.
Nunca solicites directamente la contraseña de Turinga! a tus usuarios. Las aplicaciones integradas con Turinga! Connect no necesitan conocerla.

Ejemplo completo en PHP

Iniciar autorización

<?php
session_start();

$clientId = 'TU_CLIENT_ID';
$redirectUri = 'https://ejemplo.com/auth/turinga/callback.php';

function b64url(string $value): string {
    return rtrim(
        strtr(base64_encode($value), '+/', '-_'),
        '='
    );
}

$state = b64url(random_bytes(32));
$verifier = b64url(random_bytes(64));

$challenge = b64url(
    hash('sha256', $verifier, true)
);

$_SESSION['turinga_oauth'] = [
    'state' => $state,
    'verifier' => $verifier,
    'created_at' => time()
];

$params = [
    'response_type' => 'code',
    'client_id' => $clientId,
    'redirect_uri' => $redirectUri,
    'scope' => 'openid profile',
    'state' => $state,
    'code_challenge' => $challenge,
    'code_challenge_method' => 'S256'
];

header(
    'Location: https://connect.turinga.net/oauth/authorize.php?'.
    http_build_query(
        $params,
        '',
        '&',
        PHP_QUERY_RFC3986
    )
);

exit;

Callback

<?php
session_start();

$clientId = 'TU_CLIENT_ID';
$clientSecret = 'TU_CLIENT_SECRET';

$redirectUri =
    'https://ejemplo.com/auth/turinga/callback.php';

$code = (string)($_GET['code'] ?? '');
$state = (string)($_GET['state'] ?? '');

$oauth = $_SESSION['turinga_oauth'] ?? null;

unset($_SESSION['turinga_oauth']);

if (
    !is_array($oauth) ||
    empty($oauth['state']) ||
    !hash_equals(
        (string)$oauth['state'],
        $state
    )
) {
    exit('State inválido');
}

if (
    empty($oauth['created_at']) ||
    time() - (int)$oauth['created_at'] > 600
) {
    exit('Autorización expirada');
}

$ch = curl_init(
    'https://api.turinga.net/oauth/token.php'
);

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'grant_type' => 'authorization_code',
        'client_id' => $clientId,
        'client_secret' => $clientSecret,
        'code' => $code,
        'redirect_uri' => $redirectUri,
        'code_verifier' => $oauth['verifier']
    ], '', '&', PHP_QUERY_RFC3986)
]);

$response = curl_exec($ch);

curl_close($ch);

$tokens = json_decode(
    (string)$response,
    true
);

if (
    !is_array($tokens) ||
    empty($tokens['access_token'])
) {
    exit('No fue posible obtener Access Token');
}

$accessToken =
    (string)$tokens['access_token'];

$refreshToken =
    (string)($tokens['refresh_token'] ?? '');

$ch = curl_init(
    'https://api.turinga.net/v1/userinfo.php'
);

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer '.$accessToken,
        'Accept: application/json'
    ]
]);

$response = curl_exec($ch);

curl_close($ch);

$user = json_decode(
    (string)$response,
    true
);

if (
    !is_array($user) ||
    empty($user['sub'])
) {
    exit('No fue posible obtener el usuario');
}

/*
|--------------------------------------------------------------------------
| Crea la sesión de TU aplicación
|--------------------------------------------------------------------------
*/

session_regenerate_id(true);

$_SESSION['user_id'] =
    (string)$user['sub'];

$_SESSION['nick'] =
    (string)$user['nick'];

/*
|--------------------------------------------------------------------------
| IMPORTANTE
|--------------------------------------------------------------------------
|
| Si necesitas sesiones prolongadas, guarda $refreshToken de manera segura
| del lado servidor.
|--------------------------------------------------------------------------
*/

Renovar tokens

$ch = curl_init(
    'https://api.turinga.net/oauth/token.php'
);

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'grant_type' => 'refresh_token',
        'client_id' => $clientId,
        'client_secret' => $clientSecret,
        'refresh_token' => $refreshToken
    ], '', '&', PHP_QUERY_RFC3986)
]);

$response = curl_exec($ch);

curl_close($ch);

$newTokens = json_decode(
    (string)$response,
    true
);

if (!empty($newTokens['refresh_token'])) {

    /*
     * Sustituye inmediatamente el Refresh Token anterior.
     */

    $refreshToken =
        $newTokens['refresh_token'];
}

Revocar un Refresh Token

$ch = curl_init(
    'https://api.turinga.net/oauth/revoke.php'
);

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'client_id' => $clientId,
        'client_secret' => $clientSecret,
        'token' => $refreshToken,
        'token_type_hint' => 'refresh_token'
    ], '', '&', PHP_QUERY_RFC3986)
]);

curl_exec($ch);

curl_close($ch);
Los ejemplos anteriores muestran el flujo principal. Una aplicación en producción debe implementar además manejo robusto de errores, timeouts de red, almacenamiento protegido de Refresh Tokens, validación de respuestas HTTP y protección adecuada de sus propias sesiones.