← Início
Plataforma Semente — Integração SSO
Versão 1.0

Integração SSO — Especificação Técnica

Plataforma Semente

Versão 1.0 — Documento destinado a parceiros integradores

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

  1. O usuário se autentica normalmente no sistema do parceiro.
  2. Em algum momento posterior, o usuário clica em uma opção do tipo "Acessar Plataforma Semente" dentro do sistema do parceiro.
  3. Esse clique aciona o sistema do parceiro a gerar um token JWT assinado, identificando o usuário.
  4. 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).
  5. A Semente valida o token.
  6. 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.
Importante: Toda a lógica de geração e assinatura do token deve ocorrer no back-end do parceiro. O secret de assinatura nunca deve ser exposto no front-end, em aplicativos móveis ou em qualquer código acessível ao usuário final.
A chamada POST deve partir do navegador do usuário, nunca do back-end do parceiro: A resposta da Semente é um redirecionamento (302) com um cookie de sessão (Set-Cookie) endereçado diretamente ao navegador — não um token reutilizável no corpo da resposta. Por isso, a requisição a /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:

Usuário Sistema do Parceiro Plataforma Semente 1. Login no sistema do parceiro 2. Clica em "Acessar Plataforma Semente" 3. Gera JWT (assinado com o secret HS256) 4. Entrega formulário (auto-submit) com o JWT 5. POST /sso_custom — enviado pelo navegador token=<JWT> (form-urlencoded) 6. Validações • Assinatura (secret) • Audience / Issuer • Expiração (exp) • Reuso do jti (replay) 7. Autentica e define cookie de sessão (Set-Cookie) 8. 302 Found + Set-Cookie — redireciona o navegador 9. Usuário navega na Plataforma Semente, já autenticado Se alguma validação do passo 6 falhar: a Semente responde com erro (401 ou 400) diretamente ao navegador do usuário — não ao parceiro. Nenhum usuário é autenticado e o fluxo é interrompido ali.

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).
Atenção: O secret deve ser armazenado de forma segura (cofre de segredos, variável de ambiente protegida, etc.) e nunca deve ser versionado em repositórios de código.

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

ClaimObrigatórioDescrição
subSimLogin do usuário na Plataforma Semente (o mesmo identificador já usado hoje no login direto).
issSimClient ID do parceiro, fornecido pela Semente no momento do provisionamento.
audSimValor fixo "plataforma-semente". Deve ser copiado literalmente (case-sensitive).
jtiSimIdentificador único do token (UUID v4). Usado para impedir reuso do mesmo token.
iatSimData/hora de emissão do token (UTC).
nbfSimData/hora a partir da qual o token é válido (UTC). Deve ser igual a iat.
expSimData/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 iat e exp.
  • Cada token deve conter um jti único (recomenda-se UUID v4). Um token com o mesmo jti nã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:

Endpoint
POST https://integracao.sementeeducacao.com.br/api/v0/accounts/sso_custom
Essa requisição precisa partir do navegador do usuário final, não do back-end do parceiro. Veja o motivo na seção 1.2.

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.

Requisição
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:

302 Found
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:

401 Unauthorized
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "mensagem": "Token expirado ou já utilizado"
}

Códigos de retorno possíveis:

HTTPSituaçãoDescrição
302SucessoToken válido. Usuário autenticado e redirecionado para a Plataforma Semente com sessão já ativa.
401Não autorizadoAssinatura inválida, client_id desconhecido, token expirado, ou token já utilizado (replay).
400Requisição inválidaCorpo 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:

redirecionamento.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="SEU_JWT_GERADO_AQUI">
    <noscript>
      <button type="submit">Clique aqui para continuar</button>
    </noscript>
  </form>

  <script>
    document.getElementById('sso-form').submit();
  </script>
</body>
</html>
Atenção ao tratamento de erro: como o formulário provoca uma navegação real, se a Semente responder 400/401 o navegador é levado para uma página exibindo o JSON de erro cru — o JavaScript desta página intermediária não tem como interceptar essa resposta antes da navegação acontecer. Vale planejar como isso deve se comportar (por exemplo, uma página de erro amigável do lado da Semente, ou uma orientação própria do parceiro sobre o que o usuário deve fazer se cair numa tela assim).

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.

Escape o token ao embuti-lo no HTML: Hoje o alfabeto de um JWT (base64url + pontos) já é seguro dentro de um atributo HTML, mas escapar o valor antes de interpolá-lo é uma boa prática defensiva de baixo custo — os exemplos abaixo fazem isso com o utilitário padrão de cada linguagem.

Exemplo com ASP.NET Core (minimal API):

C#
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:

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

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

Node.js
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
<?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).

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

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

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

Node.js
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
<?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");

6. Checklist de Implementação

7. Perguntas Frequentes

A segunda tentativa será rejeitada com erro 401, mesmo que o token ainda esteja dentro da janela de validade de 5 minutos. Cada token só pode ser usado uma vez.
Não. A janela de 5 minutos é fixa e faz parte da especificação de segurança da integração.
Tokens podem ser rejeitados por parecerem expirados ou ainda não válidos, mesmo tendo sido gerados corretamente. Recomendamos manter o servidor sincronizado via NTP.
Não é recomendado. Cada integração deve ter seu próprio par de credenciais, fornecido pela Semente conforme a necessidade.
Entre em contato com o time técnico da Semente através do canal combinado no momento do provisionamento das credenciais.