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

Jef*_*con 6 access-token jwt openid-connect blazor blazor-webassembly

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

我有一个 Blazor WebAssembly 前端。

我尝试在前端(有效)登录到 AWS Cognito(设置为 OpenId 提供商),然后在每个请求上将不记名令牌 (JWT) 传递到我的后端 API,以便后端 API 可以使用以下方式访问 AWS 资源:临时凭证 (CognitoAWSCredentials)。

我能够将每个请求的承载令牌从 Blazor 前端传递到后端,但是我能找到在 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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

缺少的链接是如何在 Blazor 中访问 ID 令牌,因此我可以将其作为授权 HTTP 标头的不记名令牌而不是访问令牌。

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

程序.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>()
                ;
Run Code Online (Sandbox Code Playgroud)

发送 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);
Run Code Online (Sandbox Code Playgroud)

当 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
Run Code Online (Sandbox Code Playgroud)

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

仅当请求openid范围时才会返回 ID 令牌。如果请求 aws.cognito.signin.user.admin 范围,则访问令牌只能用于 Amazon Cognito 用户池。

由于我的普通用户不是管理员,因此我不请求管理范围。

因此,根据文档,Cognito 应该返回一个 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"
  }
Run Code Online (Sandbox Code Playgroud)

但是,添加到 Http 请求中的 AccessToken 是 access_token。以下是token_use添加到 HTTP 请求中的已解码 JWT 令牌的声明:

  {
    "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>"
  }
Run Code Online (Sandbox Code Playgroud)

这是有道理,因为 Blazor API ...... IAccessTokenProvider.RequestAccessToken()似乎没有一个 API 来请求 ID 令牌。

Jef*_*con 4

感谢有关如何在 blazor web assembly 中获取 id_token 的答案,我能够获取 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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑:但是...如果您将 id_token 与 CognitoAWSCredentials 一起使用,那么您将遇到此错误(https://github.com/aws/aws-sdk-net/pull/1603),该错误正在等待合并。如果没有它,您将无法直接在 Blazor WebAssembly 中使用 AWS 开发工具包客户端,只能将 id_token 传递到后端才能创建CognitoAWSCredentials。