自訂.NET Core Identity身份驗證和授權教學:OAuth 和 Oidc 自訂.NET Core Identity身份驗證和授權教學:OAuth 和 Oidc

Published on Sunday, April 30, 2023

OAuth

在之前的文章中我們利用微軟提供的 Package Microsoft.AspNetCore.Authentication.Google 成功實踐了 Google 外部登入 我們只要在 Program.csIServiceCollection 呼叫 AddGoogle 方法即可,接下來的流程就交給內建的 SignInManager 處理

能夠做到這樣開箱即用是因為微軟已經幫我們建立了 GoogleOptionsGoogleHandler 這兩個 Class 並且底層使用 OAuth2.0 protocol 正是因為大多數的外部登入方式底層都能兼容 OAuth2.0 所以各家登入廠家不需要再次發明自己的登入流程 在Github 上可以看到官方提供的四種方式 Google, Facebook, MicrosoftAccount, Twitter 底層其實都是用到 OAuth

public static AuthenticationBuilder AddGoogle(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<GoogleOptions> configureOptions)
        => builder.AddOAuth<GoogleOptions, GoogleHandler>(authenticationScheme, displayName, configureOptions);
        
public static AuthenticationBuilder AddFacebook(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<FacebookOptions> configureOptions)
        => builder.AddOAuth<FacebookOptions, FacebookHandler>(authenticationScheme, displayName, configureOptions);

public static AuthenticationBuilder AddMicrosoftAccount(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<MicrosoftAccountOptions> configureOptions)
        => builder.AddOAuth<MicrosoftAccountOptions, MicrosoftAccountHandler>(authenticationScheme, displayName, configureOptions);

 public static AuthenticationBuilder AddTwitter(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<TwitterOptions> configureOptions)
{
    builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TwitterOptions>, TwitterPostConfigureOptions>());
    return builder.AddRemoteScheme<TwitterOptions, TwitterHandler>(authenticationScheme, displayName, configureOptions);
}

從這邊可以看出一個問題那就是每添加一個登入方式最少就要添加一個 Options 和 Handler 我們需要詳細閱讀各家登入的文檔才能夠寫出這套流程
我們可以參考 GoogleOptionsFacebookOptions 的區別,因為 OAuth 沒有強制規範的原因所以會造成各家跟各家的方法都有些微差異
有些資料可以在 Facebook 取得但 Google 沒有辦法取得,所以這個問題會造成不小的困擾

public GoogleOptions()
{
    CallbackPath = new PathString("/signin-google");
    AuthorizationEndpoint = GoogleDefaults.AuthorizationEndpoint;
    TokenEndpoint = GoogleDefaults.TokenEndpoint;
    UserInformationEndpoint = GoogleDefaults.UserInformationEndpoint;
    UsePkce = true;
    Scope.Add("openid");
    Scope.Add("profile");
    Scope.Add("email");

    ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); // v2
    ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub"); // v3
    ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
    ClaimActions.MapJsonKey(ClaimTypes.GivenName, "given_name");
    ClaimActions.MapJsonKey(ClaimTypes.Surname, "family_name");
    ClaimActions.MapJsonKey("urn:google:profile", "link");
    ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
}
public FacebookOptions()
{
    CallbackPath = new PathString("/signin-facebook");
    SendAppSecretProof = true;
    AuthorizationEndpoint = FacebookDefaults.AuthorizationEndpoint;
    TokenEndpoint = FacebookDefaults.TokenEndpoint;
    UserInformationEndpoint = FacebookDefaults.UserInformationEndpoint;
    UsePkce = true;
    Scope.Add("email");
    Fields.Add("name");
    Fields.Add("email");
    Fields.Add("first_name");
    Fields.Add("last_name");

    ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
    ClaimActions.MapJsonSubKey("urn:facebook:age_range_min", "age_range", "min");
    ClaimActions.MapJsonSubKey("urn:facebook:age_range_max", "age_range", "max");
    ClaimActions.MapJsonKey(ClaimTypes.DateOfBirth, "birthday");
    ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
    ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
    ClaimActions.MapJsonKey(ClaimTypes.GivenName, "first_name");
    ClaimActions.MapJsonKey("urn:facebook:middle_name", "middle_name");
    ClaimActions.MapJsonKey(ClaimTypes.Surname, "last_name");
    ClaimActions.MapJsonKey(ClaimTypes.Gender, "gender");
    ClaimActions.MapJsonKey("urn:facebook:link", "link");
    ClaimActions.MapJsonSubKey("urn:facebook:location", "location", "name");
    ClaimActions.MapJsonKey(ClaimTypes.Locality, "locale");
    ClaimActions.MapJsonKey("urn:facebook:timezone", "timezone");
}

OIDC

為了解決 OAuth 的短板所有就有了 OpenID Connect(OIDC) 的出現,因為 OIDC 是建立在 OAuth 之上的並且解決許多問題 所以我們可以將目前程式的 OAuth 方法直接轉換成 OIDC 方式,接下來實做看看

首先先添加 OpenIdConnect Package 到專案內

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

註解原本的 AddGoogle 方法改用 AddOpenIdConnect

.AddOpenIdConnect("Google", o =>
{
    o.Authority = "https://accounts.google.com";
    o.ClientId = configuration["Authentication:Google:ClientId"];
    o.ClientSecret = configuration["Authentication:Google:ClientSecret"];
    o.ResponseType = OpenIdConnectResponseType.Code;
    o.GetClaimsFromUserInfoEndpoint = true;
    o.Scope.Add("openid");
    o.Scope.Add("email");

    o.CallbackPath = "/signin-google";
    o.SaveTokens = true;
    o.SignInScheme = IdentityConstants.ExternalScheme;
});

這邊需要注意 CallbackPath 這邊修改成 /signin-google 是因為當出在申請 ClientId 時有在 Google 網頁上填入網址 https://localhost:7011/signin-google 因為 oidc 預設的路徑為 https://localhost:7011/signin-oidc 所以你可以選擇回到當初申請的頁面額外添加一筆 https://localhost:7011/signin-oidc 或者直接修改 CallbackPath 也能達到同樣的效果

我們經程式運行起來之後呼叫 ExternalSignin 方法進行測試,也能正常的進行轉跳以及使用者授權,我們並不需要在額外修改我們的商業邏輯


Summary

改成使用 OIDC 之後我們基本上就不需要在研究各家的文檔了因為現在只要各家廠商都按照 OIDC 的 Protocol 來寫即可
並且我們可以架設自己的 OIDC 伺服器就像 Google 與 Facebook 提供的登入頁面那樣,我們自己架設伺服器可以用來給 API 與 API 之間進行授權,架設 OIDC 伺服器在微服務框架中基本上是必備的

今天的進度 Github