Programação Por Equipe DevBras

Capa do artigo: ID token diz quem entrou, não o que a pessoa pode fazer no seu sistema

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

  1. Valide o ID token no servidor: assinatura com a chave publicada pelo emissor, iss igual ao esperado, aud contendo o seu client id, exp no futuro e nonce igual ao que você gerou.
  2. Monte a chave da identidade com o par iss + sub. É estável e único; e-mail não é.
  3. Procure a conta local por esse par. Se não existir, a pessoa não tem papel nenhum, mesmo autenticada.
  4. Leia papéis e permissões da sua tabela e coloque no usuário da sessão.
  5. Proteja rotas e ações com políticas que checam esses papéis, nunca o domínio do e-mail.
  6. 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, exp e nonce de 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?

Avalie este artigo

Referência: https://openid.net/specs/openid-connect-core-1_0.html