Blazor Server obtains Token to access external Web Api

Blazor Server obtains Token to access external Web Api

 

Identity Server family catalog

 

  1. Blazor Server access Identity Server 4 single sign on - SunnyTrudeau - blog Garden (cnblogs.com)
  2. Blazor Server access Identity Server 4 single sign on 2 - Integrated Asp.Net role - SunnyTrudeau - blog Park (cnblogs.com)
  3. Blazor Server access Identity Server 4 - Mobile authentication code login - SunnyTrudeau - blog Park (cnblogs.com)
  4. Blazor MAUI client access Identity Server login - SunnyTrudeau - blog Garden (cnblogs.com)
  5. Integrate the Blazor component - SunnyTrudeau - blog Park (cnblogs.com) in the Identity Server 4 project
  6. Identity Server 4 exit login automatic jump back - SunnyTrudeau - blog Park (cnblogs.com)
  7. Identity Server returns user role - SunnyTrudeau - blog Garden (cnblogs.com) through ProfileService
  8. Identity Server 4 returns the custom user Claim - SunnyTrudeau - blog Garden (cnblogs.com)

 

 

 

 

 

An enterprise may contain several subsystems of different businesses. All subsystems share an Identity Server 4 authentication center. After logging in to one subsystem, users can obtain a token to access the protected web APIs of other subsystems. How to obtain a token for the Blazor Server project is described on the Microsoft official website: ASP.NET Core Blazor Server other security solutions | Microsoft Docs

 

 

 

New Web Api project

 

The project name is MyWebApi, and the WeatherForecastController created with the template is sufficient.

 

Program.cs adds the configuration of authentication and authorization, and the Web Api project adopts Bearer authentication.

 

//NuGet install Microsoft.AspNetCore.Authentication.JwtBearer
//NuGet install IdentityServer4.AccessTokenValidation
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        //appoint IdentityServer Address of
        options.Authority = "https://localhost:5001"; ;

        options.ApiName = "https://localhost:5001/resources";
    });

//Add authentication and authorization
app.UseAuthentication();
app.UseAuthorization();

 

WeatherForecastController.cs controller increase access

 

    [ApiController]
    [Route("[controller]")]
    [Authorize]
    public class WeatherForecastController : ControllerBase

 

Add printing HttpContext to obtain token and user declaration claims debugging information.

 

[HttpGet(Name = "GetWeatherForecast")]
        public async Task<IEnumerable<WeatherForecast>> Get()
        {
            var claims = User.Claims.Select(x => $"{x.Type}={x.Value}").ToList();

            var accessToken = await HttpContext.GetTokenAsync("access_token");
            var refreshToken = await HttpContext.GetTokenAsync("refresh_token");

            string msg = $"from HttpContext obtain accessToken={accessToken}{Environment.NewLine}, refreshToken={refreshToken}{Environment.NewLine}, User statement={string.Join(", ", claims)}";
            _logger.LogInformation(msg);

            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            })
            .ToArray();
        }

 

 

Get token for Blazor Server project

BlzOidc project refers to the official website code through_ HttpContext of the Host.cshtml web page and get the token.

First, define the token provider

 

public class TokenProvider
    {
        public string? AccessToken { get; set; }
        public string? RefreshToken { get; set; }
    }

 

Program.cs register Token provider

        // Register Token provider

        builder.Services.AddScoped<TokenProvider>();

 

On pages/_ In the host.cshtml file, get the Token through HttpContext and pass it to the App.razor component as a parameter.

 

@page "/"
@namespace BlzOidc.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using Microsoft.AspNetCore.Authentication
@using BlzOidc.Data
@{
    Layout = "_Layout";
}

@{
    var tokens = new TokenProvider
            {
                AccessToken = await HttpContext.GetTokenAsync("access_token"),
                RefreshToken = await HttpContext.GetTokenAsync("refresh_token")
            };
}

<component type="typeof(App)" render-mode="ServerPrerendered" param-InitialToken="tokens" />

 

The App.razor component saves tokens, which can be copied from the official website code, but I don't understand why_ If you get the Token from Host.cshtml, you have to transfer it to App.razor to save it? Directly in_ Can't Host.cshtml be saved?

@using BlzOidc.Data
@inject TokenProvider TokenProvider

@code {
    [Parameter]
    public TokenProvider? InitialToken { get; set; }

    protected override Task OnInitializedAsync()
    {
        TokenProvider.AccessToken = InitialToken?.AccessToken;
        TokenProvider.RefreshToken = InitialToken?.RefreshToken;

        Console.WriteLine($"Initialize acquisition AccessToken={TokenProvider.AccessToken}, RefreshToken={TokenProvider.RefreshToken}");

        return base.OnInitializedAsync();
    }
}

The official website code is to manually fill in the token in HttpClient every time and then access the external Web Api

 

public class WeatherForecastService
{
    private readonly HttpClient http;
    private readonly TokenProvider tokenProvider;

    public WeatherForecastService(IHttpClientFactory clientFactory, 
        TokenProvider tokenProvider)
    {
        http = clientFactory.CreateClient();
        this.tokenProvider = tokenProvider;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        var token = tokenProvider.AccessToken;
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://localhost:5003/WeatherForecast");
        request.Headers.Add("Authorization", $"Bearer {token}");
        var response = await http.SendAsync(request);
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsAsync<WeatherForecast[]>();
    }
}

 

I prefer to write a typed HttpClient and register the service.

 

using System.Net.Http.Headers;

namespace BlzOidc.Data
{
    public class WeatherForecastApiClient
    {
        private readonly HttpClient _httpClient;
        private readonly TokenProvider _tokenProvider;

        public WeatherForecastApiClient(HttpClient httpClient, TokenProvider tokenProvider)
        {
            _httpClient = httpClient;
            _tokenProvider = tokenProvider;
        }

        public async Task<WeatherForecast[]?> GetForecastAsync()
        {
            var token = _tokenProvider.AccessToken;
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

            var result = await _httpClient.GetFromJsonAsync<WeatherForecast[]>("/WeatherForecast");

            return result;
        }
    }
}

 

Program.cs registration service

        // Register WeatherForecastApiClient

        builder.Services.AddHttpClient<WeatherForecastApiClient>(client => client.BaseAddress = new Uri("https://localhost:5601"));

 

Then inject the typed weatherforecast API client into the web page to get the weather data from MyWebApi.

 

@page "/fetchdataapi"
@attribute [Authorize]

<PageTitle>Weather forecast</PageTitle>

@using BlzOidc.Data
@inject WeatherForecastApiClient ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync();
    }
}

 

Run the AspNetId4Web authentication server, MyWebApi project and BlzOidc project at the same time. When the BlzOidc project is not logged in, directly access the Fetch Data Api menu. The browser will automatically jump to the AspNetId4Web login page, enter the seed user's mobile number 13512345001 to obtain the verification code, watch the AspNetId4Web console output to obtain the verification code, and fill in the verification code to log in, The browser automatically jumps back to the Fetch Data Api page of the BlzOidc project and obtains the weather data.

 

problem

What value should be filled in for the MyWebApi authentication parameter ApiName? By default, it is a fixed route for Identity Server 4 servers:

        options.ApiName = "https://localhost:5001/resources";

 

I can also modify config.cs

 

        public static IEnumerable<ApiResource> ApiResources =>
            new ApiResource[]
            {
                new ApiResource("api1", "api1")
                {
                    Scopes = { "scope1" },

                    //Additional identity properties returned by the authentication server
                    UserClaims =
                    {
                        //increase aud="api1"
                        JwtClaimTypes.Audience,
                    },
                }
            };

 

AddIdentityServer add configuration AddInMemoryApiResources

 

            var builder = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;

                // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
                options.EmitStaticAudienceClaim = true;
            })
                .AddInMemoryIdentityResources(Config.IdentityResources)
                .AddInMemoryApiScopes(Config.ApiScopes)
                .AddInMemoryClients(Config.Clients)
                .AddExtensionGrantValidator<PhoneCodeGrantValidator>()
                .AddInMemoryApiResources(Config.ApiResources)
                .AddAspNetIdentity<ApplicationUser>();

 

The authentication parameters of Web Api can adopt "api1"

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddIdentityServerAuthentication(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        //appoint IdentityServer Address of
        options.Authority = "https://localhost:5001"; ;

        //default aud="https://localhost:5001/resources"
        //options.ApiName = "https://localhost:5001/resources";
        //Bearer was not authenticated. Failure message: IDX10214: Audience validation failed. Audiences: 'System.String'. Did not match: validationParameters.ValidAudience: 'System.String' or validationParameters.ValidAudiences: 'System.String'.
        //Identity Server 4 config.cs of ApiResources increase JwtClaimTypes.Audience,AddInMemoryApiResources(Config.ApiResources),Can increase aud="api1"
        options.ApiName = "api1";
    });

At this time, you can view the token returned by Identity Server 4. It does have two AUDs:

[20:54:59 Debug] IdentityServer4.Validation.TokenValidator

Token validation success

{"ClientId": null, "ClientName": null, "ValidateLifetime": true, "AccessTokenType": "Jwt", "ExpectedScope": "openid", "TokenHandle": null, "JwtId": "4F3D0DE1EE8E8DEFD3EC27E602F0790C", "Claims": {"nbf": 1638708899, "exp": 1638712499, "iss": "https://localhost:5001", "aud": ["api1", "https://localhost:5001/resources "],   "client_id": "BlazorServerOidc", "sub": "d2f64bb2-789a-4546-9107-547fcb9cdfce", "auth_time": 1638708898, "idp": "local", "name": "Alice Smith", "role": ["Admin", "Guest"], "email": " AliceSmith@email.com "," phone_number ":" 13512345001 "," nation ":" Han nationality "," JTI ":" 4f3d0de1ee8defd3ec27e602f0790c "," Sid ":" fdb59080b24468b76300ae9554354d67 "," IAT "“ : 1638708899, "scope": ["openid", "profile", "scope1"], "amr": "pwd"}, "$type": "TokenValidationLog"}

 

The token printed in MyWebApi project also has two AUDs:

info: MyWebApi.Controllers.WeatherForecastController[0]

       Get accessToken=eyJh...WlA from HttpContext

      , refreshToken=

      , User declaration = nbf=1638708899, exp=1638712499, iss=https://localhost:5001, aud=api1, aud=https://localhost:5001/resources, client_id=BlazorServerOidc, sub=d2f64bb2-789a-4546-9107-547fcb9cdfce, auth_time=1638708898, idp=local, name=Alice Smith, role=Admin, role=Guest, email=AliceSmith@email.com, phone_number=13512345001, nation = Han nationality, JTI = 4f3d0de1ee8defd3ec27e602f0790c, sid = fdb590880b24468b76300ae9554354d67, IAT = 1638708899, scope = openid, scope = profile, scope = scope1, AMR = PWD

 

I don't quite understand, but I think it's better to use api1.

 

DEMO code address: https://gitee.com/woodsun/blzid4

 

Posted on Mon, 06 Dec 2021 00:43:09 -0500 by tempa