Reputation: 388
I've got a .Net Core 3.1 WebApi backend.
I've got a Blazor WebAssembly front-end.
I'm trying to login on the front-end (works) to AWS Cognito (setup as an OpenId provider) and then pass a Bearer token (JWT) to my backend API on each request so that the backend API can access AWS resources using temporary credentials (CognitoAWSCredentials).
I am able to pass a Bearer token on each request from my Blazor front-end to the backend, however the only token I can find to access in Blazor is the Access Token. I need the ID Token in order to allow the backend to generate credentials on my user's behalf.
In my Blazor code I have successfully registered a custom AuthorizationMessageHandler which gets invokes on each HttpClient's SendAsync when accessing my API:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
HttpRequestHeaders headers = request?.Headers;
AuthenticationHeaderValue authHeader = headers?.Authorization;
if (headers is object && authHeader is null)
{
AccessTokenResult result = await TokenProvider.RequestAccessToken();
if (result.TryGetToken(out AccessToken token))
{
authHeader = new AuthenticationHeaderValue("Bearer", token.Value);
request.Headers.Authorization = authHeader;
}
logger.LogObjectDebug(request);
}
return await base.SendAsync(request, cancellationToken);
}
This adds the Access Token and the backend picks up the token and validates it fine. However, to create the CognitoAWSCredentials for AWS services to use for privileges, I need the ID Token.
I cannot find any way to access the ID Token within Blazor.
If I access my backend WebApi directly, it will properly forward me to Cognito to login and then return back. When it does, the HttpContext contains the "id_token". This can then be used to create the CognitoAWSCredentials I need.
The missing link is how to access the ID Token in Blazor so I can put that as the Authorization HTTP header's Bearer token instead of the Access Token.
adding a bit more code context ....
string CognitoMetadataAddress = $"{settings.Cognito.Authority?.TrimEnd('/')}/.well-known/openid-configuration";
builder.Services.AddOidcAuthentication<RemoteAuthenticationState, CustomUserAccount>(options =>
{
options.ProviderOptions.Authority = settings.Cognito.Authority;
options.ProviderOptions.MetadataUrl = CognitoMetadataAddress;
options.ProviderOptions.ClientId = settings.Cognito.ClientId;
options.ProviderOptions.RedirectUri = $"{builder.HostEnvironment.BaseAddress.TrimEnd('/')}/authentication/login-callback";
options.ProviderOptions.ResponseType = OpenIdConnectResponseType.Code;
})
.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, CustomUserAccount, CustomAccountFactory>()
;
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
string APIBaseUrl = builder.Configuration.GetSection("Deployment")["APIBaseUrl"];
builder.Services.AddSingleton<CustomAuthorizationMessageHandler>();
builder.Services.AddHttpClient(settings.HttpClientName, client =>
{
client.BaseAddress = new Uri(APIBaseUrl);
})
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>()
;
HttpRequestMessage requestMessage = new HttpRequestMessage()
{
Method = new HttpMethod(method),
RequestUri = new Uri(uri),
Content = string.IsNullOrEmpty(requestBody) ? null : new StringContent(requestBody)
};
foreach (RequestHeader header in requestHeaders)
{
// StringContent automatically adds its own Content-Type header with default value "text/plain"
// If the developer is trying to specify a content type explicitly, we need to replace the default value,
// rather than adding a second Content-Type header.
if (header.Name.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) && requestMessage.Content != null)
{
requestMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(header.Value);
continue;
}
if (!requestMessage.Headers.TryAddWithoutValidation(header.Name, header.Value))
{
requestMessage.Content?.Headers.TryAddWithoutValidation(header.Name, header.Value);
}
}
HttpClient Http = HttpClientFactory.CreateClient(Settings.HttpClientName);
HttpResponseMessage response = await Http.SendAsync(requestMessage);
When the OpenIdConnect middleware tries to authorize with Cognito, it calls:
https://<DOMAIN>/oauth2/authorize?client_id=<CLIENT-ID>&redirect_uri=https%3A%2F%2Flocalhost%3A44356%2Fauthentication%2Flogin-callback&response_type=code&scope=openid%20profile&state=<HIDDEN>&code_challenge=<HIDDEN>&code_challenge_method=S256&response_mode=query
(HIDDEN: inserted by me for some values that might be sensitive)
An ID token is only returned if openid scope is requested. The access token can be only used against Amazon Cognito User Pools if aws.cognito.signin.user.admin scope is requested.
Since my normal users are not admins, I'm not requesting the admin scope.
So according to the docs, Cognito should be returning an ID token.
When I print out the claims for the ClaimsPrincipal created by the OIDC middleware in Blazor the token_use is id
:
{
"Type": "token_use",
"Value": "id",
"ValueType": "http://www.w3.org/2001/XMLSchema#string",
"Subject": null,
"Properties": {},
"OriginalIssuer": "LOCAL AUTHORITY",
"Issuer": "LOCAL AUTHORITY"
}
However the AccessToken added to the Http request is an access_token.
Here's the token_use
claim from the decoded JWT token added to the HTTP request:
{
"Type": "token_use",
"Value": "access",
"ValueType": "http://www.w3.org/2001/XMLSchema#string",
"Subject": null,
"Properties": {},
"OriginalIssuer": "https://cognito-idp.ca-central-1.amazonaws.com/<USER-POOL-ID>",
"Issuer": "https://cognito-idp.ca-central-1.amazonaws.com/<USER-POOL-ID>"
}
Which sort of makes sense since the Blazor API is IAccessTokenProvider.RequestAccessToken()
... there just doesn't seem to be an API to request the ID token.
Upvotes: 6
Views: 3513
Reputation: 388
Thanks to the answers on How to get the id_token in blazor web assembly I was able to get the id_token. Sample code below:
@page "/"
@using System.Text.Json
@inject IJSRuntime JSRuntime
<AuthorizeView>
<Authorized>
<div>
<b>CachedAuthSettings</b>
<pre>
@JsonSerializer.Serialize(authSettings, indented);
</pre>
<br/>
<b>CognitoUser</b><br/>
<pre>
@JsonSerializer.Serialize(user, indented);
</pre>
</div>
</Authorized>
<NotAuthorized>
<div class="alert alert-warning" role="alert">
Everything requires you to <a href="/authentication/login">Log In</a> first.
</div>
</NotAuthorized>
</AuthorizeView>
@code {
JsonSerializerOptions indented = new JsonSerializerOptions() { WriteIndented = true };
CachedAuthSettings authSettings;
CognitoUser user;
protected override async Task OnInitializedAsync()
{
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);
user = JsonSerializer.Deserialize<CognitoUser>(userRAW);
}
public class CachedAuthSettings
{
public string authority { get; set; }
public string metadataUrl { get; set; }
public string client_id { get; set; }
public string[] defaultScopes { get; set; }
public string redirect_uri { get; set; }
public string post_logout_redirect_uri { get; set; }
public string response_type { get; set; }
public string response_mode { get; set; }
public string scope { get; set; }
public string OIDCUserKey => $"oidc.user:{authority}:{client_id}";
}
public class CognitoUser
{
public string id_token { get; set; }
public string access_token { get; set; }
public string refresh_token { get; set; }
public string token_type { get; set; }
public string scope { get; set; }
public int expires_at { get; set; }
}
}
EDIT: However... if you are using the id_token with CognitoAWSCredentials then you will run into this bug (https://github.com/aws/aws-sdk-net/pull/1603) which is awaiting merging. Without it, you will not be able to use the AWS SDK Clients directly in Blazor WebAssembly, only pass the id_token to your backend for it to be able to create CognitoAWSCredentials.
Upvotes: 4