Turinga! Connect
Turinga! Connect permite que aplicaciones externas identifiquen usuarios mediante su cuenta de Turinga! sin recibir ni conocer su contraseña.
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:
Flujo de autenticación
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:
Tu aplicación obtiene:
client_id: identificador público de la aplicación.client_secret: credencial privada de aplicaciones confidenciales.
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
| 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
state antes de utilizar el Authorization Code.
Token Endpoint
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"
}
Refresh Token Rotation
Cuando el Access Token expire, utiliza el Refresh Token para obtener una nueva pareja de tokens.
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"
}
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.
UserInfo
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.
Revocación de tokens
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_tokenrefresh_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.
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 |
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."
}
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
statecriptográficamente aleatorio para cada autorización. - Valida siempre
stateen 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.
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);