Bagikan melalui


Mempertahankan klaim dan token tambahan dari penyedia eksternal di ASP.NET Core

Aplikasi ASP.NET Core dapat membuat klaim dan token tambahan dari penyedia autentikasi eksternal, seperti Facebook, Google, Microsoft, dan Twitter. Setiap penyedia mengungkapkan informasi yang berbeda tentang pengguna di platformnya, tetapi pola untuk menerima dan mengubah data pengguna menjadi klaim tambahan sama.

Prasyarat

Tentukan penyedia autentikasi eksternal mana yang akan didukung di aplikasi. Untuk setiap penyedia, daftarkan aplikasi dan dapatkan ID klien dan rahasia klien. Untuk informasi selengkapnya, lihat Autentikasi Facebook dan Google di ASP.NET Core. Aplikasi sampel menggunakan penyedia autentikasi Google.

Mengatur ID klien dan rahasia klien

Penyedia autentikasi OAuth membuat hubungan kepercayaan dengan aplikasi menggunakan ID klien dan rahasia klien. ID Klien dan nilai rahasia klien dibuat untuk aplikasi oleh penyedia autentikasi eksternal saat aplikasi terdaftar di penyedia. Setiap penyedia eksternal yang digunakan aplikasi harus dikonfigurasi secara independen dengan ID klien penyedia dan rahasia klien. Untuk informasi selengkapnya, lihat topik penyedia autentikasi eksternal yang berlaku:

Klaim opsional yang dikirim dalam ID atau token akses dari penyedia autentikasi biasanya dikonfigurasi di portal online penyedia. Misalnya, MICROSOFT Entra ID mengizinkan penetapan klaim opsional ke token ID aplikasi di bilah konfigurasi Token pendaftaran aplikasi. Untuk informasi selengkapnya, lihat Cara: Memberikan klaim opsional ke aplikasi Anda (dokumentasi Azure). Untuk penyedia lain, lihat kumpulan dokumentasi eksternal mereka.

Aplikasi sampel mengonfigurasi penyedia autentikasi Google dengan ID klien dan rahasia klien yang disediakan oleh Google:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebGoogOauth.Data;

var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;

builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => 
                                  options.SignIn.RequireConfirmedAccount = true)
                                 .AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();

var app = builder.Build();

// Remaining code removed for brevity.

Menetapkan cakupan autentikasi

Tentukan daftar izin yang akan diambil dari penyedia dengan menentukan Scope. Cakupan autentikasi untuk penyedia eksternal umum muncul dalam tabel berikut.

Penyedia Cakupan
Facebook https://www.facebook.com/dialog/oauth
Google profile, , emailopenid
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

Dalam aplikasi sampel, cakupan Google profile, email, dan openid secara otomatis ditambahkan oleh kerangka kerja saat AddGoogle dipanggil di AuthenticationBuilder. Jika aplikasi memerlukan cakupan tambahan, tambahkan ke opsi . Dalam contoh berikut, cakupan Google https://www.googleapis.com/auth/user.birthday.read ditambahkan untuk mengambil ulang tahun pengguna:

options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");

Memetakan kunci data pengguna dan membuat klaim

Dalam opsi penyedia, tentukan MapJsonKey atau MapJsonSubKey untuk setiap kunci atau subkunci dalam data pengguna JSON penyedia eksternal agar aplikasi identity dapat dibaca saat masuk. Untuk informasi selengkapnya tentang jenis klaim, lihat ClaimTypes.

Aplikasi sampel membuat klaim lokal (urn:google:locale) dan gambar (urn:google:picture) dari locale kunci dan picture di data pengguna Google:

builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Di Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, IdentityUser (ApplicationUser) masuk ke aplikasi dengan SignInAsync. Selama proses masuk, UserManager<TUser> dapat menyimpan klaim untuk data pengguna yang ApplicationUser Principaltersedia dari .

Di aplikasi sampel, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) menetapkan klaim lokal (urn:google:locale) dan gambar (urn:google:picture) untuk yang masuk ApplicationUser, termasuk klaim untuk GivenName:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ErrorMessage = "Error loading external login information during confirmation.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

        var result = await _userManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);
            if (result.Succeeded)
            {
                _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);

                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                // using Microsoft.AspNetCore.Authentication;
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = false;

                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code },
                    protocol: Request.Scheme);

                await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                // If account confirmation is required, we need to show the link if we don't have a real email sender
                if (_userManager.Options.SignIn.RequireConfirmedAccount)
                {
                    return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                }

                await _signInManager.SignInAsync(user, props, info.LoginProvider);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    ProviderDisplayName = info.ProviderDisplayName;
    ReturnUrl = returnUrl;
    return Page();
}

Secara default, klaim pengguna disimpan dalam autentikasi cookie. Jika autentikasi terlalu besar, autentikasi cookie dapat menyebabkan aplikasi gagal karena:

  • Browser mendeteksi bahwa cookie header terlalu panjang.
  • Ukuran keseluruhan permintaan terlalu besar.

Jika sejumlah besar data pengguna diperlukan untuk memproses permintaan pengguna:

  • Batasi jumlah dan ukuran klaim pengguna untuk pemrosesan permintaan hanya untuk apa yang dibutuhkan aplikasi.
  • Gunakan kustom ITicketStore untuk Cookie Middleware SessionStore Autentikasi untuk menyimpan identity di seluruh permintaan. Pertahankan sejumlah besar identity informasi di server sambil hanya mengirim kunci pengidentifikasi sesi kecil ke klien.

Menyimpan token akses

SaveTokens menentukan apakah token akses dan refresh harus disimpan setelah AuthenticationProperties otorisasi berhasil. SaveTokens diatur ke false secara default untuk mengurangi ukuran autentikasi cookieakhir .

Aplikasi sampel menetapkan nilai SaveTokens menjadi true di GoogleOptions:

builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Saat OnPostConfirmationAsync dijalankan, simpan token akses (ExternalLoginInfo.AuthenticationTokens) dari penyedia eksternal di ApplicationUser.AuthenticationProperties

Aplikasi sampel menyimpan token akses di OnPostConfirmationAsync (pendaftaran pengguna baru) dan OnGetCallbackAsync (pengguna yang terdaftar sebelumnya) di Account/ExternalLogin.cshtml.cs:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ErrorMessage = "Error loading external login information during confirmation.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = CreateUser();

        await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

        var result = await _userManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);
            if (result.Succeeded)
            {
                _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider);

                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user,
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                // using Microsoft.AspNetCore.Authentication;
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = false;

                var userId = await _userManager.GetUserIdAsync(user);
                var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));
                var callbackUrl = Url.Page(
                    "/Account/ConfirmEmail",
                    pageHandler: null,
                    values: new { area = "Identity", userId = userId, code = code },
                    protocol: Request.Scheme);

                await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                // If account confirmation is required, we need to show the link if we don't have a real email sender
                if (_userManager.Options.SignIn.RequireConfirmedAccount)
                {
                    return RedirectToPage("./RegisterConfirmation", new { Email = Input.Email });
                }

                await _signInManager.SignInAsync(user, props, info.LoginProvider);
                return LocalRedirect(returnUrl);
            }
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    ProviderDisplayName = info.ProviderDisplayName;
    ReturnUrl = returnUrl;
    return Page();
}

Catatan

Untuk informasi tentang meneruskan token ke Razor komponen aplikasi sisi Blazor server, lihat Skenario keamanan tambahan sisi server ASP.NET CoreBlazor.

Cara menambahkan token kustom tambahan

Untuk menunjukkan cara menambahkan token kustom, yang disimpan sebagai bagian SaveTokensdari , aplikasi sampel menambahkan AuthenticationToken dengan saat ini DateTime untuk AuthenticationToken.Name dari TicketCreated:

builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
    googleOptions.ClientId = configuration["Authentication:Google:ClientId"];
    googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"];

    googleOptions.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    googleOptions.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");

    googleOptions.SaveTokens = true;

    googleOptions.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList();

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated",
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Membuat dan menambahkan klaim

Kerangka kerja menyediakan tindakan umum dan metode ekstensi untuk membuat dan menambahkan klaim ke koleksi. Untuk informasi selengkapnya, lihat ClaimActionCollectionMapExtensions dan ClaimActionCollectionUniqueExtensions.

Pengguna dapat menentukan tindakan kustom dengan berasal dari ClaimAction dan menerapkan metode abstrak Run .

Untuk informasi selengkapnya, lihat Microsoft.AspNetCore.Authentication.OAuth.Claims .

Menambahkan dan memperbarui klaim pengguna

Klaim disalin dari penyedia eksternal ke database pengguna pada pendaftaran pertama, bukan saat masuk. Jika klaim tambahan diaktifkan di aplikasi setelah pengguna mendaftar untuk menggunakan aplikasi, panggil SignInManager.RefreshSignInAsync pada pengguna untuk memaksa pembuatan autentikasi cookiebaru .

Di lingkungan Pengembangan yang bekerja dengan akun pengguna uji, hapus dan buat ulang akun pengguna. Untuk sistem produksi, klaim baru yang ditambahkan ke aplikasi dapat diisi ulang ke akun pengguna. Setelah membuat perancah ExternalLogin halaman ke dalam aplikasi di Areas/Pages/Identity/Account/Manage, tambahkan kode berikut ke ExternalLoginModel dalam ExternalLogin.cshtml.cs file.

Tambahkan kamus klaim tambahan. Gunakan kunci kamus untuk menahan jenis klaim, dan gunakan nilai untuk menyimpan nilai default. Tambahkan baris berikut ke bagian atas kelas. Contoh berikut mengasumsikan bahwa satu klaim ditambahkan untuk gambar Google pengguna dengan gambar headshot generik sebagai nilai default:

private readonly IReadOnlyDictionary<string, string> _claimsToSync =
     new Dictionary<string, string>()
     {
             { "urn:google:picture", "https://localhost:5001/headshot.png" },
     };

Ganti kode OnGetCallbackAsync default metode dengan kode berikut. Kode mengulang melalui kamus klaim. Klaim ditambahkan (diisi ulang) atau diperbarui untuk setiap pengguna. Saat klaim ditambahkan atau diperbarui, rincian masuk pengguna di-refresh menggunakan SignInManager<TUser>, mempertahankan properti autentikasi yang ada (AuthenticationProperties).

private readonly IReadOnlyDictionary<string, string> _claimsToSync =
     new Dictionary<string, string>()
     {
             { "urn:google:picture", "https://localhost:5001/headshot.png" },
     };

public async Task<IActionResult> OnGetCallbackAsync(string returnUrl = null, string remoteError = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    if (remoteError != null)
    {
        ErrorMessage = $"Error from external provider: {remoteError}";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        ErrorMessage = "Error loading external login information.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    // Sign in the user with this external login provider if the user already has a login.
    var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);
    if (result.Succeeded)
    {
        _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
        if (_claimsToSync.Count > 0)
        {
            var user = await _userManager.FindByLoginAsync(info.LoginProvider,
                info.ProviderKey);
            var userClaims = await _userManager.GetClaimsAsync(user);
            bool refreshSignIn = false;

            foreach (var addedClaim in _claimsToSync)
            {
                var userClaim = userClaims
                    .FirstOrDefault(c => c.Type == addedClaim.Key);

                if (info.Principal.HasClaim(c => c.Type == addedClaim.Key))
                {
                    var externalClaim = info.Principal.FindFirst(addedClaim.Key);

                    if (userClaim == null)
                    {
                        await _userManager.AddClaimAsync(user,
                            new Claim(addedClaim.Key, externalClaim.Value));
                        refreshSignIn = true;
                    }
                    else if (userClaim.Value != externalClaim.Value)
                    {
                        await _userManager
                            .ReplaceClaimAsync(user, userClaim, externalClaim);
                        refreshSignIn = true;
                    }
                }
                else if (userClaim == null)
                {
                    // Fill with a default value
                    await _userManager.AddClaimAsync(user, new Claim(addedClaim.Key,
                        addedClaim.Value));
                    refreshSignIn = true;
                }
            }

            if (refreshSignIn)
            {
                await _signInManager.RefreshSignInAsync(user);
            }
        }

        return LocalRedirect(returnUrl);
    }
    if (result.IsLockedOut)
    {
        return RedirectToPage("./Lockout");
    }
    else
    {
        // If the user does not have an account, then ask the user to create an account.
        ReturnUrl = returnUrl;
        ProviderDisplayName = info.ProviderDisplayName;
        if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            Input = new InputModel
            {
                Email = info.Principal.FindFirstValue(ClaimTypes.Email)
            };
        }
        return Page();
    }
}

Pendekatan serupa diambil ketika klaim berubah saat pengguna masuk tetapi langkah isi ulang tidak diperlukan. Untuk memperbarui klaim pengguna, panggil yang berikut ini pada pengguna:

Menghapus tindakan dan klaim klaim

ClaimActionCollection.Remove(String) menghapus semua tindakan klaim untuk yang diberikan ClaimType dari koleksi. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) menghapus klaim yang diberikan ClaimType dari identity. DeleteClaim terutama digunakan dengan OpenID Connect (OIDC) untuk menghapus klaim yang dihasilkan protokol.

Contoh output aplikasi

Jalankan aplikasi sampel dan pilih tautan MyClaims :

User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    someone@gmail.com
AspNet.Identity.SecurityStamp
    7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
    Judy
urn:google:locale
    en
urn:google:picture
    https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
    yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
    Bearer
.Token.expires_at
    2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
    4/11/2019 9:14:52 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.persistent
.issued
    Thu, 11 Apr 2019 20:51:06 GMT
.expires
    Thu, 25 Apr 2019 20:51:06 GMT

Meneruskan informasi permintaan dengan proksi atau penyeimbang beban

Jika aplikasi disebarkan di belakang server proksi atau penyeimbang beban, beberapa informasi permintaan asli mungkin diteruskan ke aplikasi di header permintaan. Informasi ini biasanya mencakup skema permintaan aman (https), host, dan alamat IP klien. Aplikasi tidak secara otomatis membaca header permintaan ini untuk menemukan dan menggunakan informasi permintaan asli.

Skema ini digunakan dalam pembuatan tautan yang memengaruhi alur autentikasi dengan penyedia eksternal. Kehilangan skema aman (https) akan menyebabkan aplikasi menghasilkan URL pengalihan tidak aman yang salah.

Pakai Forwarded Headers Middleware guna menyediakan informasi permintaan asli untuk aplikasi untuk pemrosesan permintaan.

Untuk informasi selengkapnya, lihat Mengonfigurasi ASP.NET Core untuk bekerja dengan server proxy dan memuat penyeimbang.

Melihat atau mengunduh kode sampel (cara mengunduh)

Aplikasi ASP.NET Core dapat membuat klaim dan token tambahan dari penyedia autentikasi eksternal, seperti Facebook, Google, Microsoft, dan Twitter. Setiap penyedia mengungkapkan informasi yang berbeda tentang pengguna di platformnya, tetapi pola untuk menerima dan mengubah data pengguna menjadi klaim tambahan sama.

Melihat atau mengunduh kode sampel (cara mengunduh)

Prasyarat

Tentukan penyedia autentikasi eksternal mana yang akan didukung di aplikasi. Untuk setiap penyedia, daftarkan aplikasi dan dapatkan ID klien dan rahasia klien. Untuk informasi selengkapnya, lihat Autentikasi Facebook dan Google di ASP.NET Core. Aplikasi sampel menggunakan penyedia autentikasi Google.

Mengatur ID klien dan rahasia klien

Penyedia autentikasi OAuth membuat hubungan kepercayaan dengan aplikasi menggunakan ID klien dan rahasia klien. ID Klien dan nilai rahasia klien dibuat untuk aplikasi oleh penyedia autentikasi eksternal saat aplikasi terdaftar di penyedia. Setiap penyedia eksternal yang digunakan aplikasi harus dikonfigurasi secara independen dengan ID klien penyedia dan rahasia klien. Untuk informasi selengkapnya, lihat topik penyedia autentikasi eksternal yang berlaku untuk skenario Anda:

Klaim opsional yang dikirim dalam ID atau token akses dari penyedia autentikasi biasanya dikonfigurasi di portal online penyedia. Misalnya, MICROSOFT Entra ID memungkinkan Anda menetapkan klaim opsional ke token ID aplikasi di bilah konfigurasi Token pendaftaran aplikasi. Untuk informasi selengkapnya, lihat Cara: Memberikan klaim opsional ke aplikasi Anda (dokumentasi Azure). Untuk penyedia lain, lihat kumpulan dokumentasi eksternal mereka.

Aplikasi sampel mengonfigurasi penyedia autentikasi Google dengan ID klien dan rahasia klien yang disediakan oleh Google:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Menetapkan cakupan autentikasi

Tentukan daftar izin yang akan diambil dari penyedia dengan menentukan Scope. Cakupan autentikasi untuk penyedia eksternal umum muncul dalam tabel berikut.

Penyedia Cakupan
Facebook https://www.facebook.com/dialog/oauth
Google profile, , emailopenid
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

Dalam aplikasi sampel, cakupan Google profile, email, dan openid secara otomatis ditambahkan oleh kerangka kerja saat AddGoogle dipanggil di AuthenticationBuilder. Jika aplikasi memerlukan cakupan tambahan, tambahkan ke opsi . Dalam contoh berikut, cakupan Google https://www.googleapis.com/auth/user.birthday.read ditambahkan untuk mengambil ulang tahun pengguna:

options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");

Memetakan kunci data pengguna dan membuat klaim

Dalam opsi penyedia, tentukan MapJsonKey atau MapJsonSubKey untuk setiap kunci/subkunci dalam data pengguna JSON penyedia eksternal agar aplikasi identity dapat dibaca saat masuk. Untuk informasi selengkapnya tentang jenis klaim, lihat ClaimTypes.

Aplikasi sampel membuat klaim lokal (urn:google:locale) dan gambar (urn:google:picture) dari locale kunci dan picture di data pengguna Google:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Di Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, IdentityUser (ApplicationUser) masuk ke aplikasi dengan SignInAsync. Selama proses masuk, UserManager<TUser> dapat menyimpan klaim untuk data pengguna yang ApplicationUser Principaltersedia dari .

Di aplikasi sampel, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) menetapkan klaim lokal (urn:google:locale) dan gambar (urn:google:picture) untuk yang masuk ApplicationUser, termasuk klaim untuk GivenName:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}

Secara default, klaim pengguna disimpan dalam autentikasi cookie. Jika autentikasi terlalu besar, autentikasi cookie dapat menyebabkan aplikasi gagal karena:

  • Browser mendeteksi bahwa cookie header terlalu panjang.
  • Ukuran keseluruhan permintaan terlalu besar.

Jika sejumlah besar data pengguna diperlukan untuk memproses permintaan pengguna:

  • Batasi jumlah dan ukuran klaim pengguna untuk pemrosesan permintaan hanya untuk apa yang dibutuhkan aplikasi.
  • Gunakan kustom ITicketStore untuk Cookie Middleware SessionStore Autentikasi untuk menyimpan identity di seluruh permintaan. Pertahankan sejumlah besar identity informasi di server sambil hanya mengirim kunci pengidentifikasi sesi kecil ke klien.

Menyimpan token akses

SaveTokens menentukan apakah token akses dan refresh harus disimpan setelah AuthenticationProperties otorisasi berhasil. SaveTokens diatur ke false secara default untuk mengurangi ukuran autentikasi cookieakhir .

Aplikasi sampel menetapkan nilai SaveTokens menjadi true di GoogleOptions:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Saat OnPostConfirmationAsync dijalankan, simpan token akses (ExternalLoginInfo.AuthenticationTokens) dari penyedia eksternal di ApplicationUser.AuthenticationProperties

Aplikasi sampel menyimpan token akses di OnPostConfirmationAsync (pendaftaran pengguna baru) dan OnGetCallbackAsync (pengguna yang terdaftar sebelumnya) di Account/ExternalLogin.cshtml.cs:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}

Catatan

Untuk informasi tentang meneruskan token ke Razor komponen aplikasi sisi Blazor server, lihat Skenario keamanan tambahan sisi server ASP.NET CoreBlazor.

Cara menambahkan token kustom tambahan

Untuk menunjukkan cara menambahkan token kustom, yang disimpan sebagai bagian SaveTokensdari , aplikasi sampel menambahkan AuthenticationToken dengan saat ini DateTime untuk AuthenticationToken.Name dari TicketCreated:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Membuat dan menambahkan klaim

Kerangka kerja menyediakan tindakan umum dan metode ekstensi untuk membuat dan menambahkan klaim ke koleksi. Untuk informasi selengkapnya, lihat ClaimActionCollectionMapExtensions dan ClaimActionCollectionUniqueExtensions.

Pengguna dapat menentukan tindakan kustom dengan berasal dari ClaimAction dan menerapkan metode abstrak Run .

Untuk informasi selengkapnya, lihat Microsoft.AspNetCore.Authentication.OAuth.Claims .

Menambahkan dan memperbarui klaim pengguna

Klaim disalin dari penyedia eksternal ke database pengguna pada pendaftaran pertama, bukan saat masuk. Jika klaim tambahan diaktifkan di aplikasi setelah pengguna mendaftar untuk menggunakan aplikasi, panggil SignInManager.RefreshSignInAsync pada pengguna untuk memaksa pembuatan autentikasi cookiebaru .

Di lingkungan Pengembangan yang bekerja dengan akun pengguna uji, Anda cukup menghapus dan membuat ulang akun pengguna. Untuk sistem produksi, klaim baru yang ditambahkan ke aplikasi dapat diisi ulang ke akun pengguna. Setelah membuat perancah ExternalLogin halaman ke dalam aplikasi di Areas/Pages/Identity/Account/Manage, tambahkan kode berikut ke ExternalLoginModel dalam ExternalLogin.cshtml.cs file.

Tambahkan kamus klaim tambahan. Gunakan kunci kamus untuk menahan jenis klaim, dan gunakan nilai untuk menyimpan nilai default. Tambahkan baris berikut ke bagian atas kelas. Contoh berikut mengasumsikan bahwa satu klaim ditambahkan untuk gambar Google pengguna dengan gambar headshot generik sebagai nilai default:

private readonly IReadOnlyDictionary<string, string> _claimsToSync = 
    new Dictionary<string, string>()
    {
        { "urn:google:picture", "https://localhost:5001/headshot.png" },
    };

Ganti kode OnGetCallbackAsync default metode dengan kode berikut. Kode mengulang melalui kamus klaim. Klaim ditambahkan (diisi ulang) atau diperbarui untuk setiap pengguna. Saat klaim ditambahkan atau diperbarui, rincian masuk pengguna di-refresh menggunakan SignInManager<TUser>, mempertahankan properti autentikasi yang ada (AuthenticationProperties).

public async Task<IActionResult> OnGetCallbackAsync(
    string returnUrl = null, string remoteError = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");

    if (remoteError != null)
    {
        ErrorMessage = $"Error from external provider: {remoteError}";

        return RedirectToPage("./Login", new {ReturnUrl = returnUrl });
    }

    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = "Error loading external login information.";
        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    // Sign in the user with this external login provider if the user already has a 
    // login.
    var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, 
        info.ProviderKey, isPersistent: false, bypassTwoFactor : true);

    if (result.Succeeded)
    {
        _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", 
            info.Principal.Identity.Name, info.LoginProvider);

        if (_claimsToSync.Count > 0)
        {
            var user = await _userManager.FindByLoginAsync(info.LoginProvider, 
                info.ProviderKey);
            var userClaims = await _userManager.GetClaimsAsync(user);
            bool refreshSignIn = false;

            foreach (var addedClaim in _claimsToSync)
            {
                var userClaim = userClaims
                    .FirstOrDefault(c => c.Type == addedClaim.Key);

                if (info.Principal.HasClaim(c => c.Type == addedClaim.Key))
                {
                    var externalClaim = info.Principal.FindFirst(addedClaim.Key);

                    if (userClaim == null)
                    {
                        await _userManager.AddClaimAsync(user, 
                            new Claim(addedClaim.Key, externalClaim.Value));
                        refreshSignIn = true;
                    }
                    else if (userClaim.Value != externalClaim.Value)
                    {
                        await _userManager
                            .ReplaceClaimAsync(user, userClaim, externalClaim);
                        refreshSignIn = true;
                    }
                }
                else if (userClaim == null)
                {
                    // Fill with a default value
                    await _userManager.AddClaimAsync(user, new Claim(addedClaim.Key, 
                        addedClaim.Value));
                    refreshSignIn = true;
                }
            }

            if (refreshSignIn)
            {
                await _signInManager.RefreshSignInAsync(user);
            }
        }

        return LocalRedirect(returnUrl);
    }

    if (result.IsLockedOut)
    {
        return RedirectToPage("./Lockout");
    }
    else
    {
        // If the user does not have an account, then ask the user to create an 
        // account.
        ReturnUrl = returnUrl;
        ProviderDisplayName = info.ProviderDisplayName;

        if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            Input = new InputModel
            {
                Email = info.Principal.FindFirstValue(ClaimTypes.Email)
            };
        }

        return Page();
    }
}

Pendekatan serupa diambil ketika klaim berubah saat pengguna masuk tetapi langkah isi ulang tidak diperlukan. Untuk memperbarui klaim pengguna, panggil yang berikut ini pada pengguna:

Penghapusan tindakan dan klaim klaim

ClaimActionCollection.Remove(String) menghapus semua tindakan klaim untuk yang diberikan ClaimType dari koleksi. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) menghapus klaim yang diberikan ClaimType dari identity. DeleteClaim terutama digunakan dengan OpenID Connect (OIDC) untuk menghapus klaim yang dihasilkan protokol.

Contoh output aplikasi

User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    someone@gmail.com
AspNet.Identity.SecurityStamp
    7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
    Judy
urn:google:locale
    en
urn:google:picture
    https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
    yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
    Bearer
.Token.expires_at
    2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
    4/11/2019 9:14:52 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.persistent
.issued
    Thu, 11 Apr 2019 20:51:06 GMT
.expires
    Thu, 25 Apr 2019 20:51:06 GMT

Meneruskan informasi permintaan dengan proksi atau penyeimbang beban

Jika aplikasi disebarkan di belakang server proksi atau penyeimbang beban, beberapa informasi permintaan asli mungkin diteruskan ke aplikasi di header permintaan. Informasi ini biasanya mencakup skema permintaan aman (https), host, dan alamat IP klien. Aplikasi tidak secara otomatis membaca header permintaan ini untuk menemukan dan menggunakan informasi permintaan asli.

Skema ini digunakan dalam pembuatan tautan yang memengaruhi alur autentikasi dengan penyedia eksternal. Kehilangan skema aman (https) akan menyebabkan aplikasi menghasilkan URL pengalihan tidak aman yang salah.

Pakai Forwarded Headers Middleware guna menyediakan informasi permintaan asli untuk aplikasi untuk pemrosesan permintaan.

Untuk informasi selengkapnya, lihat Mengonfigurasi ASP.NET Core untuk bekerja dengan server proxy dan memuat penyeimbang.

Aplikasi ASP.NET Core dapat membuat klaim dan token tambahan dari penyedia autentikasi eksternal, seperti Facebook, Google, Microsoft, dan Twitter. Setiap penyedia mengungkapkan informasi yang berbeda tentang pengguna di platformnya, tetapi pola untuk menerima dan mengubah data pengguna menjadi klaim tambahan sama.

Melihat atau mengunduh kode sampel (cara mengunduh)

Prasyarat

Tentukan penyedia autentikasi eksternal mana yang akan didukung di aplikasi. Untuk setiap penyedia, daftarkan aplikasi dan dapatkan ID klien dan rahasia klien. Untuk informasi selengkapnya, lihat Autentikasi Facebook dan Google di ASP.NET Core. Aplikasi sampel menggunakan penyedia autentikasi Google.

Mengatur ID klien dan rahasia klien

Penyedia autentikasi OAuth membuat hubungan kepercayaan dengan aplikasi menggunakan ID klien dan rahasia klien. ID Klien dan nilai rahasia klien dibuat untuk aplikasi oleh penyedia autentikasi eksternal saat aplikasi terdaftar di penyedia. Setiap penyedia eksternal yang digunakan aplikasi harus dikonfigurasi secara independen dengan ID klien penyedia dan rahasia klien. Untuk informasi selengkapnya, lihat topik penyedia autentikasi eksternal yang berlaku untuk skenario Anda:

Aplikasi sampel mengonfigurasi penyedia autentikasi Google dengan ID klien dan rahasia klien yang disediakan oleh Google:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Menetapkan cakupan autentikasi

Tentukan daftar izin yang akan diambil dari penyedia dengan menentukan Scope. Cakupan autentikasi untuk penyedia eksternal umum muncul dalam tabel berikut.

Penyedia Cakupan
Facebook https://www.facebook.com/dialog/oauth
Google https://www.googleapis.com/auth/userinfo.profile
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

Di aplikasi sampel, cakupan Google userinfo.profile secara otomatis ditambahkan oleh kerangka kerja saat AddGoogle dipanggil di AuthenticationBuilder. Jika aplikasi memerlukan cakupan tambahan, tambahkan ke opsi . Dalam contoh berikut, cakupan Google https://www.googleapis.com/auth/user.birthday.read ditambahkan untuk mengambil ulang tahun pengguna:

options.Scope.Add("https://www.googleapis.com/auth/user.birthday.read");

Memetakan kunci data pengguna dan membuat klaim

Dalam opsi penyedia, tentukan MapJsonKey atau MapJsonSubKey untuk setiap kunci/subkunci dalam data pengguna JSON penyedia eksternal agar aplikasi identity dapat dibaca saat masuk. Untuk informasi selengkapnya tentang jenis klaim, lihat ClaimTypes.

Aplikasi sampel membuat klaim lokal (urn:google:locale) dan gambar (urn:google:picture) dari locale kunci dan picture di data pengguna Google:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Di Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync, IdentityUser (ApplicationUser) masuk ke aplikasi dengan SignInAsync. Selama proses masuk, UserManager<TUser> dapat menyimpan klaim untuk data pengguna yang ApplicationUser Principaltersedia dari .

Di aplikasi sampel, OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) menetapkan klaim lokal (urn:google:locale) dan gambar (urn:google:picture) untuk yang masuk ApplicationUser, termasuk klaim untuk GivenName:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}

Secara default, klaim pengguna disimpan dalam autentikasi cookie. Jika autentikasi terlalu besar, autentikasi cookie dapat menyebabkan aplikasi gagal karena:

  • Browser mendeteksi bahwa cookie header terlalu panjang.
  • Ukuran keseluruhan permintaan terlalu besar.

Jika sejumlah besar data pengguna diperlukan untuk memproses permintaan pengguna:

  • Batasi jumlah dan ukuran klaim pengguna untuk pemrosesan permintaan hanya untuk apa yang dibutuhkan aplikasi.
  • Gunakan kustom ITicketStore untuk Cookie Middleware SessionStore Autentikasi untuk menyimpan identity di seluruh permintaan. Pertahankan sejumlah besar identity informasi di server sambil hanya mengirim kunci pengidentifikasi sesi kecil ke klien.

Menyimpan token akses

SaveTokens menentukan apakah token akses dan refresh harus disimpan setelah AuthenticationProperties otorisasi berhasil. SaveTokens diatur ke false secara default untuk mengurangi ukuran autentikasi cookieakhir .

Aplikasi sampel menetapkan nilai SaveTokens menjadi true di GoogleOptions:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Saat OnPostConfirmationAsync dijalankan, simpan token akses (ExternalLoginInfo.AuthenticationTokens) dari penyedia eksternal di ApplicationUser.AuthenticationProperties

Aplikasi sampel menyimpan token akses di OnPostConfirmationAsync (pendaftaran pengguna baru) dan OnGetCallbackAsync (pengguna yang terdaftar sebelumnya) di Account/ExternalLogin.cshtml.cs:

public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
    returnUrl = returnUrl ?? Url.Content("~/");
    // Get the information about the user from the external login provider
    var info = await _signInManager.GetExternalLoginInfoAsync();

    if (info == null)
    {
        ErrorMessage = 
            "Error loading external login information during confirmation.";

        return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
    }

    if (ModelState.IsValid)
    {
        var user = new IdentityUser
        {
            UserName = Input.Email, 
            Email = Input.Email 
        };

        var result = await _userManager.CreateAsync(user);

        if (result.Succeeded)
        {
            result = await _userManager.AddLoginAsync(user, info);

            if (result.Succeeded)
            {
                // If they exist, add claims to the user for:
                //    Given (first) name
                //    Locale
                //    Picture
                if (info.Principal.HasClaim(c => c.Type == ClaimTypes.GivenName))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst(ClaimTypes.GivenName));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:locale"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:locale"));
                }

                if (info.Principal.HasClaim(c => c.Type == "urn:google:picture"))
                {
                    await _userManager.AddClaimAsync(user, 
                        info.Principal.FindFirst("urn:google:picture"));
                }

                // Include the access token in the properties
                var props = new AuthenticationProperties();
                props.StoreTokens(info.AuthenticationTokens);
                props.IsPersistent = true;

                await _signInManager.SignInAsync(user, props);

                _logger.LogInformation(
                    "User created an account using {Name} provider.", 
                    info.LoginProvider);

                return LocalRedirect(returnUrl);
            }
        }

        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(string.Empty, error.Description);
        }
    }

    LoginProvider = info.LoginProvider;
    ReturnUrl = returnUrl;
    return Page();
}

Cara menambahkan token kustom tambahan

Untuk menunjukkan cara menambahkan token kustom, yang disimpan sebagai bagian SaveTokensdari , aplikasi sampel menambahkan AuthenticationToken dengan saat ini DateTime untuk AuthenticationToken.Name dari TicketCreated:

services.AddAuthentication().AddGoogle(options =>
{
    // Provide the Google Client ID
    options.ClientId = "XXXXXXXXXXXXXXX.apps.googleusercontent.com";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientId" "{Client ID}"

    // Provide the Google Client Secret
    options.ClientSecret = "{Client Secret}";
    // Register with User Secrets using:
    // dotnet user-secrets set "Authentication:Google:ClientSecret" "{Client Secret}"

    options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");
    options.ClaimActions.MapJsonKey("urn:google:locale", "locale", "string");
    options.SaveTokens = true;

    options.Events.OnCreatingTicket = ctx =>
    {
        List<AuthenticationToken> tokens = ctx.Properties.GetTokens().ToList(); 

        tokens.Add(new AuthenticationToken()
        {
            Name = "TicketCreated", 
            Value = DateTime.UtcNow.ToString()
        });

        ctx.Properties.StoreTokens(tokens);

        return Task.CompletedTask;
    };
});

Membuat dan menambahkan klaim

Kerangka kerja menyediakan tindakan umum dan metode ekstensi untuk membuat dan menambahkan klaim ke koleksi. Untuk informasi selengkapnya, lihat ClaimActionCollectionMapExtensions dan ClaimActionCollectionUniqueExtensions.

Pengguna dapat menentukan tindakan kustom dengan berasal dari ClaimAction dan menerapkan metode abstrak Run .

Untuk informasi selengkapnya, lihat Microsoft.AspNetCore.Authentication.OAuth.Claims .

Penghapusan tindakan dan klaim klaim

ClaimActionCollection.Remove(String) menghapus semua tindakan klaim untuk yang diberikan ClaimType dari koleksi. ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) menghapus klaim yang diberikan ClaimType dari identity. DeleteClaim terutama digunakan dengan OpenID Connect (OIDC) untuk menghapus klaim yang dihasilkan protokol.

Contoh output aplikasi

User Claims

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
    9b342344f-7aab-43c2-1ac1-ba75912ca999
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
    someone@gmail.com
AspNet.Identity.SecurityStamp
    7D4312MOWRYYBFI1KXRPHGOSTBVWSFDE
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
    Judy
urn:google:locale
    en
urn:google:picture
    https://lh4.googleusercontent.com/-XXXXXX/XXXXXX/XXXXXX/XXXXXX/photo.jpg

Authentication Properties

.Token.access_token
    yc23.AlvoZqz56...1lxltXV7D-ZWP9
.Token.token_type
    Bearer
.Token.expires_at
    2019-04-11T22:14:51.0000000+00:00
.Token.TicketCreated
    4/11/2019 9:14:52 PM
.TokenNames
    access_token;token_type;expires_at;TicketCreated
.persistent
.issued
    Thu, 11 Apr 2019 20:51:06 GMT
.expires
    Thu, 25 Apr 2019 20:51:06 GMT

Meneruskan informasi permintaan dengan proksi atau penyeimbang beban

Jika aplikasi disebarkan di belakang server proksi atau penyeimbang beban, beberapa informasi permintaan asli mungkin diteruskan ke aplikasi di header permintaan. Informasi ini biasanya mencakup skema permintaan aman (https), host, dan alamat IP klien. Aplikasi tidak secara otomatis membaca header permintaan ini untuk menemukan dan menggunakan informasi permintaan asli.

Skema ini digunakan dalam pembuatan tautan yang memengaruhi alur autentikasi dengan penyedia eksternal. Kehilangan skema aman (https) akan menyebabkan aplikasi menghasilkan URL pengalihan tidak aman yang salah.

Pakai Forwarded Headers Middleware guna menyediakan informasi permintaan asli untuk aplikasi untuk pemrosesan permintaan.

Untuk informasi selengkapnya, lihat Mengonfigurasi ASP.NET Core untuk bekerja dengan server proxy dan memuat penyeimbang.

Sumber Daya Tambahan:

  • aplikasi SocialSample rekayasa dotnet/AspNetCore: Aplikasi sampel yang ditautkan ada di cabang teknik repo main GitHub dotnet/AspNetCore. Cabang main berisi kode dalam pengembangan aktif untuk rilis ASP.NET Core berikutnya. Untuk melihat versi aplikasi sampel untuk versi ASP.NET Core yang dirilis, gunakan daftar drop-down Cabang untuk memilih cabang rilis (misalnya release/{X.Y}).