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.
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,
httpsobrigatório fora de desenvolvimento (IPs de loopback comohttp://127.0.0.1:4567/callbacksã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.
Endpoints
| Endpoint | Finalidade | Cross-origin |
|---|---|---|
GET /.well-known/openid-configuration | Discovery OIDC — comece por aqui | CORS aberto |
GET /oauth/authorize | Tela de consentimento (navegação de nível superior, nunca iframe nem XHR) | — |
POST /oauth/token | Code → tokens; refresh | CORS aberto |
POST /oauth/revoke | Revogação de tokens RFC 7009 | CORS aberto |
GET/POST /oauth/userinfo | Userinfo OIDC (token Bearer) | CORS aberto |
GET /oauth/discovery/keys | JWKS — a chave pública RS256 para verificar o id_token | CORS aberto |
GET /api/oauth/v1/me | Espelho do userinfo no namespace da API de recursos | — |
GET /api/oauth/v1/me/enrollments | Os 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)
| Escopo | Concede | O aluno vê como |
|---|---|---|
openid | O id_token e a claim sub. Sempre obrigatório (é o escopo padrão) | Confirmar quem você é na AcademyOS |
email | email, 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 userinfo | Ver seu endereço de e-mail |
profile | name, given_name, family_name, preferred_username, locale, picture — apenas no userinfo | Ver seu perfil básico (nome, usuário, idioma, foto) |
enrollments:read | GET /api/oauth/v1/me/enrollments | Ver 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.
-
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 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.
A aprovação redireciona para a sua
redirect_uricom?code=…&state=…. O code é de uso único e vive 10 minutos.-
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.
Memória de consentimento
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 porsub, nunca por e-mail: um e-mail pode mudar,subnã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_verifiedviajam diretamente noid_token.
Ciclo de vida dos tokens
| Credencial | Duração |
|---|---|
| Authorization code | 10 minutos, uso único |
| Access token | 2 horas |
id_token | 10 minutos (uma afirmação de autenticação, não uma credencial de acesso) |
| Refresh token | Sem 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.
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:
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:
| HTTP | code | Significado |
|---|---|---|
| 401 | unauthorized | Token ausente/expirado/revogado — ou a conta de parceiro está desativada |
| 403 | insufficient_scope | Token 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:
{ "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:
{
"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 deactive/pending/expired/revoked/none;days_remainingénullpara acesso vitalício.pendingem 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 responde401 invalid_tokena partir do mesmo instante. A reativação restaura os tokens que não expiraram. /oauth/authorizetem limite por IP (30/min por padrão),/oauth/tokenpor IP (60/min por padrão);429com 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
httpsfora 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_tokene do userinfo.
Checklist de integração
- Registre a sua organização e o seu cliente no portal de desenvolvedores; guarde o
client_id(e oclient_secretde exibição única, se confidencial) no seu gerenciador de segredos. - Aponte uma biblioteca cliente OIDC para
https://app.academyos.app/.well-known/openid-configuration. - Envie os alunos pelo authorize com PKCE +
state+nonce; valide tudo na volta. - Indexe as suas contas por
sub. Trateemailcomo dado de exibição, a menos queemail_verifiedseja true. - Peça
enrollments:readapenas se realmente exibir os cursos do aluno. - 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".
- 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.
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