使用 OpenId (Cognito) 进行身份验证后,如何在 Blazor WebAssembly 中获取 id_token?

How do I get the id_token in Blazor WebAssembly after authenticating with OpenId (Cognito)?

我有一个 .Net Core 3.1 WebApi 后端。

我有一个 Blazor WebAssembly front-end。

我正在尝试在 front-end(有效)上登录 AWS Cognito(设置为 OpenId 提供程序),然后将 Bearer 令牌 (JWT) 传递到我的后端 API请求以便后端 API 可以使用临时凭证 (CognitoAWSCredentials) 访问 AWS 资源。

我可以将 Bearer 令牌从我的 Blazor front-end 的每个请求传递到后端,但是我可以在 Blazor 中找到访问的唯一令牌是访问令牌。我需要 ID 令牌才能让后端代表我的用户生成凭据。

在我的 Blazor 代码中,我已经成功注册了一个自定义 AuthorizationMessageHandler,当访问我的 API:

时,它会调用每个 HttpClient 的 SendAsync
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);
}

这会添加 访问令牌 并且后端会获取令牌并对其进行正确验证。 但是,要为 AWS 服务创建用于特权的 CognitoAWSCredentials,我需要 ID Token.

我找不到在 Blazor 中访问 ID 令牌的任何方式。

如果我直接访问我的后端 WebApi,它会正确地将我转到 Cognito 进行登录,然后 return 返回。当它出现时,HttpContext 包含“id_token”。然后可以使用它来创建我需要的 CognitoAWSCredentials。

缺少的 link 是如何在 Blazor 中访问 ID 令牌,因此我可以将其作为授权 HTTP header 的 Bearer 令牌而不是访问令牌。

添加更多代码上下文....

Program.cs:主要

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

正在发送 http 请求(对 Blazor 示例代码进行了细微更改)...

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

当 OpenIdConnect 中间件尝试使用 Cognito 进行授权时,它会调用:

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

(隐藏:我为一些可能敏感的值插入)

如果请求 openid 范围,则仅 return 编辑 ID 令牌。如果请求 aws.cognito.signin.user.admin 范围,则访问令牌只能用于 Amazon Cognito 用户池。

因为我的普通用户不是管理员,所以我不请求管理员范围。

所以根据文档,Cognito 应该 returning 一个 ID 令牌。 当我打印出由 Blazor 中的 OIDC 中间件创建的 ClaimsPrincipal 的声明时,token_use 是 id:

  {
    "Type": "token_use",
    "Value": "id",
    "ValueType": "http://www.w3.org/2001/XMLSchema#string",
    "Subject": null,
    "Properties": {},
    "OriginalIssuer": "LOCAL AUTHORITY",
    "Issuer": "LOCAL AUTHORITY"
  }

然而,添加到 Http 请求的 AccessToken 是一个 access_token。 这是来自添加到 HTTP 请求的已解码 JWT 令牌的 token_use 声明:

  {
    "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>"
  }

哪种类型的有意义,因为Blazor API IAccessTokenProvider.RequestAccessToken() ...似乎是 API 请求 ID 令牌。

感谢 How to get the id_token in blazor web assembly 上的回答,我能够得到 id_token。示例代码如下:

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

编辑:但是...如果您将 id_token 与 CognitoAWSCredentials 一起使用,那么您将 运行 进入等待合并的错误 (https://github.com/aws/aws-sdk-net/pull/1603)。没有它,您将无法直接在 Blazor WebAssembly 中使用 AWS SDK 客户端,只能将 id_token 传递给您的后端,以便 it 能够创建 CognitoAWSCredentials。