Integraciones para partners
Iniciar sesión con AcademyOS
OAuth 2.0 authorization code + PKCE con OpenID Connect, para que los alumnos inicien sesión en tu sitio con su cuenta de AcademyOS y autoricen lo que puedes leer.
Cómo encaja todo
Con la API de partners tú actúas como tú mismo: una clave a nivel de organización lista tu catálogo licenciado y concede acceso a tus clientes. Iniciar sesión con AcademyOS es la dirección opuesta: un alumno de AcademyOS inicia sesión en tu sitio y autoriza que leas su identidad — y, con un alcance adicional, los cursos del marketplace que tiene. Nunca ves su contraseña; puede revocarte en cualquier momento; cada token que tienes está limitado exactamente a lo que aceptó.
Es OAuth 2.0 authorization code + PKCE estándar con OpenID Connect encima (discovery, id_tokens RS256, JWKS, userinfo). Cualquier biblioteca cliente OIDC funciona: apúntala al documento de discovery y ya tienes la mayor parte hecha.
Registro
Registra tu organización partner en el portal de desarrolladores — basta una cuenta de AcademyOS con el e-mail confirmado — y crea allí tu cliente. Los clientes funcionan de inmediato; cada uso pasa por el consentimiento del alumno. La licencia de catálogo para la clave de la API de partners es una decisión comercial aparte que solicitas desde la misma página. Necesitarás:
- Tus URI de redirección — coincidencia exacta,
httpsobligatorio fuera de desarrollo (se aceptan IPs de loopback comohttp://127.0.0.1:4567/callbackpara pruebas locales). - Si tu cliente es confidencial (puedes guardar un secreto en el servidor: una app web clásica) o público (SPA o app móvil; sin secreto, PKCE aporta la prueba). El tipo de cliente no puede cambiarse después — otro tipo es otro cliente.
- Opcionalmente, un conjunto de alcances más reducido que los cuatro por defecto (ver Alcances).
- El nombre que se mostrará en la pantalla de consentimiento y un e-mail de contacto técnico.
Recibes un client_id y, para clientes confidenciales, un client_secret que se muestra exactamente una vez, al crearlo — solo guardamos su digest SHA-256, igual que con las claves de la API de partners. Un secreto perdido se rota, no se recupera; la rotación muestra un secreto nuevo una vez e invalida el anterior de inmediato.
Un partner puede tener varios clientes (uno por plataforma). Todos tus clientes, tokens y consentimientos mueren en el momento en que se desactiva tu cuenta de partner — el mismo interruptor de apagado que tus claves de API.
Endpoints
| Endpoint | Propósito | Cross-origin |
|---|---|---|
GET /.well-known/openid-configuration | Discovery OIDC — empieza aquí | CORS abierto |
GET /oauth/authorize | Pantalla de consentimiento (navegación de nivel superior, nunca un iframe ni XHR) | — |
POST /oauth/token | Code → tokens; refresh | CORS abierto |
POST /oauth/revoke | Revocación de tokens RFC 7009 | CORS abierto |
GET/POST /oauth/userinfo | Userinfo OIDC (token Bearer) | CORS abierto |
GET /oauth/discovery/keys | JWKS — la clave pública RS256 para verificar el id_token | CORS abierto |
GET /api/oauth/v1/me | Espejo de userinfo en el espacio de la API de recursos | — |
GET /api/oauth/v1/me/enrollments | Los cursos del marketplace del alumno | — |
CORS está abierto deliberadamente solo en los endpoints públicos sin cookies que un cliente PKCE en el navegador debe llamar; todo lo demás conserva el comportamiento same-origin del navegador. /oauth/authorize es un destino de redirección, no una API — no hagas fetch de él.
Alcances (scopes)
| Alcance | Concede | El alumno lo ve como |
|---|---|---|
openid | El id_token y la claim sub. Siempre obligatorio (es el alcance por defecto) | Confirmar quién eres en AcademyOS |
email | email, email_verified — copiados dentro del id_token además de userinfo, así un inicio de sesión simple no necesita llamar a userinfo | Ver tu dirección de correo |
profile | name, given_name, family_name, preferred_username, locale, picture — solo en userinfo | Ver tu perfil básico (nombre, usuario, idioma, foto) |
enrollments:read | GET /api/oauth/v1/me/enrollments | Ver los cursos a los que tienes acceso |
Fíjate en los dos puntos de enrollments:read. Los alcances desconocidos se rechazan con el error estándar invalid_scope, y una configuración por cliente puede reducir aún más esta lista. Pide el conjunto más pequeño que necesites: la pantalla de consentimiento muestra cada alcance al alumno, y pedir enrollments:read sin usarlo te cuesta confianza.
El flujo de autorización
Authorization code con PKCE (S256). PKCE es obligatorio — los clientes públicos no pueden completar el flujo sin él, y a los confidenciales que lo envían se les verifica. Sin implicit, sin password, sin client-credentials: un partner que actúa como él mismo usa su clave de la API de partners, no OAuth.
-
Redirige al alumno (navegación de nivel superior) a:
HTTP GET https://app.academyos.app/oauth/authorize?client_id=…&redirect_uri=…&response_type=code &scope=openid+email+enrollments:read &state=<aleatorio>&nonce=<aleatorio> &code_challenge=<S256(code_verifier)>&code_challenge_method=S256 El alumno inicia sesión en AcademyOS si aún no lo ha hecho — incluido su paso de 2FA — y vuelve automáticamente a la pantalla de consentimiento. La pantalla nombra tu app y tu organización partner y lista los alcances. Aprueba o deniega.
La aprobación redirige a tu
redirect_uricon?code=…&state=…. El code es de un solo uso y vive 10 minutos.-
Intercámbialo (en el servidor, o desde la SPA — el endpoint tiene CORS abierto):
HTTP POST https://app.academyos.app/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=…&redirect_uri=… &client_id=…&code_verifier=… [&client_secret=… para clientes confidenciales]JSON { "access_token": "…", "token_type": "Bearer", "expires_in": 7200, "refresh_token": "…", "scope": "openid email enrollments:read", "created_at": 1724751600, "id_token": "eyJhbGciOiJSUzI1NiIs…" }
Los clientes confidenciales pueden autenticarse en el endpoint de token con HTTP Basic (client_secret_basic) o con parámetros de formulario (client_secret_post); se aceptan ambos.
Memoria del consentimiento
La primera autorización siempre muestra la pantalla de consentimiento. A un cliente confidencial no se le vuelve a preguntar mientras un token todavía vivo de un consentimiento anterior tenga exactamente el conjunto de alcances que vuelve a pedir — la autorización redirige directamente con un code nuevo. Cualquier otra cosa vuelve a mostrar la pantalla: un conjunto de alcances distinto (más amplio o más estrecho), ningún token superviviente (todos caducados o revocados), y cada autorización de un cliente público — sin secreto no hay prueba de que sea la misma app, así que allí el consentimiento es por autorización. La denegación redirige con el estándar error=access_denied. prompt=login y un max_age vencido fuerzan una nueva autenticación.
El id_token
JWT firmado con RS256; verifícalo contra el endpoint JWKS y valida iss (el issuer de arriba), aud (tu client_id), exp (10 minutos) y nonce (devuelve el tuyo).
subes el id público estable del alumno (user_…) — opaco, no enumerable, permanente por usuario. Indexa tus cuentas locales porsub, nunca por e-mail: un e-mail puede cambiar,subno.auth_timees cuándo se autenticó realmente el alumno por última vez, no cuándo se restauró la sesión.- Con el alcance
email,email/email_verifiedviajan directamente en elid_token.
Ciclo de vida de los tokens
| Credencial | Duración |
|---|---|
| Authorization code | 10 minutos, un solo uso |
| Access token | 2 horas |
id_token | 10 minutos (una afirmación de autenticación, no una credencial de acceso) |
| Refresh token | Sin caducidad por reloj — lo sustituye la rotación (abajo), muere con la revocación o la desactivación del partner |
El refresh rota con una ventana de traspaso: intercambiar un refresh token devuelve un nuevo par access + refresh, y el par al que sustituye sigue siendo válido hasta que el nuevo access token se usa por primera vez — una respuesta de token perdida en la red no puede dejarte fuera, porque reintentar el intercambio con el refresh token antiguo sigue funcionando hasta entonces. Desde el primer uso del nuevo access token, el par sustituido queda revocado y reutilizar su refresh token se rechaza (invalid_grant). Guarda siempre el par más nuevo.
POST https://app.academyos.app/oauth/token
grant_type=refresh_token&refresh_token=…&client_id=…[&client_secret=…]Revoca los tokens que ya no necesites (cierre de sesión, desvinculación de cuenta) según RFC 7009:
POST https://app.academyos.app/oauth/revoke
token=…&client_id=…[&client_secret=…]Los tokens se guardan como digests SHA-256, como cualquier otra credencial que guardamos por ti.
La API de recursos
Authorization: Bearer <access_token> contra /api/oauth/v1. Es un tercer régimen de credenciales: aquí no se aceptan claves de la API de partners ni tokens de la app móvil de AcademyOS, y los tokens OAuth no se aceptan en la API de partners ni en la API móvil. Los errores tienen la forma habitual:
| HTTP | code | Significado |
|---|---|---|
| 401 | unauthorized | Token ausente/caducado/revocado — o la cuenta de partner está desactivada |
| 403 | insufficient_scope | Token vivo, pero sin este alcance. WWW-Authenticate indica el alcance necesario |
GET /api/oauth/v1/me
Alcance: openid. Las claims de userinfo OIDC, filtradas por los alcances del token, en el espacio de la API de recursos para que integres contra una sola ruta base:
{ "sub": "user_9tRk…", "email": "learner@example.com", "email_verified": true, "name": "Ada Learner" }GET /api/oauth/v1/me/enrollments
Alcance: enrollments:read. Los cursos del marketplace del alumno — idénticos byte a byte en forma a lo que la propia app móvil de AcademyOS le muestra, porque ambos renderizan el mismo feed. Dos tipos de fila, distinguidos por kind:
{
"marketplace_enrollments": [
{
"kind": "purchase",
"order_id": "mord_…", "order_status": "paid",
"purchased_at": "2026-08-11T14:03:22Z", "granted_at": null,
"product_id": "prod_…", "name": "Practical Math", "slug": "practical-math",
"creator_name": "Creator One Studio", "cover_image_url": "https://…",
"access_state": "active", "access_expires_at": "2026-09-10T14:03:22Z",
"days_remaining": 14, "enrollment_status": "active"
},
{
"kind": "grant",
"order_id": null, "order_status": null,
"purchased_at": null, "granted_at": "2026-08-12T09:00:00Z",
"product_id": "prod_…", "name": "…", "slug": "…",
"creator_name": "…", "cover_image_url": "https://…",
"access_state": "active", "access_expires_at": null,
"days_remaining": null, "enrollment_status": "active"
}
]
}kind: "purchase"— un curso que el alumno compró en AcademyOS.kind: "grant"— un curso concedido sin pedido, p. ej. por un partner a través de la API de partners o por un administrador de AcademyOS. Un curso cubierto por ambos aparece una sola vez, como compra.access_statees uno deactive/pending/expired/revoked/none;days_remainingesnullpara acceso de por vida.pendingen una compra es un checkout aún en curso (en una concesión, una invitación todavía no aceptada);nonees una compra que nunca se completó — un checkout fallido, cancelado o abandonado sigue apareciendo como fila, y tu interfaz debe tratarlo como sin acceso.- El feed responde por el alumno que dio el consentimiento, sea quien sea quien concedió el acceso. No está limitado a tus concesiones ni dice quién vendió qué — es el alumno mostrándote su propia estantería.
- Sin campos de dinero: lo que pagó el alumno queda entre él y quien se lo vendió.
Qué ve y controla el alumno
- La pantalla de consentimiento lleva la marca, está localizada (en/es/pt) y nombra tanto tu aplicación como tu organización partner.
- Los alumnos revisan todas las apps que han autorizado en Configuración → Apps autorizadas, y pueden revocar cualquiera en cualquier momento. La revocación mata todos tus tokens de ese alumno a la vez; el siguiente authorize parte de una pantalla de consentimiento nueva. Diseña para que los tokens puedan morir en cualquier momento — el 401 te dice que vuelvas a autorizar.
- Un administrador de AcademyOS que suplanta a un alumno no puede dar consentimiento en su nombre; el flujo lo rechaza.
Interruptor de apagado, límites y letra pequeña
- La desactivación del partner es total: authorize muestra la página de error OAuth estándar, el endpoint de token responde
invalid_client, y todos los access/refresh tokens ya emitidos responden401 invalid_tokendesde ese mismo instante. La reactivación restaura los tokens que no hayan caducado. /oauth/authorizetiene límite por IP (30/min por defecto),/oauth/tokenpor IP (60/min por defecto);429con cuerpo de texto plano. Acotan el credential stuffing, no tu tráfico legítimo.- Las URI de redirección son de coincidencia exacta y solo
httpsfuera de desarrollo, salvo callbacks a IPs de loopback (http://127.0.0.1:<puerto>/…), cuyo puerto puede variar en el authorize según RFC 8252. Sin comodines, sin fragmentos. - El endpoint authorize debe ser una navegación de nivel superior — depende de la cookie de sesión de AcademyOS del alumno, que no existe en contextos cross-site de iframe o fetch.
- No hay endpoint de introspección de tokens; la identidad viene del
id_tokeny de userinfo.
Checklist de integración
- Registra tu organización y tu cliente en el portal de desarrolladores; guarda el
client_id(y elclient_secretde un solo vistazo, si es confidencial) en tu gestor de secretos. - Apunta una biblioteca cliente OIDC a
https://app.academyos.app/.well-known/openid-configuration. - Envía a los alumnos por authorize con PKCE +
state+nonce; valida todo a la vuelta. - Indexa tus cuentas por
sub. Trataemailcomo dato de presentación salvo queemail_verifiedsea true. - Pide
enrollments:readsolo si realmente muestras los cursos del alumno. - Refresca de forma proactiva (los access tokens viven 2 horas), guarda el par rotado y trata cualquier 401 como "enviar al alumno por authorize otra vez".
- Revoca los tokens al cerrar sesión o desvincular en lugar de dejar que caduquen.
Ejecutar el cliente de ejemplo
El repositorio de AcademyOS incluye un cliente de referencia sin framework en examples/oauth-client/app.rb que recorre toda esta página en un navegador real: discovery, PKCE, el callback, el intercambio del code como cliente confidencial, la verificación del id_token contra el JWKS, las tres llamadas de recursos, la rotación de refresh y la revocación. Cada página que renderiza imprime las respuestas crudas del servidor, así que también sirve como herramienta de depuración para tu propia integración.
cp .env.example .env # ACADEMYOS_ISSUER, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, SESSION_SECRET
ruby app.rb # luego abre http://localhost:4567 y pulsa "Sign in with AcademyOS"Contra un AcademyOS desplegado la URI de redirección debe ser https (o una IP de loopback): pon el cliente detrás de un túnel como cloudflared tunnel --url http://localhost:4567 y registra la URL de callback del túnel. El README junto al script incluye el checklist de evidencias que ejecutamos nosotros mismos.
¿Te resultó útil esta página?
Última actualización: 4 de septiembre de 2026