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

w6lpcovy  于 2023-05-02  发布在  .NET
关注(0)|答案(2)|浏览(163)

我正在努力让Blazor服务器端应用程序中的身份验证按预期工作。
我一直在遵循这个文档,并添加了注册的范围服务:ASP.NET Core Blazor Server additional security scenarios
_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过期后如何更新?

a1o7rhls

a1o7rhls1#

我通过更新存储在HttpContext中的令牌来解决这个问题。首先,我试图用一个新创建的剃刀页面来做这件事,但后来在重定向用户时遇到了一些问题。
最后,我将更新令牌的逻辑直接放在_Host中。cshtml。
编辑:
在_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);
        }
    }

希望这有帮助!

k5hmc34c

k5hmc34c2#

以下中间件可用于续订令牌。

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();

相关问题