Try   HackMD

.net OpenIdConnection setting

authOptions 這個物件是我另外建立的,從 appsettings.json 中取資料的 model

OAuth Server = IdentityServer4

.net core 3.1 / .net 5 / .net 6 / .net 7 或以上皆通用

builder.Services.AddAuthentication(options =>
       {
           options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
           options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;

           options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
           options.DefaultForbidScheme = CookieAuthenticationDefaults.AuthenticationScheme;
           options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
           options.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
       })
       .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
       {
           // 有 HA 問題可以參考這個
           // ref: https://mikerussellnz.github.io/.NET-Core-Auth-Ticket-Redis/
           options.SessionStore = new RedisCacheTicketStore(new RedisCacheOptions { Configuration = redisUrl });

           options.Events = new CookieAuthenticationEvents
           {
               OnValidatePrincipal = async cookieContext =>
               {
                   var logger = cookieContext.HttpContext.RequestServices.GetRequiredService<ILogger<CookieAuthenticationEvents>>();

                   /*
                    * cookieContext.Properties.GetTokenValue(key)
                    * cookieContext.Properties.UpdateTokenValue(key)
                    * key 可使用的參數值有
                    * 1. access_token
                    * 2. id_token       = openId connection 驗證身分所需的 token,預設 5 分鐘過期
                    * 3. refresh_token
                    * 4. token_type
                    * 5. expires_at     = access_token 的到期日
                    * 對應 OpenIdConnect 的參數物件
                    * OpenIdConnectParameterNames.AccessToken
                    * OpenIdConnectParameterNames.IdToken
                    * OpenIdConnectParameterNames.RefreshToken
                    * OpenIdConnectParameterNames.TokenType
                    *
                    * expires_at 的部分在 OpenIdConnectParameterNames 無對應
                    *
                    * cookieContext.Properties.IssuedUtc = 跟 OAuth Server 進行驗證的時間
                    *
                    * cookieContext.Properties.ExpiresUtc
                    *   * cookies 有效期的時間
                    *   * 如果 AddOpenIdConnect 裡面有設定 UseTokenLifeTime 的話,這個時間會使用 id_token 的過期時間
                    */

                   var now = DateTimeOffset.UtcNow;
                   var expiresAt = cookieContext.Properties.GetTokenValue("expires_at");
                   var accessTokenExpiration = DateTimeOffset.Parse(expiresAt);

                   var timeRemaining = accessTokenExpiration.Subtract(now);

                   // TODO: Get this from configuration with a fallback value.
                   var refreshThresholdMinutes = 5;
                   var refreshThreshold = TimeSpan.FromMinutes(refreshThresholdMinutes);

                   if (timeRemaining < refreshThreshold)
                   {
                       logger.LogTrace("OnValidatePrincipal - should be refresh token");

                       var refreshToken = cookieContext.Properties.GetTokenValue(OpenIdConnectParameterNames.RefreshToken);

                       if (refreshToken is null)
                       {
                           logger.LogWarning("OnValidatePrincipal - Refresh Token Not Found!");

                           cookieContext.RejectPrincipal();
                           await cookieContext.HttpContext.SignOutAsync();
                           //登出後強制中斷事件,讓 .net 回去走登入流程
                           return;
                       }

                       var httpClient = cookieContext.HttpContext.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient();
                       var response = await httpClient.RequestRefreshTokenAsync(
                                          new RefreshTokenRequest
                                          {
                                              Address = $"{authOptions.Authority}/connect/token",
                                              ClientId = authOptions.ClientId,
                                              ClientSecret = authOptions.ClientSecret,
                                              RefreshToken = refreshToken!
                                          });

                       //如果 refresh 錯誤就登出且強制中斷事件
                       if (response.IsError)
                       {
                           logger.LogWarning("OnValidatePrincipal - Token Refresh Error!");

                           cookieContext.RejectPrincipal();
                           await cookieContext.HttpContext.SignOutAsync();
                           return;
                       }

                       //can ref OpenIdConnectHandler
                       var expiresInSeconds = response.ExpiresIn;
                       var updatedExpiresAt = DateTimeOffset.UtcNow.AddSeconds(expiresInSeconds);

                       cookieContext.Properties.UpdateTokenValue("expires_at", updatedExpiresAt.ToString());
                       cookieContext.Properties.UpdateTokenValue(OpenIdConnectParameterNames.AccessToken, response.AccessToken);
                       cookieContext.Properties.UpdateTokenValue(OpenIdConnectParameterNames.RefreshToken, response.RefreshToken);
                       cookieContext.Properties.UpdateTokenValue(OpenIdConnectParameterNames.IdToken, response.IdentityToken);

                       // Indicate to the cookie middleware that the cookie should be
                       // remade (since we have updated it)
                       cookieContext.ShouldRenew = true;
                   }
               }
           };
       })
       .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
       {
           options.Authority = authOptions.Authority;
           options.ClientId = authOptions.ClientId;
           options.ClientSecret = authOptions.ClientSecret;

           // 如果要改 redirect url 的時候要用這個
           // options.CallbackPath = "/auth-redirect-url";

           options.RequireHttpsMetadata = true;
           options.ResponseType = OpenIdConnectResponseType.Code;
           // options.ResponseMode = OpenIdConnectResponseMode.FormPost;

           // 沒有清除的話,預設 scope 裡面有一個 profile 的項目
           //options.Scope.Clear();

           //要讓 .net 預設的 openid 認證成功需要這個 Scope
           options.Scope.Add(OpenIdConnectScope.OpenId);

           // 從設定檔中取得自訂的 Scope
           foreach (var item in authOptions.CustomScope)
           {
               options.Scope.Add(item);
           }

           // 這個 scope 要求 Auth Server 回應 Refresh Token
           options.Scope.Add(OpenIdConnectScope.OfflineAccess);

           // if true , cookies ExpiresUtc will be use id_token expires time
           // options.UseTokenLifetime = true;

           options.SaveTokens = true;

           //設定 token 中的特定欄位解析方式
           options.TokenValidationParameters = new TokenValidationParameters
           {
               // NameClaimType = "name",
               NameClaimType = JwtClaimTypes.Name,
               // RoleClaimType = "role"
               RoleClaimType = JwtClaimTypes.Role
           };

           // 設定到 user claim 裡面
           //ref: https://github.com/skoruba/IdentityServer4.Admin/issues/109
           //ref: https://stackoverflow.com/a/70279411
           options.Events.OnUserInformationReceived = context =>
           {
               // var roleElement = context.User.RootElement.GetProperty("role");
               var roleElement = context.User.RootElement.GetProperty(JwtClaimTypes.Role);

               var claims = new List<Claim>();

               if (roleElement.ValueKind == JsonValueKind.Array)
               {
                   claims.AddRange(roleElement.EnumerateArray().Select(r => new Claim(JwtClaimTypes.Role, r.GetString() ?? string.Empty)));
               }
               else
               {
                   claims.Add(new Claim(JwtClaimTypes.Role, roleElement.GetString() ?? string.Empty));
               }

               if (context.Principal?.Identity is ClaimsIdentity id)
               {
                   id.AddClaims(claims);
               }

               return Task.CompletedTask;
           };

           //..other setting
       });

//有 HA 需求可參考
//設定認證資料存放到 Redis 上面共用
//此範例未特別設定加密,基於安全性,應考慮加密問題
//REF: https://docs.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-6.0#share-authentication-cookies-among-aspnet-core-apps
//REF: https://docs.microsoft.com/zh-tw/aspnet/core/security/data-protection/implementation/key-storage-providers?view=aspnetcore-6.0&tabs=visual-studio#redis
builder.Services
       .AddDataProtection()
       .PersistKeysToStackExchangeRedis(ConnectionMultiplexer.Connect(redisUrl), "LoginKey:")
       .SetApplicationName("Sample");