Use várias Autenticação de Portador JWT

86

É possível oferecer suporte a vários emissores de token JWT no ASP.NET Core 2? Quero fornecer uma API para serviço externo e preciso usar duas fontes de tokens JWT - Firebase e emissores de tokens JWT personalizados. No ASP.NET core, posso definir a autenticação JWT para o esquema de autenticação do Bearer, mas apenas para uma autoridade:

  services
        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.Authority = "https://securetoken.google.com/my-firebase-project"
            options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidIssuer = "my-firebase-project"
                    ValidateAudience = true,
                    ValidAudience = "my-firebase-project"
                    ValidateLifetime = true
                };
        }

Posso ter vários emissores e públicos, mas não consigo definir várias autoridades.

Sane
fonte
1
AFAIK você pode adicionar qualquer número de propriedades a um JWT. Portanto, não há nada que impeça você de registrar dois nomes de emissor em um JWT. O problema é que seu aplicativo precisaria conhecer as duas chaves, se cada emissor estivesse usando uma chave diferente para assinar.
Tim Biegeleisen,

Respostas:

185

Você pode alcançar totalmente o que deseja:

services
    .AddAuthentication()
    .AddJwtBearer("Firebase", options =>
    {
        options.Authority = "https://securetoken.google.com/my-firebase-project"
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "my-firebase-project"
            ValidateAudience = true,
            ValidAudience = "my-firebase-project"
            ValidateLifetime = true
        };
    })
    .AddJwtBearer("Custom", options =>
    {
        // Configuration for your custom
        // JWT tokens here
    });

services
    .AddAuthorization(options =>
    {
        options.DefaultPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes("Firebase", "Custom")
            .Build();
    });

Vamos examinar as diferenças entre o seu código e aquele.

AddAuthentication não tem parâmetro

Se você definir um esquema de autenticação padrão, a cada solicitação, o middleware de autenticação tentará executar o manipulador de autenticação associado ao esquema de autenticação padrão. Já que agora temos dois esquemas de autenticação acessíveis, não há sentido em executar um deles.

Use outra sobrecarga de AddJwtBearer

Cada AddXXXmétodo para adicionar uma autenticação tem várias sobrecargas:

  • Aquele em que o esquema de autenticação padrão associado ao método de autenticação é usado, como você pode ver aqui para autenticação de cookies
  • Aquele onde você passa, além da configuração das opções, o nome do esquema de autenticação, como nesta sobrecarga

Agora, como você usa o mesmo método de autenticação duas vezes, mas os esquemas de autenticação devem ser exclusivos, você precisa usar a segunda sobrecarga.

Atualize a política padrão

Como as solicitações não serão mais autenticadas automaticamente, colocar [Authorize]atributos em algumas ações resultará na rejeição das solicitações e na HTTP 401emissão de um.

Como não queremos isso porque queremos dar aos manipuladores de autenticação a chance de autenticar a solicitação, alteramos a política padrão do sistema de autorização indicando que os esquemas de autenticação Firebasee Customdevem ser tentados para autenticar a solicitação.

Isso não o impede de ser mais restritivo em algumas ações; o [Authorize]atributo tem uma AuthenticationSchemespropriedade que permite substituir quais esquemas de autenticação são válidos.

Se você tiver cenários mais complexos, pode usar a autorização baseada em políticas . Acho que a documentação oficial é ótima.

Vamos imaginar que algumas ações estão disponíveis apenas para tokens JWT emitidos pelo Firebase e devem ter uma declaração com um valor específico; você poderia fazer desta forma:

// Authentication code omitted for brevity

services
    .AddAuthorization(options =>
    {
        options.DefaultPolicy = new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes("Firebase", "Custom")
            .Build();

        options.AddPolicy("FirebaseAdministrators", new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes("Firebase")
            .RequireClaim("role", "admin")
            .Build());
    });

Você pode então usar [Authorize(Policy = "FirebaseAdministrators")]em algumas ações.

Um último ponto a ser observado: se você estiver capturando AuthenticationFailedeventos e usando qualquer coisa que não seja a primeira AddJwtBearerpolítica, verá que IDX10501: Signature validation failed. Unable to match key...isso é causado pelo sistema verificando cada um AddJwtBearerpor vez até obter uma correspondência. O erro geralmente pode ser ignorado.

Mickaël Derriey
fonte
4
Isso exige que o valor do cabeçalho seja alterado do firebase ou da solução personalizada? ou seja, em vez de Authorization : Bearer <token>que o cabeçalho seja Authorization : Firebase <token>por exemplo? Quando tentei esta solução, obtive o erro: "Nenhum manipulador de autenticação está registrado para o esquema 'Portador'."
Rush Frisby
4
Não, os cabeçalhos não precisam ser alterados. A mensagem de erro sugere que você está se referindo a um esquema de autenticação inexistente (Portador). Em nossos exemplos, os dois esquemas registrados são Firebase e Custom, que são os primeiros argumentos das .AddJwtBearerchamadas de método.
Mickaël Derriey
5
Oi. Estava procurando justamente por essa solução. Infelizmente, estou recebendo uma exceção "Nenhum authenticationScheme foi especificado e nenhum DefaultChallengeScheme encontrado". options.DefaultPolicy está definido como ok. Alguma ideia?
terjetyl
11
Esta foi uma resposta extremamente útil, e juntou muito do que vi em pedaços por todo o lugar.
Aron W.
2
@TylerOhlsen não está correto; embora seja usado no caso que você descreve, não é o único. Também será usado se você não especificar um requisito de autorização no nível do terminal, mas decorar controladores MVC e / ou ações com um [Authorize]atributo vazio .
Mickaël Derriey
3

Esta é uma extensão da resposta de Mickaël Derriey.

Nosso aplicativo tem um requisito de autorização personalizada que resolvemos de uma fonte interna. Estávamos usando Auth0, mas estamos mudando para a autenticação de conta da Microsoft usando OpenID. Aqui está o código ligeiramente editado de nossa inicialização ASP.Net Core 2.1. Para futuros leitores, isso funciona até o momento desta redação para as versões especificadas. O chamador usa o id_token do OpenID em solicitações de entrada passadas como um token do portador. Espero que ajude alguém tentando fazer uma conversão de autoridade de identidade tanto quanto esta pergunta e resposta me ajudou.

const string Auth0 = nameof(Auth0);
const string MsaOpenId = nameof(MsaOpenId);

string domain = "https://myAuth0App.auth0.com/";
services.AddAuthentication()
        .AddJwtBearer(Auth0, options =>
            {
                options.Authority = domain;
                options.Audience = "https://myAuth0Audience.com";
            })
        .AddJwtBearer(MsaOpenId, options =>
            {
                options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
                {
                    ValidateAudience = true,
                    ValidAudience = "00000000-0000-0000-0000-000000000000",

                    ValidateIssuer = true,
                    ValidIssuer = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0",

                    ValidateIssuerSigningKey = true,
                    RequireExpirationTime = true,
                    ValidateLifetime = true,
                    RequireSignedTokens = true,
                    ClockSkew = TimeSpan.FromMinutes(10),
                };
                options.MetadataAddress = "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0/.well-known/openid-configuration";
            }
        );

services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes( Auth0, MsaOpenId )
        .Build();

    var approvedPolicyBuilder =  new AuthorizationPolicyBuilder()
           .RequireAuthenticatedUser()
           .AddAuthenticationSchemes(Auth0, MsaOpenId)
           ;

    approvedPolicyBuilder.Requirements.Add(new HasApprovedRequirement(domain));

    options.AddPolicy("approved", approvedPolicyBuilder.Build());
});
Sem Reembolsos Sem Devoluções
fonte