RSFXR
Iniciar sesiónEntrar Crear cuenta

Documentación

Inicio de sesión con RSFXR (OpenID Connect)

  1. Registra tu app en Mis aplicaciones. Empieza en modo desarrollo: solo tú puedes iniciar sesión con ella.
  2. Configura tu biblioteca OIDC con el documento de descubrimiento: https://rsfxr.com/.well-known/openid-configuration
  3. Redirige a https://rsfxr.com/oauth/authorize con response_type=code, client_id, redirect_uri (exacta), scope=openid profile, state, nonce y PKCE (code_challenge + code_challenge_method=S256, obligatorio para todas las apps).
  4. Canjea el code en POST https://rsfxr.com/oauth/token con grant_type=authorization_code, code_verifier y redirect_uri. Las apps confidenciales se autentican con client_secret_basic o client_secret_post.
  5. Valida el id_token (RS256): firma con el JWKS (https://rsfxr.com/.well-known/jwks.json, elige la clave por kid), iss, aud = tu client_id, exp y nonce.
  6. Usa sub como identificador de la persona. Es estable y no revela nada.
GET https://rsfxr.com/oauth/authorize?response_type=code&client_id=rsx_…&redirect_uri=https%3A%2F%2Ftuweb.com%2Fcallback
    &scope=openid%20profile&state=…&nonce=…&code_challenge=…&code_challenge_method=S256

POST https://rsfxr.com/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=…&redirect_uri=…&client_id=rsx_…&code_verifier=…

200 {"token_type":"Bearer","expires_in":900,"access_token":"eyJ…","refresh_token":"def…","id_token":"eyJ…"}

Access tokens: 15 min. Refresh tokens: 30 días, de un solo uso (cada refresh devuelve uno nuevo; reutilizar uno antiguo revoca toda la sesión de esa app). Revocación: POST https://rsfxr.com/oauth/revoke (RFC 7009). prompt=none|login|consent soportados.

Ejemplo completo y ejecutable: examples/demo-client en el repositorio.

Permisos (scopes)

ScopeQué daRequiere aprobación
openidConfirmar que eres tú (identificador anónimo y estable)No
profileTu @usuario, nombre visible, avatar y URL de perfilNo
emailTu dirección de correo electrónico verificadaNo
universesLos Universes públicos a los que perteneces y tu nivel de Señal en ellosSí, por app
posts.readLeer tus publicaciones públicasSí, por app
content.readVer en tu nombre lo que tú puedes ver, incluidos Universes privados y perfiles privados que siguesSí, por app
posts.writePublicar en tu nombre en tu perfilSí, por app

Nunca se comparten contraseñas, mensajes privados, Universes privados ni datos de pago.

API pública v1

Base: https://rsfxr.com/api/v1. Especificación completa: openapi.yaml.

EndpointAcceso
GET /universes, /universes/{slug}, /universes/{slug}/postsPúblico
GET /users/{username}, /users/{username}/postsPúblico (respeta perfiles privados)
GET /posts/{id}, /posts/{id}/comments, /hashtags/{tag}/postsPúblico
GET /search?q=&type=posts|universes|users|hashtagsPúblico
GET /meToken con openid
GET /me/universesToken con universes
GET /me/postsToken con posts.read
POST /me/postsToken con posts.write

Sin token, o con un token sin content.read, la API solo devuelve contenido público. Con content.read (permiso restringido, requiere aprobación) devuelve también lo que esa persona puede ver, como Universes privados de los que es miembro. posts.read da solo sus publicaciones públicas. Paginación por cursor: ?limit=20&cursor=…, el siguiente cursor viene en meta.next_cursor.

Endpoints internos (no soportados para terceros): las rutas web de la propia aplicación, como reacciones o seguimientos, usan sesión y CSRF y pueden cambiar sin aviso. Las operaciones de administración no tienen API.

Webhooks

Configura una dirección https en la ficha de tu app y te avisaremos de estos eventos con una petición POST en JSON:

  • authorization.revoked: Una persona retira el acceso de tu app (o lo retira la plataforma).
  • account.deleted: Una persona que usaba tu app elimina su cuenta de RSFXR.
  • ping: prueba enviada desde el portal.

El cuerpo solo incluye el identificador sub de la persona:

{"id": "evt_01jb…", "type": "authorization.revoked", "created": 1760000000, "client_id": "rsx_…", "data": {"sub": "…"}}
  • Firma. Cabecera RSFXR-Signature: t=…,v1=…: HMAC-SHA256 de t + "." + cuerpo con tu clave de firma. Compara en tiempo constante.
  • Repeticiones. Rechaza marcas de tiempo de más de 5 minutos. Cada evento tiene un id estable (también en la cabecera RSFXR-Delivery): los reintentos y los reenvíos manuales lo repiten, así que guarda los procesados e ignora los duplicados respondiendo 200.
  • Entrega. Responde 2xx en menos de 5 segundos (encola el trabajo pesado). Si no, reintentamos a 1 min, 5 min, 30 min y 2 h. Desde la ficha de tu app puedes reenviar un evento de los últimos 7 días.
  • Red. Solo https, puertos 443 u 8443, sin redirecciones ni direcciones privadas. Conectamos a la IP que comprobamos, sin volver a resolver el dominio.

En la ficha de tu app, Comprobar integración envía tres peticiones de prueba (válida, con firma falsa y antigua) con "test": true y te dice si tu servidor acepta solo la buena.

Ejemplos con bibliotecas habituales

Usa siempre una biblioteca OpenID Connect mantenida: valida el id_token, el state y el nonce por ti. En los ejemplos, ISSUER es https://rsfxr.com.

Node.js · openid-client

import { Issuer, generators } from 'openid-client';

const rsfxr = await Issuer.discover(ISSUER);
const client = new rsfxr.Client({ client_id: 'rsx_…', client_secret: 'rsxs_…', redirect_uris: ['https://tuweb.com/callback'], response_types: ['code'] });

// /login
const code_verifier = generators.codeVerifier();
const state = generators.state(), nonce = generators.nonce();
req.session.oidc = { code_verifier, state, nonce };
res.redirect(client.authorizationUrl({ scope: 'openid profile', state, nonce,
  code_challenge: generators.codeChallenge(code_verifier), code_challenge_method: 'S256' }));

// /callback
const tokens = await client.callback('https://tuweb.com/callback', client.callbackParams(req), req.session.oidc);
const { sub, acr, amr } = tokens.claims();   // sub: identificador estable de la persona

Python · Authlib (Flask)

from authlib.integrations.flask_client import OAuth

oauth = OAuth(app)
oauth.register('rsfxr', server_metadata_url=ISSUER + '/.well-known/openid-configuration',
               client_id='rsx_…', client_secret='rsxs_…',
               client_kwargs={'scope': 'openid profile', 'code_challenge_method': 'S256'})

@app.route('/login')
def login():
    return oauth.rsfxr.authorize_redirect(url_for('callback', _external=True))

@app.route('/callback')
def callback():
    token = oauth.rsfxr.authorize_access_token()   # valida state, nonce y firma
    person = token['userinfo']                     # person['sub'], person['acr']

PHP · jumbojett/openid-connect-php

$oidc = new Jumbojett\OpenIDConnectClient(ISSUER, 'rsx_…', 'rsxs_…');
$oidc->setRedirectURL('https://tuweb.com/callback');
$oidc->setCodeChallengeMethod('S256');
$oidc->addScope(['profile']);
$oidc->authenticate();                      // redirige y, a la vuelta, valida el id_token
$sub = $oidc->getVerifiedClaims('sub');

Verificar un webhook

// Node.js (Express): usa el cuerpo sin procesar
app.post('/rsfxr/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = Object.fromEntries((req.get('RSFXR-Signature') || '').split(',').map(p => p.split('=')));
  const expected = crypto.createHmac('sha256', SECRET).update(`${sig.t}.${req.body}`).digest('hex');
  const valid = sig.v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig.v1), Buffer.from(expected));
  if (!valid || Math.abs(Date.now() / 1000 - Number(sig.t)) > 300) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  if (alreadyProcessed(event.id)) return res.sendStatus(200);
  queue.add(event); res.sendStatus(200);
});

# Python
def valid(header: str, body: bytes, secret: str) -> bool:
    p = dict(part.split('=', 1) for part in header.split(','))
    expected = hmac.new(secret.encode(), p['t'].encode() + b'.' + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, p.get('v1', '')) and abs(time.time() - int(p['t'])) <= 300

Ejemplo completo sin dependencias en PHP (inicio de sesión, dos pasos, renovación, revocación y receptor de webhooks): examples/demo-client en el repositorio.

Errores y límites

{"error": {"code": "insufficient_scope", "message": "El token no tiene los permisos necesarios.", "details": {"required": ["posts.write"]}}}

Códigos: validation_failed (422), invalid_token / unauthenticated (401), insufficient_scope / forbidden (403), not_found (404), rate_limited (429, con Retry-After), server_error (500).

Límites: 60 peticiones/min por IP sin token; 120/min por app y persona con token. Cabeceras X-RateLimit-Limit y X-RateLimit-Remaining.

Versiones y cambios

  • Sin aviso podemos añadir endpoints, campos opcionales en las respuestas, valores nuevos en listas abiertas (por ejemplo, tipos de evento de webhook a los que te suscribes de forma explícita) y cabeceras. Tu código debe ignorar lo que no conoce.
  • Cambios incompatibles (quitar o renombrar campos, cambiar tipos o significados, endurecer validaciones) solo llegan en una versión nueva: /api/v2. Nunca dentro de /api/v1.
  • Retirada. Una versión se mantiene al menos 12 meses desde que anunciamos su retirada. Durante ese plazo las respuestas llevan las cabeceras Deprecation y Sunset (RFC 8594) y avisamos por correo a quien tenga apps activas.
  • RSFXR ID sigue OpenID Connect: el documento de descubrimiento es la referencia. Las claves de firma rotan con solapamiento; elige siempre por kid y vuelve a leer el JWKS si no encuentras uno.
  • Seguridad. Un cambio necesario para corregir un fallo de seguridad puede aplicarse sin plazo; lo explicaremos en la página de estado.

Estado del servicio y avisos: https://rsfxr.com/status.

Buenas prácticas obligatorias

  • Valida siempre state y nonce. Usa PKCE con S256.
  • Guarda el client secret solo en tu servidor. Las apps móviles o SPA deben registrarse como públicas (sin secreto).
  • Las URLs de retorno se comparan de forma exacta. Solo HTTPS (o http://localhost en desarrollo).
  • No guardes access tokens en localStorage; prefiere cookies de sesión de tu backend.
  • Cuando una persona revoca el acceso, tus tokens dejan de funcionar al instante: trata un 401 como «sesión terminada».