ASP.NET  

How to Build a Digital Membership Card System with ASP.NET

Introduction

Membership systems have evolved significantly, with modern public-facing applications using digital membership cards instead of traditional physical cards. Today’s systems must support real-time member validation, identity management, multi-platform access, and backend architectures that can scale and evolve over time. ASP.NET Core is well-suited for these systems due to its high performance, modular architecture, and built-in support for modern authentication protocols.

This article explains how to architect a digital membership card solution using ASP.NET Core and C#, while addressing real-world challenges, scalability, and secure member identity.

A digital membership card system should provide the following capabilities:

  • Secure member authentication

  • A uniquely assigned digital card for each member

  • Real-time validation at entry points (gyms, spas, etc.)

  • Scalability across platforms and high concurrent usage

  • Integration with external services (notifications, CRM, analytics)

To achieve this, we adopt an API-first design that decouples membership logic from presentation layers.

Architectural Overview (API-First)

ASP.NET Core is designed for API-first development with lightweight routing, dependency injection, and middleware-based pipelines.

Separation of Concerns

Authentication, card issuance, and validation logic are isolated into dedicated services.

Minimal APIs

Each endpoint has a single responsibility.

Service Abstractions

Interfaces define contracts for core services such as card generation and validation.

Example API Endpoint

app.MapGet("/api/members/{id}/card", async (int id, IMembershipService svc) =>
{
    var card = await svc.GenerateCardAsync(id);
    return card is null ? Results.NotFound() : Results.Ok(card);
});

Third-party systems can use this endpoint to fetch a digital membership card.

Building Structure of the System

1. Managing Member Identity

After authentication, a member receives a JWT (JSON Web Token) used for subsequent requests. No server-side session storage is required.

JWT claims include:

  • Member ID

  • Member role (Standard, Premium, Admin)

  • Expiration timestamp

This enables easier horizontal scaling.

2. Digital Card Representation

A digital membership card is a structured object representing access rights.

public class DigitalCard
{
    public Guid CardId { get; set; }
    public int MemberId { get; set; }
    public string MembershipTier { get; set; }
    public DateTime ValidUntil { get; set; }
    public string QrData { get; set; }
}

The QrData field stores a secure token.

Securing Digital Membership Card Data

Digital membership systems contain sensitive information such as identity, privileges, and validity periods. Security must be enforced at both API and data layers.

1. Generating Secure Card Tokens

public string GenerateSecureCardToken(int memberId)
{
    var key = new SymmetricSecurityKey(
        Encoding.UTF8.GetBytes(_config["Jwt:Key"])
    );

    var credentials = new SigningCredentials(
        key, SecurityAlgorithms.HmacSha256
    );

    var token = new JwtSecurityToken(
        issuer: "MembershipAPI",
        audience: "CardValidation",
        claims: new[]
        {
            new Claim("memberId", memberId.ToString()),
            new Claim("scope", "digital_card")
        },
        expires: DateTime.UtcNow.AddMinutes(5),
        signingCredentials: credentials
    );

    return new JwtSecurityTokenHandler().WriteToken(token);
}

This token can be embedded in a QR code.

2. Secure Card Validation Endpoint

[Authorize]
[HttpPost("validate-card")]
public IActionResult ValidateCard([FromBody] string token)
{
    var handler = new JwtSecurityTokenHandler();
    var jwt = handler.ReadJwtToken(token);

    var memberId = jwt.Claims.First(c => c.Type == "memberId").Value;
    var scope = jwt.Claims.First(c => c.Type == "scope").Value;

    if (scope != "digital_card")
        return Unauthorized();

    return Ok(new { Status = "Valid", MemberId = memberId });
}

Tokens expire automatically, cards can be revoked instantly, and validation logic remains server-controlled.

3. Encrypt Sensitive Data at Rest

public string Encrypt(string data)
{
    using var aes = Aes.Create();
    aes.Key = Convert.FromBase64String(_config["EncryptionKey"]);
    aes.GenerateIV();

    var encryptor = aes.CreateEncryptor();
    var encrypted = encryptor.TransformFinalBlock(
        Encoding.UTF8.GetBytes(data), 0, data.Length
    );

    return Convert.ToBase64String(encrypted);
}

Encryption protects data even if the database is compromised.

Integrating Native Wallet Tickets

To support Apple Wallet or Google Wallet, the backend must generate wallet-compatible passes containing secure membership tokens. ASP.NET Core handles pass issuance and validation, while the wallet serves as secure client-side storage.

Example: Shopify to Digital Membership Card Integration

Step 1: Shopify Webhook Models

public class ShopifyOrderWebhook
{
    public long Id { get; set; }
    public string FinancialStatus { get; set; }
    public ShopifyCustomer Customer { get; set; }
}

public class ShopifyCustomer
{
    public long Id { get; set; }
    public string Email { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Step 2: Secure Webhook Endpoint

[ApiController]
[Route("api/webhooks/shopify")]
public class ShopifyWebhookController : ControllerBase
{
    private readonly IMembershipService _membershipService;

    public ShopifyWebhookController(IMembershipService membershipService)
    {
        _membershipService = membershipService;
    }

    [HttpPost("order-paid")]
    public async Task<IActionResult> OrderPaid()
    {
        using var reader = new StreamReader(Request.Body);
        var body = await reader.ReadToEndAsync();

        if (!VerifyShopifySignature(body))
            return Unauthorized();

        var order = JsonSerializer.Deserialize<ShopifyOrderWebhook>(body);

        if (order.FinancialStatus == "paid")
        {
            await _membershipService.IssueMembershipAsync(order.Customer);
        }

        return Ok();
    }

    private bool VerifyShopifySignature(string requestBody)
    {
        var shopifySecret = Encoding.UTF8.GetBytes("SHOPIFY_WEBHOOK_SECRET");
        using var hmac = new HMACSHA256(shopifySecret);

        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(requestBody));
        var calculated = Convert.ToBase64String(hash);

        return Request.Headers["X-Shopify-Hmac-Sha256"] == calculated;
    }
}

Step 3: Issue Digital Membership Card

public async Task IssueMembershipAsync(ShopifyCustomer customer)
{
    var member = await _memberRepository.GetByEmailAsync(customer.Email)
        ?? await _memberRepository.CreateAsync(new Member
        {
            Email = customer.Email,
            FullName = $"{customer.FirstName} {customer.LastName}"
        });

    await _cardService.GenerateCardAsync(member.Id);
}

Step 4: Sync Membership Status Back to Shopify

public async Task UpdateShopifyCustomerAsync(long customerId)
{
    var payload = new
    {
        customer = new
        {
            id = customerId,
            tags = "digital_member"
        }
    };

    var request = new HttpRequestMessage(
        HttpMethod.Put,
        $"/admin/api/2024-01/customers/{customerId}.json"
    );

    request.Content = new StringContent(
        JsonSerializer.Serialize(payload),
        Encoding.UTF8,
        "application/json"
    );

    await _httpClient.SendAsync(request);
}

Step 5: CRM Integration Service

public class CrmIntegrationService : ICrmIntegrationService
{
    private readonly HttpClient _httpClient;

    public CrmIntegrationService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task SyncCustomerAsync(ShopifyCustomerDto customer)
    {
        var crmPayload = new
        {
            ExternalId = customer.Id,
            Email = customer.Email,
            FirstName = customer.FirstName,
            LastName = customer.LastName
        };

        var content = new StringContent(
            JsonSerializer.Serialize(crmPayload),
            Encoding.UTF8,
            "application/json"
        );

        var response = await _httpClient.PostAsync("/crm/customers", content);
        response.EnsureSuccessStatusCode();
    }
}

Step 6: Register HTTP Client for CRM

services.AddHttpClient<ICrmIntegrationService, CrmIntegrationService>(client =>
{
    client.BaseAddress = new Uri("https://your-crm-api.example.com");
    client.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_CRM_API_KEY");
});