Best Practices for PartitionKey and RowKey in Azure Table Storage

1. PartitionKey: Design for Scalability and Query Patterns

The PartitionKey determines how your data is distributed across storage nodes. Good partitioning avoids hotspots and keeps queries fast.

✔️ Best Practices

  • Group entities that you frequently query together
    Azure Tables can only efficiently query within a partition. If you often query “all orders for a customer,” then PartitionKey = CustomerId is a strong choice.

  • Avoid extremely large single partitions
    A single partition can only scale so far. If you expect millions of entities in one partition, consider adding a secondary dimension, such as:

    • CustomerId + Month
    • DeviceId + Date
    • Region + Category
  • Avoid extremely small partitions
    Too many tiny partitions can slow down scans and increase overhead.

  • Choose keys that evenly distribute load
    If all writes go to the same partition (e.g., PartitionKey = "Orders"), you create a hotspot. Spread writes across partitions.

✔️ Good PartitionKey examples

Scenario Good PartitionKey
IoT telemetry DeviceId or DeviceId + Date
Multi-tenant SaaS TenantId
Logging Date (e.g., 2025-02-04)
E‑commerce orders CustomerId or CustomerId + Year

2. RowKey: Ensure Uniqueness and Fast Lookup

The RowKey uniquely identifies an entity within a partition. Azure Tables sort RowKeys lexicographically.

✔️ Best Practices

  • Make RowKey unique within the partition
    Common patterns:

    • GUID
    • Timestamp (inverted for newest-first)
    • Natural key (OrderId, UserId, etc.)
  • Use RowKey to optimize query order
    Because RowKeys are sorted, you can:

    • Store newest items first using a descending timestamp trick:
      RowKey = (DateTime.MaxValue - timestamp).Ticks
    • Store items alphabetically or numerically for range queries.
  • Keep RowKeys short
    Long keys increase storage cost and slow down queries.

✔️ Good RowKey examples

Scenario Good RowKey
Logging Inverted timestamp (RowKey = MaxTicks - Now.Ticks)
Orders OrderId
IoT telemetry Timestamp or sequence number
User profiles UserId

3. General Key Design Principles

✔️ Keep keys ASCII-safe

Avoid characters that require escaping (/, \, #, ?).

✔️ Keep keys predictable

You want to be able to compute the key without extra lookups.

✔️ Keep keys immutable

Changing keys means deleting and re‑inserting the entity.

✔️ Think about your query patterns first

Azure Tables are not relational. You design keys based on how you read data, not how you model it.


4. Common Patterns (with examples)

Pattern A: Time-series data

PartitionKey: DeviceId
RowKey: Inverted timestamp

  • Fast “latest first” queries
  • Even distribution across devices

Pattern B: Multi-tenant SaaS

PartitionKey: TenantId
RowKey: EntityId

  • Easy to isolate tenant data
  • Scales well

Pattern C: Event logs

PartitionKey: Date (e.g., 2025-02-04)
RowKey: GUID or timestamp

  • Efficient daily queries
  • Avoids giant partitions

5. Anti‑Patterns (Avoid These)

❌ PartitionKey = same value for all rows

Creates a massive hotspot.

❌ RowKey = random GUID when you need sorted queries

GUIDs destroy ordering.

❌ PartitionKey = GUID

You lose the ability to query groups of related data.

❌ Too many partitions (e.g., PartitionKey = GUID per row)

Makes range scans impossible.


Summary

PartitionKey

  • Group related data
  • Spread load
  • Match your query patterns
  • Avoid hotspots

RowKey

  • Unique within partition
  • Sorted for fast range queries
  • Short and predictable

Together, they define your performance, scalability, and cost.

Best Practices: Securing Azure AD Configuration in Single Page Applications

Reading Time: 10 minutes

When building Single Page Applications (SPAs) with Azure AD authentication, developers often ask: “How do I protect my Tenant ID and Client ID from being exposed?”

The short answer might surprise you: You don’t—and you shouldn’t try to.

This article explains what information is truly public vs. secret in SPAs, debunks common security misconceptions, and provides practical best practices for securing your Azure AD-enabled applications.

Understanding What’s Secret and What’s Not

First, let’s clarify a critical distinction that causes much confusion:

Public Values (NOT Secrets)

These values cannot and should not be kept secret in SPAs:

  • Client ID (Application ID) – Identifies your application
  • Tenant ID – Identifies your Azure AD tenant
  • Authority URLs – The login endpoints
  • Redirect URIs – Where authentication flows return
  • API Scopes – Permissions your app requests

Why? Microsoft designs these values to be public. Any code running in a browser can be inspected by users—there are no secrets in client-side code.

Actual Secrets (NEVER in SPAs)

These must never appear in client-side code:

  • Client Secrets – Use server-side applications only
  • API Keys – Backend only
  • Connection Strings – Backend only
  • Private Keys – Backend only

Why? If these appear in your SPA, any user can extract and abuse them.

The PKCE Flow: Security Without Secrets

SPAs use the Proof Key for Code Exchange (PKCE) OAuth 2.0 flow, which is specifically designed to work securely without client secrets.

How PKCE Works:

  1. Your SPA generates a random code verifier
  2. Creates a code challenge from the verifier
  3. Sends the challenge during authentication
  4. Azure AD validates using the verifier

This cryptographic exchange prevents token interception attacks without requiring secrets.

Example: MSAL.js Configuration

// This is perfectly secure - no secrets needed
const msalConfig = {
    auth: {
        clientId: "1F5125A6-0098-493D-9C4E-2CA6DDD11998",  // Public
        authority: "https://login.microsoftonline.com/A588175B-C520-4961-BF1F-2C583DD047C8",  // Public
        redirectUri: "http://localhost:5173"  // Public, but controlled in Azure AD
    }
};

// MSAL.js automatically uses PKCE flow
const msalInstance = new msal.PublicClientApplication(msalConfig);

Best Practice #1: Register Allowed Redirect URIs

While Client IDs are public, you control where authentication tokens can be sent through Azure AD app registration.

Configuration in Azure Portal

Navigate to: Azure Portal → App Registrations → Your App → Authentication

Redirect URIs:

Key Principle: Even if someone steals your Client ID, they cannot receive tokens unless they control a registered redirect URI. This is your primary defense mechanism.

Best Practices for Redirect URIs:

  • Only register legitimate URIs you control
  • Use HTTPS in production (never HTTP)
  • Never use wildcards (e.g., https://*.yourdomain.com)
  • Avoid overly permissive patterns

Best Practice #2: Validate Tokens on the Backend

The real security happens on your API server, not in the SPA. Always validate every token.

ASP.NET Core API Token Validation

// Program.cs
builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options => 
    {
        var tenantId = builder.Configuration["AzureAd:TenantId"];
        var apiClientId = builder.Configuration["AzureAd:ApiClientId"];

        options.Authority = $"https://login.microsoftonline.com/{tenantId}/v2.0";
        
        // Critical validation settings
        options.TokenValidationParameters = new()
        {
            ValidAudience = $"api://{apiClientId}",
            ValidateIssuer = true,      // Verify token from correct tenant
            ValidateAudience = true,    // Verify token for this API
            ValidateLifetime = true,    // Reject expired tokens
            ValidateIssuerSigningKey = true  // Verify signature
        };
    });

// Apply authorization to endpoints
app.MapGet("/api/secure-data", () => "Sensitive data")
    .RequireAuthorization("RequiredPolicy");

Security Checklist:

  • Always validate issuer (prevents tokens from other tenants)
  • Always validate audience (prevents tokens for other APIs)
  • Always validate lifetime (rejects expired tokens)
  • Always validate signature (prevents tampering)
  • Use claims and scopes for authorization

Best Practice #3: Secure Token Storage

Where you store tokens matters significantly for security.

✅ Good: In-Memory Storage (MSAL.js Default)

// MSAL.js stores tokens in memory by default
const msalInstance = new msal.PublicClientApplication(msalConfig);

// Tokens are automatically managed and stored securely
const token = await msalInstance.acquireTokenSilent({
    scopes: ["api://your-api/cms.read"]
});

❌ Bad: localStorage or sessionStorage

// NEVER DO THIS - Vulnerable to XSS attacks
localStorage.setItem('accessToken', token.accessToken);  // ❌ BAD
sessionStorage.setItem('accessToken', token.accessToken);  // ❌ ALSO BAD

Why localStorage is Dangerous:

  • Accessible to any JavaScript on your domain
  • Vulnerable to XSS (Cross-Site Scripting) attacks
  • Persists across browser sessions
  • No built-in security features

Best Practice #4: Configure CORS Properly

Control which origins can call your API to prevent unauthorized cross-origin requests.

ASP.NET Core CORS Configuration

// Program.cs - Configure CORS restrictively
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy.WithOrigins("https://yourdomain.com")  // Specific origins only
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials();  // Required for authentication
    });
});

// Apply CORS middleware
app.UseCors("AllowFrontend");

Important:

  • Never use AllowAnyOrigin() in production
  • Explicitly list allowed origins
  • Use environment-specific configurations

Configuration Management Strategies

While Client IDs can be hardcoded without security risk, proper configuration management improves maintainability.

Option 1: Hardcoded Values (Acceptable)

// index.html - Perfectly acceptable for public values
const clientId = "1F5125A6-0098-493D-9C4E-2CA6DDD11998";
const tenantId = "A588175B-C520-4961-BF1F-2C583DD047C8";

Pros: Simple, fast, no dependencies
Cons: Requires rebuild for environment changes

Option 2: Environment Variables (Better for Multi-Environment)

// Use build-time environment variables (Vite example)
const clientId = import.meta.env.VITE_CLIENT_ID;
const tenantId = import.meta.env.VITE_TENANT_ID;

// .env.production
VITE_CLIENT_ID=1F5125A6-0098-493D-9C4E-2CA6DDD11998
VITE_TENANT_ID=A588175B-C520-4961-BF1F-2C583DD047C8

Pros: Environment-specific, industry standard
Cons: Still requires rebuild per environment

Option 3: Configuration Endpoint (Best for Centralization)

Serve configuration from your backend API for runtime flexibility.

Backend Configuration Endpoint (.NET)

// Program.cs
app.MapGet("/config", (IConfiguration config) => Results.Ok(new
{
    azureAd = new
    {
        clientId = config["AzureAd:ClientId"],
        tenantId = config["AzureAd:TenantId"],
        apiClientId = config["AzureAd:ApiClientId"]
    }
}));

appsettings.json

{
  "AzureAd": {
    "ClientId": "1F5125A6-0098-493D-9C4E-2CA6DDD11998",
    "TenantId": "A588175B-C520-4961-BF1F-2C583DD047C8",
    "ApiClientId": "ABDBD38F-384E-4640-B7C6-34C8340A442E"
  }
}

Frontend Initialization

// index.html - Fetch configuration at runtime
let msalInstance;
let apiScope;

(async function initializeApp() {
    try {
        // Fetch configuration from API
        const response = await fetch('https://api.yourdomain.com/config');
        const config = await response.json();

        // Initialize MSAL with fetched configuration
        const msalConfig = {
            auth: {
                clientId: config.azureAd.clientId,
                authority: `https://login.microsoftonline.com/${config.azureAd.tenantId}`,
                redirectUri: window.location.origin
            }
        };

        apiScope = `api://${config.azureAd.apiClientId}/cms.read`;
        msalInstance = new msal.PublicClientApplication(msalConfig);

        console.log('App initialized successfully');
    } catch (error) {
        console.error('Failed to load configuration:', error);
    }
})();

Pros: Single source of truth, no rebuilds needed, runtime flexibility
Cons: Additional HTTP request on startup

Additional Security Measures

1. Implement Content Security Policy (CSP)

<!-- Add to HTML head or HTTP headers -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' https://alcdn.msauth.net; 
               connect-src 'self' https://login.microsoftonline.com">

2. Enable Azure AD Conditional Access

Configure in Azure Portal to enforce:

  • Multi-factor authentication (MFA)
  • Trusted device requirements
  • IP address restrictions
  • Risk-based access policies

3. Use Short-Lived Tokens

Azure AD defaults to 1-hour access token lifetimes. Don’t increase this unnecessarily.

// MSAL.js handles token refresh automatically
const token = await msalInstance.acquireTokenSilent({
    scopes: [apiScope],
    account: msalInstance.getActiveAccount()
});

4. Implement API Rate Limiting

// ASP.NET Core rate limiting
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", opt =>
    {
        opt.Window = TimeSpan.FromMinutes(1);
        opt.PermitLimit = 100;
    });
});

app.UseRateLimiter();

app.MapGet("/api/data", () => "data")
   .RequireRateLimiting("api");

5. Monitor and Audit

  • Enable Azure AD sign-in logs
  • Monitor failed authentication attempts
  • Set up alerts for suspicious activity
  • Review API access patterns regularly

Common Security Anti-Patterns to Avoid

❌ Anti-Pattern 1: Trying to Hide Public Values

// Don't waste time obfuscating public values
const secret = atob("ZjFmYWY2NGYtMWE2NC00NWNjLTlkNGItZTIwMDhkMWNhZmM5");  // ❌ Pointless

Anyone can decode this. Accept that Client IDs are public.

❌ Anti-Pattern 2: Proxying Authentication Through Your Backend

// DON'T do this to "hide" client ID
app.post('/auth/login', (req, res) => {
    // Server exchanges credentials for token
    // Then sends token to client
});  // ❌ Defeats PKCE security, adds complexity

Use proper OAuth 2.0 flows designed for SPAs.

❌ Anti-Pattern 3: Client Secrets in SPAs

// NEVER include client secrets
const msalConfig = {
    auth: {
        clientId: "...",
        clientSecret: "super-secret-key"  // ❌ EXTREMELY DANGEROUS
    }
};  // Anyone can extract this and impersonate your app

Security Checklist for Production

Before deploying your SPA to production, verify:

Azure AD Configuration

  • Redirect URIs are restrictively configured
  • No wildcards in redirect URIs
  • HTTPS enforced for production URIs
  • Appropriate API permissions configured
  • Admin consent granted (if required)

API Security

  • Token validation enabled (issuer, audience, lifetime)
  • Authorization policies implemented
  • CORS properly configured
  • Rate limiting implemented
  • HTTPS enforced

SPA Security

  • No client secrets in code
  • Tokens stored in memory (not localStorage)
  • Content Security Policy implemented
  • Dependencies updated and scanned
  • Proper error handling (no token leaks in errors)

Monitoring

  • Sign-in logs enabled
  • Failed authentication alerts configured
  • API access logging implemented
  • Regular security reviews scheduled

Conclusion

Securing SPAs with Azure AD doesn’t require hiding Client IDs or Tenant IDs—these values are designed to be public. Instead, focus your security efforts on:

  1. Properly configuring redirect URIs in Azure AD
  2. Validating tokens rigorously on your backend API
  3. Using secure token storage (in-memory, not localStorage)
  4. Implementing proper CORS policies
  5. Enabling monitoring and alerts for suspicious activity

The security of your SPA relies on proper OAuth 2.0 flows (PKCE), token validation, and infrastructure configuration—not on attempting to hide public identifiers.

Remember: The PKCE flow is specifically designed to work securely with public Client IDs. Trust the design, follow best practices, and focus on the security measures that truly matter.

Additional Resources


Have questions or feedback? Share your thoughts in the comments below!

GitHub Identity vs Azure Token in Azure Static Web Apps

Understanding the differences, risks, benefits, and when to use each

Azure Static Web Apps supports two main authentication/authorization approaches during deployment:

  1. GitHub Identity (OIDC or GitHub Actions permissions)
  2. Azure Token (Service Principal / Azure AD App Registration)

They both let GitHub Actions deploy your app, but they work very differently.

1. GitHub Identity (OIDC)

✔️ What it is

GitHub Actions uses OpenID Connect (OIDC) to request a short‑lived Azure token at deploy time, without storing any secrets. Azure trusts GitHub’s identity provider and issues a temporary token.

⭐ Benefits

  • No secrets stored in GitHub
    Nothing to rotate, leak, or accidentally commit.
  • Short‑lived tokens
    Tokens expire quickly, reducing blast radius.
  • Least privilege by design
    You grant GitHub Actions a specific role on a specific resource.
  • Automatic rotation
    No manual maintenance.
  • Recommended by Microsoft for modern deployments
    It’s the “secure-by-default” option.

⚠️ Risks / Limitations

  • Requires Azure setup
    You must configure a federated identity credential on an Entra ID app.
  • Only works with GitHub Actions
    If you switch CI/CD providers, you must reconfigure.
  • More complex initial setup
    Especially if you’re not familiar with Entra ID.

🧭 When to use GitHub Identity

Use it when:

  • You deploy from GitHub Actions (most SWA users do).
  • You want maximum security with no secrets.
  • You want zero maintenance authentication.
  • You’re building a long‑term, production‑grade pipeline.

This is the best practice for modern Azure deployments.

2. Azure Token (Service Principal)

✔️ What it is

Service Principal (SP) is an Azure AD application with a client ID and client secret. GitHub Actions uses this secret to authenticate to Azure.

⭐ Benefits

  • Simple to understand
    It’s just a username/password for Azure.
  • Works with any CI/CD provider
    GitHub, Azure DevOps, GitLab, Jenkins, etc.
  • Good for legacy pipelines
    Many older workflows rely on SPs.

⚠️ Risks / Limitations

  • Secrets must be stored in GitHub
    Even in encrypted secrets, this is a risk.
  • Secrets can leak
    Through logs, PRs, misconfigured workflows, or compromised GitHub accounts.
  • Secrets must be rotated manually
    Developers often forget.
  • Long‑lived credentials
    If compromised, attackers have persistent access.

🧭 When to use Azure Token

Use it when:

  • You need multi‑platform CI/CD (not just GitHub).
  • You have existing SP‑based pipelines and can’t migrate yet.
  • You need fine‑grained control over the identity (e.g., custom API permissions).

This is the legacy but still valid option.

Side‑by‑Side Comparison

FeatureGitHub Identity (OIDC)Azure Token (Service Principal)
Secrets stored in GitHub❌ None✔️ Yes (client secret)
Token lifetimeShort-livedLong-lived
RotationAutomaticManual
Setup complexityMediumLow
Works outside GitHub❌ No✔️ Yes
Security posture⭐ Strong⚠️ Weaker
Recommended by Microsoft✔️ Yes⚠️ Only for legacy

How to Secure Each Approach

GitHub Identity (OIDC)

  • Restrict trust to specific GitHub repo + branch
    (e.g., only main can request tokens)
  • Assign least-privilege roles
    Usually Contributor or Static Web App Contributor.
  • Use environment protection rules
    Require approvals for production deployments.
  • Use branch protection
    Prevent unauthorized pushes to the trusted branch.

This is already extremely secure.

Azure Token (Service Principal)

If you must use SPs, harden them:

  • Store secrets only in GitHub Encrypted Secrets
  • Rotate secrets every 90 days
  • Use least-privilege roles
  • Enable Conditional Access (IP restrictions, MFA for portal access)
  • Use Managed Identity inside Azure where possible
  • Avoid using the same SP for multiple apps

SPs can be secure, but they require disciplin

Which Should You Use?

Given your background—structured learning, Azure-focused, building a real-world CMS project—the best choice is:

👉 Use GitHub Identity (OIDC) for all new Static Web App deployments.

It’s:

  • more secure
  • easier to maintain
  • aligned with modern Azure DevOps practices
  • ideal for production workloads

Use a Service Principal only if:

  • you need cross-platform CI/CD
  • you’re integrating with tools outside GitHub
  • you’re migrating legacy pipelines