使用 OpenId (Cognito) 进行身份验证后,如何在 Blazor WebAssembly 中获取 id_token? [英] How do I get the id_token in Blazor WebAssembly after authenticating with OpenId (Cognito)?

查看:44
本文介绍了使用 OpenId (Cognito) 进行身份验证后,如何在 Blazor WebAssembly 中获取 id_token?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

我有一个 Blazor WebAssembly 前端.

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

我能够在从 Blazor 前端到后端的每个请求中传递一个不记名令牌,但是我能在 Blazor 中找到的唯一可以访问的令牌是访问令牌.我需要 ID 令牌以允许后端代表我的用户生成凭据.

在我的 Blazor 代码中,我成功注册了一个自定义 AuthorizationMessageHandler,它在访问我的 API 时会在每个 HttpClient 的 SendAsync 上调用:

protected override async TaskSendAsync(HttpRequestMessage 请求,CancellationToken 取消令牌){HttpRequestHeaders headers = request?.Headers;AuthenticationHeaderValue authHeader = headers?.Authorization;if (headers 是对象 && authHeader 为空){AccessTokenResult 结果 = await TokenProvider.RequestAccessToken();if (result.TryGetToken(out AccessToken token)){authHeader = new AuthenticationHeaderValue("Bearer", token.Value);request.Headers.Authorization = authHeader;}logger.LogObjectDebug(请求);}返回等待 base.SendAsync(请求,取消令牌);}

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

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

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

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

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

Program.cs:Main

string CognitoMetadataAddress = $"{settings.Cognito.Authority?.TrimEnd('/')}/.well-known/openid-configuration";builder.Services.AddOidcAuthentication(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();builder.Services.AddOptions();builder.Services.AddAuthorizationCore();string APIBaseUrl = builder.Configuration.GetSection(部署")[APIBaseUrl"];builder.Services.AddSingleton();builder.Services.AddHttpClient(settings.HttpClientName, client =>{client.BaseAddress = new Uri(APIBaseUrl);}).AddHttpMessageHandler();

发送 http 请求(Blazor 示例代码的小改动)...

HttpRequestMessage requestMessage = new HttpRequestMessage(){Method = new HttpMethod(method),RequestUri = 新的 Uri(uri),内容 = string.IsNullOrEmpty(requestBody) ?null : 新的 StringContent(requestBody)};foreach(requestHeaders 中的 RequestHeader 标头){//StringContent 自动添加自己的 Content-Type 标头,默认值为text/plain";//如果开发者试图明确指定一个内容类型,我们需要替换默认值,//而不是添加第二个 Content-Type 标头.if (header.Name.Equals(Content-Type", StringComparison.OrdinalIgnoreCase) && requestMessage.Content != null){requestMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(header.Value);继续;}if (!requestMessage.Headers.TryAddWithoutValidation(header.Name, header.Value)){requestMessage.Content?.Headers.TryAddWithoutValidation(header.Name, header.Value);}}HttpClient Http = HttpClientFactory.CreateClient(Settings.HttpClientName);HttpResponseMessage 响应 = 等待 Http.SendAsync(requestMessage);

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

https:///oauth2/authorize?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 范围时才返回 ID 令牌.如果请求了 aws.cognito.signin.user.admin 范围,则访问令牌只能用于 Amazon Cognito 用户池.

由于我的普通用户不是管理员,所以我没有请求管理员范围.

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

<代码> {类型":token_use",值":id",ValueType":http://www.w3.org/2001/XMLSchema#string",主题":空,属性":{},原始发行人":地方当局",发行人":地方当局"}

但是添加到Http请求中的AccessToken是一个access_token.这是添加到 HTTP 请求的解码 JWT 令牌中的 token_use 声明:

<代码> {类型":token_use",价值":访问",ValueType":http://www.w3.org/2001/XMLSchema#string",主题":空,属性":{},OriginalIssuer":https://cognito-idp.ca-central-1.amazonaws.com/",发行人":https://cognito-idp.ca-central-1.amazonaws.com/"}

哪种排序有意义,因为 Blazor API IAccessTokenProvider.RequestAccessToken() ...似乎没有用于请求 ID 令牌的 API.

解决方案

感谢 如何在 blazor web assembly 中获取 id_token 我能够获得 id_token.示例代码如下:

@page "/";@使用 System.Text.Json@inject IJSRuntime JSRuntime<授权视图><授权><div><b>CachedAuthSettings</b><预>@JsonSerializer.Serialize(authSettings, indented);

<br/><b>CognitoUser</b><br/><预>@JsonSerializer.Serialize(用户,缩进);

</授权><未授权><div class="alert alert-warning";角色=警报">一切都需要您<a href="/authentication/login>登录</a>第一的.

</NotAuthorized></AuthorizeView>@代码 {JsonSerializerOptions indented = new JsonSerializerOptions() { WriteIndented = true };CachedAuthSettings authSettings;CognitoUser 用户;受保护的覆盖异步任务 OnInitializedAsync(){string key = "Microsoft.AspNetCore.Components.WebAssembly.Authentication.CachedAuthSettings";string authSettingsRAW = await JSRuntime.InvokeAsync(sessionStorage.getItem", key);authSettings = JsonSerializer.Deserialize(authSettingsRAW);string userRAW = await JSRuntime.InvokeAsync(sessionStorage.getItem", authSettings?.OIDCUserKey);user = JsonSerializer.Deserialize(userRAW);}公共类 CachedAuthSettings{公共字符串权限{获取;放;}公共字符串 metadataUrl { 获取;放;}公共字符串 client_id { 获取;放;}公共字符串[] defaultScopes { get;放;}公共字符串redirect_uri { 获取;放;}公共字符串 post_logout_redirect_uri { 获取;放;}公共字符串 response_type { 获取;放;}公共字符串 response_mode { 获取;放;}公共字符串范围{获取;放;}公共字符串 OIDCUserKey =>$oidc.user:{authority}:{client_id}";}公共类 CognitoUser{公共字符串 id_token { 获取;放;}公共字符串 access_token { 获取;放;}公共字符串 refresh_token { 获取;放;}公共字符串 token_type { 获取;放;}公共字符串范围{获取;放;}公共 int expires_at { 得到;放;}}}

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

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)

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.

解决方案

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.

这篇关于使用 OpenId (Cognito) 进行身份验证后,如何在 Blazor WebAssembly 中获取 id_token?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
其他开发最新文章
热门教程
热门工具
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆