Uwierzytelnianie JWT dla interfejsu API sieci Web ASP.NET

264

Usiłuję obsługiwać token elementu nośnego JWT (JSON Web Token) w mojej aplikacji interfejsu API sieci Web i gubię się.

Widzę obsługę .NET Core i aplikacji OWIN.
Obecnie hostuję moją aplikację w IIS.

Jak mogę uzyskać ten moduł uwierzytelniania w mojej aplikacji? Czy jest jakiś sposób na użycie <authentication>konfiguracji podobnej do sposobu, w jaki korzystam z formularzy / uwierzytelniania systemu Windows?

Amir Popowicz
źródło

Odpowiedzi:

611

Odpowiedziałem na to pytanie: Jak zabezpieczyć interfejs API sieci Web ASP.NET 4 lata temu za pomocą HMAC.

Teraz wiele rzeczy zmieniło się pod względem bezpieczeństwa, szczególnie JWT zyskuje na popularności. W tym miejscu postaram się wyjaśnić, jak korzystać z JWT w najprostszy i podstawowy sposób, w jaki mogę, abyśmy nie zgubili się w dżungli OWIN, Oauth2, ASP.NET Identity ... :).

Jeśli nie znasz tokena JWT, musisz rzucić okiem na:

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

Zasadniczo token JWT wygląda następująco:

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

Przykład:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ

Token JWT ma trzy sekcje:

  1. Nagłówek: format JSON zakodowany w Base64
  2. Roszczenia: Format JSON zakodowany w Base64.
  3. Podpis: utworzony i podpisany na podstawie nagłówka i oświadczeń zakodowanych w Base64.

Jeśli używasz witryny jwt.io z powyższym tokenem, możesz go zdekodować i zobaczyć jak poniżej:

wprowadź opis zdjęcia tutaj

Technicznie JWT używa podpisu podpisanego z nagłówków i oświadczeń za pomocą algorytmu bezpieczeństwa określonego w nagłówkach (przykład: HMACSHA256). Dlatego wymagane jest przesyłanie JWT przez HTTPs, jeśli przechowujesz poufne informacje w roszczeniach.

Teraz, aby korzystać z uwierzytelniania JWT, tak naprawdę nie potrzebujesz oprogramowania pośredniego OWIN, jeśli masz starszy system Web Api. Prosta koncepcja polega na tym, jak dostarczyć token JWT i jak sprawdzić poprawność tokena, gdy nadejdzie żądanie. Otóż ​​to.

Powrót na demo, aby zachować JWT tokena lekka, tylko przechowywanie usernamei expiration timew JWT. Ale w ten sposób musisz odbudować nową tożsamość lokalną (podmiot główny), aby dodać więcej informacji, takich jak: role .. jeśli chcesz wykonać autoryzację roli. Ale jeśli chcesz dodać więcej informacji do JWT, to zależy od ciebie: jest bardzo elastyczny.

Zamiast używać oprogramowania pośredniego OWIN, możesz po prostu podać punkt końcowy tokena JWT, używając akcji z kontrolera:

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;
    }
}

To naiwne działanie; w środowisku produkcyjnym należy użyć żądania POST lub punktu końcowego podstawowego uwierzytelniania, aby dostarczyć token JWT.

Jak wygenerować token na podstawie username?

Możesz użyć pakietu NuGet wywoływanego System.IdentityModel.Tokens.Jwtod Microsoft do wygenerowania tokena, a nawet innego pakietu, jeśli chcesz. W wersji demo używam HMACSHA256z 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;
}

Punkt końcowy do dostarczenia tokenu JWT został zakończony. Jak sprawdzić poprawność JWT, gdy nadejdzie żądanie? W wersji demonstracyjnej, którą zbudowałem, JwtAuthenticationAttributektóra dziedziczy IAuthenticationFilter(więcej szczegółów na temat filtra uwierzytelniania tutaj ).

Za pomocą tego atrybutu możesz uwierzytelnić dowolne działanie: wystarczy umieścić ten atrybut na tym działaniu.

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

Możesz również użyć oprogramowania pośredniego OWIN lub DelegateHander, jeśli chcesz zweryfikować wszystkie przychodzące żądania dotyczące interfejsu WebAPI (nie dotyczy kontrolera ani działania)

Poniżej znajduje się podstawowa metoda z filtru uwierzytelniania:

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);
}

Przepływ pracy polega na użyciu biblioteki JWT (pakiet NuGet powyżej) do sprawdzenia poprawności tokena JWT, a następnie powrotu ClaimsPrincipal. Możesz przeprowadzić większą weryfikację, np. Sprawdzić, czy użytkownik istnieje w systemie i dodać inne niestandardowe weryfikacje, jeśli chcesz. Kod sprawdzania poprawności tokenu JWT i odzyskania kwoty głównej:

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;
    }
}

Jeśli token JWT jest sprawdzony, a nazwa główna jest zwracana, należy zbudować nową tożsamość lokalną i umieścić w niej więcej informacji, aby sprawdzić autoryzację roli.

Pamiętaj, aby dodać config.Filters.Add(new AuthorizeAttribute());(domyślna autoryzacja) w zakresie globalnym, aby zapobiec anonimowym żądaniom do Twoich zasobów.

Możesz użyć Listonosza do przetestowania wersji demo:

Żeton żądania (naiwny, jak wspomniałem powyżej, tylko dla wersji demonstracyjnej):

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

Umieść token JWT w nagłówku autoryzowanego żądania, przykład:

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

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s

Demo znajduje się tutaj: https://github.com/cuongle/WebApi.Jwt

cuongle
źródło
5
Dobrze wyjaśnione przez @Cuong Le, ale chcę dodać więcej: Jeśli używasz OWIN, sprawdź UseJwtBearerAuthentication dostępny w Microsoft.Owin.Security.Jwt możesz użyć tego pośredniego oprogramowania wbudowanego w WebAPI, aby automatycznie zweryfikować każde przychodzące żądanie. użyj klasy startowej owin, aby zarejestrować oprogramowanie pośrednie
Jek
5
@AmirPopovich Nie musisz ustawiać tokena w odpowiedzi, token musi być przechowywany gdzie indziej po stronie klienta, w sieci można umieścić w pamięci lokalnej, za każdym razem, gdy wysyłasz żądanie HTTP, umieść token w nagłówku.
cuongle
7
Wow, to najprostsze wyjaśnienie, jakie widziałem od dłuższego czasu. +100 gdybym mógł
gyozo kudor
4
@ Homam: Przepraszam za tę późną odpowiedź, najlepszym sposobem na wygenerowanie jest: varhmac = new HMACSHA256();var key = Convert.ToBase64String(hmac.Key);
cuongle
4
Każdy, kto użyje kodu demonstracyjnego z repozytorium CuongLe, zauważy błąd, w którym żądania bez nagłówka autoryzacji nie są obsługiwane, co oznacza, że ​​każde zapytanie, bez którego można się przedostać (niezbyt bezpieczny punkt końcowy!). Jest prośba o ściągnięcie od @magicleon, aby rozwiązać ten problem tutaj: github.com/cuongle/WebApi.Jwt/pull/4
Chucky
11

Udało mi się to osiągnąć przy minimalnym wysiłku (tak prostym jak w przypadku ASP.NET Core).

Do tego używam Startup.cspliku i Microsoft.Owin.Security.Jwtbiblioteki OWIN .

Aby aplikacja mogła trafić Startup.cs, musimy zmienić Web.config:

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

Oto jak Startup.cspowinien wyglądać:

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
                    }
                });
        }
    }
}

Wielu z was korzysta obecnie z platformy ASP.NET Core, więc jak widać, nie różni się ona zbytnio od tego, co mamy.

Najpierw naprawdę mnie to zakłopotało, próbowałem wdrożyć niestandardowych dostawców itp. Ale nie spodziewałem się, że będzie to takie proste. OWINtylko skały!

Jeszcze jedna rzecz do wspomnienia - po włączeniu OWIN Startup NSWagbiblioteka przestała dla mnie działać (np. Niektórzy z was mogą chcieć automatycznie generować serwery proxy maszynopisu HTTP dla aplikacji Angular).

Rozwiązanie było również bardzo proste - zastąpiłem NSWagje Swashbucklei nie miałem żadnych dalszych problemów.


Ok, teraz udostępniam ConfigHelperkod:

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;
    }
}

Kolejny ważny aspekt - wysłałem Token JWT przez nagłówek autoryzacji , więc kod maszynowy szuka mnie w następujący sposób:

(poniższy kod jest generowany przez 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')
            })
        };

Zobacz część nagłówków - "Authorization": "Bearer " + localStorage.getItem('token')

Alex Herman
źródło
I replaced NSWag with Swashbuckle and didn't have any further issues.Czy Swashbuckle ma możliwość generowania plików maszynopisu, czy jest to coś, co sam do niego dodałeś?
zmiażdżyć
@crush swashbucle to biblioteka zaplecza zapewniająca json, podobnie jak biblioteka nuget nswag tylko lepiej. Aby stworzyć plik maszynopisu, powinieneś nadal używać pakietu nswag z npm.
Alex Herman
Racja, mam już w swoim projekcie zamieszanie od jakiegoś czasu, brzmiało to tak, jakbyś sugerował, że może generować modele TypeScript zamiast nswag. Nie jestem fanem nswag ... jest ciężki. Stworzyłem własną konwersję C # -> TypeScript, która jest podpięta do Swashbuckle - generuje pliki jako proces po kompilacji i publikuje je na kanale npm dla naszych projektów. Chciałem tylko upewnić się, że nie przeoczyłem projektu Swashbuckle, który już robił to samo.
zmiażdżyć
8

Oto bardzo minimalna i bezpieczna implementacja uwierzytelniania opartego na oświadczeniach przy użyciu tokenu JWT w interfejsie API sieci Web platformy ASP.NET Core.

po pierwsze musisz ujawnić punkt końcowy, który zwraca token JWT z oświadczeniami przypisanymi do użytkownika:

 /// <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");
        }

Teraz trzeba dodać do swoich usług uwierzytelniania w ConfigureServiceswewnątrz startup.cs dodać uwierzytelniania JWT jako domyślną usługę uwierzytelniania tak:

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"])),
                 };
             });

teraz możesz dodać zasady do swoich usług autoryzacyjnych w następujący sposób:

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

ALTERNATYWNIE Możesz również (niekoniecznie) wypełnić wszystkie swoje roszczenia z bazy danych, ponieważ uruchomi się to tylko raz podczas uruchamiania aplikacji i doda je do zasad takich jak to:

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

teraz możesz ustawić filtr zasad na dowolną z metod, które chcesz autoryzować w następujący sposób:

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

Mam nadzieję że to pomoże

Zeeshan Adil
źródło
3

Myślę, że powinieneś użyć serwera 3d party do obsługi tokena JWT i nie ma gotowej obsługi JWT w WEB API 2.

Istnieje jednak projekt OWIN do obsługi pewnego formatu podpisanego tokena (nie JWT). Działa jako zredukowany protokół OAuth, zapewniając prostą formę uwierzytelnienia dla strony internetowej.

Możesz przeczytać więcej na ten temat np . Tutaj .

Jest dość długi, ale większość części to szczegóły dotyczące kontrolerów i tożsamości ASP.NET, które mogą być w ogóle niepotrzebne. Najważniejsze są

Krok 9: Dodaj obsługę generowania tokenów nośnika OAuth

Krok 12: Testowanie interfejsu API zaplecza

Tam możesz przeczytać, jak skonfigurować punkt końcowy (np. „/ Token”), do którego masz dostęp z poziomu interfejsu użytkownika (i szczegółowe informacje na temat formatu żądania).

Inne kroki zawierają szczegółowe informacje na temat łączenia tego punktu końcowego z bazą danych itp., A także możesz wybrać potrzebne części.

Ilya Chernomordik
źródło
2

W moim przypadku JWT jest tworzony przez oddzielny interfejs API, więc ASP.NET musi tylko go zdekodować i sprawdzić. W przeciwieństwie do przyjętej odpowiedzi używamy RSA, który jest algorytmem niesymetrycznym, więc SymmetricSecurityKeywspomniana wyżej klasa nie będzie działać.

Oto wynik.

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
źródło