Valet Key pattern

Updated 22/07/2025

Definition

“Use a token that provides clients with restricted direct access to a specific resource, in order to offload data transfer from the application.”

Simple Exammple

Using a .Net8 ASP.NET Web Application this can be added following the steps below, this App was already secured by Microsoft Identity Provider

Configure Authentication

  1. Install nuget Microsoft.AspNetCore.Authentication.Cookies at the time I used v2.3.0
  2. Update the builder by setting the Apps authentication. This configures “Cookie” authentication for most operations like authenticating users and handling sign-in/out. For situations requiring a user to log in, it uses OpenID Connect, typically redirecting to an external identity provider. Finally, it integrates AddMicrosoftIdentityWebApp, enabling the application to use Microsoft identity platform (like Azure Active Directory) for authentication.
1
2
3
4
5
6
7
8
9
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; // default scheme for authentication (e.g., to read cookie from `ValetKey` OR `Microsoft Identity Provider` login)
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; // default scheme for challenging (e.g., redirecting to OIDC provider)
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; // default scheme for sign-in/sign-out operations
})

// This is not related to the `Valet Key` but I kept this for completeness so the reader can understand that the app now has 2 schemas, `CookieAuthentication` AND `OpenIdConnect`
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd")); // Add Microsoft Identity Web App which internally adds its own cookie handler
  1. Add app.UseAuthentication(); which adds the Authentication Middleware to the ASP.NET Core request pipeline. This middleware is responsible for actually performing the authentication process, using the schemes configured (like the Cookie and OpenID Connect schemes in AddAuthentication above). It determines the user’s identity based on credentials (e.g., from a cookie or a token) and populates the HttpContext.User property, making the authenticated user’s information available to subsequent middleware and your application code. Without app.UseAuthentication(), even if you’ve defined authentication schemes, the application won’t actually perform the authentication checks.

Define An Ingres

  1. This is just the endpoint that will be accessed, note that this needs to have [AllowAnonymous] attribute
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

namespace WebApp.Controllers;

[AllowAnonymous]
public class ValetKeyController : Controller
{
// contrived example
private readonly Dictionary<string, (string UserId, string UserName, List<string> Roles)> _valetKeys =
new()
{
{ "supersecretkey123", ("1", "Katy Perry", new List<string> { "stp.valetkey" }) },
};

public async Task<IActionResult> Index(string id)
{
if (string.IsNullOrEmpty(id))
{
return BadRequest("No key found");
}

// this contrived example just checks the key from a hard coded collection
// you could read this from a database
// the key should expire after a sensible period and contain all the claims you want the user to have
if (!_valetKeys.TryGetValue(id, out var keyData))
{
return Unauthorized("Key not found or invalid");
}

var claims = new List<Claim>
{
new (ClaimTypes.NameIdentifier, keyData.UserId), // OpenIdConnect, ClaimTypes.NameIdentifier often maps to the 'sub' or 'oid' claim.
new (ClaimTypes.Name, keyData.UserName), // OpenIdConnect, ClaimTypes.Name often maps to the 'name' claim.
new ("valet_key_used", "true") // Custom claim to indicate valet key login
};

// Add roles dynamically based on the key data
foreach (var role in keyData.Roles)
{
claims.Add(new Claim(ClaimTypes.Role, role));
}

var claimsIdentity = new ClaimsIdentity(claims, OpenIdConnectDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

// Sign in will issue an authentication cookie
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
claimsPrincipal,
new AuthenticationProperties
{
IsPersistent = false, // Session cookie is cleared when browser closes
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30) // explicit expiry for the cookie
});

// RedirectToAction - redirect to the protected resource
}
}

Key Validation

If the key is not read from a database you could encode it based on a custom Token class, the high level steps could be:

Create a new encrypted token

  1. Create the object from your token class
1
2
3
4
5
6
var token = new Token { 
UserId = Guid.Parse("00000000-0000-0000-0000-000000000001");
UserName = "foo@baz.com"
ExpireDate = DateTime.Now.AddDays(7), // Token valid for 7 days
Roles = new List<string> { "stp.valetkey" }) // self contained
};
  1. Get the bytes
1
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(token));
  1. Encrypt the bytes, you can use any cryptography thats decryptable (symmetric so able to be reversed). Ive seen most examples suggesting AES.
1
var encryptedBytes = encryptionService.Encrypt(bytes);
  1. Urlencode the encrypted bytes so it can be used in a query string
1
return Base64UrlTextEncoder.Encode(encryptedBytes);

Decrypt the token
This is the reverse of the above, thats the symmetric bit :)

  1. Get the bytes
1
var bytes = Base64UrlTextEncoder.Decode(token);
  1. Decrypt the bytes, see examples above
1
var decryptedBytes = encryptionService.Decrypt(bytes);
  1. Get the string value of the bytes
1
var jsonString = Encoding.UTF8.GetString(decryptedBytes);
  1. Finally deserialize
1
return JsonConvert.DeserializeObject<ContinuationToken>(jsonString);

References