Autenticação JWT para API da Web do ASP.NET

264

Estou tentando oferecer suporte ao token de portador JWT (JSON Web Token) no meu aplicativo de API da Web e estou me perdendo.

Vejo suporte para o .NET Core e para aplicativos OWIN.
Atualmente, estou hospedando meu aplicativo no IIS.

Como posso obter esse módulo de autenticação no meu aplicativo? Existe alguma maneira de usar a <authentication>configuração semelhante à maneira como uso a autenticação de formulários / Windows?

Amir Popovich
fonte

Respostas:

611

Respondi a esta pergunta: Como proteger uma API da Web do ASP.NET há 4 anos usando o HMAC.

Agora, muitas coisas mudaram em segurança, especialmente o JWT está ficando popular. Aqui, tentarei explicar como usar o JWT da maneira mais simples e básica possível, para que não nos percam da selva de OWIN, Oauth2, ASP.NET Identity ... :).

Se você não conhece o token JWT, precisa dar uma olhada em:

https://tools.ietf.org/html/rfc7519

Basicamente, um token JWT se parece com:

<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>

Exemplo:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

Um token JWT possui três seções:

  1. Cabeçalho: formato JSON codificado em Base64
  2. Reivindicações: formato JSON codificado em Base64.
  3. Assinatura: criada e assinada com base no cabeçalho e nas reivindicações, codificado em Base64.

Se você usar o site jwt.io com o token acima, poderá decodificar o token e vê-lo como abaixo:

insira a descrição da imagem aqui

Tecnicamente, o JWT usa assinatura assinada a partir de cabeçalhos e declarações com o algoritmo de segurança especificado nos cabeçalhos (exemplo: HMACSHA256). Portanto, é necessário que o JWT seja transferido por HTTPs se você armazenar informações confidenciais nas declarações.

Agora, para usar a autenticação JWT, você realmente não precisa de um middleware OWIN se tiver um sistema Web Api herdado. O conceito simples é como fornecer o token JWT e como validar o token quando a solicitação chegar. É isso aí.

De volta à demonstração, para manter o token JWT leve, eu apenas armazeno usernameeexpiration time no JWT. Porém, dessa maneira, é necessário recriar a nova identidade local (principal) para adicionar mais informações como: functions .. se você deseja autorizar a função. Mas, se você deseja adicionar mais informações ao JWT, você decide: é muito flexível.

Em vez de usar o middleware OWIN, você pode simplesmente fornecer um ponto de extremidade do token JWT usando a ação do controlador:

public class TokenController : ApiController
{
    // This is naive endpoint for demo, it should use Basic authentication
    // to provide token or POST request
    [AllowAnonymous]
    public string Get(string username, string password)
    {
        if (CheckUser(username, password))
        {
            return JwtManager.GenerateToken(username);
        }

        throw new HttpResponseException(HttpStatusCode.Unauthorized);
    }

    public bool CheckUser(string username, string password)
    {
        // should check in the database
        return true;
    }
}

Esta é uma ação ingênua; na produção, você deve usar uma solicitação POST ou um terminal de autenticação básica para fornecer o token JWT.

Como gerar o token com base username?

Você pode usar o pacote NuGet chamado System.IdentityModel.Tokens.Jwtda Microsoft para gerar o token ou até outro pacote, se desejar. Na demonstração, eu uso HMACSHA256com SymmetricKey:

/// <summary>
/// Use the below code to generate symmetric Secret Key
///     var hmac = new HMACSHA256();
///     var key = Convert.ToBase64String(hmac.Key);
/// </summary>
private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";

public static string GenerateToken(string username, int expireMinutes = 20)
{
    var symmetricKey = Convert.FromBase64String(Secret);
    var tokenHandler = new JwtSecurityTokenHandler();

    var now = DateTime.UtcNow;
    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = new ClaimsIdentity(new[]
        {
            new Claim(ClaimTypes.Name, username)
        }),

        Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),

        SigningCredentials = new SigningCredentials(
            new SymmetricSecurityKey(symmetricKey), 
            SecurityAlgorithms.HmacSha256Signature)
    };

    var stoken = tokenHandler.CreateToken(tokenDescriptor);
    var token = tokenHandler.WriteToken(stoken);

    return token;
}

O terminal para fornecer o token JWT está pronto. Agora, como validar o JWT quando a solicitação chega? Na demonstração que construí, JwtAuthenticationAttributeque herda de IAuthenticationFilter(mais detalhes sobre o filtro de autenticação aqui ).

Com esse atributo, você pode autenticar qualquer ação: basta colocar esse atributo nessa ação.

public class ValueController : ApiController
{
    [JwtAuthentication]
    public string Get()
    {
        return "value";
    }
}

Você também pode usar o middleware OWIN ou o DelegateHander se desejar validar todas as solicitações recebidas pela sua WebAPI (não específica ao Controller ou ação)

Abaixo está o método principal do filtro de autenticação:

private static bool ValidateToken(string token, out string username)
{
    username = null;

    var simplePrinciple = JwtManager.GetPrincipal(token);
    var identity = simplePrinciple.Identity as ClaimsIdentity;

    if (identity == null)
        return false;

    if (!identity.IsAuthenticated)
        return false;

    var usernameClaim = identity.FindFirst(ClaimTypes.Name);
    username = usernameClaim?.Value;

    if (string.IsNullOrEmpty(username))
       return false;

    // More validate to check whether username exists in system

    return true;
}

protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
    string username;

    if (ValidateToken(token, out username))
    {
        // based on username to get more information from database 
        // in order to build local identity
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, username)
            // Add more claims if needed: Roles, ...
        };

        var identity = new ClaimsIdentity(claims, "Jwt");
        IPrincipal user = new ClaimsPrincipal(identity);

        return Task.FromResult(user);
    }

    return Task.FromResult<IPrincipal>(null);
}

O fluxo de trabalho é, usando a biblioteca JWT (pacote NuGet acima) para validar o token JWT e depois retornar ClaimsPrincipal. Você pode executar mais validações, como verificar se o usuário existe no seu sistema e adicionar outras validações personalizadas, se desejar. O código para validar o token JWT e recuperar o principal:

public static ClaimsPrincipal GetPrincipal(string token)
{
    try
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;

        if (jwtToken == null)
            return null;

        var symmetricKey = Convert.FromBase64String(Secret);

        var validationParameters = new TokenValidationParameters()
        {
            RequireExpirationTime = true,
            ValidateIssuer = false,
            ValidateAudience = false,
            IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
        };

        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);

        return principal;
    }
    catch (Exception)
    {
        //should write log
        return null;
    }
}

Se o token JWT for validado e o principal for retornado, você deverá construir uma nova identidade local e colocar mais informações para verificar a autorização da função.

Lembre-se de adicionar config.Filters.Add(new AuthorizeAttribute());(autorização padrão) no escopo global para impedir qualquer solicitação anônima aos seus recursos.

Você pode usar o Postman para testar a demonstração:

Token de solicitação (ingênuo como mencionei acima, apenas para demonstração):

GET http://localhost:{port}/api/token?username=cuong&password=1

Coloque o token JWT no cabeçalho da solicitação autorizada, exemplo:

GET http://localhost:{port}/api/value

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

A demonstração é apresentada aqui: https://github.com/cuongle/WebApi.Jwt

cuongle
fonte
5
Bem explicado por @Cuong Le, mas eu gostaria de acrescentar mais: se você estiver usando OWIN, verifique o UseJwtBearerAuthentication disponível no Microsoft.Owin.Security.Jwt, você pode usar esse middleware owin no WebAPI para validar automaticamente todas as solicitações recebidas. usar a classe de inicialização Owin para registrar o middleware
Jek
5
@AmirPopovich Você não precisa definir o token na resposta, o token precisa ser armazenado em outro lugar no lado do cliente; para a web, você pode colocar no armazenamento local, sempre que enviar uma solicitação HTTP, coloque o token no cabeçalho.
cuongle
7
Uau, essa é a explicação mais simples que já vi há muito tempo. +100 se eu pudesse
gyozo kudor 24/03
4
@Homam: fro Desculpe esta resposta tardia, a melhor maneira de gerar é: varhmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);
cuongle
4
Qualquer pessoa que use o código de demonstração do repositório do CuongLe perceberá que há um erro no qual as solicitações sem cabeçalho de autorização não são tratadas, o que significa que qualquer consulta sem uma pode ser concluída (um ponto de extremidade não tão seguro!). Há uma solicitação de recebimento de @magicleon para corrigir esse problema aqui: github.com/cuongle/WebApi.Jwt/pull/4
Chucky
11

Consegui consegui-lo com o mínimo esforço (tão simples quanto no ASP.NET Core).

Para isso, uso o Startup.csarquivo e a Microsoft.Owin.Security.Jwtbiblioteca OWIN .

Para que o aplicativo seja exibido Startup.cs, precisamos alterar Web.config:

<configuration>
  <appSettings>
    <add key="owin:AutomaticAppStartup" value="true" />
    ...

Veja como Startup.csdeve ficar:

using MyApp.Helpers;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Jwt;
using Owin;

[assembly: OwinStartup(typeof(MyApp.App_Start.Startup))]

namespace MyApp.App_Start
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseJwtBearerAuthentication(
                new JwtBearerAuthenticationOptions
                {
                    AuthenticationMode = AuthenticationMode.Active,
                    TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidAudience = ConfigHelper.GetAudience(),
                        ValidIssuer = ConfigHelper.GetIssuer(),
                        IssuerSigningKey = ConfigHelper.GetSymmetricSecurityKey(),
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true
                    }
                });
        }
    }
}

Muitos de vocês usam o ASP.NET Core atualmente, portanto, como você pode ver, não difere muito do que temos lá.

Isso realmente me deixou perplexo primeiro, eu estava tentando implementar provedores personalizados etc. Mas não esperava que fosse tão simples. OWINapenas pedras!

Apenas uma coisa a mencionar - depois que eu habilitei a NSWagbiblioteca OWIN Startup , parei de funcionar para mim (por exemplo, alguns de vocês podem querer gerar automaticamente proxies HTTP datilografados para aplicativo Angular).

A solução também foi muito simples - I substituído NSWagcom Swashbucklee não ter quaisquer outras questões.


Ok, agora compartilhando ConfigHelpercódigo:

public class ConfigHelper
{
    public static string GetIssuer()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Issuer"];
        return result;
    }

    public static string GetAudience()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["Audience"];
        return result;
    }

    public static SigningCredentials GetSigningCredentials()
    {
        var result = new SigningCredentials(GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
        return result;
    }

    public static string GetSecurityKey()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["SecurityKey"];
        return result;
    }

    public static byte[] GetSymmetricSecurityKeyAsBytes()
    {
        var issuerSigningKey = GetSecurityKey();
        byte[] data = Encoding.UTF8.GetBytes(issuerSigningKey);
        return data;
    }

    public static SymmetricSecurityKey GetSymmetricSecurityKey()
    {
        byte[] data = GetSymmetricSecurityKeyAsBytes();
        var result = new SymmetricSecurityKey(data);
        return result;
    }

    public static string GetCorsOrigins()
    {
        string result = System.Configuration.ConfigurationManager.AppSettings["CorsOrigins"];
        return result;
    }
}

Outro aspecto importante - enviei o JWT Token pelo cabeçalho de autorização , para que o código de texto digitado me procure da seguinte maneira:

(o código abaixo é gerado pelo NSWag )

@Injectable()
export class TeamsServiceProxy {
    private http: HttpClient;
    private baseUrl: string;
    protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;

    constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
        this.http = http;
        this.baseUrl = baseUrl ? baseUrl : "https://localhost:44384";
    }

    add(input: TeamDto | null): Observable<boolean> {
        let url_ = this.baseUrl + "/api/Teams/Add";
        url_ = url_.replace(/[?&]$/, "");

        const content_ = JSON.stringify(input);

        let options_ : any = {
            body: content_,
            observe: "response",
            responseType: "blob",
            headers: new HttpHeaders({
                "Content-Type": "application/json", 
                "Accept": "application/json",
                "Authorization": "Bearer " + localStorage.getItem('token')
            })
        };

Veja a parte dos cabeçalhos - "Authorization": "Bearer " + localStorage.getItem('token')

Alex Herman
fonte
I replaced NSWag with Swashbuckle and didn't have any further issues.O Swashbuckle tem a capacidade de gerar arquivos datilografados ou é algo que você adicionou a ele mesmo?
esmagar
O @crush swashbucle é uma biblioteca de back-end que fornece json, como a biblioteca nuget nswag, apenas melhor. Para produzir um arquivo datilografado, você ainda deve usar o pacote nswag do npm.
Alex Herman
Certo, já tenho swashbuckle no meu projeto há algum tempo, parecia que você estava sugerindo que poderia gerar os modelos TypeScript em vez de nswag. Eu não sou fã de nswag ... é pesado. Eu criei minha própria conversão C # -> TypeScript conectada ao Swashbuckle - gera os arquivos como um processo pós-compilação e os publica em um feed npm para nossos projetos. Eu só queria ter certeza de que não havia esquecido um projeto Swashbuckle que já estava fazendo a mesma coisa.
esmagar
8

Aqui está uma implementação muito mínima e segura de uma autenticação baseada em declarações usando o token JWT em uma API da Web do ASP.NET Core.

Antes de tudo, você precisa expor um terminal que retorna um token JWT com declarações atribuídas a um usuário:

 /// <summary>
        /// Login provides API to verify user and returns authentication token.
        /// API Path:  api/account/login
        /// </summary>
        /// <param name="paramUser">Username and Password</param>
        /// <returns>{Token: [Token] }</returns>
        [HttpPost("login")]
        [AllowAnonymous]
        public async Task<IActionResult> Login([FromBody] UserRequestVM paramUser, CancellationToken ct)
        {

            var result = await UserApplication.PasswordSignInAsync(paramUser.Email, paramUser.Password, false, lockoutOnFailure: false);

            if (result.Succeeded)
            {
                UserRequestVM request = new UserRequestVM();
                request.Email = paramUser.Email;


                ApplicationUser UserDetails = await this.GetUserByEmail(request);
                List<ApplicationClaim> UserClaims = await this.ClaimApplication.GetListByUser(UserDetails);

                var Claims = new ClaimsIdentity(new Claim[]
                                {
                                    new Claim(JwtRegisteredClaimNames.Sub, paramUser.Email.ToString()),
                                    new Claim(UserId, UserDetails.UserId.ToString())
                                });


                //Adding UserClaims to JWT claims
                foreach (var item in UserClaims)
                {
                    Claims.AddClaim(new Claim(item.ClaimCode, string.Empty));
                }

                var tokenHandler = new JwtSecurityTokenHandler();
                  // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                var encryptionkey = Configuration["Jwt:Encryptionkey"];
                var key = Encoding.ASCII.GetBytes(encryptionkey);
                var tokenDescriptor = new SecurityTokenDescriptor
                {
                    Issuer = Configuration["Jwt:Issuer"],
                    Subject = Claims,

                // this information will be retrived from you Configuration
                //I have injected Configuration provider service into my controller
                    Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(Configuration["Jwt:ExpiryTimeInMinutes"])),

                    //algorithm to sign the token
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)

                };

                var token = tokenHandler.CreateToken(tokenDescriptor);
                var tokenString = tokenHandler.WriteToken(token);

                return Ok(new
                {
                    token = tokenString
                });
            }

            return BadRequest("Wrong Username or password");
        }

agora você precisa adicionar autenticação aos seus serviços ConfigureServicesdentro do seu startup.cs para adicionar a autenticação JWT como seu serviço de autenticação padrão como este:

services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
             .AddJwtBearer(cfg =>
             {
                 cfg.RequireHttpsMetadata = false;
                 cfg.SaveToken = true;
                 cfg.TokenValidationParameters = new TokenValidationParameters()
                 {
                     //ValidateIssuerSigningKey = true,
                     IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Encryptionkey"])),
                     ValidateAudience = false,
                     ValidateLifetime = true,
                     ValidIssuer = configuration["Jwt:Issuer"],
                     //ValidAudience = Configuration["Jwt:Audience"],
                     //IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"])),
                 };
             });

agora você pode adicionar políticas aos seus serviços de autorização como este:

services.AddAuthorization(options =>
            {
                options.AddPolicy("YourPolicyNameHere",
                                policy => policy.RequireClaim("YourClaimNameHere"));
            });

ALTERNATIVAMENTE , você também pode (não necessário) preencher todas as suas reivindicações do banco de dados, pois isso só será executado uma vez na inicialização do aplicativo e as adicionará a políticas como esta:

  services.AddAuthorization(async options =>
            {
                var ClaimList = await claimApplication.GetList(applicationClaim);
                foreach (var item in ClaimList)
                {                        
                    options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));                       
                }
            });

agora você pode colocar o filtro de política em qualquer um dos métodos que você deseja autorizar assim:

 [HttpPost("update")]
        [Authorize(Policy = "ACC_UP")]
        public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
        {
//your logic goes here
}

Espero que isto ajude

Zeeshan Adil
fonte
3

Eu acho que você deve usar algum servidor de festa em 3D para suportar o token JWT e não há suporte JWT pronto para uso na API WEB 2.

No entanto, há um projeto OWIN para oferecer suporte a algum formato de token assinado (não JWT). Funciona como um protocolo OAuth reduzido para fornecer apenas uma forma simples de autenticação para um site.

Você pode ler mais sobre isso, por exemplo, aqui .

É bastante longo, mas a maioria das partes são detalhes com controladores e identidade do ASP.NET que você pode não precisar. Os mais importantes são

Etapa 9: adicionar suporte à geração de tokens de portador OAuth

Etapa 12: testando a API de back-end

Lá, você pode ler como configurar o ponto de extremidade (por exemplo, "/ token") que você pode acessar do front-end (e detalhes sobre o formato da solicitação).

Outras etapas fornecem detalhes sobre como conectar esse terminal ao banco de dados, etc., e você pode escolher as partes necessárias.

Ilya Chernomordik
fonte
2

No meu caso, o JWT é criado por uma API separada, portanto o ASP.NET precisa apenas decodificá-lo e validá-lo. Ao contrário da resposta aceita, estamos usando o RSA, que é um algoritmo não simétrico, portanto a SymmetricSecurityKeyclasse mencionada acima não funcionará.

Aqui está o resultado.

using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Threading;
using System.Threading.Tasks;

    public static async Task<JwtSecurityToken> VerifyAndDecodeJwt(string accessToken)
    {
        try
        {
            var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{securityApiOrigin}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
            var openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
            var validationParameters = new TokenValidationParameters()
            {
                ValidateLifetime = true,
                ValidateAudience = false,
                ValidateIssuer = false,
                RequireSignedTokens = true,
                IssuerSigningKeys = openIdConfig.SigningKeys,
            };
            new JwtSecurityTokenHandler().ValidateToken(accessToken, validationParameters, out var validToken);
            // threw on invalid, so...
            return validToken as JwtSecurityToken;
        }
        catch (Exception ex)
        {
            logger.Info(ex.Message);
            return null;
        }
    }
Ron Newcomb
fonte