刷新的令牌Blazor Server端OpenID Connect

发布于 2025-02-09 16:19:10 字数 1640 浏览 2 评论 0原文

我正在努力在Blazor Server端应用程序中获得身份验证,以按预期工作。

我一直在遵循此文档,并添加了注册范围的服务: 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中的访问已到期。发生这种情况时,我只想刷新令牌。我有适当的代码,以确保在存在访问时,令牌是有效的。

但是,每次发送新请求,或者在导航到另一个页面时,tokenProvider都会被清除,因此shordSetSetInitialValues始终设置为true。 然后,始终将过期的访问权传递到TokenProvider中。

一旦过期,如何更新访问权限?

I'm having struggling to get the authentication in a Blazor server side app to work as expected.

I have been following this documentation, and added registered the scoped service: ASP.NET Core Blazor Server additional security scenarios

From _Host.cshtml I am fetching the tokens from 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,
            };

They are then passed to 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();
    }

The problem I am having, is that sometimes the AccessToken in the HttpContext has expired. When this happens, I simply want to refresh the token. I have code in place that makes sure that the token is valid when an AccessToken is present.

But every time a new request is sent, or when navigating to a different page, the TokenProvider is cleared, so shouldSetInitialValuesis always set to true.
Then a expired AccessToken is always passed into the TokenProvider.

How do I update the AccessToken once expired?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

漆黑的白昼 2025-02-16 16:19:10

我通过更新存储在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);
        }
    }

希望这会有所帮助!

I solved this by updating the tokens stored in the HttpContext instead.
First I was trying to do this with a newly created razor page, but ran into some issues with redirecting the user afterwards.

Eventually I placed the logic for updating the tokens directly in the _Host.cshtml.

EDIT:

Inside of _Host.cshtml I check if the token has expired:

@{
    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" />

The method RefreshAccessTokenAsync() looks something like this:

 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 = 
quot;{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);
        }
    }

Hope this helps!

江南月 2025-02-16 16:19:10

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

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

The following middlerware can be used to renew the token.

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; }
}

This is how I configured my authentication:

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();
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文