Tingle.AspNetCore.Tokens 4.7.0

dotnet add package Tingle.AspNetCore.Tokens --version 4.7.0
NuGet\Install-Package Tingle.AspNetCore.Tokens -Version 4.7.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Tingle.AspNetCore.Tokens" Version="4.7.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Tingle.AspNetCore.Tokens --version 4.7.0
#r "nuget: Tingle.AspNetCore.Tokens, 4.7.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Tingle.AspNetCore.Tokens as a Cake Addin
#addin nuget:?package=Tingle.AspNetCore.Tokens&version=4.7.0

// Install Tingle.AspNetCore.Tokens as a Cake Tool
#tool nuget:?package=Tingle.AspNetCore.Tokens&version=4.7.0

Tingle.AspNetCore.Tokens

This library adds support for generation of continuation tokens in ASP.NET Core with optional expiry. This is particularly useful for pagination, user invite tokens, expiring operation tokens, etc. The functionality is availed through the ContinuationToken<T> and TimedContinuationToken<T> types. These are backed using the DataProtection sub-system in ASP.NET Core.

See sample.

First step is to register the required services.

var builder = WebApplication.CreateBuilder(args);

// see https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-8.0
builder.Services.AddDataProtection();

builder.Services.AddControllers()
                .AddTokens();

var app = builder.Build();

app.MapControllers();

app.Run();

Pagination

Pagination is best served using ContinuationToken<T> with DateTimeOffset or an incrementing identifier in your database.

[ApiController]
[Route("/books")]
[ProducesErrorResponseType(typeof(ValidationProblemDetails))]
public class BooksController : ControllerBase
{
    static readonly List<Book> Books = new();

    [HttpGet]
    public IActionResult List([FromQuery] ContinuationToken<DateTimeOffset>? token)
    {
        var last = token?.GetValue();
        var query = last is not null ? Books.Where(b => b.Created > last) : Books;
        query = query.Take(10); // limit the number of items to pull from the database

        var books = query.ToList(); // pull from the database
        last = books.Any() ? books.Last().Created : null;

        if (last is not null)
        {
            var ct = new ContinuationToken<DateTimeOffset>(last.Value);
            return this.Ok(books, ct);
        }

        return Ok(books);
    }
}

User invitation and account transfer tokens

User invitation tokens that do not expire are best served using ContinuationToken<T> with a custom model inside that can carry extra information with it. The same can be done for other scenarios such as account transfer and email validation.

[ApiController]
[Route("/users")]
[ProducesErrorResponseType(typeof(ValidationProblemDetails))]
public class UsersController : ControllerBase
{
    private readonly ITokenProtector<InvitationLinkToken> tokenProtector;

    public UsersController(ITokenProtector<InvitationLinkToken> tokenProtector)
    {
        this.tokenProtector = tokenProtector ?? throw new ArgumentNullException(nameof(tokenProtector));
    }

    [HttpPost]
    public IActionResult Invite([FromBody] UserCreateModel model)
    {
        // create the invitation in the database
        var invitationId = Guid.NewGuid().ToString();

        // send invite email
        var token = tokenProtector.Protect(new InvitationLinkToken { Id = invitationId, });

        // send email using the token

        return Ok();
    }

    [HttpPost("{id}/resend")]
    public IActionResult Resend([FromRoute, Required] string id)
    {
        // find the invitation from the database
        var invitationId = Guid.NewGuid().ToString();

        // resend invite email
        var token = tokenProtector.Protect(new InvitationLinkToken { Id = invitationId, });

        // send email using the token

        return Ok();
    }
}

[ApiController]
[Route("/invites")]
[ProducesErrorResponseType(typeof(ValidationProblemDetails))]
public class InvitesController : ControllerBase
{
    [HttpPost("accept")]
    public IActionResult Accept([FromQuery, Required] ContinuationToken<InvitationLinkToken> token)
    {
        // ensure the invite exists
        var model = token.GetValue();
        var invitationId = model.Id;
        // do the database magic here

        return Ok();
    }

    [HttpPost("reject")]
    public IActionResult Reject([FromQuery, Required] ContinuationToken<InvitationLinkToken> token)
    {
        // ensure the invite exists
        var model = token.GetValue();
        var invitationId = model.Id;
        // do the database magic here

        return Ok();
    }
}

Expiration

For certain scenarios, expiration is desired. Such as confirmation of monetary disbursement. In such, you should use TimedContinuationToken<T> when receiving the token and pass an absolute/relative expiration when protecting the data.

[ApiController]
[Route("/disbursement")]
[ProducesErrorResponseType(typeof(ValidationProblemDetails))]
public class DisbursementController : ControllerBase
{
    private readonly ITokenProtector<DisbursementToken> tokenProtector;

    public DisbursementController(ITokenProtector<DisbursementToken> tokenProtector)
    {
        this.tokenProtector = tokenProtector ?? throw new ArgumentNullException(nameof(tokenProtector));
    }

    [HttpPost("initiate")]
    public IActionResult Initiate([FromBody] DisbursementInitiateModel model)
    {
        // do light weight checks e.g. enough funds in the account

        // generate token and send it back in the response
        var ttl = TimeSpan.FromMinutes(1);
        var token = tokenProtector.Protect(new DisbursementToken { Amount = model.Amount, Iban = model.Iban, }, ttl);

        // to confirm, the user will call /confirmed?token={token}
        return Ok(new DisbursementResponseModel { Token = token, });
    }

    [HttpPost("confirmed")]
    public IActionResult Confirmed([FromQuery, Required] TimedContinuationToken<DisbursementToken> token)
    {
        // do light weight checks that may have changed e.g. enough funds in the account

        // do the disbursement here (if expired, we never get here)
        var model = token.GetValue();
        var amount = model.Amount;
        var iban = model.Iban;

        return Ok();
    }
}
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net6.0

    • No dependencies.
  • net7.0

    • No dependencies.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
4.7.0 187 3/25/2024
4.6.0 336 3/8/2024
4.5.0 2,196 11/22/2023
4.4.1 241 11/20/2023
4.4.0 222 11/15/2023
4.3.0 636 10/18/2023
4.2.2 777 9/20/2023
4.2.1 1,188 8/4/2023
4.2.0 1,586 5/31/2023
4.1.1 272 5/26/2023
4.1.0 260 5/22/2023
4.0.0 1,629 3/14/2023
2.5.0 2,432 11/21/2022
2.4.2 4,588 7/25/2022
2.4.1 4,798 3/22/2022
2.4.0 4,584 11/10/2021
2.3.1 2,869 9/20/2021
2.3.0 1,400 7/22/2021