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");
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up