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 ....
Program.cs:Main
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>()
;
Sending the http request (minor changes from the Blazor sample code)...
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)
- Cognito Docs on /oauth2/authorize: https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html
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.