ASP.NET Core 5.0 - The TempData Challenge

Ken Haggerty
Created 06/21/2021 - Updated 08/16/2021 04:00

This article will describe the implementation of the TempData attribute to persist the challenge on the server between requests. The challenge is a unique code used by FIDO to verify the response from the authenticating device. I will assume you have downloaded the ASP.NET Core 5.0 - Users Without Passwords Project.

Users Without Passwords Project and Article Series

I have developed two separate projects in the Users Without Passwords Project (UWPP) solution. The Users Without Passwords v4 project supports Bootstrap v4 and the new Users Without Passwords project supports Bootstrap v5. The new version is published at Fido.KenHaggerty.Com. You can register a new user with Windows Hello or a FIDO2 security key. Details, screenshots, and related articles can be found at ASP.NET Core 5.0 - Users Without Passwords Project. The details page includes the version change log.

The Users Without Passwords Project's (UWPP) registration and login processes involve communication between the server, client-js, authenticator, and user. The server provides a unique code called a challenge to the client. The client transforms the challenge to a UInt8Array expected by the authenticator. The challenge and username are used to register or verify a public key with the authenticator. The client sends the response from the authenticator to the server. The response is decoded and verified. The response includes the challenge which is decoded to verify a match to the server's original code. If the response and challenge are verified the response is stored and action is implemented like create or login the user.

The challenge code must persist on the server between the initial request and the callback. An early prototype for UWPP implemented Application Session State but I found the Cookie TempDataProvider met my requirements and was easier to implement. See ASP.NET Core 3.1 - Round Trip Challenge.

Add the CookieTempDataProvider with options to the RazorPages service.

Startup.cs > ConfigureServices
services.AddRazorPages()
    .AddCookieTempDataProvider(options =>
    {
        options.Cookie.IsEssential = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
    });

During research for this article, I found injecting the ITempDataProvider to the PageModel is redundant. The ITempDataProvider has two methods which return or save an IDictionary<string, object>.

Microsoft. AspNetCore. Mvc. ViewFeatures. ITempDataProvider:
//
// Summary:
//     Loads the temporary data.
//
// Parameters:
//   context:
//     The Microsoft.AspNetCore.Http.HttpContext.
//
// Returns:
//     The temporary data.
IDictionary<string, object> LoadTempData(HttpContext context);
//
// Summary:
//     Saves the temporary data.
//
// Parameters:
//   context:
//     The Microsoft.AspNetCore.Http.HttpContext.
//
//   values:
//     The values to save.
void SaveTempData(HttpContext context, IDictionary<string, object> values);

The PageModel implements an ITempDataDictionary named TempData.

Microsoft. AspNetCore. Mvc. RazorPages. PageModel:
// Summary:
//     Gets or sets Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionary used by
//     Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
public ITempDataDictionary TempData { get; set; }

The ITempDataDictionary inherits IDictionary and adds methods.

Microsoft. AspNetCore. Mvc. ViewFeatures. ITempDataDictionary:
//
// Summary:
//     Marks all keys in the dictionary for retention.
void Keep();
//
// Summary:
//     Marks the specified key in the dictionary for retention.
//
// Parameters:
//   key:
//     The key to retain in the dictionary.
void Keep(string key);
//
// Summary:
//     Loads the dictionary by using the registered Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataProvider.
void Load();
//
// Summary:
//     Returns an object that contains the element that is associated with the specified
//     key, without marking the key for deletion.
//
// Parameters:
//   key:
//     The key of the element to return.
//
// Returns:
//     An object that contains the element that is associated with the specified key.
object Peek(string key);
//
// Summary:
//     Saves the dictionary by using the registered Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataProvider.
void Save();

I updated the UWPP using the TempData attribute and functions rather than injecting the ITempDataProvider. I developed a couple of pages to demonstrate some of the TempData functions I use in the UWPP. The first page demonstrates the transfer of a of a user input (LoginName) to a second page. I implement the IDictionary key accessor because the TempData attribute overrides the BindProperty attribute on the LoginName property. The demo is included the UWPP and is published on fido.kenhaggerty.com at TempData Demo.

TempDataDemoP1.cs
public class TempDataDemoP1Model : PageModel
{
    [BindProperty]
    [Required]
    [Display(Name = "Login Name")]
    [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
    public string LoginNameInput { get; set; }

    public void OnGet()
    {
        TempData.Clear();
    }

    public IActionResult OnPost()
    {
        if (!ModelState.IsValid) return Page();

        TempData["LoginName"] = LoginName;

        return RedirectToPage("./TempDataDemoP2");
    }
}
TempData Demo Page 1
TempData Demo Page 1 Mobile

One of the tasks is creating the challenge code which is required by FIDO2 authenticators. The UWPP implements a new Guid and a Guid property named ChallengeGuid for the challenge code. When the response from the authenticator is posted to the server for validation, the original ChallengeGuid is compared to the ChallengeGuid included in the response. Page 2 persists the LoginName property from page 1 and the ChallengeGuid property on the server between the initial request and the AJAX callback with TempData attributes.

TempDataDemoP2.cshtml.cs
public class TempDataDemoP2Model : PageModel
{
    [TempData]
    public string LoginName { get; set; }
    [TempData]
    public Guid ChallengeGuid { get; set; }
       
    public IActionResult OnGet()
    {
        if (TempData.Peek("LoginName") == null)
            return RedirectToPage("./TempDataDemoP1");

        TempData.Keep();

        // Generate challenge
        ChallengeGuid = Guid.NewGuid();

        return Page();
    }

    public JsonResult OnPostCallback([FromBody] TempDataDemoPostDataModel postData)
    {
        if (TempData.Peek("ChallengeGuid") == null || TempData.Peek("LoginName") == null)
            return new JsonResult(new { Status = "Failed", Error = "TempData not found." });

        if (ChallengeGuid.ToString() != postData.ChallengeGuid || LoginName != postData.LoginName)
            return new JsonResult(new { Status = "Failed", Error = "TempData does not match post data." });

        return new JsonResult(new { Status = "Success", Message = "TempData matches post data." });
    }
}

public class TempDataDemoPostDataModel
{
    public string LoginName { get; set; }
    public string ChallengeGuid { get; set; }
}
TempData Demo Page 2
TempData Demo Page 2 Mobile

I updated mesage-modal.js (Bootstrap v5 Message Modal) which dynamically creates a modal to communicate with the user. The UWPP implements two separate projects. The Users Without Passwords v4 project supports Bootstrap v4 and the new Users Without Passwords project supports Bootstrap v5. I added mesage-modal-v4.js (Bootstrap v4 Message Modal) to the Users Without Passwords v4 project. The JavaScript for Page 2's AJAX callback implements the showMessageModal() function from mesage-modal.js. See ASP.NET Core 3.1 - Message Modal.

TempDataDemoP2.cshtml
<script src="/js/message-modal.js"></script>
<script>
    let postData = {
        LoginName: '@@Model.LoginName',
        ChallengeGuid: '@@Model.ChallengeGuid'
    }

    function ajaxPost() {
        return fetch(window.location.pathname + '?handler=callback',
            {
                method: 'post',
                credentials: 'same-origin',
                headers: {
                    'Content-Type': 'application/json;charset=UTF-8',
                    'RequestVerificationToken':
                        document.querySelector('input[type="hidden"][name="__RequestVerificationToken"]').value
                },
                body: JSON.stringify(postData)
            })
            .then(function (response) {
                if (response.ok) return response.text();
                else throw Error('Response Not OK');
            })
            .then(function (text) {
                try { return JSON.parse(text); }
                catch (err) { throw Error('Method Not Found'); }
            })
            .then(function (responseJSON) {
                if (responseJSON.Status == 'Success')
                    showMessageModal(responseJSON.Message, 'alert-success', true, 'Done', '', false, '', '', '', '', false);
                else
                    showMessageModal(responseJSON.Error, 'alert-danger', false, '', '', true, '/admin/tempdatademop1',
                        '_self', 'Try Again', 'btn-primary', false);
            })
            .catch(function (error) {
                showMessageModal(error, 'alert-danger', false, '', '', true, '/admin/tempdatademop1',
                    '_self', 'Try Again', 'btn-primary', false);
            })
    }

    document.addEventListener('DOMContentLoaded', function () {
        document.querySelector('.ajax-button').addEventListener('click', function () {
            ajaxPost();
        });
    });
</script>

The callback compares the posted data properties to the TempData properties.

AJAX Post TempData Match
AJAX Post TempData Match Mobile

The first callback matches but a second callback fails for TempData not found.

AJAX Post TempData Not Found
AJAX Post TempData Not Found Mobile

The only issue I found was when I tried to save then read the challenge Guid as string.

[TempData]
public string ChallengeGuidString { get; set; }
InvalidCastException: Unable to cast object of type 'System.Guid' to type 'System.String'.

I tried casting with no joy, so I just use the type it was expecting.

[TempData]
public Guid ChallengeGuid { get; set; }

Article Tags:

FIDO JavaScript Modal

Comment Count = 0

Please log in to comment.

Login Register
Logged in users receive web notifications.
Web Notifications