I think what we ended up with is mostly based on Auth0 ASP.NET Core Web API SDK Quickstarts: Authorization
Thanks for sharing that with the rest of community!
@Tim_Bassett I donāt know am I suppose to do that. Because the same API is used by two different Angular applications. It may effect the existing application. Is there any way to do that from blazor client side application by implementing Custom Authorization Message Handler?
Anyhow I will give a try this option too.
@Tim_Bassett I have resolved it on my own trails by implementing a few custom handlers. I have taken a AuthorizationMessageHandler.cs class file and added it in my wasm client project with CustomAuthorizationMessageHandler.cs this name and modified the SendAsync method with my own logic to get identity token from the session storage and adding that token in the request authorization header.
Code Snippet: CustomAuthorizationMessageHandler.cs
public class CustomAuthorizationMessageHandler : DelegatingHandler
{
private readonly IAccessTokenProvider _provider;
private readonly NavigationManager _navigation;
private readonly IJSRuntime jsRuntime;
private AuthenticationHeaderValue _cachedHeader;
private Uri[] _authorizedUris;
private AccessTokenRequestOptions _tokenOptions;
private CachedAuthSettings authSettings;
private CachedOidcUser _user;
private DateTime? _tokenExpiresAt;
/// <summary>
/// Initializes a new instance of <see cref="AuthorizationMessageHandler"/>.
/// </summary>
/// <param name="provider">The <see cref="IAccessTokenProvider"/> to use for provisioning tokens.</param>
/// <param name="navigation">The <see cref="NavigationManager"/> to use for performing redirections.</param>
public CustomAuthorizationMessageHandler(
IAccessTokenProvider provider,
NavigationManager navigation,
IJSRuntime jsRuntime)
{
_provider = provider;
_navigation = navigation;
this.jsRuntime = jsRuntime;
}
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var now = DateTimeOffset.Now;
var utcDefaultDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc);
if (_authorizedUris == null)
{
throw new InvalidOperationException($"The '{nameof(AuthorizationMessageHandler)}' is not configured. " +
$"Call '{nameof(AuthorizationMessageHandler.ConfigureHandler)}' and provide a list of endpoint urls to attach the token to.");
}
if (_authorizedUris.Any(uri => uri.IsBaseOf(request.RequestUri)))
{
if (_user == null || _tokenExpiresAt == null || now >= _tokenExpiresAt?.AddMinutes(-5))
{
string key = "Microsoft.AspNetCore.Components.WebAssembly.Authentication.CachedAuthSettings";
string authSettingsRAW = await jsRuntime.InvokeAsync<string>("sessionStorage.getItem", key);
authSettings = JsonSerializer.Deserialize<CachedAuthSettings>(authSettingsRAW);
string userRAW = await jsRuntime.InvokeAsync<string>("sessionStorage.getItem", authSettings?.OidcUserKey);
var cachedUser = JsonSerializer.Deserialize<CachedOidcUser>(userRAW);
if (!string.IsNullOrEmpty(cachedUser?.IdentityToken))
{
var handler = new JwtSecurityTokenHandler();
var decodedValue = handler.ReadJwtToken(cachedUser?.IdentityToken);
_tokenExpiresAt = decodedValue.ValidTo.ToLocalTime();
_user = cachedUser;
_cachedHeader = new AuthenticationHeaderValue("Bearer", _user.IdentityToken);
}
else
{
var tokenResult = _tokenOptions != null ?
await _provider.RequestAccessToken(_tokenOptions) :
await _provider.RequestAccessToken();
throw new AccessTokenNotAvailableException(_navigation, tokenResult, _tokenOptions?.Scopes);
}
}
// We don't try to handle 401s and retry the request with a new token automatically since that would mean we need to copy the request
// headers and buffer the body and we expect that the user instead handles the 401s. (Also, we can't really handle all 401s as we might
// not be able to provision a token without user interaction).
request.Headers.Authorization = _cachedHeader;
}
return await base.SendAsync(request, cancellationToken);
}
/// <summary>
/// Configures this handler to authorize outbound HTTP requests using an access token. The access token is only attached if at least one of
/// <paramref name="authorizedUrls" /> is a base of <see cref="HttpRequestMessage.RequestUri" />.
/// </summary>
/// <param name="authorizedUrls">The base addresses of endpoint URLs to which the token will be attached.</param>
/// <param name="scopes">The list of scopes to use when requesting an access token.</param>
/// <param name="returnUrl">The return URL to use in case there is an issue provisioning the token and a redirection to the
/// identity provider is necessary.
/// </param>
/// <returns>This <see cref="AuthorizationMessageHandler"/>.</returns>
public CustomAuthorizationMessageHandler ConfigureHandler(
IEnumerable<string> authorizedUrls,
IEnumerable<string> scopes = null,
string returnUrl = null)
{
if (_authorizedUris != null)
{
throw new InvalidOperationException("Handler already configured.");
}
if (authorizedUrls == null)
{
throw new ArgumentNullException(nameof(authorizedUrls));
}
var uris = authorizedUrls.Select(uri => new Uri(uri, UriKind.Absolute)).ToArray();
if (uris.Length == 0)
{
throw new ArgumentException("At least one URL must be configured.", nameof(authorizedUrls));
}
_authorizedUris = uris;
var scopesList = scopes?.ToArray();
if (scopesList != null || returnUrl != null)
{
_tokenOptions = new AccessTokenRequestOptions
{
Scopes = scopesList,
ReturnUrl = returnUrl
};
}
return this;
}
}
And then I have created a ConfiguredAuthorizationMessageHandler.cs class which is derived from CustomAuthorizationMessageHandler class for setting authorized URLs.
Code Snippet: ConfiguredAuthorizationMessageHandler.cs
public class ConfiguredAuthorizationMessageHandler : CustomAuthorizationMessageHandler
{
public ConfiguredAuthorizationMessageHandler(IAccessTokenProvider provider,
NavigationManager navigationManager, IJSRuntime jsRuntime, IConfiguration configuration)
: base(provider, navigationManager, jsRuntime)
{
ConfigureHandler(
authorizedUrls: new[] { configuration["ApiBaseUrl"] }
);
}
}
After that, I have injected those classes into the Program Main method.
Code Snippet: Program.cs
builder.Services.AddScoped<ConfiguredAuthorizationMessageHandler>();
builder.Services.AddHttpClient("ServerAPI", client => client.BaseAddress = new Uri(builder.Configuration["ApiBaseUrl"]))
.AddHttpMessageHandler<ConfiguredAuthorizationMessageHandler>();
builder.Services.AddScoped<ITestService>(provider =>
new TestService(provider.GetService<IHttpClientFactory>().CreateClient("ServerAPI")));
I have created two models for serializing session storage JSON data.
Code Snippet: CachedAuthSettings.cs and CachedOidcUser.cs
public class CachedAuthSettings
{
[JsonPropertyName("authority")]
public string Authority { get; set; }
[JsonPropertyName("metadataUrl")]
public string MetadataUrl { get; set; }
[JsonPropertyName("client_id")]
public string ClientId { get; set; }
[JsonPropertyName("defaultScopes")]
public IEnumerable<string> DefaultScopes { get; set; }
[JsonPropertyName("redirect_uri")]
public string RedirectUri { get; set; }
[JsonPropertyName("post_logout_redirect_uri")]
public string PostLogoutRedirectUri { get; set; }
[JsonPropertyName("response_type")]
public string ResponseType { get; set; }
[JsonPropertyName("response_mode")]
public string ResponseMode { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
public string OidcUserKey => $"oidc.user:{Authority}:{ClientId}";
}
public class CachedOidcUser
{
[JsonPropertyName("id_token")]
public string IdentityToken { get; set; }
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }
[JsonPropertyName("scope")]
public string Scope { get; set; }
[JsonPropertyName("expires_at")]
public int ExpiresAt { get; set; }
}
Reference:
Wait @Prasanth_Reddy, Iām not sure about what you are trying to accomplish, but only access tokens should be used to call APIs. ID tokens are meant just to prove that the user has been authenticated and they may also provide userās data. ID tokens are not meant to call APIs
Ohh my bad @andrea.chiarelli, till now we are using id_token for authenticating the user which is not supposed to do.
We have implemented our backend API to accept id_token instead of the access token. Thatās the reason I have configured it in that way. Thanks for letting me know about security. Please share if there are any documents to configure access token authentication at the API end and why we are not supposed to use id_token. These can help us to take a step ahead, Thanks in Advance.
You can start from this document to get an overview about tokens.
This topic was automatically closed 27 days after the last reply. New replies are no longer allowed.
I started with a blank .NET 6 blazor wasm project (hosted) and followed your steps. All ok until I have secured the API.
Even if I added in program.cs the AddHttpClient I get always a 401 and the token is not passed (I donāt have any authorization: response.header)
How can I be sure that in my quiz page Iām using the ārightā āServerApiā httpClient that append the token and not the usual one?
thanks
Hey @SandroRiz, welcome to the Auth0 Community!
Before trying to guess whatās the problem with your application, I warn you that the sample project accompanying my article was tested with .NET 5.0, so I canāt guarantee that everything goes smoothly even with .NET 6.0.
Anyway, in the next few weeks, we will update it to .NET 6.0.
Based on what you tell me, especially if you are not getting any error, it looks like you defined a named HTTP client but are not using it to make your requests to the server.
Please, make sure that the name you assigned to the client at definition time (AddHttpClient ()
) matches the name at creation time (CreateClient ()
). Consider that the name is case sensitive (āServerAPIā != āServerApiā).
In a way, I hope this is the case
unfortunately the snippet in the program.cs is ok (I copied yours )
in the quizviewer.razor you says
@inject HttpClient Http
is a new addition (it is inside the comment with the pointing-hand) but that inject was already there in the previous stepsā¦ am I missing something ?
Hey all,
Iāve done a bit of a deep dive into the Blazor + Auth0 logout issues that a lot of people, including myself, are plagued with and my findings are here along with a workaround for .NET 6:
Iāll put up an API proposal and hopefully the extended metadata settings will be available out the box for .NET 7.
in the quizviewer.razor you says
@inject HttpClient Http
is a new addition (it is inside the comment with the pointing-hand) but that inject was already there in the previous steps
Honestly, Iām not sure I understand correctly
You say it was there from the previous steps, but I canāt see another place where this is done in the article.
I find just two places where you should add @inject HttpClient Http
: in Client/Pages/QuizViewer.cs
and in Client/Pages/QuizViewer.razor
Aside question: have you tried to download and run the sample app project?
If not, please try it. Just to be sure itās not a matter of code.
Hey @Hawxy, thank you so much for going deep on this issue. Thatās a great news!
Hi Andrea, thanks for your patience and support
you donāt have a quizviewer.cs (you use @code inside the razor component)
Check my screenshot please about my perplexity
here if you want my Project (is the standard template where I secured weather service)
https://github.com/microgate-it/BlazorWithAuth0
About your sample project I downloaded and changed the 2 appsettings but I had another issue (both with 5 or 6 and with/without a default audience)
Thanks again
Have you tried deleting this line?
https://github.com/microgate-it/BlazorWithAuth0/blob/master/BlazorWasmWithAuth0/Client/Program.cs#L19
The call to builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>() .CreateClient("ServerAPI"));
above will cause the default HttpClient resolved to be the one that includes the bearer token.
Hi @SandroRiz, in my experience, the error you get usually depends on a wrong domain in the appsettings.json
. Please, double check it is written correctly.
Regarding the missing token in your HTTP requests, please, consider @Hawxyās suggestion.
Sorry for the misunderstanding on the code. I will plan to correct the article.
You got it !! Iām very dumb to not see what was below thanks a lot
I have to say that I must add Default Audience even with .NET 6, so maybe the bug is not fixed
I have to say that I must add Default Audience even with .NET 6, so maybe the bug is not fixed
The audience has to be passed via an additional parameter:
options.ProviderOptions.AdditionalProviderParameters.Add("audience", builder.Configuration["Auth0:Audience"]);