AI Agents

How We Built Google OAuth Authentication Between a Next.js Frontend and a Headless CMS Backend

By Ginbok4 min read

Overview

When building a modern content platform, one of the most interesting architectural challenges is connecting a Next.js frontend to a headless CMS backend with a smooth, secure authentication experience. In this post, we share how we implemented Google OAuth-based visitor authentication — fully decoupled from the CMS admin system — using a custom JWT flow.

The Two-System Problem

Our stack has two distinct layers:

The CMS has its own built-in admin authentication system (cookie-based). We needed to add a separate visitor authentication — using Google Sign-In — without touching or conflicting with CMS admin sessions. This meant running two auth systems in parallel on the same ASP.NET Identity database.

Architecture at a Glance


┌───────────────────────┐         ┌───────────────────────────┐
│   Next.js Frontend    │         │   CMS Backend (ASP.NET)   │
│                       │         │                           │
│  AuthProvider         │◄───────►│  /api/auth/*              │
│  (React Context)      │  JWT    │  GoogleAuthService        │
│                       │  Cookie │  JwtService               │
│  GoogleLoginButton    │         │                           │
│  UserMenu             │         │  ASP.NET Identity         │
│  AuthGuard            │         │  (Users + Roles)          │
└───────────────────────┘         └───────────────────────────┘
           │                                 │
           └──────── Google OAuth ───────────┘

Authentication Schemes: Three in One

The backend registers three authentication schemes that coexist peacefully:

  1. Default Cookie Scheme — handled by the CMS framework for admin login. We never touch this.
  2. Google OAuth Scheme — used only during the OAuth callback flow. A temporary short-lived cookie (GoogleExternalCookie) passes the Google result to our callback handler.
  3. JWT Bearer Scheme — reads the JWT from an httpOnly cookie (auth_token) for all visitor-facing API endpoints.

The Login Flow, Step by Step

1. User Clicks "Sign in with Google"

The button calls a simple function that redirects the browser to the backend login endpoint, passing the current page URL as returnUrl:

GET /api/auth/google-login?returnUrl=https://yoursite.com/blog/some-post

2. Backend Challenges Google

The backend issues a Challenge(GoogleDefaults.AuthenticationScheme), redirecting the user to Google's consent screen. The returnUrl is stored in authentication properties.

3. Google Callback

After the user approves, Google redirects back to /api/auth/google-callback. The backend:

  1. Authenticates the result via the Google scheme
  2. Calls FindOrCreateUserAsync() to resolve the visitor account
  3. Issues a JWT token with user claims (name, avatar, roles)
  4. Sets an httpOnly cookie (auth_token, 7 days)
  5. Cleans up the temporary OAuth cookie
  6. Redirects back to the original frontend page

4. Frontend Picks Up the Session

After the redirect, the AuthProvider (React Context) runs on mount and calls GET /api/auth/me with the cookie automatically attached. The backend validates the JWT, fetches the profile, and returns it. The UI updates to show the logged-in state.

User Account Resolution: Find or Create

The FindOrCreateUserAsync method handles three scenarios on every Google login:

  1. Returning user (linked Google account) — Find by Google provider ID directly. Fastest path.
  2. Existing account with same email — Auto-link the Google login to the existing account (for non-admin accounts).
  3. New user — Create a new visitor account, assign the Visitors role, and link the Google login.

Session Lifecycle on the Frontend

The AuthProvider React context manages the entire session lifecycle:

JWT Token Design

The JWT is self-contained and includes:

The token is delivered and read via an httpOnly cookie — the frontend JavaScript never touches the raw token. The JwtBearer middleware reads it directly from the cookie on every API request.

Route Protection

On the frontend, an AuthGuard component wraps protected pages. It checks the authentication state from context and redirects unauthenticated users to the home page. Loading states are shown while the session is being resolved.

On the backend, any endpoint requiring authentication is decorated with:

[Authorize(AuthenticationSchemes = "JwtBearer")]

This explicitly uses the JWT scheme, completely separate from the CMS admin scheme.

Key Takeaways

#next.js#asp.net-core#google-oauth#jwt#authentication#headless-cms#architecture
← Back to Articles