天天看點

IdentityServer4環境部署失敗分析貼(一)

前言:

在部署Idv4站點和其用戶端在外網時,發現了許多問題,折騰了許久,翻看了許多代碼,寫個MD記錄一下。

1.受保護站點提示錯誤: Unable to obtain configuration from: '[PII is hidden]'.

fail: Microsoft.AspNetCore.Server.Kestrel[13]
      Connection id "0HLL3MB34N0G5", Request id "0HLL3MB34N0G5:00000009": An unhandled exception was thrown by the application.
System.InvalidOperationException: IDX20803: Unable to obtain configuration from: '[PII is hidden]'.
   在 Microsoft.IdentityModel.Protocols.ConfigurationManager`1.<GetConfigurationAsync>d__24.MoveNext()
--- 引發異常的上一位置中堆棧跟蹤的末尾 ---
   在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.<HandleChallengeAsync>d__18.MoveNext()
--- 引發異常的上一位置中堆棧跟蹤的末尾 ---
   在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Microsoft.AspNetCore.Authentication.AuthenticationHandler`1.<ChallengeAsync>d__54.MoveNext()
--- 引發異常的上一位置中堆棧跟蹤的末尾 ---
           
1.1 受保護站點的代碼執行流程
graph TB
User--1.進入-->A(Home/Index)
A--2.Authorize-->AA{是否授權}
AA--YES-->完事
AA--NO-->B(傳回ChallengeResult)
B--3.ExecuteResult-->C(進入AuthenticationMiddleware中間件)
C--4.根據authenticationScheme找到相應Handler-->D(OpenIDConenctHandler)
D--5.調用HandleChallengeAsync-->F(調用OpenIdConnectConfigurationRetriever.IConfigurationRetriever)
F--6.通過HTTP調用Idv4站點的/.well-known/openid-configuration-->F1(Idv4Config)
F1--7.通過上步驟取到的config.jwks_uri擷取加密key資訊-->F2(獲得公鑰私鑰等資訊)
F2--8.設定JsonWebKeySet-->F3(完成openIdConnectConfiguration.SigningKeysKeys的添加)
F3-->F4(完成)
           
IdentityServer4環境部署失敗分析貼(一)
1.1 受保護站點的部分源碼

Idv4部分源碼

//注入Idev相關,配置authenticationScheme為oidc
public static IIdentityServerBuilder AddIdentityServer(this IServiceCollection services, Action<IdentityServerOptions> setupAction)
{
    services.Configure(setupAction);
    return services.AddIdentityServer();
}
           

PS: 這裡要額外提一下,在Abp的模闆項目中,如下配置使用Idv4可能無法正常跳轉IDV4授權站完成單點登入

services.AddOpenIdConnect("oidc", options =>
{
   ...
}
           

需要額外添加下面這段,原因是Abp引入Microsoft.Identity,會重複設定schme導緻錯誤,需如下代碼添加設定。

services.AddAuthentication(options =>
{
        options.DefaultAuthenticateScheme = "oidc";
        options.DefaultChallengeScheme = "oidc";
        options.DefaultSignOutScheme = "oidc";
})
           

步驟5部分源碼

protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            Logger.EnteringOpenIdAuthenticationHandlerHandleUnauthorizedAsync(GetType().FullName);

            // order for local RedirectUri
            // 1. challenge.Properties.RedirectUri
            // 2. CurrentUri if RedirectUri is not set)
            if (string.IsNullOrEmpty(properties.RedirectUri))
            {
                properties.RedirectUri = CurrentUri;
            }
            Logger.PostAuthenticationLocalRedirect(properties.RedirectUri);

            if (_configuration == null && Options.ConfigurationManager != null)
            {
                //問題在這裡
                _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
            }
            ...
           

步驟6部分源碼

public static async Task<OpenIdConnectConfiguration> GetAsync(string address, IDocumentRetriever retriever, CancellationToken cancel)
        {
            if (string.IsNullOrWhiteSpace(address))
                throw LogHelper.LogArgumentNullException(nameof(address));

            if (retriever == null)
            {
                throw LogHelper.LogArgumentNullException(nameof(retriever));
            }
            //當站點沒啟動的時候,這裡可能出現問題
            string doc = await retriever.GetDocumentAsync(address, cancel).ConfigureAwait(false);

            LogHelper.LogVerbose(LogMessages.IDX21811, doc);
            OpenIdConnectConfiguration openIdConnectConfiguration = JsonConvert.DeserializeObject<OpenIdConnectConfiguration>(doc);
            if (!string.IsNullOrEmpty(openIdConnectConfiguration.JwksUri))
            {
                LogHelper.LogVerbose(LogMessages.IDX21812, openIdConnectConfiguration.JwksUri);
                //當證書配置或讀取錯誤的時候,這裡會出現問題
                string keys = await retriever.GetDocumentAsync(openIdConnectConfiguration.JwksUri, cancel).ConfigureAwait(false);

                LogHelper.LogVerbose(LogMessages.IDX21813, openIdConnectConfiguration.JwksUri);
                openIdConnectConfiguration.JsonWebKeySet = JsonConvert.DeserializeObject<JsonWebKeySet>(keys);
                foreach (SecurityKey key in openIdConnectConfiguration.JsonWebKeySet.GetSigningKeys())
                {
                    openIdConnectConfiguration.SigningKeys.Add(key);
                }
            }

            return openIdConnectConfiguration;
        }
           

通過流程進行問題解析:

通過檢視日志可發現在流程5後開始報錯

步驟一

通路:http://{ssoHost}/.well-known/openid-configuration

如果通路成功,則排除單點登入站點部署失敗問題,前往步驟二

如果通路失敗,則去解決單點登入站點部署問題。

步驟二

http://{ssoHost}/.well-known/openid-configuration/jwks

如果通路成功,則重新開機用戶端

如果通路失敗,那麼有意思了...繼續往下看

此時單點登入站點的日志

[10:25:24 Error] Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware
An unhandled exception has occurred while executing the request.
Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: 出現了内部錯誤。
   at Internal.Cryptography.Helpers.OpenStorageProvider(CngProvider provider)
   at System.Security.Cryptography.CngKey.Import(Byte[] keyBlob, String curveName, CngKeyBlobFormat format, CngProvider provider)
   at System.Security.Cryptography.CngKey.Import(Byte[] keyBlob, CngKeyBlobFormat format)
   at Internal.Cryptography.Pal.X509Pal.DecodePublicKey(Oid oid, Byte[] encodedKeyValue, Byte[] encodedParameters, ICertificatePal certificatePal)
   at System.Security.Cryptography.X509Certificates.PublicKey.get_Key()
   at Microsoft.IdentityModel.Tokens.X509SecurityKey.get_PublicKey()
   at IdentityServer4.ResponseHandling.DiscoveryResponseGenerator.CreateJwkDocumentAsync()
   at IdentityServer4.Endpoints.DiscoveryKeyEndpoint.ProcessAsync(HttpContext context)
   at IdentityServer4.Hosting.IdentityServerMiddleware.Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events)
   at IdentityServer4.Hosting.IdentityServerMiddleware.Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at IdentityServer4.Hosting.BaseUrlMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.Invoke(HttpContext context)
           
單點登入站點在此時的工作流程
graph TB

A(通過Http擷取發現服務的配置資訊)--1.進入SSO站點-->B(IdentityServerMiddleware)
B--2.調用Process-->C(進入DiscoveryKeyEndpoint)
C--3.周遊注入的加密Key-->D(傳回JsonBWebKey)
           
部分源碼解析:
IdentityServer4環境部署失敗分析貼(一)
public async Task Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events)
        {
            // this will check the authentication session and from it emit the check session
            // cookie needed from JS-based signout clients.
            await session.EnsureSessionIdCookieAsync();

            try
            {
                //根據請求路徑,進入相應endpoint處理器
                var endpoint = router.Find(context);
                if (endpoint != null)
                {
                    _logger.LogInformation("Invoking IdentityServer endpoint: {endpointType} for {url}", endpoint.GetType().FullName, context.Request.Path.ToString());

                    var result = await endpoint.ProcessAsync(context);

                ...
           
//建立JwtDocument
  public virtual async Task<IEnumerable<Models.JsonWebKey>> CreateJwkDocumentAsync()
        {
            var webKeys = new List<Models.JsonWebKey>();
            var signingCredentials = await Keys.GetSigningCredentialsAsync();
            var algorithm = signingCredentials?.Algorithm ?? Constants.SigningAlgorithms.RSA_SHA_256;

            foreach (var key in await Keys.GetValidationKeysAsync())
            {
                if (key is X509SecurityKey x509Key)
                {
                    var cert64 = Convert.ToBase64String(x509Key.Certificate.RawData);
                    var thumbprint = Base64Url.Encode(x509Key.Certificate.GetCertHash());
                    //當證書讀取不正常的時候,PublicKey的屬性構造器中會報錯。
                    var pubKey = x509Key.PublicKey as RSA;
                    var parameters = pubKey.ExportParameters(false);
                    var exponent = Base64Url.Encode(parameters.Exponent);
                    var modulus = Base64Url.Encode(parameters.Modulus);

                    var webKey = new Models.JsonWebKey
                    {
                        kty = "RSA",
                        use = "sig",
                        kid = x509Key.KeyId,
                        x5t = thumbprint,
                        e = exponent,
                        n = modulus,
                        x5c = new[] { cert64 },
                        alg = algorithm
                    };

                    webKeys.Add(webKey);
                    continue;
                }

           
問題解析

通過日志可發現,問題出現在PublicKey的擷取上,即證書的讀取失敗了。

解決方案!

終于到解決問題的時候了...

步驟一:

檢查證書是否是臨時證書

//在正式環境中,這可能會報錯
builder.AddDeveloperSigningCredential(true, "tempkey.rsa");

//正确方式
builder.AddSigningCredential(new X509Certificate2(path,
                         Configuration["Certificates:Password"]))
這裡可以參見郭的随筆:
https://www.cnblogs.com/guolianyu/p/9872661.html                        
           

步驟二:

如果已經添加了證書,且證書路徑正确讀取,且證書密碼正确;

但是!在Windows Server2008伺服器上部署仍失敗時,請檢查 CNG Key Isolation 服務是否啟動!

如果 該服務未啟動,則啟動後即可!

IdentityServer4環境部署失敗分析貼(一)

如果該服務已啟動,則尴尬的很...如果您仍排除不了問題,可以評論告訴我

繼續閱讀