Integrações para parceiros

Entrar com AcademyOS

OAuth 2.0 authorization code + PKCE com OpenID Connect, para que os alunos entrem no seu site com a conta AcademyOS e autorizem o que você pode ler.

Como tudo se encaixa

Com a API de parceiros você age como você mesmo: uma chave no nível da organização lista o seu catálogo licenciado e libera acesso para os seus clientes. Entrar com AcademyOS é a direção oposta: um aluno da AcademyOS entra no seu site e consente que você leia a identidade dele — e, com um escopo extra, os cursos do marketplace que ele possui. Você nunca vê a senha dele; ele pode revogar você a qualquer momento; cada token que você tem está limitado exatamente ao que ele concordou.

É OAuth 2.0 authorization code + PKCE padrão com OpenID Connect por cima (discovery, id_tokens RS256, JWKS, userinfo). Qualquer biblioteca cliente OIDC funciona: aponte-a para o documento de discovery e a maior parte já está feita.

PARCEIRO · MARKETPLACE ACADEMYOS · PLATAFORMA Vitrine — todos os cursos funciona para visitantes, sem login GET /api/partners/v1/courses catálogo licenciado ao parceiro · preço, capa, termos de acesso CHAVE GET · chave de parceiro Entrar / Criar conta um único botão “Entrar com AcademyOS” o único ponto de cadastro — sem contas locais GET /oauth/authorize login ou criação de conta + consentimento (code + PKCE) POST /oauth/token id_token (sub, e-mail verificado) + access + refresh ALUNO redirect de nível superior code → tokens Meus cursos toda a estante do aluno, não só as suas vendas GET /api/oauth/v1/me/enrollments os cursos do aluno · escopo enrollments:read ALUNO Bearer · token do aluno Página do curso GET /api/partners/v1/courses/{id} cruza com os cursos do aluno e decide: inscrito? → acessar · ainda não? → comprar já inscrito? Checkout do parceiro pagamento e preço ficam do lado do parceiro o e-mail de comprovante continua sendo seu comprar POST /api/partners/v1/enrollments { course_id, e-mail verificado, external_reference } → status: "active" na hora — sem convite, sem e-mail CHAVE POST · ref = nº do pedido Curso na AcademyOS o conteúdo é sempre consumido dentro da AcademyOS acessar link comum, sessão já existe acesso imediato
Duas credenciais, duas raias: a chave de parceiro (servidor, sua organização) move o catálogo e a liberação de acesso; o token OAuth do aluno (emitido com consentimento) move tudo o que pertence ao aluno.

Registro

Registre a sua organização parceira no portal de desenvolvedores — basta uma conta AcademyOS com e-mail confirmado — e crie o seu cliente lá. Os clientes funcionam imediatamente; cada uso passa pelo consentimento do aluno. A licença de catálogo para a chave da API de parceiros é uma decisão comercial separada, que você solicita na mesma página. Você vai precisar de:

  • Suas URIs de redirecionamento — correspondência exata, https obrigatório fora de desenvolvimento (IPs de loopback como http://127.0.0.1:4567/callback são aceitos para testes locais).
  • Se o seu cliente é confidencial (você consegue guardar um segredo no servidor: um app web clássico) ou público (SPA ou app móvel; sem segredo, o PKCE faz a prova). O tipo de cliente não pode ser alterado depois — outro tipo é outro cliente.
  • Opcionalmente, um conjunto de escopos menor que os quatro padrão (veja Escopos).
  • O nome a exibir na tela de consentimento e um e-mail de contato técnico.

Você recebe um client_id e, para clientes confidenciais, um client_secret que é mostrado exatamente uma vez, na criação — guardamos apenas o digest SHA-256, exatamente como nas chaves da API de parceiros. Segredos perdidos são rotacionados, não recuperados; a rotação mostra um segredo novo uma vez e invalida o anterior imediatamente.

Um parceiro pode ter vários clientes (um por plataforma). Todos os seus clientes, tokens e consentimentos morrem no momento em que a sua conta de parceiro é desativada — o mesmo kill switch das suas chaves de API.

Registrar seu aplicativo

Endpoints

EndpointFinalidadeCross-origin
GET /.well-known/openid-configurationDiscovery OIDC — comece por aquiCORS aberto
GET /oauth/authorizeTela de consentimento (navegação de nível superior, nunca iframe nem XHR)
POST /oauth/tokenCode → tokens; refreshCORS aberto
POST /oauth/revokeRevogação de tokens RFC 7009CORS aberto
GET/POST /oauth/userinfoUserinfo OIDC (token Bearer)CORS aberto
GET /oauth/discovery/keysJWKS — a chave pública RS256 para verificar o id_tokenCORS aberto
GET /api/oauth/v1/meEspelho do userinfo no namespace da API de recursos
GET /api/oauth/v1/me/enrollmentsOs cursos do marketplace do aluno

O CORS está aberto deliberadamente apenas nos endpoints públicos sem cookies que um cliente PKCE no navegador precisa chamar; todo o resto mantém o padrão same-origin do navegador. /oauth/authorize é um destino de redirecionamento, não uma API — não faça fetch dele.

Escopos (scopes)

EscopoConcedeO aluno vê como
openidO id_token e a claim sub. Sempre obrigatório (é o escopo padrão)Confirmar quem você é na AcademyOS
emailemail, email_verified — copiados para dentro do id_token além do userinfo, então um login simples não precisa de uma chamada extra ao userinfoVer seu endereço de e-mail
profilename, given_name, family_name, preferred_username, locale, picture — apenas no userinfoVer seu perfil básico (nome, usuário, idioma, foto)
enrollments:readGET /api/oauth/v1/me/enrollmentsVer os cursos aos quais você tem acesso

Repare nos dois pontos em enrollments:read. Escopos desconhecidos são rejeitados com o erro padrão invalid_scope, e uma configuração por cliente pode restringir ainda mais esta lista. Peça o menor conjunto de que precisa: a tela de consentimento mostra cada escopo ao aluno, e pedir enrollments:read sem usar custa confiança.

O fluxo de autorização

Authorization code com PKCE (S256). PKCE é obrigatório — clientes públicos não conseguem completar o fluxo sem ele, e clientes confidenciais que o enviam têm-no verificado. Sem implicit, sem password, sem client-credentials: um parceiro agindo como ele mesmo usa a sua chave da API de parceiros, não OAuth.

  1. Redirecione o aluno (navegação de nível superior) para:

    HTTP
    GET https://app.academyos.app/oauth/authorize?client_id=…&redirect_uri=…&response_type=code
        &scope=openid+email+enrollments:read
        &state=<aleatório>&nonce=<aleatório>
        &code_challenge=<S256(code_verifier)>&code_challenge_method=S256
  2. O aluno faz login na AcademyOS se ainda não estiver logado — incluindo a etapa de 2FA — e volta automaticamente para a tela de consentimento. A tela nomeia o seu app e a sua organização parceira e lista os escopos. Ele aprova ou nega.

  3. A aprovação redireciona para a sua redirect_uri com ?code=…&state=…. O code é de uso único e vive 10 minutos.

  4. Troque-o (no servidor, ou a partir da SPA — o endpoint tem CORS aberto):

    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 confidenciais]
    JSON
    {
      "access_token": "…", "token_type": "Bearer", "expires_in": 7200,
      "refresh_token": "…", "scope": "openid email enrollments:read",
      "created_at": 1724751600, "id_token": "eyJhbGciOiJSUzI1NiIs…"
    }

Clientes confidenciais podem se autenticar no endpoint de token com HTTP Basic (client_secret_basic) ou com parâmetros de formulário (client_secret_post); ambos são aceitos.

A primeira autorização sempre mostra a tela de consentimento. Um cliente confidencial não é perguntado de novo enquanto um token ainda vivo de um consentimento anterior tiver exatamente o conjunto de escopos que ele pede novamente — a autorização redireciona direto com um code novo. Qualquer outra situação mostra a tela de novo: um conjunto de escopos diferente (mais amplo ou mais estreito), nenhum token sobrevivente (todos expirados ou revogados), e toda autorização de um cliente público — sem segredo não há prova de que é o mesmo app, então ali o consentimento é por autorização. A negação redireciona com o padrão error=access_denied. prompt=login e um max_age vencido forçam nova autenticação.

O id_token

JWT assinado com RS256; verifique-o contra o endpoint JWKS e valide iss (o issuer acima), aud (o seu client_id), exp (10 minutos) e nonce (ecoa o seu).

  • sub é o id público estável do aluno (user_…) — opaco, não enumerável, permanente por usuário. Indexe as suas contas locais por sub, nunca por e-mail: um e-mail pode mudar, sub não.
  • auth_time é quando o aluno realmente se autenticou pela última vez, não quando a sessão foi restaurada.
  • Com o escopo email, email / email_verified viajam diretamente no id_token.

Ciclo de vida dos tokens

CredencialDuração
Authorization code10 minutos, uso único
Access token2 horas
id_token10 minutos (uma afirmação de autenticação, não uma credencial de acesso)
Refresh tokenSem expiração por relógio — substituído pela rotação (abaixo), morre na revogação ou na desativação do parceiro

O refresh rotaciona com uma janela de transição: trocar um refresh token devolve um novo par access + refresh, e o par substituído continua válido até o novo access token ser usado pela primeira vez — uma resposta de token perdida na rede não consegue deixar você trancado para fora, porque repetir a troca com o refresh token antigo ainda funciona até lá. A partir do primeiro uso do novo access token, o par substituído é revogado e reutilizar o refresh token dele é recusado (invalid_grant). Guarde sempre o par mais novo.

HTTP
POST https://app.academyos.app/oauth/token
grant_type=refresh_token&refresh_token=…&client_id=…[&client_secret=…]

Revogue os tokens de que não precisa mais (logout, desvinculação de conta) via RFC 7009:

HTTP
POST https://app.academyos.app/oauth/revoke
token=…&client_id=…[&client_secret=…]

Os tokens são guardados como digests SHA-256, como toda credencial que guardamos por você.

A API de recursos

Authorization: Bearer <access_token> contra /api/oauth/v1. É um terceiro regime de credenciais: chaves da API de parceiros e tokens do app móvel da AcademyOS não são aceitos aqui, e tokens OAuth não são aceitos na API de parceiros nem na API móvel. Os erros têm o formato conhecido:

HTTPcodeSignificado
401unauthorizedToken ausente/expirado/revogado — ou a conta de parceiro está desativada
403insufficient_scopeToken vivo, mas sem este escopo. WWW-Authenticate indica o escopo necessário

GET /api/oauth/v1/me

Escopo: openid. As claims do userinfo OIDC, filtradas pelos escopos do token, no namespace da API de recursos para você integrar contra um único caminho base:

JSON
{ "sub": "user_9tRk…", "email": "learner@example.com", "email_verified": true, "name": "Ada Learner" }

GET /api/oauth/v1/me/enrollments

Escopo: enrollments:read. Os cursos do marketplace do aluno — idênticos byte a byte, em formato, ao que o próprio app móvel da AcademyOS mostra a ele, porque ambos renderizam o mesmo feed. Dois tipos de linha, 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" — um curso que o aluno comprou na AcademyOS. kind: "grant" — um curso liberado sem pedido, por exemplo por um parceiro via API de parceiros ou por um administrador da AcademyOS. Um curso coberto pelos dois aparece uma única vez, como compra.
  • access_state é um de active / pending / expired / revoked / none; days_remaining é null para acesso vitalício. pending em uma compra é um checkout ainda em andamento (em uma liberação, um convite ainda não aceito); none é uma compra que nunca foi concluída — um checkout falho, cancelado ou abandonado ainda aparece como linha, e a sua interface deve tratá-lo como sem acesso.
  • O feed responde pelo aluno que consentiu, seja quem for que liberou o acesso. Não é limitado às suas liberações nem diz quem vendeu o quê — é o aluno mostrando a própria estante para você.
  • Sem campos de dinheiro: o que o aluno pagou fica entre ele e quem vendeu.

O que o aluno vê e controla

  • A tela de consentimento tem a marca, é localizada (en/es/pt) e nomeia tanto o seu aplicativo quanto a sua organização parceira.
  • Os alunos revisam todos os apps que autorizaram em Configurações → Apps autorizados, e podem revogar qualquer um a qualquer momento. A revogação mata todos os seus tokens daquele aluno de uma vez; o próximo authorize parte de uma tela de consentimento nova. Projete para tokens morrerem a qualquer momento — o 401 diz para autorizar de novo.
  • Um administrador da AcademyOS personificando um aluno não consegue consentir em nome dele; o fluxo recusa.

Kill switch, limites de requisições e letras miúdas

  • A desativação do parceiro é total: o authorize mostra a página de erro OAuth padrão, o endpoint de token responde invalid_client, e todo access/refresh token já emitido responde 401 invalid_token a partir do mesmo instante. A reativação restaura os tokens que não expiraram.
  • /oauth/authorize tem limite por IP (30/min por padrão), /oauth/token por IP (60/min por padrão); 429 com corpo em texto simples. Eles contêm credential stuffing, não o seu tráfego legítimo.
  • As URIs de redirecionamento são de correspondência exata e apenas https fora de desenvolvimento, exceto callbacks em IPs de loopback (http://127.0.0.1:<porta>/…), cuja porta pode variar no authorize conforme a RFC 8252. Sem curingas, sem fragmentos.
  • O endpoint authorize precisa ser uma navegação de nível superior — ele depende do cookie de sessão da AcademyOS do aluno, que não existe em contextos cross-site de iframe ou fetch.
  • Não há endpoint de introspecção de tokens; a identidade vem do id_token e do userinfo.

Checklist de integração

  1. Registre a sua organização e o seu cliente no portal de desenvolvedores; guarde o client_id (e o client_secret de exibição única, se confidencial) no seu gerenciador de segredos.
  2. Aponte uma biblioteca cliente OIDC para https://app.academyos.app/.well-known/openid-configuration.
  3. Envie os alunos pelo authorize com PKCE + state + nonce; valide tudo na volta.
  4. Indexe as suas contas por sub. Trate email como dado de exibição, a menos que email_verified seja true.
  5. Peça enrollments:read apenas se realmente exibir os cursos do aluno.
  6. Faça refresh de forma proativa (access tokens vivem 2 horas), guarde o par rotacionado e trate qualquer 401 como "mandar o aluno pelo authorize de novo".
  7. Revogue tokens no logout/desvinculação em vez de deixá-los expirar.

Executar o cliente de exemplo

O repositório da AcademyOS inclui um cliente de referência sem framework em examples/oauth-client/app.rb que percorre toda esta página em um navegador real: discovery, PKCE, o callback, a troca do code como cliente confidencial, a verificação do id_token contra o JWKS, as três chamadas de recursos, a rotação de refresh e a revogação. Cada página que ele renderiza imprime as respostas brutas do servidor, então ele também serve como ferramenta de depuração para a sua própria integração.

BASH
cp .env.example .env   # ACADEMYOS_ISSUER, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, SESSION_SECRET
ruby app.rb            # depois abra http://localhost:4567 e clique em "Sign in with AcademyOS"

Contra uma AcademyOS implantada a URI de redirecionamento precisa ser https (ou um IP de loopback): coloque o cliente atrás de um túnel como cloudflared tunnel --url http://localhost:4567 e registre a URL de callback do túnel. O README ao lado do script traz o checklist de evidências que nós mesmos executamos.

Esta página foi útil?

Última atualização: 04 de setembro de 2026