Otentikasi Berbasis Token di ASP.NET Core

161

Saya bekerja dengan aplikasi ASP.NET Core. Saya mencoba menerapkan Otentikasi Berbasis Token tetapi tidak dapat menemukan cara menggunakan Sistem Keamanan baru untuk kasus saya. Saya telah melalui contoh tetapi mereka tidak banyak membantu saya, mereka menggunakan otentikasi cookie atau otentikasi eksternal (GitHub, Microsoft, Twitter).

Apa skenario saya: aplikasi angularjs harus meminta /tokenurl lewat nama pengguna dan kata sandi. WebApi harus memberi otorisasi kepada pengguna dan mengembalikan access_tokenyang akan digunakan oleh aplikasi angularjs dalam permintaan berikut.

Saya telah menemukan artikel yang bagus tentang mengimplementasikan apa yang saya perlukan dalam versi ASP.NET saat ini - Otentikasi Berbasis Token menggunakan ASP.NET Web API 2, Owin, dan Identity . Tetapi tidak jelas bagi saya bagaimana melakukan hal yang sama di ASP.NET Core.

Pertanyaan saya adalah: bagaimana cara mengkonfigurasi aplikasi ASP.NET Core WebApi agar berfungsi dengan otentikasi berbasis token?

Hibah
sumber
Saya memiliki masalah yang sama dan saya berencana untuk melakukan semua itu sendiri, FYI ada pertanyaan lain stackoverflow.com/questions/29055477/... tapi belum ada server, mari kita lihat apa yang terjadi
Son_of_Sam
Saya juga menghadapi masalah yang sama tetapi belum menemukan solusi ... Saya perlu menulis otentikasi khusus menggunakan layanan lain yang mengotentikasi Token saya.
Mayank Gupta

Jawaban:

137

Pembaruan untuk .Net Core 3.1:

David Fowler (arsitek untuk tim ASP .NET Core) telah mengumpulkan satu set aplikasi tugas yang sangat sederhana, termasuk aplikasi sederhana yang menunjukkan JWT . Saya akan segera memasukkan pembaruan dan gaya sederhana ke pos ini.

Diperbarui untuk .Net Core 2:

Versi sebelumnya dari jawaban ini menggunakan RSA; itu benar-benar tidak perlu jika kode Anda yang sama yang menghasilkan token juga memverifikasi token. Namun, jika Anda membagikan tanggung jawab, Anda mungkin masih ingin melakukan ini menggunakan instance dari Microsoft.IdentityModel.Tokens.RsaSecurityKey.

  1. Buat beberapa konstanta yang akan kita gunakan nanti; inilah yang saya lakukan:

    const string TokenAudience = "Myself";
    const string TokenIssuer = "MyProject";
  2. Tambahkan ini ke Startup.cs's Anda ConfigureServices. Kami akan menggunakan injeksi ketergantungan nanti untuk mengakses pengaturan ini. Saya berasumsi bahwa Anda authenticationConfigurationadalah objek ConfigurationSectionatau Configurationsedemikian rupa sehingga Anda dapat memiliki konfigurasi berbeda untuk debug dan produksi. Pastikan Anda menyimpan kunci Anda dengan aman! Itu bisa berupa string apa saja.

    var keySecret = authenticationConfiguration["JwtSigningKey"];
    var symmetricKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keySecret));
    
    services.AddTransient(_ => new JwtSignInHandler(symmetricKey));
    
    services.AddAuthentication(options =>
    {
        // This causes the default authentication scheme to be JWT.
        // Without this, the Authorization header is not checked and
        // you'll get no results. However, this also means that if
        // you're already using cookies in your app, they won't be 
        // checked by default.
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    })
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters.ValidateIssuerSigningKey = true;
            options.TokenValidationParameters.IssuerSigningKey = symmetricKey;
            options.TokenValidationParameters.ValidAudience = JwtSignInHandler.TokenAudience;
            options.TokenValidationParameters.ValidIssuer = JwtSignInHandler.TokenIssuer;
        });

    Saya telah melihat jawaban lain mengubah pengaturan lain, seperti ClockSkew; standarnya diatur sedemikian rupa sehingga harus bekerja untuk lingkungan terdistribusi yang jamnya tidak tepat dalam sinkronisasi. Ini adalah satu-satunya pengaturan yang perlu Anda ubah.

  3. Siapkan Otentikasi. Anda harus memiliki baris ini sebelum middleware apa pun yang memerlukan Userinfo Anda , seperti app.UseMvc().

    app.UseAuthentication();

    Perhatikan bahwa ini tidak akan menyebabkan token Anda dipancarkan dengan SignInManageratau apa pun. Anda perlu menyediakan mekanisme sendiri untuk mengeluarkan JWT Anda - lihat di bawah.

  4. Anda mungkin ingin menentukan AuthorizationPolicy. Ini akan memungkinkan Anda untuk menentukan pengontrol dan tindakan yang hanya mengizinkan token Bearer sebagai otentikasi menggunakan [Authorize("Bearer")].

    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType)
            .RequireAuthenticatedUser().Build());
    });
  5. Inilah bagian yang sulit: membangun token.

    class JwtSignInHandler
    {
        public const string TokenAudience = "Myself";
        public const string TokenIssuer = "MyProject";
        private readonly SymmetricSecurityKey key;
    
        public JwtSignInHandler(SymmetricSecurityKey symmetricKey)
        {
            this.key = symmetricKey;
        }
    
        public string BuildJwt(ClaimsPrincipal principal)
        {
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    
            var token = new JwtSecurityToken(
                issuer: TokenIssuer,
                audience: TokenAudience,
                claims: principal.Claims,
                expires: DateTime.Now.AddMinutes(20),
                signingCredentials: creds
            );
    
            return new JwtSecurityTokenHandler().WriteToken(token);
        }
    }

    Kemudian, di controller Anda di mana Anda ingin token Anda, sesuatu seperti berikut ini:

    [HttpPost]
    public string AnonymousSignIn([FromServices] JwtSignInHandler tokenFactory)
    {
        var principal = new System.Security.Claims.ClaimsPrincipal(new[]
        {
            new System.Security.Claims.ClaimsIdentity(new[]
            {
                new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "Demo User")
            })
        });
        return tokenFactory.BuildJwt(principal);
    }

    Di sini, saya berasumsi Anda sudah memiliki kepala sekolah. Jika Anda menggunakan Identity, Anda dapat menggunakan IUserClaimsPrincipalFactory<>untuk mengubah Anda Usermenjadi a ClaimsPrincipal.

  6. Untuk mengujinya : Dapatkan token, memasukkannya ke dalam formulir di jwt.io . Instruksi yang saya berikan di atas juga memungkinkan Anda untuk menggunakan rahasia dari konfigurasi Anda untuk memvalidasi tanda tangan!

  7. Jika Anda merender ini dalam tampilan parsial pada halaman HTML Anda dalam kombinasi dengan otentikasi hanya pengguna di .Net 4.5, Anda sekarang dapat menggunakan a ViewComponentuntuk melakukan hal yang sama. Sebagian besar sama dengan kode Action Controller di atas.

Matt DeKrey
sumber
1
Anda harus benar-benar menyuntikkan IOptions<OAuthBearerAuthenticationOptions>untuk menggunakan Opsi; menggunakan objek Opsi secara langsung tidak didukung karena konfigurasi bernama yang didukung oleh kerangka Model Opsi.
Matt DeKrey
2
Diperbarui pada apa yang saya gunakan, meskipun sekarang jawabannya harus mendapatkan penulisan ulang. Terima kasih telah menyodok saya!
Matt DeKrey
5
Sejak saat itu # 5 telah diubah menjadi yang berikut di Microsoft.AspNet.Authentication.OAuthBearer - beta 5 - 6 dan mungkin beta sebelumnya tetapi belum mengonfirmasi hal tersebut. auth.AddPolicy ("Bearer", AuthorizationPolicyBuilder baru () .AddAuthenticationSchemes (OAuthBearerAuthenticationDefaults.AuthenticationScheme) .RequireAuthenticatedUser (). Build ());
dynamiclynk
5
@MattDeKrey Saya telah menggunakan jawaban ini sebagai titik awal untuk contoh auth berbasiskan token yang sederhana, dan memperbaruinya untuk bekerja melawan beta 7 - lihat github.com/mrsheepuk/ASPNETSelfCreatedTokenAuthExample - juga memasukkan beberapa petunjuk dari komentar ini.
Mark Hughes
2
Diperbarui lagi untuk RC1 - versi lama untuk Beta7 dan Beta8 tersedia di cabang di GitHub.
Mark Hughes
83

Bekerja dari jawaban luar biasa Matt Dekrey , saya telah membuat contoh yang sepenuhnya berfungsi untuk otentikasi berbasis token, yang bekerja melawan ASP.NET Core (1.0.1). Anda dapat menemukan kode lengkap dalam repositori ini di GitHub (cabang alternatif untuk 1.0.0-rc1 , beta8 , beta7 ), tetapi secara singkat, langkah-langkah penting adalah:

Buat kunci untuk aplikasi Anda

Dalam contoh saya, saya membuat kunci acak setiap kali aplikasi dimulai, Anda harus membuatnya dan menyimpannya di suatu tempat dan menyediakannya untuk aplikasi Anda. Lihat file ini untuk bagaimana saya membuat kunci acak dan bagaimana Anda dapat mengimpornya dari file .json . Seperti yang disarankan dalam komentar oleh @kspearrin, API Perlindungan Data tampaknya merupakan kandidat yang ideal untuk mengelola kunci "dengan benar", tetapi saya belum berhasil jika itu memungkinkan. Silakan kirim permintaan tarik jika Anda berhasil!

Startup.cs - ConfigureServices

Di sini, kita perlu memuat kunci pribadi agar token kita dapat ditandatangani, yang juga akan kita gunakan untuk memverifikasi token seperti yang disajikan. Kami menyimpan kunci dalam variabel tingkat kelas keyyang akan kami gunakan kembali dalam metode Konfigurasi di bawah ini. TokenAuthOptions adalah kelas sederhana yang memegang identitas penandatangan, pemirsa, dan penerbit yang kita perlukan di TokenController untuk membuat kunci kita.

// Replace this with some sort of loading from config / file.
RSAParameters keyParams = RSAKeyUtils.GetRandomKey();

// Create the key, and a set of token options to record signing credentials 
// using that key, along with the other parameters we will need in the 
// token controlller.
key = new RsaSecurityKey(keyParams);
tokenOptions = new TokenAuthOptions()
{
    Audience = TokenAudience,
    Issuer = TokenIssuer,
    SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.Sha256Digest)
};

// Save the token options into an instance so they're accessible to the 
// controller.
services.AddSingleton<TokenAuthOptions>(tokenOptions);

// Enable the use of an [Authorize("Bearer")] attribute on methods and
// classes to protect.
services.AddAuthorization(auth =>
{
    auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
        .RequireAuthenticatedUser().Build());
});

Kami juga telah menetapkan kebijakan otorisasi untuk memungkinkan kami menggunakan [Authorize("Bearer")]pada titik akhir dan kelas yang ingin kami lindungi.

Startup.cs - Konfigurasikan

Di sini, kita perlu mengkonfigurasi JwtBearerAuthentication:

app.UseJwtBearerAuthentication(new JwtBearerOptions {
    TokenValidationParameters = new TokenValidationParameters {
        IssuerSigningKey = key,
        ValidAudience = tokenOptions.Audience,
        ValidIssuer = tokenOptions.Issuer,

        // When receiving a token, check that it is still valid.
        ValidateLifetime = true,

        // This defines the maximum allowable clock skew - i.e.
        // provides a tolerance on the token expiry time 
        // when validating the lifetime. As we're creating the tokens 
        // locally and validating them on the same machines which 
        // should have synchronised time, this can be set to zero. 
        // Where external tokens are used, some leeway here could be 
        // useful.
        ClockSkew = TimeSpan.FromMinutes(0)
    }
});

TokenController

Di pengontrol token, Anda harus memiliki metode untuk menghasilkan kunci yang ditandatangani menggunakan kunci yang dimuat di Startup.cs. Kami telah mendaftarkan instance TokenAuthOptions di Startup, jadi kami harus menyuntikkannya di konstruktor untuk TokenController:

[Route("api/[controller]")]
public class TokenController : Controller
{
    private readonly TokenAuthOptions tokenOptions;

    public TokenController(TokenAuthOptions tokenOptions)
    {
        this.tokenOptions = tokenOptions;
    }
...

Maka Anda harus membuat token di handler Anda untuk titik akhir login, dalam contoh saya, saya mengambil nama pengguna dan kata sandi dan memvalidasi yang menggunakan pernyataan if, tetapi hal utama yang perlu Anda lakukan adalah membuat atau memuat klaim identitas berbasis dan menghasilkan token untuk itu:

public class AuthRequest
{
    public string username { get; set; }
    public string password { get; set; }
}

/// <summary>
/// Request a new token for a given username/password pair.
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
[HttpPost]
public dynamic Post([FromBody] AuthRequest req)
{
    // Obviously, at this point you need to validate the username and password against whatever system you wish.
    if ((req.username == "TEST" && req.password == "TEST") || (req.username == "TEST2" && req.password == "TEST"))
    {
        DateTime? expires = DateTime.UtcNow.AddMinutes(2);
        var token = GetToken(req.username, expires);
        return new { authenticated = true, entityId = 1, token = token, tokenExpires = expires };
    }
    return new { authenticated = false };
}

private string GetToken(string user, DateTime? expires)
{
    var handler = new JwtSecurityTokenHandler();

    // Here, you should create or look up an identity for the user which is being authenticated.
    // For now, just creating a simple generic identity.
    ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user, "TokenAuth"), new[] { new Claim("EntityID", "1", ClaimValueTypes.Integer) });

    var securityToken = handler.CreateToken(new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor() {
        Issuer = tokenOptions.Issuer,
        Audience = tokenOptions.Audience,
        SigningCredentials = tokenOptions.SigningCredentials,
        Subject = identity,
        Expires = expires
    });
    return handler.WriteToken(securityToken);
}

Dan seharusnya begitu. Tambahkan saja [Authorize("Bearer")]ke metode atau kelas apa pun yang ingin Anda lindungi, dan Anda akan mendapatkan kesalahan jika Anda mencoba mengaksesnya tanpa hadiah token. Jika Anda ingin mengembalikan 401 alih-alih kesalahan 500, Anda harus mendaftarkan penangan pengecualian kustom seperti yang saya miliki dalam contoh saya di sini .

Mark Hughes
sumber
1
Ini adalah contoh yang sangat bagus, dan termasuk semua bagian yang hilang yang saya butuhkan untuk membuat contoh @ MattDeKrey bekerja, terima kasih banyak! Perhatikan bahwa siapa pun yang masih menargetkan beta7 dan bukan beta8 masih dapat menemukan contoh itu dalam sejarah github
nickspoon
1
Senang itu membantu @nickspoon - jika Anda memiliki masalah sama sekali, beri tahu saya atau masukkan permintaan tarik pada github dan saya akan memperbarui!
Mark Hughes
2
Terima kasih untuk ini, namun saya tidak begitu mengerti mengapa sesuatu yang bekerja di luar kotak di ASP.Net 4 Web API sekarang memerlukan sedikit konfigurasi di ASP.Net 5. Sepertinya langkah mundur.
JMK
1
Saya pikir mereka benar-benar mendorong "auth sosial" untuk ASP.NET 5, yang masuk akal saya kira, tetapi ada aplikasi yang tidak sesuai untuk jadi saya tidak yakin saya setuju dengan arahan mereka @JMK
Mark Hughes
1
@ YuriyP Saya perlu memperbarui jawaban ini untuk RC2 - Saya belum memperbarui aplikasi internal kami yang menggunakan ini untuk RC2, jadi saya tidak yakin apa yang terlibat. Saya akan memperbarui setelah saya menyelesaikan perbedaannya ...
Mark Hughes
3

Anda dapat melihat contoh koneksi OpenId yang menggambarkan cara menangani berbagai mekanisme otentikasi, termasuk Token JWT:

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Samples

Jika Anda melihat proyek Cordova Backend, konfigurasi untuk API adalah seperti ini:

           // Create a new branch where the registered middleware will be executed only for non API calls.
        app.UseWhen(context => !context.Request.Path.StartsWithSegments(new PathString("/api")), branch => {
            // Insert a new cookies middleware in the pipeline to store
            // the user identity returned by the external identity provider.
            branch.UseCookieAuthentication(new CookieAuthenticationOptions {
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
                AuthenticationScheme = "ServerCookie",
                CookieName = CookieAuthenticationDefaults.CookiePrefix + "ServerCookie",
                ExpireTimeSpan = TimeSpan.FromMinutes(5),
                LoginPath = new PathString("/signin"),
                LogoutPath = new PathString("/signout")
            });

            branch.UseGoogleAuthentication(new GoogleOptions {
                ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com",
                ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"
            });

            branch.UseTwitterAuthentication(new TwitterOptions {
                ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g",
                ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"
            });
        });

Logika di /Providers/AuthorizationProvider.cs dan RessourceController dari proyek itu juga patut dilihat;).

Atau Anda juga dapat menggunakan kode berikut untuk memvalidasi token (ada juga cuplikan untuk membuatnya berfungsi dengan signalR):

        // Add a new middleware validating access tokens.
        app.UseOAuthValidation(options =>
        {
            // Automatic authentication must be enabled
            // for SignalR to receive the access token.
            options.AutomaticAuthenticate = true;

            options.Events = new OAuthValidationEvents
            {
                // Note: for SignalR connections, the default Authorization header does not work,
                // because the WebSockets JS API doesn't allow setting custom parameters.
                // To work around this limitation, the access token is retrieved from the query string.
                OnRetrieveToken = context =>
                {
                    // Note: when the token is missing from the query string,
                    // context.Token is null and the JWT bearer middleware will
                    // automatically try to retrieve it from the Authorization header.
                    context.Token = context.Request.Query["access_token"];

                    return Task.FromResult(0);
                }
            };
        });

Untuk menerbitkan token, Anda dapat menggunakan paket server OpenId Connect seperti:

        // Add a new middleware issuing access tokens.
        app.UseOpenIdConnectServer(options =>
        {
            options.Provider = new AuthenticationProvider();
            // Enable the authorization, logout, token and userinfo endpoints.
            //options.AuthorizationEndpointPath = "/connect/authorize";
            //options.LogoutEndpointPath = "/connect/logout";
            options.TokenEndpointPath = "/connect/token";
            //options.UserinfoEndpointPath = "/connect/userinfo";

            // Note: if you don't explicitly register a signing key, one is automatically generated and
            // persisted on the disk. If the key cannot be persisted, an exception is thrown.
            // 
            // On production, using a X.509 certificate stored in the machine store is recommended.
            // You can generate a self-signed certificate using Pluralsight's self-cert utility:
            // https://s3.amazonaws.com/pluralsight-free/keith-brown/samples/SelfCert.zip
            // 
            // options.SigningCredentials.AddCertificate("7D2A741FE34CC2C7369237A5F2078988E17A6A75");
            // 
            // Alternatively, you can also store the certificate as an embedded .pfx resource
            // directly in this assembly or in a file published alongside this project:
            // 
            // options.SigningCredentials.AddCertificate(
            //     assembly: typeof(Startup).GetTypeInfo().Assembly,
            //     resource: "Nancy.Server.Certificate.pfx",
            //     password: "Owin.Security.OpenIdConnect.Server");

            // Note: see AuthorizationController.cs for more
            // information concerning ApplicationCanDisplayErrors.
            options.ApplicationCanDisplayErrors = true // in dev only ...;
            options.AllowInsecureHttp = true // in dev only...;
        });

EDIT: Saya telah mengimplementasikan aplikasi satu halaman dengan implementasi otentikasi berbasis token menggunakan kerangka kerja ujung depan Aurelia dan inti ASP.NET. Ada juga koneksi pers R sinyal. Namun saya belum melakukan implementasi DB. Kode dapat dilihat di sini: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth

Semoga ini membantu,

Terbaik,

Alex

Darxtar
sumber
1

Lihat OpenIddict - ini adalah proyek baru (pada saat penulisan) yang membuatnya mudah untuk mengkonfigurasi pembuatan token JWT dan menyegarkan token di ASP.NET 5. Validasi token ditangani oleh perangkat lunak lain.

Dengan asumsi Anda menggunakan Identitydengan Entity Framework, baris terakhir adalah apa yang Anda tambahkan ke ConfigureServicesmetode:

services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddOpenIddictCore<Application>(config => config.UseEntityFramework());

Di Configure, Anda mengatur OpenIddict untuk melayani token JWT:

app.UseOpenIddictCore(builder =>
{
    // tell openiddict you're wanting to use jwt tokens
    builder.Options.UseJwtTokens();
    // NOTE: for dev consumption only! for live, this is not encouraged!
    builder.Options.AllowInsecureHttp = true;
    builder.Options.ApplicationCanDisplayErrors = true;
});

Anda juga mengonfigurasi validasi token di Configure:

// use jwt bearer authentication
app.UseJwtBearerAuthentication(options =>
{
    options.AutomaticAuthenticate = true;
    options.AutomaticChallenge = true;
    options.RequireHttpsMetadata = false;
    options.Audience = "http://localhost:58292/";
    options.Authority = "http://localhost:58292/";
});

Ada satu atau dua hal kecil lainnya, seperti DbContext Anda harus berasal dari OpenIddictContext.

Anda dapat melihat penjelasan panjang penuh di posting blog ini: http://capesean.co.za/blog/asp-net-5-jwt-tokens/

Demo yang berfungsi tersedia di: https://github.com/capesean/openiddict-test

Sean
sumber