Integração SSO — Especificação Técnica
Plataforma Semente
1. Visão Geral
1.1 Objetivo
Este documento especifica como um sistema parceiro deve se integrar ao mecanismo de Single Sign-On (SSO) da Plataforma Semente. O objetivo é permitir que um usuário já autenticado no sistema do parceiro (por exemplo, o sistema de gestão escolar) acesse a Plataforma Semente sem precisar realizar um novo login manual.
A integração é feita através da geração de um token assinado (JWT) pelo parceiro, enviado a um endpoint HTTP da Semente, que valida o token e autentica o usuário correspondente.
1.2 Fluxo Resumido
- O usuário se autentica normalmente no sistema do parceiro.
- Em algum momento posterior, o usuário clica em uma opção do tipo "Acessar Plataforma Semente" dentro do sistema do parceiro.
- Esse clique aciona o sistema do parceiro a gerar um token JWT assinado, identificando o usuário.
- O parceiro entrega esse token ao navegador do usuário: o back-end do parceiro responde a esse clique devolvendo uma página HTML com o token embutido, que se auto-submete via POST ao endpoint de SSO da Semente (ver seção 4.5).
- A Semente valida o token.
- Se válido, a Semente autentica o usuário, define um cookie de sessão no navegador e redireciona o navegador diretamente para a Plataforma Semente, já autenticada.
/sso_custom precisa ser feita pelo navegador do usuário final (ex.: uma página intermediária do parceiro com um formulário HTML que carrega o JWT e se auto-submete via POST). Se a chamada for feita servidor-a-servidor (back-end do parceiro chamando o back-end da Semente), o redirecionamento e o cookie chegam ao servidor do parceiro, não ao navegador do usuário, e o SSO não funciona.
1.3 Diagrama do Fluxo
O diagrama abaixo ilustra a sequência completa da integração. Note que, a partir do passo 5, quem conversa com a Semente é o navegador do usuário — não o back-end do parceiro:
Caso qualquer validação do passo 6 falhe (assinatura, audience/issuer, expiração ou reuso do jti), a Semente responde com erro diretamente ao navegador do usuário — a página intermediária do parceiro (a mesma que fez o POST no passo 5) precisa estar preparada para tratar essa resposta de erro.
2. Pré-requisitos
2.1 Dados fornecidos pela Semente
Antes de iniciar a implementação, a Semente fornecerá ao parceiro, por canal seguro, os seguintes dados:
- Client ID: identificador único do parceiro.
- Secret: chave de 256 bits, codificada em Base64, usada para assinar o token (HMAC-SHA256).
2.2 Sobre a Codificação em Base64
O secret é gerado pela Semente como uma sequência de bytes aleatórios e entregue ao parceiro já codificado em Base64 — essa codificação existe apenas para que um valor binário possa ser transmitido e copiado como texto (e-mail, painel, etc.).
O parceiro não precisa gerar nem codificar nada em Base64. O único passo necessário do lado do parceiro é decodificar a string recebida de volta para bytes brutos antes de usá-la como chave de assinatura HMAC — exatamente como mostrado nos exemplos da seção 5 (Convert.FromBase64String, base64.b64decode, Base64.getDecoder().decode, entre outros).
3. Especificação do Token (JWT)
O token utilizado é um JSON Web Token (JWT), assinado com o algoritmo HMAC-SHA256 (HS256). É um padrão aberto, com bibliotecas disponíveis para praticamente qualquer linguagem de programação.
3.1 Claims Obrigatórias
| Claim | Obrigatório | Descrição |
|---|---|---|
| sub | Sim | Login do usuário na Plataforma Semente (o mesmo identificador já usado hoje no login direto). |
| iss | Sim | Client ID do parceiro, fornecido pela Semente no momento do provisionamento. |
| aud | Sim | Valor fixo "plataforma-semente". Deve ser copiado literalmente (case-sensitive). |
| jti | Sim | Identificador único do token (UUID v4). Usado para impedir reuso do mesmo token. |
| iat | Sim | Data/hora de emissão do token (UTC). |
| nbf | Sim | Data/hora a partir da qual o token é válido (UTC). Deve ser igual a iat. |
| exp | Sim | Data/hora de expiração do token (UTC). Deve ser iat + 5 minutos. |
3.2 Algoritmo e Chave
- Algoritmo de assinatura: HS256 (HMAC-SHA256). Nenhum outro algoritmo é aceito.
- Chave de assinatura: o secret fornecido pela Semente, decodificado de Base64 para bytes brutos antes de assinar.
3.3 Regras de Validade
- O token deve ter validade de exatamente 5 minutos entre
iateexp. - Cada token deve conter um
jtiúnico (recomenda-se UUID v4). Um token com o mesmojtinão pode ser reenviado — a segunda tentativa será rejeitada, mesmo dentro da janela de validade. - O relógio do sistema do parceiro deve estar sincronizado (NTP). Diferenças de horário podem causar rejeição indevida do token.
4. Endpoint de Integração
4.1 URL e Método
A requisição HTTP POST com o token deve ser enviada para:
POST https://integracao.sementeeducacao.com.br/api/v0/accounts/sso_custom
4.2 Formato da Requisição
O corpo da requisição deve conter o token no campo token, como application/x-www-form-urlencoded — o mesmo formato que um <form> HTML nativo envia. Esse formato (em vez de JSON) é proposital: é o que permite que a requisição seja feita por uma navegação real do navegador (um formulário que se auto-submete), sem depender de JavaScript assíncrono (fetch/XHR), que esbarraria em CORS e em restrições de cookie de terceiros. É a mesma técnica do HTTP-POST Binding usado em autenticações SAML 2.0 — a integração da Semente não usa SAML, mas resolve o mesmo problema (entregar uma credencial ao navegador do usuário) da mesma forma consolidada. Veja o exemplo completo na seção 4.4.
POST /api/v0/accounts/sso_custom HTTP/1.1
Host: integracao.sementeeducacao.com.br
Content-Type: application/x-www-form-urlencoded
token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2FvLnNpbHZhIiwi...
4.3 Respostas
Em caso de sucesso, a Semente não retorna um token em JSON — ela autentica o usuário, define um cookie de sessão no navegador e redireciona a resposta para a Plataforma Semente:
HTTP/1.1 302 Found
Location: <URL da Plataforma Semente>
Set-Cookie: <cookie de sessão>
O header Location aponta para a Plataforma Semente e o Set-Cookie estabelece a sessão de acesso — como essa resposta é endereçada ao navegador (não a um caller server-to-server), o próprio navegador segue o redirecionamento automaticamente e chega à Plataforma Semente já autenticado.
Em caso de falha de validação:
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"mensagem": "Token expirado ou já utilizado"
}
Códigos de retorno possíveis:
| HTTP | Situação | Descrição |
|---|---|---|
| 302 | Sucesso | Token válido. Usuário autenticado e redirecionado para a Plataforma Semente com sessão já ativa. |
| 401 | Não autorizado | Assinatura inválida, client_id desconhecido, token expirado, ou token já utilizado (replay). |
| 400 | Requisição inválida | Corpo da requisição malformado ou claims obrigatórias ausentes. |
4.4 Página de Redirecionamento (Exemplo)
Abaixo, um exemplo completo da página intermediária que o back-end do parceiro deve gerar e devolver ao navegador do usuário logo após o clique em "Acessar Plataforma Semente" (passo 2 do diagrama). O back-end preenche o campo value com o JWT recém-gerado; o formulário se auto-submete assim que a página carrega, e o <noscript> garante um botão manual como alternativa para o raro caso de JavaScript desabilitado:
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Redirecionando para a Plataforma Semente...</title>
</head>
<body>
<p>Redirecionando, aguarde…</p>
<form id="sso-form" method="POST" action="https://integracao.sementeeducacao.com.br/api/v0/accounts/sso_custom">
<input type="hidden" name="token" value="SEU_JWT_GERADO_AQUI">
<noscript>
<button type="submit">Clique aqui para continuar</button>
</noscript>
</form>
<script>
document.getElementById('sso-form').submit();
</script>
</body>
</html>
4.5 Gerando e Devolvendo a Página no Back-end (Exemplo Completo)
O exemplo da seção 4.4 mostra apenas o HTML final — o que falta é o código do back-end do parceiro que responde ao clique em "Acessar Plataforma Semente" (passo 2 do diagrama) gerando o JWT (seção 5), embutindo-o nesse template e devolvendo o resultado como resposta HTTP com Content-Type: text/html. É esse handler que fecha o ciclo entre "gerar o token" e "entregar a página" ao navegador.
Exemplo com ASP.NET Core (minimal API):
using System.Net;
// GET /sso/iniciar — chamado quando o usuário clica em "Acessar Plataforma Semente"
app.MapGet("/sso/iniciar", () =>
{
string jwt = GerarJwt(); // ver seção 5.1
string tokenEscapado = WebUtility.HtmlEncode(jwt);
string html = $@"<!DOCTYPE html>
<html lang=""pt-BR"">
<head>
<meta charset=""UTF-8"">
<title>Redirecionando para a Plataforma Semente...</title>
</head>
<body>
<p>Redirecionando, aguarde…</p>
<form id=""sso-form"" method=""POST"" action=""https://integracao.sementeeducacao.com.br/api/v0/accounts/sso_custom"">
<input type=""hidden"" name=""token"" value=""{tokenEscapado}"">
<noscript><button type=""submit"">Clique aqui para continuar</button></noscript>
</form>
<script>document.getElementById('sso-form').submit();</script>
</body>
</html>";
return Results.Content(html, "text/html");
});
Exemplo com Flask:
from flask import Flask, Response
from markupsafe import escape
app = Flask(__name__)
@app.route("/sso/iniciar")
def sso_iniciar():
jwt_token = gerar_jwt() # ver seção 5.2
html = f"""<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Redirecionando para a Plataforma Semente...</title>
</head>
<body>
<p>Redirecionando, aguarde…</p>
<form id="sso-form" method="POST" action="https://integracao.sementeeducacao.com.br/api/v0/accounts/sso_custom">
<input type="hidden" name="token" value="{escape(jwt_token)}">
<noscript><button type="submit">Clique aqui para continuar</button></noscript>
</form>
<script>document.getElementById('sso-form').submit();</script>
</body>
</html>"""
return Response(html, mimetype="text/html")
Exemplo com Spring Boot:
import org.springframework.web.util.HtmlUtils;
@GetMapping("/sso/iniciar")
public ResponseEntity<String> ssoIniciar() {
String jwt = gerarJwt(); // ver seção 5.3
String tokenEscapado = HtmlUtils.htmlEscape(jwt);
String html = """
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Redirecionando para a Plataforma Semente...</title>
</head>
<body>
<p>Redirecionando, aguarde…</p>
<form id="sso-form" method="POST" action="https://integracao.sementeeducacao.com.br/api/v0/accounts/sso_custom">
<input type="hidden" name="token" value="%s">
<noscript><button type="submit">Clique aqui para continuar</button></noscript>
</form>
<script>document.getElementById('sso-form').submit();</script>
</body>
</html>
""".formatted(tokenEscapado);
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(html);
}
Exemplo com Express:
const express = require("express");
const escapeHtml = require("escape-html");
const app = express();
app.get("/sso/iniciar", (req, res) => {
const jwt = gerarJwt(); // ver seção 5.4
const html = `<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Redirecionando para a Plataforma Semente...</title>
</head>
<body>
<p>Redirecionando, aguarde…</p>
<form id="sso-form" method="POST" action="https://integracao.sementeeducacao.com.br/api/v0/accounts/sso_custom">
<input type="hidden" name="token" value="${escapeHtml(jwt)}">
<noscript><button type="submit">Clique aqui para continuar</button></noscript>
</form>
<script>document.getElementById('sso-form').submit();</script>
</body>
</html>`;
res.set("Content-Type", "text/html");
res.send(html);
});
Exemplo em PHP puro:
<?php
// GET /sso/iniciar
$jwt = gerarJwt(); // ver seção 5.5
$tokenEscapado = htmlspecialchars($jwt, ENT_QUOTES, 'UTF-8');
header("Content-Type: text/html; charset=UTF-8");
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Redirecionando para a Plataforma Semente...</title>
</head>
<body>
<p>Redirecionando, aguarde…</p>
<form id="sso-form" method="POST" action="https://integracao.sementeeducacao.com.br/api/v0/accounts/sso_custom">
<input type="hidden" name="token" value="<?= $tokenEscapado ?>">
<noscript><button type="submit">Clique aqui para continuar</button></noscript>
</form>
<script>document.getElementById('sso-form').submit();</script>
</body>
</html>
5. Exemplos de Geração do JWT
A seguir, exemplos de geração do token nas linguagens mais comuns. Todos produzem um token equivalente, seguindo a especificação da seção 3.
5.1 C# (.NET)
Bibliotecas: System.IdentityModel.Tokens.Jwt e Microsoft.IdentityModel.Tokens (NuGet).
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
// client_id e secret são fornecidos pela Semente no provisionamento
string clientId = "SEU_CLIENT_ID";
string secretBase64 = "SEU_SECRET_BASE64";
string login = "joao.silva";
var key = new SymmetricSecurityKey(Convert.FromBase64String(secretBase64));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var now = DateTime.UtcNow;
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, login),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(
issuer: clientId,
audience: "plataforma-semente",
claims: claims,
notBefore: now,
expires: now.AddMinutes(5),
signingCredentials: credentials
);
string jwt = new JwtSecurityTokenHandler().WriteToken(token);
5.2 Python
Biblioteca: PyJWT (pip install pyjwt).
import jwt
import uuid
import base64
from datetime import datetime, timedelta, timezone
# client_id e secret são fornecidos pela Semente no provisionamento
client_id = "SEU_CLIENT_ID"
secret = base64.b64decode("SEU_SECRET_BASE64")
login = "joao.silva"
now = datetime.now(timezone.utc)
payload = {
"sub": login,
"iss": client_id,
"aud": "plataforma-semente",
"jti": str(uuid.uuid4()),
"iat": now,
"nbf": now,
"exp": now + timedelta(minutes=5),
}
jwt_token = jwt.encode(payload, secret, algorithm="HS256")
5.3 Java
Biblioteca: io.jsonwebtoken:jjwt (jjwt-api, jjwt-impl, jjwt-jackson).
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
// client_id e secret são fornecidos pela Semente no provisionamento
String clientId = "SEU_CLIENT_ID";
byte[] secretBytes = Base64.getDecoder().decode("SEU_SECRET_BASE64");
SecretKey key = Keys.hmacShaKeyFor(secretBytes);
String login = "joao.silva";
Date now = new Date();
Date exp = new Date(now.getTime() + 5 * 60 * 1000);
String jwt = Jwts.builder()
.setSubject(login)
.setIssuer(clientId)
.setAudience("plataforma-semente")
.setId(UUID.randomUUID().toString())
.setIssuedAt(now)
.setNotBefore(now)
.setExpiration(exp)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
5.4 Node.js
Biblioteca: jsonwebtoken (npm install jsonwebtoken).
const jwt = require("jsonwebtoken");
const { randomUUID } = require("crypto");
// client_id e secret são fornecidos pela Semente no provisionamento
const clientId = "SEU_CLIENT_ID";
const secret = Buffer.from("SEU_SECRET_BASE64", "base64");
const login = "joao.silva";
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
{
sub: login,
iss: clientId,
aud: "plataforma-semente",
jti: randomUUID(),
iat: now,
nbf: now,
exp: now + 5 * 60,
},
secret,
{ algorithm: "HS256" }
);
5.5 PHP
Biblioteca: firebase/php-jwt (composer require firebase/php-jwt).
<?php
require 'vendor/autoload.php';
use Firebase\JWT\JWT;
// client_id e secret são fornecidos pela Semente no provisionamento
$clientId = "SEU_CLIENT_ID";
$secret = base64_decode("SEU_SECRET_BASE64");
$login = "joao.silva";
$now = time();
$payload = [
"sub" => $login,
"iss" => $clientId,
"aud" => "plataforma-semente",
"jti" => bin2hex(random_bytes(16)),
"iat" => $now,
"nbf" => $now,
"exp" => $now + (5 * 60),
];
$jwt = JWT::encode($payload, $secret, "HS256");