Click here to Skip to main content
15,904,023 members
Articles / Security / Identity
Article

Share Identity Bear Tokens among ASP.NET Core Web APIs

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
11 May 2024CPOL3 min read 3.6K   3   1
Share Identity Bear Tokens among Distributed ASP.NET Core Web APIs

Introduction

This is a sequel of "Decouple ASP.NET Core Identity, Authentication and Database Engines" which introduce various software engineering practices of decoupling, friendly to TDD and building  enterprise applications rapidly, in contrast to god assembly implicitly promoted by the scaffolding codes of Visual Studio project templates.

Background

Since the "good old days" of .NET Framework 1 and 2, Microsoft has been introducing elegant architectural design of app code security for various program hosts, like WinForms, WPF, Windows services and ASP.NET (Core) etc. As a .NET programmer, I just need to decorate respective functions with AuthorizeAttribute of various namespaces, according to the type of the host, then .NET runtime will take good care of the authentication and authorization with appropriate configuration through app codes or config files. And if you endorse .NET Component Design and avoid the coupling between main business logic and the host, the codes of current host can be short, and the migration to the new type of hosting in the future will be easy.

The security architecture introduced in this article has been existing in ASP.NET, well before ASP.NET Core, and the difference is ASP.NET Core has better DI/IoC.

Using the code

Comparing with the prior article, this article mainly uses "PetWebApi" as an example. The PetController was generated through some Swagger code gen upon "PetStore.yaml", and the "PetStoreClientApi" is generated using OpenApiClientGen.

Here we just need to focus on some Web API functions implemented.

Decorate Controller or Function with AuthorizeAttribute

C#
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiController]
public partial class PetController : ControllerBase
{
    public PetController()
    {
    }

    /// <summary>Add a new pet to the store<. If you give header transaction-id, it will give back the same/summary>
    /// <param name="accept_Language">The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US</param>
    /// <param name="cookieParam">Some cookie</param>
    [HttpPost, Route("pet")]
    public async Task<Pet> AddPet([FromBody] Pet body)//, [FromHeader(Name = "Accept-Language")] string accept_Language, long cookieParam)
    {
        long key = PetData.Instance.GetCurrentMax();
        body.Id = key;
        PetData.Instance.Dic.TryAdd(key, body);
        Response.Headers.Add("transaction-id", Request.Headers["transaction-id"]);
        return body;
    }

    /// <summary>Update an existing pet</summary>
    /// <param name="accept_Language">The language you prefer for messages. Supported values are en-AU, en-CA, en-GB, en-US</param>
    /// <param name="cookieParam">Some cookie</param>
    [HttpPut, Route("pet")]
    public async Task<IActionResult> UpdatePet([FromBody] object body, [FromHeader(Name = "Accept-Language")] string accept_Language, long cookieParam)
    {
        throw new NotImplementedException();
    }

    /// <summary>Find pet by ID</summary>
    /// <param name="petId">ID of pet to return</param>
    /// <returns>successful operation</returns>
    [HttpGet, Route("pet/{petId}")]
    public async Task<ActionResult<Pet>> GetPetById(long petId)
    {
        if (PetData.Instance.Dic.TryGetValue(petId, out Pet p))
        {
            return p;
        }
        else
        {
            return NotFound();
        }
    }

    /// <summary>Updates a pet in the store with form data</summary>
    /// <param name="petId">ID of pet that needs to be updated</param>
    [HttpPost, Route("pet/{petId}")]
    public async Task<IActionResult> UpdatePetWithForm(long petId, Microsoft.AspNetCore.Http.IFormFile body)
    {
        throw new NotImplementedException();
    }

    /// <summary>Deletes a pet</summary>
    /// <param name="petId">Pet id to delete</param>
    [HttpDelete, Route("pet/{petId}")]
    public async Task<IActionResult> DeletePet(long petId)
    {
        if (PetData.Instance.Dic.TryGetValue(petId, out _)) //not to TryRemove for testing
        {
            return Ok();
        }
        else
        {
            return NotFound("NoSuchPet");
        }
    }

Configurates the Host Program

Now we configurate the host program for checking the right bearer tokens.

Please note, PetWebApi has no knowledge of ASP.NET Core Identity and its database, and it will just trust the bearer token produced by "Core3WebApi".

 

C#
builder.Services.AddAuthentication(
    options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; //Bearer
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    }
).AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.RequireHttpsMetadata = false;
    options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidAudience = authSettings.Audience,
        ValidIssuer = authSettings.Issuer,
        IssuerSigningKey = issuerSigningKey,
#if DEBUG
        ClockSkew = TimeSpan.FromSeconds(2), //Default is 300 seconds. This is for testing the correctness of the auth protocol implementation between C/S.
#endif
    }; 
});

PetWebApi Trusts the Bearer Token Produced by Core3WebApi

The clients need to obtain a proper bearer token from localhost:5000 before talking to PetWebApi on localhost:6000:

C#
public class PetsFixture : DefaultHttpClientWithUsername
{
    public PetsFixture()
    {
        Uri baseUri = new("http://localhost:6000");

        httpClient = new System.Net.Http.HttpClient
        {
            BaseAddress = baseUri,
        };

        httpClient.DefaultRequestHeaders.Authorization = AuthorizedClient.DefaultRequestHeaders.Authorization;

        Api = new PetClient(httpClient, new Newtonsoft.Json.JsonSerializerSettings()
        {
            NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore
        });
    }

    public PetClient Api { get; private set; }

    readonly System.Net.Http.HttpClient httpClient;
...
}

public partial class PetsApiIntegration : IClassFixture<PetsFixture>
{
    public PetsApiIntegration(PetsFixture fixture)
    {
        api = fixture.Api;
    }

    readonly PetClient api;

    [Fact]
    public async Task TestGetPet()
    {
        Pet d = await api.GetPetByIdAsync(12);
        Assert.Equal("Narco", d.Name);
    }

    [Fact]
    public async Task TestAddPet()
    {
        await api.AddPetAsync(new Pet()
        {
            //Id=339,
            Name = "KKK", //required
            PhotoUrls = new string[] { "http://somewhere.com/mydog.jpg" }, //required,
            Tags = new Tag[] { //not required. However, when presented, it must contain at least one item.
                new Tag()
                {
                    //Id=3,
                    Name="Hey"
                }
            },
        });
    }

    [Fact]
    public async Task TestPetsDelete()
    {
        WebApiRequestException ex = await Assert.ThrowsAsync<WebApiRequestException>(() => api.DeletePetAsync(9));
        Assert.Equal("NoSuchPet", ex.Response);
    }

Before running the test suite, launch Core3WebApi through "StartCoreWebApi.ps1" and PetWebApi through "StartPetStoreapi.ps1".

Now you see how JWT is stateless.

And if the token is expired, the client will get the Unauthorized status code.

C#
[Fact]
public async Task TestFindPetsTokenExpiresThrows()
{
    Pet[] aa = await api.FindPetsByStatusAsync(PetStatus.sold);
    Assert.Equal(3, aa.Length);
    Thread.Sleep(7050);
    var ex = await Assert.ThrowsAsync<WebApiRequestException>(() => api.FindPetsByStatusAsync(PetStatus.sold));
    Assert.Equal(System.Net.HttpStatusCode.Unauthorized, ex.StatusCode);
}

Shared Secret

To make such "distributed" authentication work, surely there should be some shared secret among parties of Web APIs and the primary secret is "IssuerSigningkey".

About ValidateIssuerSigningKey

According to Microsoft Learn: 

It is possible for tokens to contain the public key needed to check the signature. For example, X509Data can be hydrated into an X509Certificate, which can be used to validate the signature. In these cases it is important to validate the SigningKey that was used to validate the signature. This boolean only applies to default signing key validation. If IssuerSigningKeyValidator is set, it will be called regardless of whether this property is true or false. The default is false.

However, at least with Bearer token, even if this property is set to false, invalid or different IssuerSigningKey causes Unauthorize error. And this should obviously be the way, since the key is the primary shared secret.

Windows and Azure Cloud as well as other cloud providers provide some valet to store shared secrets. Discussing how to store such secret for production is out of the scope of this article, while there are many good references:

Points of Interest

While in "Introduction to Identity on ASP.NET Core", Microsoft suggests:

ASP.NET Core Identity adds user interface (UI) login functionality to ASP.NET Core web apps. To secure web APIs and SPAs, use one of the following:

Microsoft actually has provided about using Identity with SPA:

Such features have been long existing with ASP.NET Identity on .NET Framework.

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Australia Australia
I started my IT career in programming on different embedded devices since 1992, such as credit card readers, smart card readers and Palm Pilot.

Since 2000, I have mostly been developing business applications on Windows platforms while also developing some tools for myself and developers around the world, so we developers could focus more on delivering business values rather than repetitive tasks of handling technical details.

Beside technical works, I enjoy reading literatures, playing balls, cooking and gardening.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA14-May-24 3:43
professionalȘtefan-Mihai MOGA14-May-24 3:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.