-
-
Notifications
You must be signed in to change notification settings - Fork 305
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update the Dantooine sample to detect expired tokens and support toke…
…n refreshing
- Loading branch information
1 parent
0d2fb7c
commit ee86345
Showing
7 changed files
with
219 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingDelegatingHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
using System; | ||
using System.Globalization; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Net.Http.Headers; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using OpenIddict.Client; | ||
using static OpenIddict.Abstractions.OpenIddictConstants; | ||
using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants; | ||
using static OpenIddict.Client.OpenIddictClientModels; | ||
|
||
namespace Dantooine.WebAssembly.Server.Helpers | ||
{ | ||
internal sealed class TokenRefreshingDelegatingHandler( | ||
OpenIddictClientService service, HttpMessageHandler innerHandler) : DelegatingHandler(innerHandler) | ||
{ | ||
private readonly OpenIddictClientService _service = service ?? throw new ArgumentNullException(nameof(service)); | ||
|
||
protected override async Task<HttpResponseMessage> SendAsync( | ||
HttpRequestMessage request, CancellationToken cancellationToken) | ||
{ | ||
// If an access token expiration date was returned by the authorization server and stored | ||
// in the authentication cookie, use it to determine whether the token is about to expire. | ||
// If it's not, try to use it: if the resource server returns a 401 error response, try | ||
// to refresh the tokens before replaying the request with the new access token attached. | ||
var date = GetBackchannelAccessTokenExpirationDate(request.Options); | ||
if (date is null || DateTimeOffset.UtcNow <= date?.AddMinutes(-5)) | ||
{ | ||
request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, GetBackchannelAccessToken(request.Options)); | ||
|
||
var response = await base.SendAsync(request, cancellationToken); | ||
if (response.StatusCode is not HttpStatusCode.Unauthorized) | ||
{ | ||
return response; | ||
} | ||
|
||
// Note: this handler can be called concurrently for the same user if multiple HTTP | ||
// requests are processed in parallel: while this results in multiple refresh token | ||
// requests being sent concurrently, this is something OpenIddict allows during a short | ||
// period of time (called refresh token reuse leeway and set to 30 seconds by default). | ||
var result = await _service.AuthenticateWithRefreshTokenAsync(new RefreshTokenAuthenticationRequest | ||
{ | ||
CancellationToken = cancellationToken, | ||
DisableUserinfo = true, | ||
RefreshToken = GetRefreshToken(request.Options) | ||
}); | ||
|
||
request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, result.AccessToken); | ||
|
||
return new TokenRefreshingHttpResponseMessage(result, await base.SendAsync(request, cancellationToken)); | ||
} | ||
|
||
// Otherwise, don't bother using the existing access token and refresh tokens immediately. | ||
else | ||
{ | ||
var result = await _service.AuthenticateWithRefreshTokenAsync(new RefreshTokenAuthenticationRequest | ||
{ | ||
CancellationToken = cancellationToken, | ||
DisableUserinfo = true, | ||
RefreshToken = GetRefreshToken(request.Options) | ||
}); | ||
|
||
request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, result.AccessToken); | ||
|
||
return new TokenRefreshingHttpResponseMessage(result, await base.SendAsync(request, cancellationToken)); | ||
} | ||
|
||
static string GetBackchannelAccessToken(HttpRequestOptions options) => | ||
options.TryGetValue(new(Tokens.BackchannelAccessToken), out string token) ? token : | ||
throw new InvalidOperationException("The access token couldn't be found in the request options."); | ||
|
||
static DateTimeOffset? GetBackchannelAccessTokenExpirationDate(HttpRequestOptions options) => | ||
options.TryGetValue(new(Tokens.BackchannelAccessTokenExpirationDate), out string token) && | ||
DateTimeOffset.TryParse(token, CultureInfo.InvariantCulture, out DateTimeOffset date) ? date : null; | ||
|
||
static string GetRefreshToken(HttpRequestOptions options) => | ||
options.TryGetValue(new(Tokens.RefreshToken), out string token) ? token : | ||
throw new InvalidOperationException("The refresh token couldn't be found in the request options."); | ||
} | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
...ntooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingForwarderHttpClientFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
using System; | ||
using System.Net.Http; | ||
using OpenIddict.Client; | ||
using Yarp.ReverseProxy.Forwarder; | ||
|
||
namespace Dantooine.WebAssembly.Server.Helpers | ||
{ | ||
internal sealed class TokenRefreshingForwarderHttpClientFactory(OpenIddictClientService service) : ForwarderHttpClientFactory | ||
{ | ||
private readonly OpenIddictClientService _service = service ?? throw new ArgumentNullException(nameof(service)); | ||
|
||
protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) | ||
{ | ||
ArgumentNullException.ThrowIfNull(context); | ||
ArgumentNullException.ThrowIfNull(handler); | ||
|
||
return new TokenRefreshingDelegatingHandler(_service, handler); | ||
} | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
samples/Dantooine/Dantooine.WebAssembly.Server/Helpers/TokenRefreshingHttpResponseMessage.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
using System; | ||
using System.Net.Http; | ||
using static OpenIddict.Client.OpenIddictClientModels; | ||
|
||
namespace Dantooine.WebAssembly.Server.Helpers | ||
{ | ||
internal sealed class TokenRefreshingHttpResponseMessage : HttpResponseMessage | ||
{ | ||
public TokenRefreshingHttpResponseMessage(RefreshTokenAuthenticationResult result, HttpResponseMessage response) | ||
{ | ||
ArgumentNullException.ThrowIfNull(response); | ||
ArgumentNullException.ThrowIfNull(result); | ||
|
||
RefreshTokenAuthenticationResult = result; | ||
|
||
Content = response.Content; | ||
StatusCode = response.StatusCode; | ||
Version = response.Version; | ||
|
||
foreach (var header in response.Headers) | ||
{ | ||
Headers.Add(header.Key, header.Value); | ||
} | ||
} | ||
|
||
public RefreshTokenAuthenticationResult RefreshTokenAuthenticationResult { get; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters