刷新令牌Blazor服务器端openId连接



我正在努力在Blazor服务器端应用程序中获得认证,以按预期工作。

我一直在遵循这个文档,并添加了注册的范围服务:ASP。. NET Core Blazor Server附加安全方案

_Host.cshtml我从HttpContext获取令牌:

var tokens = new InitialApplicationState
{
AccessToken = await HttpContext.GetTokenAsync("access_token"),
RefreshToken = await HttpContext.GetTokenAsync("refresh_token"),
IdToken = await HttpContext.GetTokenAsync("id_token"),
ExpiresAtUtc = expiryTime.UtcDateTime,
};

然后将它们传递给App.razor

protected override Task OnInitializedAsync()
{
var shouldSetInitialValues = TokenProvider.AccessToken is null || TokenProvider.RefreshToken is null;
if (shouldSetInitialValues)
{
TokenProvider.AccessToken = InitialState!.AccessToken;
TokenProvider.RefreshToken = InitialState.RefreshToken;
TokenProvider.IdToken = InitialState.IdToken;
TokenProvider.ExpiresAtUtc = InitialState.ExpiresAtUtc;
}
return base.OnInitializedAsync();
}

我遇到的问题是,有时HttpContext中的AccessToken已经过期。发生这种情况时,我只想刷新令牌。我已经编写了一些代码,以确保当AccessToken存在时令牌是有效的。

但是每次发送新的请求,或者导航到不同的页面时,TokenProvider被清除,因此shouldSetInitialValues总是设置为true。然后一个过期的AccessToken总是被传递到TokenProvider.

如何更新过期的AccessToken ?

我通过更新存储在HttpContext中的令牌来解决这个问题。首先,我试着用一个新创建的razor页面来做这件事,但后来在重定向用户时遇到了一些问题。

最后,我把更新令牌的逻辑直接放在_Host.cshtml中。

编辑:

Inside of _Host。cshtml检查令牌是否过期:

@{
Layout = "_Layout";
var tokenExpiry = await HttpContext.GetTokenAsync("expires_at");
DateTimeOffset.TryParse(tokenExpiry, out var expiresAt);
var accessToken = await HttpContext.GetTokenAsync("access_token");
var tokenShouldBeRefreshed = accessToken != null && expiresAt < DateTime.UtcNow.AddMinutes(20);
if (tokenShouldBeRefreshed)
{
await RefreshAccessTokenAsync();
}
accessToken = await HttpContext.GetTokenAsync("access_token");
var refreshToken = await HttpContext.GetTokenAsync("refresh_token");
}
<component type="typeof(App)" param-InitialAccessToken="accessToken" param-InitialRefreshToken="refreshToken" render-mode="Server" />

方法RefreshAccessTokenAsync()看起来像这样:

async Task RefreshAccessTokenAsync()
{
var auth = await HttpContext.AuthenticateAsync();
if (!auth.Succeeded)
{
await HttpContext.SignOutAsync();
return;
}
var injectedIOptionsHere= injectedIOptionsHere.Value;
var refreshToken = await HttpContext.GetTokenAsync("refresh_token");
if (refreshToken == null)
{
await HttpContext.SignOutAsync();
return;
}
var httpClient = HttpClientFactory.CreateClient();
var refreshTokenUrl = $"{injectedIOptionsHere.Authority}/common/oauth/tokens?";
var postData = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("client_id", injectedIOptionsHere.ClientId!),
new KeyValuePair<string, string>("client_secret", injectedIOptionsHere.ClientSecret!),
new KeyValuePair<string, string>("refresh_token", refreshToken!),
new KeyValuePair<string, string>("redirect_url", injectedIOptionsHere.RedirectUrl!),
};
var content = new FormUrlEncodedContent(postData);
HttpResponseMessage? responseMessage = await httpClient.PostAsync(refreshTokenUrl, content);
responseMessage.EnsureSuccessStatusCode();
var responseJson = await responseMessage.Content.ReadAsStringAsync();
var responseJObject = JObject.Parse(responseJson);
var newAccessToken = responseJObject.GetStringValue("access_token");
var expiresInSeconds = responseJObject.GetIntValue("expires_in");
var newExpiryTime = DateTime.UtcNow.AddSeconds(expiresInSeconds).ToString(CultureInfo.InvariantCulture);
var expiresAtTokenUpdated = auth.Properties!.UpdateTokenValue("expires_at", newExpiryTime);
var accessTokenUpdated = auth.Properties!.UpdateTokenValue("access_token", newAccessToken);
var tokensUpdatedCorrectly = expiresAtTokenUpdated && accessTokenUpdated;
if (tokensUpdatedCorrectly)
{
await HttpContext.SignInAsync(auth.Principal, auth.Properties);
}
}

希望这对你有帮助!

以下中间件可用于更新令牌。

public class TokenRefreshMiddleware
{
private readonly RequestDelegate _next;
public TokenRefreshMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context, IHttpClientFactory httpClientFactory)
{
var authenticateResult = await context.AuthenticateAsync();
if (authenticateResult.Succeeded)
{
var accessTokenExpiration = authenticateResult.Properties.GetTokenValue("expires_at");
if (!string.IsNullOrEmpty(accessTokenExpiration))
{
var expiration = DateTimeOffset.Parse(accessTokenExpiration, CultureInfo.InvariantCulture);
if (expiration <= DateTimeOffset.UtcNow)
{
var refreshToken = authenticateResult.Properties.GetTokenValue("refresh_token");
if (!string.IsNullOrEmpty(refreshToken))
{
var openIdConnectOptions = context.RequestServices.GetRequiredService<IOptionsSnapshot<OpenIdConnectOptions>>().Get(OpenIdConnectDefaults.AuthenticationScheme);
var tokenResponse = await RenewAccessTokenAsync(refreshToken, httpClientFactory, openIdConnectOptions.Value);
if (tokenResponse != null)
{
var newExpiration = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn);
authenticateResult.Properties.UpdateTokenValue("access_token", tokenResponse.AccessToken);
authenticateResult.Properties.UpdateTokenValue("expires_at", newExpiration.ToString("o", CultureInfo.InvariantCulture));
authenticateResult.Properties.UpdateTokenValue("refresh_token", tokenResponse.RefreshToken);
await context.SignInAsync(authenticateResult.Principal, authenticateResult.Properties);
}
}
}
}
}
await _next(context);
}
private async Task<TokenResponse> RenewAccessTokenAsync(string refreshToken, IHttpClientFactory httpClientFactory, OpenIdConnectOptions options)
{
var tokenClient = httpClientFactory.CreateClient();
var tokenEndpoint = options.Authority + "/protocol/openid-connect/token";
var tokenRequest = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint)
{
Content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "refresh_token"),
new KeyValuePair<string, string>("client_id", options.ClientId),
new KeyValuePair<string, string>("client_secret", options.ClientSecret),
new KeyValuePair<string, string>("refresh_token", refreshToken)
})
};
var response = await tokenClient.SendAsync(tokenRequest);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var tokenResponse = JsonSerializer.Deserialize<TokenResponse>(content);
return tokenResponse;
}
return null;
}
}
public class TokenResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("expires_in")]
public int ExpiresIn { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
}

我是这样配置我的身份验证的:

builder.Services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = _ => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = builder.Configuration.GetValue<string>("Oidc:Authority");
options.ClientId = builder.Configuration.GetValue<string>("Oidc:ClientId");
options.ClientSecret = builder.Configuration.GetValue<string>("Oidc:ClientSecret");
options.RequireHttpsMetadata =
builder.Configuration.GetValue<bool>("Oidc:RequireHttpsMetadata"); // disable only in dev env
options.ResponseType = OpenIdConnectResponseType.Code;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.MapInboundClaims = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
options.Events = new OpenIdConnectEvents
{
OnUserInformationReceived = context =>
{
if (context.Principal?.Identity is not ClaimsIdentity claimsIdentity) return Task.CompletedTask;
var accessToken = context.ProtocolMessage.AccessToken;
if (!string.IsNullOrEmpty(accessToken))
{
claimsIdentity.AddClaim(new Claim("access_token", accessToken));
}

var refreshToken = context.ProtocolMessage.RefreshToken;
if (!string.IsNullOrEmpty(refreshToken))
{
claimsIdentity.AddClaim(new Claim("refresh_token", refreshToken));
}

if (context.User.RootElement.TryGetProperty("preferred_username", out var username))
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, username.ToString()));
}
var parsedToken = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
var realmAccess = parsedToken.Claims.FirstOrDefault(c => c.Type == "realm_access");
if (realmAccess == null)
{
return Task.CompletedTask;
}
var roles = JObject.Parse(realmAccess.Value).GetValue("roles")?.ToObject<string[]>() ?? Array.Empty<string>();
foreach (var role in roles)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto
});
app.UseCookiePolicy(new CookiePolicyOptions
{
Secure = CookieSecurePolicy.Always
});
app.UseAuthentication();
app.UseMiddleware<TokenRefreshMiddleware>();
app.UseAuthorization();

最新更新