# .net OpenIdConnection setting :::info authOptions 這個物件是我另外建立的,從 appsettings.json 中取資料的 model OAuth Server = IdentityServer4 .net core 3.1 / .net 5 / .net 6 / .net 7 或以上皆通用 ::: ```csharp 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"); ```