在 ASP.NET Core 中保存外部提供者的其他宣告和權杖

ASP.NET Core 應用程式可以從外部驗證提供者建立額外的宣告和權杖,例如 Facebook、Google、Microsoft 和 Twitter。 每個提供者都會在其平台上顯示使用者的不同資訊,但接收和轉換使用者資料到其他宣告的模式相同。

必要條件

決定要在應用程式中支援哪些外部驗證提供者。 針對每個提供者,註冊應用程式並取得用戶端識別碼和用戶端密碼。 如需詳細資訊,請參閱 ASP.NET Core 中的 Facebook 和 Google 驗證。 範例應用程式會使用 Google 驗證提供者

設定用戶端識別碼和用戶端密碼

OAuth 驗證提供者會使用用戶端識別碼和用戶端密碼,與應用程式建立信任關係。 向提供者註冊應用程式時,外部驗證提供者會為應用程式建立用戶端識別碼和用戶端密碼值。 應用程式使用的每個外部提供者都必須使用提供者的用戶端識別碼和用戶端密碼獨立進行設定。 如需詳細資訊,請參閱適用的外部驗證提供者主題:

來自驗證提供者的識別碼或存取權杖中傳送的選擇性宣告通常是在提供者的線上入口網站中所設定。 例如,Microsoft Azure Active Directory (AAD) 可讓在應用程式註冊的 [權杖設定] 刀鋒視窗中,將選擇性宣告指派至應用程式的識別碼權杖。 如需詳細資訊,請參閱使用方式:為您的應用程式提供選擇性宣告 (Azure 文件)。 若為其他提供者,請參閱其外部文件集。

範例應用程式會使用 Google 所提供的用戶端識別碼和用戶端密碼來設定 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.

建立驗證範圍

透過指定 Scope 來指定要從提供者擷取的權限清單。 常見外部提供者的驗證範圍會出現在下表中。

Provider 範圍
Facebook https://www.facebook.com/dialog/oauth
Google profileemailopenid
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

在範例應用程式中,當在 AuthenticationBuilder 上呼叫 AddGoogle 時,Google 的 profileemailopenid 範圍會自動新增。 如果應用程式需要其他範圍,請將其新增至選項。 在下列範例中,會新增 Google https://www.googleapis.com/auth/user.birthday.read 範圍來擷取使用者的生日:

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

對應使用者資料索引鍵並建立宣告

在提供者的選項中,針對外部提供者 JSON 使用者資料中的每個金鑰或子機碼指定 MapJsonKeyMapJsonSubKey,讓應用程式身分識別在登入時讀取。 如需宣告類型的詳細資訊,請參閱 ClaimTypes

範例應用程式會從 Google 使用者資料中的 localepicture 金鑰建立地區設定 (urn:google:locale) 和圖片 (urn:google:picture) 宣告:

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

Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync中,IdentityUser (ApplicationUser) 會使用 SignInAsync 登入應用程式。 在登入流程中,UserManager<TUser> 可以儲存 ApplicationUser 宣告,以便從 Principal 取得可用的使用者資料。

在範例應用程式中,OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) 會為登入的 ApplicationUser 建立地區設定 (urn:google:locale) 和圖片(urn:google:picture) 宣告,包括 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();
}

根據預設,使用者的宣告會儲存在驗證 cookie 中。 如果驗證 cookie 過大,可能會導致應用程式失敗,因為:

  • 瀏覽器會偵測 cookie 標頭過長。
  • 要求的整體大小過大。

如果在處理使用者要求時需要大量使用者資料:

  • 將要求處理的使用者宣告數目和大小限制在應用程式所需的範圍內。
  • 針對 Cookie 驗證中介軟體的 SessionStore 使用自訂 ITicketStore,跨要求儲存身分識別。 在伺服器上保留大量的身分識別資訊,同時只將小型工作階段識別碼金鑰傳送給用戶端。

儲存存取權杖

SaveTokens 定義了在成功授權之後,是否應該將存取和重新整理權杖儲存在 AuthenticationProperties 中。 SaveTokens 預設會設為 false,以減少最終驗證 cookie 的大小。

範例應用程式會將 GoogleOptionsSaveTokens 的值設定為 true

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

執行 OnPostConfirmationAsync 時,將來自外部提供者的存取權杖 (ExternalLoginInfo.AuthenticationTokens) 儲存在 ApplicationUserAuthenticationProperties 中。

範例應用程式會將存取權杖儲存在 Account/ExternalLogin.cshtml.cs 中的 OnPostConfirmationAsync (新的使用者註冊) 和 OnGetCallbackAsync (先前已註冊的使用者) 內:

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

注意

如需將權杖傳遞至伺服器端 Blazor 應用程式 Razor 元件的詳細資訊,請參閱伺服器端 ASP.NET Core Blazor 其他安全性案例

如何新增其他自訂權杖

為了示範如何新增自訂權杖 (其儲存為 SaveTokens 的一部分),範例應用程式會針對 TicketCreatedAuthenticationToken.Name 新增目前帶有 DateTimeAuthenticationToken

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

建立和新增宣告

架構提供用於建立宣告和向集合新增宣告的常見動作和擴充方法。 如需詳細資訊,請參閱 ClaimActionCollectionMapExtensionsClaimActionCollectionUniqueExtensions

使用者可以藉由衍生自 ClaimAction 的項目來定義自訂動作並實作抽象 Run 方法。

如需詳細資訊,請參閱Microsoft.AspNetCore.Authentication.OAuth.Claims

新增和更新使用者宣告

宣告會在第一次註冊時 (而不是登入時) 從外部提供者複製到使用者資料庫。 如果使用者註冊使用應用程式之後,在應用程式中啟用其他宣告,請在使用者上呼叫 SignInManager.RefreshSignInAsync,以強制產生新的驗證 cookie。

在使用測試使用者帳戶的開發環境中,刪除並重新建立使用者帳戶。 針對生產系統,新增至應用程式的新宣告可以回填至使用者帳戶。 在 Areas/Pages/Identity/Account/ManageExternalLogin 頁面 Scaffolding 到應用程式中之後,請將下列程式碼新增至 ExternalLogin.cshtml.cs 檔案中的 ExternalLoginModel

新增已新增宣告的字典。 使用字典索引鍵來保存宣告類型,並使用值來保存預設值。 將下列這一行加入至類別的頂端。 下列範例假設已為使用者的 Google 圖片新增一個宣告,並將一般大頭照影像新增為預設值:

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

以下列程式碼取代 OnGetCallbackAsync 方法的預設程式碼。 程式碼會迴圈查看宣告字典。 系統會針對每個使用者新增宣告 (回填) 或更新。 新增或更新宣告時,會使用 SignInManager<TUser> 來重新整理使用者登入,並保留現有的驗證屬性 (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();
    }
}

當使用者登入時宣告變更,但不需要回填步驟時,就會採用類似的方法。 若要更新使用者的宣告,請在使用者上呼叫下列命令:

移除宣告動作和宣告

ClaimActionCollection.Remove (字串) 會從集合中移除指定 ClaimType 的所有宣告動作。 ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) 從身分識別中刪除指定 ClaimType 的宣告。 DeleteClaim 主要用於與 OpenID Connect 物件識別 (OID) 搭配使用,以移除通訊協定產生的宣告。

範例應用程式輸出

執行範例應用程式並選取 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

使用 Proxy 或負載平衡器轉送要求資訊

如果將應用程式部署於 Proxy 伺服器或負載平衡器後方,可能就會在要求標頭中將一些原始要求資訊轉送到應用程式。 此資訊通常會包括安全要求配置 (https)、主機和用戶端 IP 位址。 應用程式不會自動讀取這些要求標頭來探索並使用原始要求資訊。

此配置可用於產生連結,其會對使用外部提供者的驗證流程產生影響。 遺失安全配置 (https) 會導致應用程式產生不正確且不安全的重新導向 URL。

使用轉送標頭中介軟體,使應用程式能夠使用原始要求資訊來處理要求。

如需詳細資訊,請參閱設定 ASP.NET Core 以處理 Proxy 伺服器和負載平衡器

檢視或下載範例程式碼 \(英文\) (如何下載)

ASP.NET Core 應用程式可以從外部驗證提供者建立額外的宣告和權杖,例如 Facebook、Google、Microsoft 和 Twitter。 每個提供者都會在其平台上顯示使用者的不同資訊,但接收和轉換使用者資料到其他宣告的模式相同。

檢視或下載範例程式碼 \(英文\) (如何下載)

必要條件

決定要在應用程式中支援哪些外部驗證提供者。 針對每個提供者,註冊應用程式並取得用戶端識別碼和用戶端密碼。 如需詳細資訊,請參閱 ASP.NET Core 中的 Facebook 和 Google 驗證。 範例應用程式會使用 Google 驗證提供者

設定用戶端識別碼和用戶端密碼

OAuth 驗證提供者會使用用戶端識別碼和用戶端密碼,與應用程式建立信任關係。 向提供者註冊應用程式時,外部驗證提供者會為應用程式建立用戶端識別碼和用戶端密碼值。 應用程式使用的每個外部提供者都必須使用提供者的用戶端識別碼和用戶端密碼獨立進行設定。 如需詳細資訊,請參閱適用於您案例的外部驗證提供者主題:

來自驗證提供者的識別碼或存取權杖中傳送的選擇性宣告通常是在提供者的線上入口網站中所設定。 例如,Microsoft Azure Active Directory (AAD) 可讓您在應用程式註冊的 [權杖設定] 刀鋒視窗中,將選擇性宣告指派至應用程式的識別碼權杖。 如需詳細資訊,請參閱使用方式:為您的應用程式提供選擇性宣告 (Azure 文件)。 若為其他提供者,請參閱其外部文件集。

範例應用程式會使用 Google 所提供的用戶端識別碼和用戶端密碼來設定 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;
    };
});

建立驗證範圍

透過指定 Scope 來指定要從提供者擷取的權限清單。 常見外部提供者的驗證範圍會出現在下表中。

Provider 範圍
Facebook https://www.facebook.com/dialog/oauth
Google profileemailopenid
Microsoft https://login.microsoftonline.com/common/oauth2/v2.0/authorize
Twitter https://api.twitter.com/oauth/authenticate

在範例應用程式中,當在 AuthenticationBuilder 上呼叫 AddGoogle 時,Google 的 profileemailopenid 範圍會自動新增。 如果應用程式需要其他範圍,請將其新增至選項。 在下列範例中,會新增 Google https://www.googleapis.com/auth/user.birthday.read 範圍來擷取使用者的生日:

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

對應使用者資料索引鍵並建立宣告

在提供者的選項中,針對外部提供者 JSON 使用者資料中的每個金鑰/子機碼指定 MapJsonKeyMapJsonSubKey,讓應用程式身分識別在登入時讀取。 如需宣告類型的詳細資訊,請參閱 ClaimTypes

範例應用程式會從 Google 使用者資料中的 localepicture 金鑰建立地區設定 (urn:google:locale) 和圖片 (urn:google:picture) 宣告:

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

Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync中,IdentityUser (ApplicationUser) 會使用 SignInAsync 登入應用程式。 在登入流程中,UserManager<TUser> 可以儲存 ApplicationUser 宣告,以便從 Principal 取得可用的使用者資料。

在範例應用程式中,OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) 會為登入的 ApplicationUser 建立地區設定 (urn:google:locale) 和圖片(urn:google:picture) 宣告,包括 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();
}

根據預設,使用者的宣告會儲存在驗證 cookie 中。 如果驗證 cookie 過大,可能會導致應用程式失敗,因為:

  • 瀏覽器會偵測 cookie 標頭過長。
  • 要求的整體大小過大。

如果在處理使用者要求時需要大量使用者資料:

  • 將要求處理的使用者宣告數目和大小限制在應用程式所需的範圍內。
  • 針對 Cookie 驗證中介軟體的 SessionStore 使用自訂 ITicketStore,跨要求儲存身分識別。 在伺服器上保留大量的身分識別資訊,同時只將小型工作階段識別碼金鑰傳送給用戶端。

儲存存取權杖

SaveTokens 定義了在成功授權之後,是否應該將存取和重新整理權杖儲存在 AuthenticationProperties 中。 SaveTokens 預設會設為 false,以減少最終驗證 cookie 的大小。

範例應用程式會將 GoogleOptionsSaveTokens 的值設定為 true

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

執行 OnPostConfirmationAsync 時,將來自外部提供者的存取權杖 (ExternalLoginInfo.AuthenticationTokens) 儲存在 ApplicationUserAuthenticationProperties 中。

範例應用程式會將存取權杖儲存在 Account/ExternalLogin.cshtml.cs 中的 OnPostConfirmationAsync (新的使用者註冊) 和 OnGetCallbackAsync (先前已註冊的使用者) 內:

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

注意

如需將權杖傳遞至伺服器端 Blazor 應用程式 Razor 元件的詳細資訊,請參閱伺服器端 ASP.NET Core Blazor 其他安全性案例

如何新增其他自訂權杖

為了示範如何新增自訂權杖 (其儲存為 SaveTokens 的一部分),範例應用程式會針對 TicketCreatedAuthenticationToken.Name 新增目前帶有 DateTimeAuthenticationToken

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

建立和新增宣告

架構提供用於建立宣告和向集合新增宣告的常見動作和擴充方法。 如需詳細資訊,請參閱 ClaimActionCollectionMapExtensionsClaimActionCollectionUniqueExtensions

使用者可以藉由衍生自 ClaimAction 的項目來定義自訂動作並實作抽象 Run 方法。

如需詳細資訊,請參閱Microsoft.AspNetCore.Authentication.OAuth.Claims

新增和更新使用者宣告

宣告會在第一次註冊時 (而不是登入時) 從外部提供者複製到使用者資料庫。 如果使用者註冊使用應用程式之後,在應用程式中啟用其他宣告,請在使用者上呼叫 SignInManager.RefreshSignInAsync,以強制產生新的驗證 cookie。

在使用測試使用者帳戶的開發環境中,您可以直接刪除並重新建立使用者帳戶。 針對生產系統,新增至應用程式的新宣告可以回填至使用者帳戶。 在 Areas/Pages/Identity/Account/ManageExternalLogin 頁面 Scaffolding 到應用程式中之後,請將下列程式碼新增至 ExternalLogin.cshtml.cs 檔案中的 ExternalLoginModel

新增已新增宣告的字典。 使用字典索引鍵來保存宣告類型,並使用值來保存預設值。 將下列這一行加入至類別的頂端。 下列範例假設已為使用者的 Google 圖片新增一個宣告,並將一般大頭照影像新增為預設值:

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

以下列程式碼取代 OnGetCallbackAsync 方法的預設程式碼。 程式碼會迴圈查看宣告字典。 系統會針對每個使用者新增宣告 (回填) 或更新。 新增或更新宣告時,會使用 SignInManager<TUser> 來重新整理使用者登入,並保留現有的驗證屬性 (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();
    }
}

當使用者登入時宣告變更,但不需要回填步驟時,就會採用類似的方法。 若要更新使用者的宣告,請在使用者上呼叫下列命令:

移除宣告動作和宣告

ClaimActionCollection.Remove (字串) 會從集合中移除指定 ClaimType 的所有宣告動作。 ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) 從身分識別中刪除指定 ClaimType 的宣告。 DeleteClaim 主要用於與 OpenID Connect 物件識別 (OID) 搭配使用,以移除通訊協定產生的宣告。

範例應用程式輸出

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

使用 Proxy 或負載平衡器轉送要求資訊

如果將應用程式部署於 Proxy 伺服器或負載平衡器後方,可能就會在要求標頭中將一些原始要求資訊轉送到應用程式。 此資訊通常會包括安全要求配置 (https)、主機和用戶端 IP 位址。 應用程式不會自動讀取這些要求標頭來探索並使用原始要求資訊。

此配置可用於產生連結,其會對使用外部提供者的驗證流程產生影響。 遺失安全配置 (https) 會導致應用程式產生不正確且不安全的重新導向 URL。

使用轉送標頭中介軟體,使應用程式能夠使用原始要求資訊來處理要求。

如需詳細資訊,請參閱設定 ASP.NET Core 以處理 Proxy 伺服器和負載平衡器

ASP.NET Core 應用程式可以從外部驗證提供者建立額外的宣告和權杖,例如 Facebook、Google、Microsoft 和 Twitter。 每個提供者都會在其平台上顯示使用者的不同資訊,但接收和轉換使用者資料到其他宣告的模式相同。

檢視或下載範例程式碼 \(英文\) (如何下載)

必要條件

決定要在應用程式中支援哪些外部驗證提供者。 針對每個提供者,註冊應用程式並取得用戶端識別碼和用戶端密碼。 如需詳細資訊,請參閱 ASP.NET Core 中的 Facebook 和 Google 驗證。 範例應用程式會使用 Google 驗證提供者

設定用戶端識別碼和用戶端密碼

OAuth 驗證提供者會使用用戶端識別碼和用戶端密碼,與應用程式建立信任關係。 向提供者註冊應用程式時,外部驗證提供者會為應用程式建立用戶端識別碼和用戶端密碼值。 應用程式使用的每個外部提供者都必須使用提供者的用戶端識別碼和用戶端密碼獨立進行設定。 如需詳細資訊,請參閱適用於您案例的外部驗證提供者主題:

範例應用程式會使用 Google 所提供的用戶端識別碼和用戶端密碼來設定 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;
    };
});

建立驗證範圍

透過指定 Scope 來指定要從提供者擷取的權限清單。 常見外部提供者的驗證範圍會出現在下表中。

Provider 範圍
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

在範例應用程式中,當在 AuthenticationBuilder 上呼叫 AddGoogle 時,Google 的 userinfo.profile 範圍會自動新增。 如果應用程式需要其他範圍,請將其新增至選項。 在下列範例中,會新增 Google https://www.googleapis.com/auth/user.birthday.read 範圍,以擷取使用者的生日:

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

對應使用者資料索引鍵並建立宣告

在提供者的選項中,針對外部提供者 JSON 使用者資料中的每個金鑰/子機碼指定 MapJsonKeyMapJsonSubKey,讓應用程式身分識別在登入時讀取。 如需宣告類型的詳細資訊,請參閱 ClaimTypes

範例應用程式會從 Google 使用者資料中的 localepicture 金鑰建立地區設定 (urn:google:locale) 和圖片 (urn:google:picture) 宣告:

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

Microsoft.AspNetCore.Identity.UI.Pages.Account.Internal.ExternalLoginModel.OnPostConfirmationAsync中,IdentityUser (ApplicationUser) 會使用 SignInAsync 登入應用程式。 在登入流程中,UserManager<TUser> 可以儲存 ApplicationUser 宣告,以便從 Principal 取得可用的使用者資料。

在範例應用程式中,OnPostConfirmationAsync (Account/ExternalLogin.cshtml.cs) 會為登入的 ApplicationUser 建立地區設定 (urn:google:locale) 和圖片(urn:google:picture) 宣告,包括 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();
}

根據預設,使用者的宣告會儲存在驗證 cookie 中。 如果驗證 cookie 過大,可能會導致應用程式失敗,因為:

  • 瀏覽器會偵測 cookie 標頭過長。
  • 要求的整體大小過大。

如果在處理使用者要求時需要大量使用者資料:

  • 將要求處理的使用者宣告數目和大小限制在應用程式所需的範圍內。
  • 針對 Cookie 驗證中介軟體的 SessionStore 使用自訂 ITicketStore,跨要求儲存身分識別。 在伺服器上保留大量的身分識別資訊,同時只將小型工作階段識別碼金鑰傳送給用戶端。

儲存存取權杖

SaveTokens 定義了在成功授權之後,是否應該將存取和重新整理權杖儲存在 AuthenticationProperties 中。 SaveTokens 預設會設為 false,以減少最終驗證 cookie 的大小。

範例應用程式會將 GoogleOptionsSaveTokens 的值設定為 true

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

執行 OnPostConfirmationAsync 時,將來自外部提供者的存取權杖 (ExternalLoginInfo.AuthenticationTokens) 儲存在 ApplicationUserAuthenticationProperties 中。

範例應用程式會將存取權杖儲存在 Account/ExternalLogin.cshtml.cs 中的 OnPostConfirmationAsync (新的使用者註冊) 和 OnGetCallbackAsync (先前已註冊的使用者) 內:

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

如何新增其他自訂權杖

為了示範如何新增自訂權杖 (其儲存為 SaveTokens 的一部分),範例應用程式會針對 TicketCreatedAuthenticationToken.Name 新增目前帶有 DateTimeAuthenticationToken

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

建立和新增宣告

架構提供用於建立宣告和向集合新增宣告的常見動作和擴充方法。 如需詳細資訊,請參閱 ClaimActionCollectionMapExtensionsClaimActionCollectionUniqueExtensions

使用者可以藉由衍生自 ClaimAction 的項目來定義自訂動作並實作抽象 Run 方法。

如需詳細資訊,請參閱Microsoft.AspNetCore.Authentication.OAuth.Claims

移除宣告動作和宣告

ClaimActionCollection.Remove (字串) 會從集合中移除指定 ClaimType 的所有宣告動作。 ClaimActionCollectionMapExtensions.DeleteClaim(ClaimActionCollection, String) 從身分識別中刪除指定 ClaimType 的宣告。 DeleteClaim 主要用於與 OpenID Connect 物件識別 (OID) 搭配使用,以移除通訊協定產生的宣告。

範例應用程式輸出

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

使用 Proxy 或負載平衡器轉送要求資訊

如果將應用程式部署於 Proxy 伺服器或負載平衡器後方,可能就會在要求標頭中將一些原始要求資訊轉送到應用程式。 此資訊通常會包括安全要求配置 (https)、主機和用戶端 IP 位址。 應用程式不會自動讀取這些要求標頭來探索並使用原始要求資訊。

此配置可用於產生連結,其會對使用外部提供者的驗證流程產生影響。 遺失安全配置 (https) 會導致應用程式產生不正確且不安全的重新導向 URL。

使用轉送標頭中介軟體,使應用程式能夠使用原始要求資訊來處理要求。

如需詳細資訊,請參閱設定 ASP.NET Core 以處理 Proxy 伺服器和負載平衡器

其他資源