1. What is B2C Authentication?

B2C (Business-to-Consumer) authentication is the process of verifying the identities of individual end-users who interact with consumer-facing applications.

Unlike B2B authentication — which manages organisational identities and enterprise SSO for employee access — B2C authentication is built around individual user accounts at scale. Every time a consumer logs in to an e-commerce platform, a streaming service, a mobile app, or a SaaS product's self-serve tier, they are going through a B2C authentication flow. B2C auth must handle everything from first-time sign-up through to account recovery, MFA, social login, and eventually GDPR-compliant account deletion.

In 2026, B2C authentication has evolved far beyond username and password systems. Modern B2C auth platforms implement OAuth 2.0 and OpenID Connect (OIDC) — open standards that power consumer login experiences at every scale, from a startup's first 100 users to a platform serving hundreds of millions. Consumers now expect passwordless options, one-tap social login, and instant account recovery. They will abandon any sign-up flow that asks them for too much information or too many steps.

Key characteristics that define B2C authentication

  • Massive scale: B2C systems must handle millions of concurrent sessions, registration spikes during launches, and global distribution without performance degradation.
  • Experience-first design: Every additional step in a sign-up or login flow reduces conversions. B2C auth must be frictionless — fast, intuitive, and mobile-optimised.
  • Self-service lifecycle: Consumers register, verify their email, reset passwords, enrol in MFA, and delete their accounts without any support interaction.
  • Social and federated login: Google, Apple, Microsoft, GitHub, and other identity providers must be first-class options, not afterthoughts.
  • Regulatory compliance: GDPR, CCPA, DSA, and other regulations mandate explicit consent flows, data portability, audit logs, and the right to erasure — all built into the auth layer.
  • Adaptive security: Risk-based anomaly detection, device fingerprinting, and bot protection must challenge attackers without adding friction for legitimate users.

The central challenge of B2C authentication is balancing two opposing forces: maximum security (which adds friction) and maximum convenience (which reduces security). Purpose-built B2C identity platforms are engineered to resolve this tension through intelligent risk assessment, standards-based protocols, and progressive security policies.

2. Why B2C Authentication Matters More Than Ever in 2026

Three converging forces are reshaping B2C authentication in 2026, making the choice of identity infrastructure more consequential than it has ever been.

The password era is genuinely ending

After two decades of "passwords are dead" predictions, 2026 is the year passkeys and WebAuthn have achieved genuine mainstream adoption. Apple, Google, and Microsoft have all made passkeys the default login method across their platforms and operating systems. FIDO2/WebAuthn support is now available on over 95% of active devices. This means your B2C authentication layer must support passkeys or risk offering an inferior experience to competitors who do.

Despite this progress, credential stuffing attacks remain the leading vector for B2C account takeovers. Billions of username and password combinations from historical breaches are freely available to attackers. Any application that relies on passwords alone without MFA is accepting a level of risk that regulators and security-conscious customers in 2026 find unacceptable.

AI-powered attacks are outpacing traditional defences

AI-powered credential stuffing tools, deepfake-assisted social engineering, and synthetic identity fraud have all accelerated dramatically since 2024. Traditional rate limiting and CAPTCHA are no longer sufficient countermeasures on their own. Modern B2C auth requires device fingerprinting, behavioural anomaly detection, and risk-based step-up authentication — capabilities that need to be deeply integrated into the authentication layer itself, not bolted on afterward.

Regulatory pressure is intensifying globally

The EU's Digital Services Act, updated GDPR enforcement guidance from 2025, and a wave of new US state privacy laws all place specific requirements on how consumer identity data is collected, stored, and shared. Authentication systems must now provide cryptographically verifiable audit logs, consent withdrawal mechanisms, and data portability exports at the infrastructure level. Building and maintaining all of this in-house is a significant engineering undertaking that diverts resources from your core product.

3. The Core Protocols Behind B2C Authentication

All modern B2C authentication is built on a stack of open standards. Understanding these protocols is essential for making good architectural decisions and diagnosing authentication issues.

OAuth 2.0 — The Authorisation Framework

OAuth 2.0 (RFC 6749) is an authorisation framework that allows applications to obtain limited, scoped access to user accounts on a third-party service. Critically, OAuth 2.0 is an authorisation protocol — it defines how to grant access permissions, not how to verify user identity. It forms the foundation on which OpenID Connect is built. In a B2C context, OAuth 2.0 governs how your application requests access tokens that are then used to call protected APIs on behalf of the authenticated user.

OpenID Connect (OIDC) — The Authentication Layer

OpenID Connect 1.0 is an identity layer built on top of OAuth 2.0 that adds authentication. Where OAuth 2.0 issues access tokens (opaque credentials that grant API access), OIDC adds the ID token — a signed JWT that cryptographically attests to who the user is. The ID token contains standard claims: sub (subject identifier), email, name, iat (issued-at), and exp (expiry). Every modern B2C auth flow should use OpenID Connect rather than bare OAuth 2.0. The /.well-known/openid-configuration discovery document allows any OIDC client library to automatically configure itself against any compliant provider.

JWT (JSON Web Tokens) — The Token Format

JWTs (RFC 7519) are the standard format for OIDC ID tokens and OAuth 2.0 access tokens. A JWT has three base64url-encoded segments: a header (algorithm and key ID), a payload (claims), and a cryptographic signature. The signature is verified against the provider's public keys published at the JWKS (JSON Web Key Set) endpoint. Security rules for JWTs in B2C: keep access token lifetimes short (5–15 minutes), use refresh tokens for session persistence, never store JWTs in localStorage, and always validate the signature, iss, aud, and exp claims on every use.

PKCE — Proof Key for Code Exchange

PKCE (pronounced "pixy", RFC 7636) is an extension to the authorisation code flow that prevents code interception attacks. The client generates a random code verifier (43–128 characters), computes its SHA-256 hash as the code challenge, and sends the challenge with the initial authorisation request. When exchanging the authorisation code for tokens, the client sends the original verifier — the server hashes it and compares it to the stored challenge. Without the verifier, an intercepted authorisation code is useless to an attacker. PKCE is mandatory for all public clients in 2026. The OAuth 2.1 specification makes PKCE required for all authorisation code flows, eliminating the implicit flow entirely.

4. Securing a B2C Web App: The Direct Answer

What is the best way to secure a B2C web app in 2026?

The most secure method for B2C web app authentication in 2026 is the OAuth 2.0 authorisation code flow with PKCE combined with OpenID Connect. This approach:

  1. Eliminates client secrets from the browser using a cryptographic code challenge (PKCE — RFC 7636)
  2. Issues short-lived access tokens (15 minutes) with longer-lived refresh tokens stored in HttpOnly cookies
  3. Validates the ID token signature against the JWKS endpoint on every login
  4. Enforces HTTPS everywhere — tokens exchanged over plain HTTP are immediately compromised
  5. Applies risk-based TOTP MFA only when anomalous behaviour is detected, preserving UX for trusted sessions

For maximum protection, complement PKCE with a strict Content Security Policy (CSP) to prevent XSS, implement refresh token rotation so replayed tokens are immediately detected, and consider the Backend-for-Frontend (BFF) pattern to eliminate token exposure to browser JavaScript entirely.

For traditional server-rendered web apps, HttpOnly cookie-based sessions are preferable — they are inaccessible to JavaScript, preventing the entire class of XSS token theft attacks. For SPAs calling APIs directly, short-lived JWTs with refresh token rotation are appropriate. The key rule: never store JWTs in localStorage. Use sessionStorage at minimum, or better still, adopt the Backend-for-Frontend (BFF) pattern where a server-side layer holds tokens and issues session cookies to the browser — the SPA never sees a raw token.

The BFF pattern places a thin server-side layer (a Next.js API route, an Express middleware, or a dedicated service) between your browser SPA and the identity provider. The BFF handles the OIDC authorisation code flow, exchanges the code for tokens, stores tokens server-side, and sets an HttpOnly session cookie in the browser. The SPA sends the cookie on every request — it never sees the underlying access or refresh token. This eliminates XSS-based token theft entirely because there is nothing in the browser to steal. The BFF pattern is the recommended architecture for high-security B2C SPAs in 2026.

5. B2C Authentication Flows in 2026

Choosing the correct OAuth 2.0 / OIDC flow is critical. Using the wrong flow can expose tokens to attackers even when the implementation is otherwise correct.

✅ Authorisation Code + PKCE — Use This for All B2C Apps

The only correct flow for B2C web apps, SPAs, and mobile apps in 2026. Step by step:

  1. App generates a random code verifier (43–128 chars, high entropy) and computes its SHA-256 hash as the code challenge
  2. User is redirected to /connect/authorize with the code challenge, response_type=code, scopes, and redirect URI
  3. After successful authentication, the server redirects back with a short-lived authorisation code (one-time use, 60-second expiry)
  4. App POSTs the code + original code verifier to /connect/token
  5. Server hashes the verifier, compares to stored challenge — if they match, issues access token, ID token, and refresh token
❌ Implicit Flow — Deprecated, Do Not Use

The implicit flow issues tokens directly in the URL fragment (#access_token=...). It is deprecated in OAuth 2.1 and vulnerable to token leakage through browser history, referrer headers, and JavaScript injection. Any existing application using implicit flow must migrate to authorisation code + PKCE immediately.

Client Credentials — Machine-to-Machine Only

Uses a client ID and secret to obtain an access token representing the application itself — not a user. Appropriate for backend service-to-service calls (e.g. a microservice calling a protected internal API). Never use for user-facing B2C applications.

6. Deep-Dive Guides

Each B2C authentication scenario has its own architecture and implementation considerations. Explore our dedicated guides below.

Web App Auth vs. Mobile Auth

How B2C auth differs between server-rendered web apps, SPAs, and native mobile apps. Redirect flows, secure token storage, and deep linking.

Read Guide →

Passwordless B2C Authentication

Implement magic links, passkeys (WebAuthn/FIDO2), and TOTP-based passwordless B2C auth. Full implementation guide with security trade-offs.

Read Guide →

Auth for Next.js, React & Vue

Framework-specific B2C auth patterns for Next.js App Router, React SPA, and Vue 3. Token handling, protected routes, and SSR session management.

Read Guide →

Multi-Tenant vs. B2C Architectures

When to use B2C versus B2B multi-tenant identity. Architectural trade-offs, data isolation models, and supporting both from one platform.

Read Guide →

7. Implementation Examples

The following examples show how to integrate Ailacs Identity B2C authentication in the most popular languages and frameworks. All use the authorisation code flow with PKCE + OpenID Connect. Register your application in the Ailacs Identity Portal to get your client ID before starting.

// Program.cs — B2C Authentication with Ailacs Identity (.NET 10)
// dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.Cookie.Name     = "b2c.session";
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy  = CookieSecurePolicy.Always;
    options.Cookie.SameSite      = SameSiteMode.Lax;
    options.ExpireTimeSpan       = TimeSpan.FromHours(8);
    options.SlidingExpiration    = true;
})
.AddOpenIdConnect(options =>
{
    options.Authority    = "https://auth.ailacs.com";
    options.ClientId     = builder.Configuration["Ailacs:ClientId"];
    options.ResponseType = "code";
    options.UsePkce      = true;   // Mandatory for B2C
    options.SaveTokens   = true;
    options.MapInboundClaims = false;
    options.GetClaimsFromUserInfoEndpoint = true;
    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");
    options.TokenValidationParameters = new TokenValidationParameters
    {
        NameClaimType = "name",
        RoleClaimType = "role"
    };
});

// Require auth by default; use [AllowAnonymous] on public pages
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});
// B2C Auth with Ailacs Identity (Node.js / Express)
// npm install openid-client express-session

const express = require('express');
const { Issuer, generators } = require('openid-client');
const session = require('express-session');

const app = express();
app.use(session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: { secure: true, httpOnly: true, sameSite: 'lax' }
}));

let client;
Issuer.discover('https://auth.ailacs.com').then(issuer => {
    client = new issuer.Client({
        client_id:      process.env.AILACS_CLIENT_ID,
        redirect_uris:  ['https://yourapp.com/callback'],
        response_types: ['code'],
    });
});

// Initiate login — generate PKCE verifier + challenge
app.get('/login', (req, res) => {
    const codeVerifier = generators.codeVerifier();
    req.session.codeVerifier = codeVerifier;
    req.session.state        = generators.state();
    const authUrl = client.authorizationUrl({
        scope:                  'openid profile email',
        state:                  req.session.state,
        code_challenge:         generators.codeChallenge(codeVerifier),
        code_challenge_method:  'S256',
    });
    res.redirect(authUrl);
});

// Exchange authorisation code for tokens
app.get('/callback', async (req, res) => {
    const params   = client.callbackParams(req);
    const tokenSet = await client.callback(
        'https://yourapp.com/callback', params,
        { code_verifier: req.session.codeVerifier, state: req.session.state }
    );
    req.session.user = tokenSet.claims();
    res.redirect('/dashboard');
});

app.get('/logout', (req, res) => {
    req.session.destroy();
    res.redirect(client.endSessionUrl({ post_logout_redirect_uri: 'https://yourapp.com' }));
});
# B2C Auth with Ailacs Identity (Python / FastAPI)
# pip install authlib fastapi starlette uvicorn python-dotenv

from fastapi import FastAPI, Request, Depends, HTTPException
from fastapi.responses import RedirectResponse
from authlib.integrations.starlette_client import OAuth
from starlette.middleware.sessions import SessionMiddleware
import os

app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key=os.environ['SESSION_SECRET'])

oauth = OAuth()
oauth.register(
    name='ailacs',
    server_metadata_url='https://auth.ailacs.com/.well-known/openid-configuration',
    client_id=os.environ['AILACS_CLIENT_ID'],
    client_kwargs={
        'scope': 'openid profile email',
        'code_challenge_method': 'S256',  # Enables PKCE automatically
    }
)

@app.get('/login')
async def login(request: Request):
    redirect_uri = str(request.url_for('auth_callback'))
    return await oauth.ailacs.authorize_redirect(request, redirect_uri)

@app.get('/callback')
async def auth_callback(request: Request):
    token = await oauth.ailacs.authorize_access_token(request)
    user  = token['userinfo']
    request.session['user'] = {'sub': user['sub'], 'name': user.get('name'), 'email': user.get('email')}
    return RedirectResponse(url='/dashboard')

@app.get('/logout')
async def logout(request: Request):
    request.session.pop('user', None)
    return RedirectResponse(url='/')

# Reusable auth dependency for protected routes
def require_auth(request: Request):
    if 'user' not in request.session:
        raise HTTPException(status_code=401, detail='Not authenticated')
    return request.session['user']

@app.get('/dashboard')
async def dashboard(user=Depends(require_auth)):
    return {'message': f"Hello, {user['name']}"}
// B2C Auth with Ailacs Identity (Java / Spring Boot 3)
// application.yml:
// spring.security.oauth2.client.registration.ailacs:
//   client-id: ${AILACS_CLIENT_ID}
//   client-authentication-method: none   # public client — PKCE, no secret
//   authorization-grant-type: authorization_code
//   redirect-uri: "{baseUrl}/login/oauth2/code/ailacs"
//   scope: openid,profile,email
// spring.security.oauth2.client.provider.ailacs:
//   issuer-uri: https://auth.ailacs.com

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final ClientRegistrationRepository clientRegistrationRepository;

    public SecurityConfig(ClientRegistrationRepository repo) {
        this.clientRegistrationRepository = repo;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/public/**", "/error").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .defaultSuccessUrl("/dashboard", true)
            )
            .logout(logout -> logout
                .logoutSuccessHandler(oidcLogoutHandler())
            );
        return http.build();
    }

    private OidcClientInitiatedLogoutSuccessHandler oidcLogoutHandler() {
        var handler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
        handler.setPostLogoutRedirectUri("https://yourapp.com");
        return handler;
    }
}
// B2C Auth with Ailacs Identity (Go)
// go get github.com/coreos/go-oidc/v3/oidc golang.org/x/oauth2

package main

import (
    "context"
    "crypto/rand"
    "encoding/base64"
    "net/http"
    "github.com/coreos/go-oidc/v3/oidc"
    "golang.org/x/oauth2"
)

var (
    provider     *oidc.Provider
    oauth2Conf   oauth2.Config
    oidcVerifier *oidc.IDTokenVerifier
)

func init() {
    ctx := context.Background()
    provider, _ = oidc.NewProvider(ctx, "https://auth.ailacs.com")
    oidcVerifier = provider.Verifier(&oidc.Config{ClientID: "your-client-id"})
    oauth2Conf = oauth2.Config{
        ClientID:    "your-client-id",
        RedirectURL: "https://yourapp.com/callback",
        Endpoint:    provider.Endpoint(),
        Scopes:      []string{oidc.ScopeOpenID, "profile", "email"},
    }
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    verifier := oauth2.GenerateVerifier() // PKCE code verifier
    state    := randomState()
    // Store in session (use gorilla/sessions in production)
    http.SetCookie(w, &http.Cookie{Name: "pkce_v", Value: verifier, HttpOnly: true, Secure: true})
    http.SetCookie(w, &http.Cookie{Name: "state",  Value: state,    HttpOnly: true, Secure: true})
    authURL := oauth2Conf.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier))
    http.Redirect(w, r, authURL, http.StatusFound)
}

func callbackHandler(w http.ResponseWriter, r *http.Request) {
    ctx := context.Background()
    vc, _ := r.Cookie("pkce_v")
    token, err := oauth2Conf.Exchange(ctx, r.URL.Query().Get("code"),
        oauth2.VerifierOption(vc.Value))
    if err != nil { http.Error(w, "Exchange failed", 500); return }

    rawIDToken, _ := token.Extra("id_token").(string)
    idToken, err  := oidcVerifier.Verify(ctx, rawIDToken)
    if err != nil { http.Error(w, "Invalid token", 401); return }

    var claims struct{ Sub, Name, Email string }
    idToken.Claims(&claims)
    // Store claims in session and redirect to dashboard
    http.Redirect(w, r, "/dashboard", http.StatusFound)
}

func randomState() string {
    b := make([]byte, 16); rand.Read(b)
    return base64.URLEncoding.EncodeToString(b)
}
<?php
// B2C Auth with Ailacs Identity (PHP / Laravel 11)
// composer require laravel/socialite league/oauth2-client

// app/Http/Controllers/AuthController.php
class AuthController extends Controller
{
    public function login(Request $request)
    {
        // Generate PKCE code verifier + challenge
        $verifier  = bin2hex(random_bytes(32));
        $challenge = rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '=');

        session([
            'pkce_verifier' => $verifier,
            'oauth_state'   => bin2hex(random_bytes(16)),
        ]);

        $query = http_build_query([
            'response_type'         => 'code',
            'client_id'             => config('services.ailacs.client_id'),
            'redirect_uri'          => config('services.ailacs.redirect'),
            'scope'                 => 'openid profile email',
            'state'                 => session('oauth_state'),
            'code_challenge'        => $challenge,
            'code_challenge_method' => 'S256',
        ]);

        return redirect('https://auth.ailacs.com/connect/authorize?' . $query);
    }

    public function callback(Request $request)
    {
        abort_unless($request->state === session('oauth_state'), 422, 'State mismatch');

        $response = Http::asForm()->post('https://auth.ailacs.com/connect/token', [
            'grant_type'    => 'authorization_code',
            'code'          => $request->code,
            'redirect_uri'  => config('services.ailacs.redirect'),
            'client_id'     => config('services.ailacs.client_id'),
            'code_verifier' => session('pkce_verifier'),
        ]);

        $tokens = $response->json();
        // Parse + verify ID token claims (use firebase/php-jwt or web-token/jwt-framework)
        $claims = $this->verifyIdToken($tokens['id_token']);

        $user = User::updateOrCreate(
            ['sub' => $claims['sub']],
            ['name' => $claims['name'] ?? '', 'email' => $claims['email'] ?? '']
        );
        Auth::login($user, remember: true);
        return redirect('/dashboard');
    }

    public function logout(Request $request)
    {
        Auth::logout();
        $request->session()->invalidate();
        return redirect('https://auth.ailacs.com/connect/logout?post_logout_redirect_uri=' . urlencode(config('app.url')));
    }
}
# B2C Auth with Ailacs Identity (Ruby on Rails 7+)
# Gemfile:
# gem 'omniauth-openid-connect'
# gem 'omniauth-rails_csrf_protection'

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :openid_connect,
    name: :ailacs,
    scope: [:openid, :profile, :email],
    response_type: :code,
    issuer: 'https://auth.ailacs.com',
    discovery: true,
    pkce: true,
    client_options: {
      identifier:   ENV['AILACS_CLIENT_ID'],
      secret:       ENV['AILACS_CLIENT_SECRET'],
      redirect_uri: ENV['AILACS_REDIRECT_URI']
    }
end

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  skip_before_action :require_login, only: [:create, :failure]

  def create
    auth = request.env['omniauth.auth']
    user = User.find_or_initialize_by(sub: auth.uid)
    user.update!(name: auth.info.name, email: auth.info.email)
    session[:user_id] = user.id
    redirect_to dashboard_path, notice: 'Signed in.'
  end

  def destroy
    session[:user_id] = nil
    redirect_to "https://auth.ailacs.com/connect/logout" \
                "?post_logout_redirect_uri=#{CGI.escape(root_url)}"
  end

  def failure
    redirect_to root_path, alert: "Login failed: #{params[:message]}"
  end
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :require_login

  private

  def require_login
    redirect_to '/auth/ailacs' unless current_user
  end

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
  helper_method :current_user
end
// B2C Auth with Ailacs Identity (TypeScript / React + react-oidc-context)
// npm install oidc-client-ts react-oidc-context

// src/auth/authConfig.ts
import { UserManager, WebStorageStateStore } from 'oidc-client-ts';

export const userManager = new UserManager({
    authority:                'https://auth.ailacs.com',
    client_id:                import.meta.env.VITE_AILACS_CLIENT_ID,
    redirect_uri:             `${window.location.origin}/callback`,
    post_logout_redirect_uri: `${window.location.origin}/`,
    response_type:            'code',
    scope:                    'openid profile email',
    // PKCE is enabled by default in oidc-client-ts — no client_secret needed
    automaticSilentRenew:     true,
    silent_redirect_uri:      `${window.location.origin}/silent-renew`,
    userStore: new WebStorageStateStore({ store: window.sessionStorage }),
});

// src/main.tsx
import { AuthProvider } from 'react-oidc-context';
import { userManager } from './auth/authConfig';

ReactDOM.createRoot(document.getElementById('root')!).render(
    <AuthProvider userManager={userManager}>
        <App />
    </AuthProvider>
);

// src/components/ProtectedRoute.tsx
import { useAuth } from 'react-oidc-context';

export function ProtectedRoute({ children }: { children: React.ReactNode }) {
    const auth = useAuth();
    if (auth.isLoading) return <div>Loading...</div>;
    if (!auth.isAuthenticated) { auth.signinRedirect(); return null; }
    return <>{children}</>;
}

// src/pages/Callback.tsx
export function Callback() {
    const auth = useAuth();
    useEffect(() => {
        if (!auth.isLoading && !auth.activeNavigator) {
            window.location.replace('/dashboard');
        }
    }, [auth]);
    return <div>Completing sign-in...</div>;
}

8. B2C Authentication Security Best Practices

PKCE + OIDC is the foundation, but a production B2C auth system requires multiple layers of defence. This checklist represents the current 2026 baseline for consumer-facing applications.

Always use PKCE — mandatory for all public clients. Prevents code interception even if HTTPS is compromised at the redirect URI.
HttpOnly, Secure, SameSite=Lax cookies for session state — never store tokens in localStorage. HttpOnly blocks JavaScript access; SameSite=Lax prevents CSRF without breaking OIDC redirects.
Refresh token rotation — every refresh token use issues a new token and invalidates the old. If a previously used token is presented, invalidate the entire token family and force re-login.
Short access token lifetimes (5–15 minutes) — limits blast radius from token theft. Use silent renewal to transparently refresh before expiry without user interaction.
Validate ID token signature and all claims — verify against JWKS; check iss, aud, exp, and iat. Never trust an unverified JWT.
Strict Content Security Policy (CSP) — prevents XSS attacks that steal tokens from sessionStorage. Use script-src 'self' and avoid 'unsafe-inline'.
Risk-based MFA, not mandatory MFA — mandatory MFA for all B2C logins destroys conversion rates. Use device fingerprinting to trigger TOTP only for anomalous sessions.
Rate limit auth endpoints aggressively — limit by IP, account, and device fingerprint. Implement exponential backoff and account lockout against credential stuffing.
Register exact redirect URIs — no wildcards — wildcard URIs (https://yourapp.com/*) enable open redirect attacks. Register exact URIs and validate strictly server-side.
Backend-for-Frontend (BFF) for SPAs — the BFF pattern moves token handling to a server component that sets HttpOnly cookies. The browser never sees raw tokens, eliminating the XSS token theft attack class entirely.

9. Choosing the Right B2C Authentication Platform

For most teams, building a B2C authentication system from scratch is the wrong decision. A custom auth implementation requires deep expertise in cryptography, OAuth 2.0 security, PKCE, token management, regulatory compliance, and continuous maintenance as the threat landscape evolves. The engineering cost is substantial and ongoing.

Build vs. buy

Build if your organisation has unique regulatory requirements that no platform supports, your security team has deep identity engineering expertise, and you have the ongoing engineering resources to maintain a security-critical system as standards evolve.

Use a platform if your core business is not identity infrastructure, you need to ship quickly, you want to focus engineering on product differentiation rather than commodity infrastructure, or you need enterprise features — audit logs, SCIM, SAML federation, GDPR controls — without building them yourself.

What to evaluate in a B2C auth platform

  • Full OAuth 2.1 / OpenID Connect Core 1.0 / PKCE compliance
  • Passkeys / WebAuthn support for passwordless B2C auth
  • Multi-tenant capable for when your product expands to enterprise customers
  • Transparent, predictable pricing — no per-MAU fees that explode at scale
  • Self-hostable or dedicated deployment for data residency requirements
  • Audit logs and GDPR-compliant data handling built in

Ailacs Identity: B2C + B2B on One Platform

Ailacs Identity handles both B2C consumer authentication and B2B multi-tenant SSO from a single, standards-based OpenID Connect platform. Flat-rate pricing — no per-MAU surprises. Free up to 500 users.

See Pricing Read Docs

10. Frequently Asked Questions

B2C authentication manages individual consumer accounts at massive scale — users self-register, manage their own credentials, and expect social login options. B2B authentication manages organisational identities, where companies connect their existing corporate identity provider (Azure AD, Okta, Google Workspace) to your application via enterprise SSO. B2B requires multi-tenant architecture, invitation flows, per-tenant SSO configuration, and organisation-scoped RBAC. Ailacs Identity supports both patterns from a single platform — see our Multi-Tenant vs. B2C guide for the full comparison.

Yes. PKCE is mandatory for all public clients — any application that cannot securely store a client secret, which includes all browser SPAs, mobile apps, and desktop applications. The OAuth 2.1 specification makes PKCE required for all authorisation code flows. Even for confidential server-side clients that can store a secret, PKCE provides defence-in-depth and is strongly recommended for all new B2C implementations.

The most secure approach for SPAs is the Backend-for-Frontend (BFF) pattern: a Next.js server route, Express middleware, or dedicated BFF service handles token exchange and stores tokens server-side, issuing the browser an HttpOnly session cookie. The SPA never sees raw tokens. For teams that must handle tokens in the browser, use sessionStorage (never localStorage) and implement silent renewal via the automaticSilentRenew option in oidc-client-ts. Enable refresh token rotation on the identity server so replayed refresh tokens are immediately detected and the token family invalidated.

TOTP via an authenticator app (Google Authenticator, Microsoft Authenticator, Aegis) is the recommended MFA for most B2C apps in 2026. It is more secure than SMS OTP — which is vulnerable to SIM-swapping and SS7 interception attacks — and more widely accessible than hardware security keys. Passkeys (WebAuthn/FIDO2) offer the best combination of security and UX where device support exists. For B2C specifically, implement risk-based MFA that challenges only anomalous logins rather than every login, preserving conversion rates for legitimate users.

GDPR compliance for B2C authentication requires: (1) Lawful basis — typically legitimate interest for authentication, explicit consent for optional profile data; (2) Data minimisation — collect only the claims you need (sub, email, name); (3) Right to erasure — users must be able to delete their account and all associated identity data; (4) Data portability — users must be able to export their profile data in a machine-readable format; (5) Audit logs — retain verifiable logs of authentication events; (6) Data residency — store identity data in the geographically appropriate region. Using a platform with GDPR controls built in is significantly simpler than implementing all of this from scratch.

Yes. Ailacs Identity is designed to handle both B2C consumer authentication and B2B multi-tenant enterprise SSO from a single platform and a single integration. Configure applications for B2C flows — self-registration, social login, passwordless — while simultaneously supporting B2B tenants that federate via their own corporate identity providers. This eliminates the need for two separate identity systems as your product grows from a consumer app to an enterprise SaaS platform.

Start Implementing B2C Auth Today

Ailacs Identity handles all of the above — OAuth 2.0, PKCE, OIDC, MFA, social login, and GDPR compliance — so you can focus on building your product.