Múltiplos esquemas de autenticação no ASP.NET Core

Múltiplos esquemas de autenticação no ASP.NET Core

Os múltiplos esquemas de autenticação no ASP.NET Core permitem que uma mesma aplicação reconheça usuários por caminhos diferentes, cada um com seu próprio handler e suas próprias opções. Neste artigo, você vai entender como combinar vários cookies com OpenID Connect, escolher o esquema certo a cada requisição e encerrar a sessão corretamente.

O cenário de referência vem de um exemplo publicado por Damien Bowden, que precisou migrar uma aplicação grande para um novo provedor de identidade. Em vez de reescrever toda a autorização em torno de um único cookie, ele manteve vários esquemas e passou a definir o esquema correto logo após a autenticação.

O que são múltiplos esquemas de autenticação

No ASP.NET Core, um esquema de autenticação é um nome associado a um handler e às opções de configuração daquela instância. Em outras palavras, quando você chama AddCookie ou AddOpenIdConnect, registra um componente que sabe autenticar, desafiar e negar o acesso de um usuário.

Por isso, a plataforma aceita múltiplos esquemas de autenticação na mesma aplicação, desde que cada um tenha um nome único. A combinação mais comum une cookie para a interface web e JWT Bearer para as chamadas de API.

A própria documentação da Microsoft cita outro exemplo interessante. Nele, a aplicação mantém dois handlers de cookie, um com a identidade básica e outro criado quando o usuário passa por uma autenticação multifator.

Além disso, existe um comportamento que muda bastante o jogo desde o .NET 7. Quando apenas um esquema existe, o framework o adota como padrão automaticamente.

Por outro lado, ao registrar múltiplos esquemas de autenticação, o ASP.NET Core não escolhe nenhum padrão por conta própria. Assim, você precisa informar explicitamente qual esquema atende cada operação.

Se os conceitos de identidade e permissão ainda parecem confusos, vale revisar o artigo o que é autenticação e autorização antes de seguir. A visão geral da autenticação no ASP.NET Core também detalha handlers, desafios e proibições.

Quando faz sentido usar mais de um esquema

O caso de Damien Bowden ilustra bem a motivação. A aplicação original conversava com vários servidores OpenID Connect, e o objetivo da migração era reduzir tudo a um único provedor.

Contudo, os controllers já estavam amarrados a nomes de esquemas específicos. Migrar tudo para um cookie padrão exigiria revisar cada regra de autorização, o que tornaria o projeto lento e arriscado.

Então, a saída mais rápida foi preservar os esquemas existentes e decidir, após o login, em qual deles o usuário entra. Na prática, o provedor de identidade passou a ser único, enquanto a aplicação continuou enxergando sessões distintas.

A interface usa o padrão BFF, com Angular no front-end e ASP.NET Core no back-end. Dessa forma, o navegador nunca manipula tokens, porque a sessão vive em cookies HTTP only controlados pelo servidor.

Outro cenário frequente é a federação de identidade. Nesse caso, o servidor central delega o login a provedores externos, e uma claim como idp indica de onde o usuário veio.

Em ambos os casos, os múltiplos esquemas de autenticação funcionam como uma camada de compatibilidade. Eles permitem que a aplicação evolua por etapas, sem uma reescrita completa das regras de acesso.

Ainda assim, antes de adotar múltiplos esquemas de autenticação, documente por que cada esquema existe e quais rotas dependem dele. Esse registro simples evita que a solução temporária se torne permanente sem que ninguém perceba.

Como configurar múltiplos esquemas de autenticação com AddPolicyScheme

O ponto central da solução é o método AddPolicyScheme. Ele cria um esquema lógico que não autentica nada sozinho, mas encaminha cada operação para outro esquema real.

Primeiro, a aplicação define esse esquema de política como padrão. Em seguida, configura o OpenID Connect como esquema de desafio e de saída.

builder.Services.AddAuthentication(options =>
{
    // O esquema de política decide qual cookie usar em cada requisição
    options.DefaultScheme = "Dynamic";
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    options.DefaultSignOutScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddPolicyScheme("Dynamic", "Select SchemeOne or SchemeTwo", options =>
{
    options.ForwardDefaultSelector = context =>
    {
        if (context.Request.Cookies.ContainsKey("__Host-Http-one"))
            return "SchemeOne";

        if (context.Request.Cookies.ContainsKey("__Host-Http-two"))
            return "SchemeTwo";

        // Valor de fallback quando nenhum cookie existe
        return "SchemeOne";
    };
})
.AddCookie("SchemeOne", options =>
{
    options.Cookie.Name = "__Host-Http-one";
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.EventsType = typeof(CookieEventHandler);
})
.AddCookie("SchemeTwo", options =>
{
    options.Cookie.Name = "__Host-Http-two";
    options.Cookie.SameSite = SameSiteMode.Lax;
    options.EventsType = typeof(CookieEventHandler);
})
.AddCookie("SchemeOidcAuth");

Com isso, a aplicação ganha três esquemas de cookie e um esquema de política que escolhe entre eles, formando um conjunto de múltiplos esquemas de autenticação sob uma única entrada. Mesmo com várias opções disponíveis, apenas um cookie participa da autenticação em cada requisição HTTP.

Como o ForwardDefaultSelector escolhe o esquema

O ForwardDefaultSelector recebe o HttpContext e devolve o nome do esquema de destino. Nesse exemplo, a decisão depende apenas da presença de um cookie específico.

O framework segue uma ordem bem definida ao encaminhar operações. Primeiro, verifica as opções mais específicas, como ForwardAuthenticate, ForwardChallenge e ForwardSignOut, depois o ForwardDefaultSelector e, por fim, o ForwardDefault.

Ou seja, o primeiro resultado não nulo vence. Por isso, o seletor precisa sempre devolver um valor coerente, inclusive no caso de fallback.

Na prática, o fallback para SchemeOne só importa quando o usuário ainda não tem sessão. Nesse caso, a autenticação falha de forma limpa e o desafio redireciona o usuário para o login.

Em aplicações com múltiplos esquemas de autenticação, esse seletor vira o ponto mais sensível da configuração, já que toda requisição passa por ele. Por isso, mantenha a lógica curta, sem acesso a banco de dados ou chamadas externas.

Além da presença de cookies, o seletor pode considerar o caminho da URL ou o cabeçalho Authorization. A documentação oficial mostra, por exemplo, como enviar rotas iniciadas por /api para um esquema de API enquanto o restante usa cookies.

Protegendo controllers e endpoints com um esquema específico

Com os esquemas registrados, cada controller declara qual deles aceita. O atributo Authorize recebe a propriedade AuthenticationSchemes, que funciona mesmo quando existe um esquema padrão.

[ValidateAntiForgeryToken]
[Authorize(AuthenticationSchemes = "SchemeOne")]
[ApiController]
[Route("api/[controller]")]
public class SchemeOneController : ControllerBase
{
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new List<string> { "some data scheme 1", "more data", "loads of data" };
    }
}

Nesse caso, somente usuários autenticados pelo SchemeOne acessam a rota. Quando você informa uma lista separada por vírgulas, o middleware de autorização aceita qualquer um dos esquemas listados.

Se dois esquemas autenticarem o usuário ao mesmo tempo, o ASP.NET Core mescla as identidades em um único ClaimsPrincipal. Embora isso seja útil em alguns cenários, também pode gerar claims duplicadas e surpresas na autorização.

É justamente essa declaração explícita que dá sentido aos múltiplos esquemas de autenticação, pois cada rota deixa claro qual sessão aceita.

Também vale lembrar que esquemas de atributos e de políticas se somam. Em outras palavras, o endpoint passa a aceitar a união de todos os esquemas informados, e não a interseção.

Minimal APIs e políticas de autorização

O mesmo controle funciona em Minimal APIs. Você pode usar o atributo diretamente no delegate ou configurar a política com RequireAuthorization.

app.MapGet("/api/reports", () => Results.Ok(new[] { "report one", "report two" }))
    .RequireAuthorization(policy => policy
        // Aceita usuários de qualquer um dos dois esquemas de cookie
        .AddAuthenticationSchemes("SchemeOne", "SchemeTwo")
        .RequireAuthenticatedUser());

Além disso, políticas nomeadas deixam as regras mais legíveis quando o projeto cresce. Dessa forma, você declara os esquemas uma única vez no AddAuthorizationBuilder e reutiliza o nome da política em todos os endpoints.

Em projetos com múltiplos esquemas de autenticação, essa centralização reduz o risco de um endpoint aceitar um esquema por engano. Além disso, uma revisão de código fica mais rápida, porque basta conferir o nome da política.

Integrando o OpenID Connect com múltiplos esquemas de autenticação

O login acontece via OpenID Connect, com o Duende IdentityServer atuando como servidor de identidade. A configuração usa o fluxo de código com PKCE, Pushed Authorization Requests e client assertion.

.AddOpenIdConnect(options =>
{
    // A identidade validada vai primeiro para o esquema temporário
    options.SignInScheme = "SchemeOidcAuth";

    options.Events = OidcEventHandlers.OidcEvents(builder.Configuration);

    options.ClientId = builder.Configuration["WebOidcClientId"];
    options.Authority = builder.Configuration["WebOidcAuthority"];
    options.ResponseType = OpenIdConnectResponseType.Code;

    // A client assertion substitui o client secret e é definida nos eventos
    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;
    options.MapInboundClaims = false;

    options.ClaimActions.MapUniqueJsonKey("loa", "loa");
    options.ClaimActions.MapUniqueJsonKey("loi", "loi");
    options.ClaimActions.MapUniqueJsonKey(JwtClaimTypes.Email, JwtClaimTypes.Email);

    options.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Require;

    options.Scope.Add("scope2");
    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "name"
    };
});

A propriedade SignInScheme aponta para o esquema temporário. Por isso, o handler não grava a sessão diretamente em SchemeOne ou SchemeTwo, e a decisão fica para um evento posterior.

Com MapInboundClaims desligado, as claims chegam com os nomes originais do token. Já GetClaimsFromUserInfoEndpoint busca dados extras, e o MapUniqueJsonKey garante que claims como loa e loi cheguem ao principal.

PKCE, PAR e client assertion

O PKCE protege o fluxo de código contra a interceptação do código de autorização. Ele é praticamente obrigatório em qualquer aplicação moderna, mesmo em clientes confidenciais.

O PAR, definido na RFC 9126, move os parâmetros de autorização do navegador para uma chamada direta entre servidores. Assim, um atacante no navegador não consegue ler nem alterar esses parâmetros.

Desde o .NET 9, o handler do OpenID Connect usa PAR automaticamente quando o documento de descoberta do provedor anuncia suporte. O valor Require vai além e faz a autenticação falhar caso o endpoint não exista.

Contudo, o Microsoft Entra não oferece suporte a PAR. Portanto, essa configuração faz sentido com provedores como Duende IdentityServer, Keycloak ou Curity.

A client assertion, por sua vez, autentica a aplicação com um JWT assinado em vez de um segredo compartilhado. Com isso, nenhum segredo estático trafega entre a aplicação e o servidor de identidade.

Esses mecanismos valem para qualquer aplicação, mas ganham peso extra quando múltiplos esquemas de autenticação dependem do mesmo provedor. Afinal, uma falha no login comprometeria todas as sessões de uma só vez.

Escolhendo o esquema no evento OnTokenValidated

A decisão sobre o esquema definitivo acontece no evento OnTokenValidated. Nesse momento, o token já passou pela validação e as claims estão disponíveis.

Esse evento é a peça que conecta o provedor único aos múltiplos esquemas de autenticação da aplicação.

private static async Task OnTokenValidatedHandler(
    TokenValidatedContext context,
    IConfiguration configuration)
{
    if (context.Principal is null)
        return;

    // A claim loa define o nível de garantia da autenticação
    var loa = context.Principal.FindFirst("loa")?.Value;
    var targetScheme = loa == "loa.400" ? "SchemeOne" : "SchemeTwo";

    await context.HttpContext.SignInAsync(targetScheme, context.Principal, context.Properties);
    await context.HttpContext.SignOutAsync("SchemeOidcAuth");
}

Nesse exemplo, a claim loa decide o destino do usuário. Quem autenticou com o nível loa.400 entra em SchemeOne, enquanto os demais entram em SchemeTwo.

Em um cenário de federação, a claim idp cumpre o mesmo papel. Porém, em produção você normalmente precisa normalizar as claims entre provedores, porque cada um pode usar nomes e formatos diferentes.

Um ponto que merece teste é o ciclo de vida do cookie temporário. O handler remoto conclui o próprio sign-in no SignInScheme depois dos eventos, então vale conferir no navegador se o cookie de SchemeOidcAuth realmente desaparece.

Logout com múltiplos esquemas de autenticação

Encerrar a sessão exige atenção especial quando existem múltiplos esquemas de autenticação. Afinal, a aplicação precisa remover o cookie local e também avisar o provedor de identidade.

A implementação original encerra os dois cookies e o OpenID Connect em uma única chamada.

[IgnoreAntiforgeryToken]
[Authorize(AuthenticationSchemes = "SchemeOne,SchemeTwo")]
[HttpPost("Logout")]
public IActionResult Logout()
{
    return SignOut(
        new AuthenticationProperties { RedirectUri = "/" },
        "SchemeOne",
        "SchemeTwo",
        OpenIdConnectDefaults.AuthenticationScheme);
}

Essa abordagem funciona, porque o SignOut aceita vários esquemas ao mesmo tempo. No entanto, o próprio autor deixou um comentário indicando que a escolha do cookie a encerrar ainda poderia melhorar.

Uma alternativa simples é reaproveitar a mesma lógica do seletor e encerrar apenas o esquema ativo.

[IgnoreAntiforgeryToken]
[Authorize(AuthenticationSchemes = "SchemeOne,SchemeTwo")]
[HttpPost("Logout")]
public IActionResult Logout()
{
    // Encerra somente o esquema de cookie presente na requisição
    var cookieScheme = Request.Cookies.ContainsKey("__Host-Http-one")
        ? "SchemeOne"
        : "SchemeTwo";

    return SignOut(
        new AuthenticationProperties { RedirectUri = "/" },
        cookieScheme,
        OpenIdConnectDefaults.AuthenticationScheme);
}

Dessa forma, o código deixa explícita a intenção e evita chamadas desnecessárias. Ainda assim, encerrar todos os cookies continua sendo uma opção segura quando você prefere garantir uma limpeza completa.

Em qualquer abordagem, o redirecionamento para o endpoint de saída do provedor é essencial. Sem ele, o usuário continuaria autenticado no servidor de identidade e voltaria a entrar sem digitar credenciais no próximo acesso.

Cuidados ao usar múltiplos esquemas de autenticação em produção

O primeiro cuidado é evitar strings soltas pelo código. Um erro de digitação no nome de um esquema gera falhas silenciosas de autorização, que costumam ser difíceis de rastrear.

public static class AuthSchemes
{
    public const string Dynamic = "Dynamic";
    public const string SchemeOne = "SchemeOne";
    public const string SchemeTwo = "SchemeTwo";
    public const string OidcTemporary = "SchemeOidcAuth";
}

Em seguida, revise a lógica de fallback do seletor. Um fallback mal escolhido pode direcionar a requisição para o handler errado e mascarar problemas de sessão.

Também é importante cobrir os fluxos com testes de integração. Por exemplo, valide o login em cada esquema, o acesso cruzado entre controllers e o logout completo.

Outro ponto é o tamanho dos cookies. Com SaveTokens ativo, os tokens ficam dentro do cookie, que pode crescer rapidamente e exigir fragmentação.

Por fim, mantenha a proteção contra CSRF em todas as rotas que alteram estado. Em uma arquitetura BFF, os cookies acompanham cada requisição automaticamente, então o antiforgery token continua indispensável.

Com múltiplos esquemas de autenticação, os logs também precisam registrar qual esquema autenticou cada requisição. Sem essa informação, investigar um acesso negado vira um exercício de adivinhação.

Uma forma simples de fazer isso é ler a propriedade AuthenticationType da identidade no middleware de logging. Em seguida, inclua esse valor no escopo do log para correlacionar requisições, usuários e esquemas.

Quando evitar múltiplos esquemas de autenticação

O próprio Damien Bowden deixa claro que esse arranjo não é o caminho ideal para projetos novos. Um único esquema padrão costuma ser mais simples de entender, testar e manter.

Portanto, use múltiplos esquemas de autenticação quando houver uma restrição real, como uma migração em andamento ou dependências legadas. Fora desses casos, prefira representar as diferenças entre usuários com claims e políticas de autorização.

Com essa estratégia, você mantém um único cookie e decide o acesso com base no conteúdo da identidade. Assim, o pipeline fica mais previsível e a equipe gasta menos tempo depurando encaminhamentos entre handlers.

Conclusão

Os múltiplos esquemas de autenticação no ASP.NET Core oferecem uma saída pragmática para migrações complexas. Com AddPolicyScheme, a aplicação escolhe o cookie certo a cada requisição sem reescrever a autorização existente.

Além disso, o evento OnTokenValidated permite decidir o esquema definitivo com base nas claims do token. Somado a PKCE, PAR e client assertion, esse fluxo mantém um bom nível de segurança.

No entanto, a flexibilidade cobra seu preço em complexidade. Por isso, trate esse modelo como uma ponte durante a transição e planeje, sempre que possível, a convergência para um único esquema padrão.

FAQ

O que é um esquema de autenticação no ASP.NET Core?

É um nome que identifica um handler de autenticação e suas opções. Ele define como a aplicação autentica, desafia e nega o acesso de um usuário.

Para que serve o AddPolicyScheme?

Ele cria um esquema lógico que encaminha as operações para outros esquemas. A escolha pode acontecer dinamicamente em cada requisição com o ForwardDefaultSelector.

Posso aceitar dois esquemas no mesmo endpoint?

Sim, basta informar os nomes separados por vírgula no atributo Authorize ou na política. O endpoint aceita qualquer um dos esquemas listados.

O Microsoft Entra suporta Pushed Authorization Requests?

Não, o Entra não oferece suporte a PAR. Nesse caso, use o valor padrão UseIfAvailable ou desative o recurso.

Devo usar múltiplos esquemas em um projeto novo?

Normalmente não. Um único esquema padrão com claims e políticas de autorização resolve a maioria dos casos com menos complexidade.