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 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.

PARTNER · MARKETPLACE ACADEMYOS · PLATAFORMA Vitrina — todos los cursos funciona para visitantes, sin iniciar sesión GET /api/partners/v1/courses catálogo licenciado al partner · precio, portada, acceso CLAVE GET · clave de partner Iniciar sesión / Registrarse un solo botón “Iniciar sesión con AcademyOS” el único punto de registro — sin cuentas locales GET /oauth/authorize inicio de sesión o alta + consentimiento (code + PKCE) POST /oauth/token id_token (sub, e-mail verificado) + access + refresh ALUMNO redirección de nivel superior code → tokens Mis cursos toda la estantería del alumno, no solo tus ventas GET /api/oauth/v1/me/enrollments los cursos del alumno · alcance enrollments:read ALUMNO Bearer · token del alumno Página del curso GET /api/partners/v1/courses/{id} cruza con los cursos del alumno y decide: ¿inscrito? → acceder · ¿aún no? → comprar ¿ya inscrito? Checkout del partner pago y precio quedan del lado del partner el comprobante por e-mail sigue siendo tuyo comprar POST /api/partners/v1/enrollments { course_id, e-mail verificado, external_reference } → status: "active" al instante — sin invitación, sin e-mail CLAVE POST · ref = nº de pedido Curso en AcademyOS el contenido siempre se consume dentro de AcademyOS acceder enlace normal, sesión ya activa acceso inmediato
Dos credenciales, dos carriles: la clave de partner (servidor, tu organización) mueve el catálogo y la concesión de acceso; el token OAuth del alumno (emitido con consentimiento) mueve todo lo que pertenece al alumno.

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, https obligatorio fuera de desarrollo (se aceptan IPs de loopback como http://127.0.0.1:4567/callback para 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.

Registrar tu aplicación

Endpoints

EndpointPropósitoCross-origin
GET /.well-known/openid-configurationDiscovery OIDC — empieza aquíCORS abierto
GET /oauth/authorizePantalla de consentimiento (navegación de nivel superior, nunca un iframe ni XHR)
POST /oauth/tokenCode → tokens; refreshCORS abierto
POST /oauth/revokeRevocación de tokens RFC 7009CORS abierto
GET/POST /oauth/userinfoUserinfo OIDC (token Bearer)CORS abierto
GET /oauth/discovery/keysJWKS — la clave pública RS256 para verificar el id_tokenCORS abierto
GET /api/oauth/v1/meEspejo de userinfo en el espacio de la API de recursos
GET /api/oauth/v1/me/enrollmentsLos 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)

AlcanceConcedeEl alumno lo ve como
openidEl id_token y la claim sub. Siempre obligatorio (es el alcance por defecto)Confirmar quién eres en AcademyOS
emailemail, email_verified — copiados dentro del id_token además de userinfo, así un inicio de sesión simple no necesita llamar a userinfoVer tu dirección de correo
profilename, given_name, family_name, preferred_username, locale, picture — solo en userinfoVer tu perfil básico (nombre, usuario, idioma, foto)
enrollments:readGET /api/oauth/v1/me/enrollmentsVer 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.

  1. 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
  2. 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.

  3. La aprobación redirige a tu redirect_uri con ?code=…&state=…. El code es de un solo uso y vive 10 minutos.

  4. 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.

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).

  • sub es el id público estable del alumno (user_…) — opaco, no enumerable, permanente por usuario. Indexa tus cuentas locales por sub, nunca por e-mail: un e-mail puede cambiar, sub no.
  • auth_time es cuándo se autenticó realmente el alumno por última vez, no cuándo se restauró la sesión.
  • Con el alcance email, email / email_verified viajan directamente en el id_token.

Ciclo de vida de los tokens

CredencialDuración
Authorization code10 minutos, un solo uso
Access token2 horas
id_token10 minutos (una afirmación de autenticación, no una credencial de acceso)
Refresh tokenSin 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.

HTTP
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:

HTTP
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:

HTTPcodeSignificado
401unauthorizedToken ausente/caducado/revocado — o la cuenta de partner está desactivada
403insufficient_scopeToken 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:

JSON
{ "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:

JSON
{
  "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_state es uno de active / pending / expired / revoked / none; days_remaining es null para acceso de por vida. pending en una compra es un checkout aún en curso (en una concesión, una invitación todavía no aceptada); none es 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 responden 401 invalid_token desde ese mismo instante. La reactivación restaura los tokens que no hayan caducado.
  • /oauth/authorize tiene límite por IP (30/min por defecto), /oauth/token por IP (60/min por defecto); 429 con 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 https fuera 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_token y de userinfo.

Checklist de integración

  1. Registra tu organización y tu cliente en el portal de desarrolladores; guarda el client_id (y el client_secret de un solo vistazo, si es confidencial) en tu gestor de secretos.
  2. Apunta una biblioteca cliente OIDC a https://app.academyos.app/.well-known/openid-configuration.
  3. Envía a los alumnos por authorize con PKCE + state + nonce; valida todo a la vuelta.
  4. Indexa tus cuentas por sub. Trata email como dato de presentación salvo que email_verified sea true.
  5. Pide enrollments:read solo si realmente muestras los cursos del alumno.
  6. 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".
  7. 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.

BASH
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