Documentación
Inicio de sesión con RSFXR (OpenID Connect)
- Registra tu app en Mis aplicaciones. Empieza en modo desarrollo: solo tú puedes iniciar sesión con ella.
- Configura tu biblioteca OIDC con el documento de descubrimiento:
https://rsfxr.com/.well-known/openid-configuration - Redirige a
https://rsfxr.com/oauth/authorizeconresponse_type=code,client_id,redirect_uri(exacta),scope=openid profile,state,noncey PKCE (code_challenge+code_challenge_method=S256, obligatorio para todas las apps). - Canjea el
codeenPOST https://rsfxr.com/oauth/tokencongrant_type=authorization_code,code_verifieryredirect_uri. Las apps confidenciales se autentican conclient_secret_basicoclient_secret_post. - Valida el
id_token(RS256): firma con el JWKS (https://rsfxr.com/.well-known/jwks.json, elige la clave porkid),iss,aud= tu client_id,expynonce. - Usa
subcomo 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)
| Scope | Qué da | Requiere aprobación |
|---|---|---|
openid | Confirmar que eres tú (identificador anónimo y estable) | No |
profile | Tu @usuario, nombre visible, avatar y URL de perfil | No |
email | Tu dirección de correo electrónico verificada | No |
universes | Los Universes públicos a los que perteneces y tu nivel de Señal en ellos | Sí, por app |
posts.read | Leer tus publicaciones públicas | Sí, por app |
content.read | Ver en tu nombre lo que tú puedes ver, incluidos Universes privados y perfiles privados que sigues | Sí, por app |
posts.write | Publicar en tu nombre en tu perfil | Sí, 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.
| Endpoint | Acceso |
|---|---|
GET /universes, /universes/{slug}, /universes/{slug}/posts | Público |
GET /users/{username}, /users/{username}/posts | Público (respeta perfiles privados) |
GET /posts/{id}, /posts/{id}/comments, /hashtags/{tag}/posts | Público |
GET /search?q=&type=posts|universes|users|hashtags | Público |
GET /me | Token con openid |
GET /me/universes | Token con universes |
GET /me/posts | Token con posts.read |
POST /me/posts | Token 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 det + "." + cuerpocon tu clave de firma. Compara en tiempo constante. - Repeticiones. Rechaza marcas de tiempo de más de 5 minutos. Cada evento tiene un
idestable (también en la cabeceraRSFXR-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
DeprecationySunset(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
kidy 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
stateynonce. 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».