ID token diz quem entrou, não o que a pessoa pode fazer no seu sistema
Separe identidade de permissão no login com OpenID Connect: valide o ID token, ligue o sujeito a uma conta local e guarde papéis numa tabela sua, com convite explícito.
O sistema de chamados ganhou "Entrar com Microsoft" numa tarde. Na manhã seguinte, um estagiário que nunca tinha sido cadastrado abriu o painel de administração, porque a regra era "quem tem e-mail @empresa.com.br é admin". A resposta curta: o ID token prova que o provedor reconheceu a pessoa; ele não diz o que ela pode fazer no seu sistema. Valide o token, use o par emissor e sujeito para achar uma conta local e leia o papel de uma tabela que só você altera.
Este guia mostra a separação na prática, com ASP.NET Core e o mesmo sistema de chamados como exemplo: o que conferir no token, como ligar a pessoa à conta, onde o papel mora e como convidar alguém sem promover por domínio.
O que o OpenID Connect resolve e o que deixa para você
O OpenID Connect é uma camada de identidade sobre o OAuth 2.0. No fim do login, o provedor entrega ao seu aplicativo um ID token: um JWT assinado com afirmações sobre a autenticação. As principais são iss (quem emitiu), sub (o identificador da pessoa naquele emissor), aud (para qual cliente o token foi feito), exp (até quando vale) e, se você mandou, nonce.
Repare no que falta: nada ali diz "pode apagar chamado" ou "é supervisor do suporte". O provedor sabe quem a pessoa é na conta dele. Quem decide o que ela faz no seu sistema de chamados é o seu sistema. Identidade e autorização são duas portas seguidas: a primeira abre o caminho para a segunda, mas não a substitui.
Há também uma confusão de tipo de token. O ID token é feito para o cliente que pediu o login ler e confiar. Para chamar uma API, o que se manda é o access token, emitido com a audiência daquela API. Mandar o ID token como credencial para a sua API é usar um documento feito para outro destinatário.
Como validar e ligar à conta local
- Valide o ID token no servidor: assinatura com a chave publicada pelo emissor,
issigual ao esperado,audcontendo o seu client id,expno futuro enonceigual ao que você gerou. - Monte a chave da identidade com o par
iss+sub. É estável e único; e-mail não é. - Procure a conta local por esse par. Se não existir, a pessoa não tem papel nenhum, mesmo autenticada.
- Leia papéis e permissões da sua tabela e coloque no usuário da sessão.
- Proteja rotas e ações com políticas que checam esses papéis, nunca o domínio do e-mail.
- Dê papel por convite explícito, gravado por alguém que já tem permissão para isso.
As tabelas do sistema de chamados
CREATE TABLE usuario (
id INT PRIMARY KEY,
nome VARCHAR(120) NOT NULL
);
CREATE TABLE identidade_externa (
emissor VARCHAR(200) NOT NULL,
sujeito VARCHAR(200) NOT NULL,
usuario_id INT NOT NULL REFERENCES usuario(id),
PRIMARY KEY (emissor, sujeito)
);
CREATE TABLE papel_usuario (
usuario_id INT NOT NULL REFERENCES usuario(id),
papel VARCHAR(40) NOT NULL,
concedido_por INT NOT NULL REFERENCES usuario(id),
concedido_em TIMESTAMP NOT NULL,
PRIMARY KEY (usuario_id, papel)
);
A coluna concedido_por responde à pergunta que a auditoria vai fazer um dia: quem deu esse acesso? Com a regra de domínio, a resposta seria "ninguém, aconteceu".
Ligando no ASP.NET Core
O handler AddOpenIdConnect já valida assinatura, emissor, audiência, prazo e nonce quando configurado com a Authority do provedor. O ponto de encaixe para a sua parte é o evento OnTokenValidated, que roda depois da validação e antes do cookie de sessão ser criado.
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration["Oidc:Authority"];
options.ClientId = builder.Configuration["Oidc:ClientId"];
options.ClientSecret = builder.Configuration["Oidc:ClientSecret"];
options.ResponseType = "code";
options.MapInboundClaims = false;
options.Events.OnTokenValidated = async ctx =>
{
var principal = ctx.Principal!;
var claimSub = principal.FindFirst("sub");
var sujeito = claimSub?.Value;
var emissor = claimSub?.Issuer;
if (emissor is null || sujeito is null)
{
ctx.Fail("Token sem iss ou sub.");
return;
}
var contas = ctx.HttpContext.RequestServices.GetRequiredService<IContas>();
var conta = await contas.BuscarPorIdentidadeAsync(emissor, sujeito);
if (conta is null) return;
var identidade = (ClaimsIdentity)principal.Identity!;
identidade.AddClaim(new Claim("usuario_id", conta.Id.ToString()));
foreach (var papel in conta.Papeis)
identidade.AddClaim(new Claim("papel", papel));
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Admin", p => p.RequireClaim("papel", "admin"));
});
O segredo do cliente vem da configuração, não do código; em produção ele fica num cofre ou variável de ambiente. A interface IContas é sua e consulta as duas tabelas acima. Na rota do painel, [Authorize(Policy = "Admin")] faz o resto. O estagiário autenticado chega até o login, mas sem linha em papel_usuario a política recusa com 403. Se você usa um SPA, o fluxo de login muda (veja Authorization Code com PKCE), mas a separação entre identidade e papel é a mesma.
Por que o domínio do e-mail não serve como permissão
A regra "e-mail da empresa é admin" falha de vários jeitos. Primeiro, ela promove todo mundo do domínio: estagiário, terceirizado, a conta compartilhada do financeiro. Segundo, nem todo provedor garante que o e-mail foi verificado; em alguns, a pessoa escolhe o endereço que aparece no perfil. Terceiro, e-mail muda: alguém sai da empresa, o endereço é reaproveitado, e o novo dono herda o acesso do antigo. O sub não tem esse problema dentro do mesmo emissor.
O domínio pode ser útil como filtro de quem pode pedir acesso. Nunca como concessão automática de papel.
Convite explícito
No sistema de chamados, um admin cria um convite com o e-mail e o papel. Quando a pessoa entra pela primeira vez, o sistema confere que o e-mail do token bate com o convite (e que o provedor marcou o e-mail como verificado, se ele expõe essa informação), cria a linha em identidade_externa com iss e sub e grava o papel com concedido_por preenchido. A partir daí, o e-mail não importa mais para a autorização. Só o par emissor e sujeito.
Erros comuns
- Dar papel por domínio de e-mail. Qualquer conta nova do domínio vira admin no primeiro login, sem ninguém ter decidido isso.
- Usar o e-mail como chave da conta. Quando o endereço muda ou é reaproveitado, a conta vai para outra pessoa.
- Mandar o ID token para a API como se fosse access token. A audiência é o cliente, não a API, e a API acaba aceitando um token que não foi emitido para ela.
- Decodificar o token no navegador e decidir o menu por ele. Esconder o botão não é proteger a rota; a checagem precisa estar no servidor.
- Confiar numa claim de grupo sem saber quem a controla. Se o grupo é editável por qualquer um no provedor, a permissão também é.
- Não registrar quem concedeu o papel. Na hora de investigar um acesso indevido, não há trilha.
Checklist
- O servidor valida assinatura,
iss,aud,expenoncede todo ID token. - A conta local é encontrada pelo par
iss+sub, não pelo e-mail. - Uma conta nova do provedor, sem convite, entra sem nenhum papel.
- Papéis ficam numa tabela sua, com quem concedeu e quando.
- Rotas administrativas usam política de autorização no servidor.
- A API aceita apenas access tokens emitidos para ela.
- O segredo do cliente não está no código nem no repositório.
Se o assunto é prazo e revogação do token, o guia de JWT que expira complementa este. Para o caso específico do Google, veja conferir o token do Google no servidor. Agora faça o teste que importa: crie uma conta nova no seu provedor de login, entre no seu sistema com ela e abra a página mais sensível. Ela chega lá? Se chega, onde está gravado o papel que deixou, e quem pode alterar essa linha?
Referência: https://openid.net/specs/openid-connect-core-1_0.html