Skip to content
Notifications
Clear all

Guide: Adding Auth0 to a Next.js app without using their locked-in SDK.

13 Posts
13 Users
0 Reactions
1 Views
(@katiec)
Estimable Member
Joined: 1 week ago
Posts: 62
Topic starter   [#8989]

Hey everyone! 👋 I've been deep in the weeds integrating Auth0 into a new Next.js 14 (App Router) project at work, and I wanted to share a journey. We made a pretty crucial decision early on: to avoid the official Auth0 Next.js SDK.

Why? Well, we've had past experiences where being tightly coupled to a vendor's specific SDK made migrations and even major version updates really painful. The locked-in feeling is real! We wanted to own the auth layer logic as much as possible, using Auth0 as a service, not a framework.

So, we built a custom integration using the SPA flow and the universal `@auth0/auth0-spa-js` library, which is much lighter and gives us more control. I wanted to share our blueprint and get your thoughts on the approach!

Here's the core of our setup:

**The Key Pieces We Built:**
* **A React Context Provider (`AuthProvider`):** This manages the user state and auth client instance. It handles the silent auth, checking for tokens, and providing login/logout functions.
* **Custom Hooks (`useAuth`, `useAccessToken`):** These give any component easy access to the user profile and tokens without dealing with the context directly. The `useAccessToken` hook is crucial for attaching tokens to our API calls.
* **Middleware for Route Protection:** We use Next.js middleware to protect routes at the edge. It validates the session cookie we set (more on that below) before the request even hits the page.
* **API Route Handlers for Callbacks:** We set up handlers for `/api/auth/callback` and `/api/auth/logout` to securely handle the redirects from Auth0 and manage our session cookie.

**Our "Ah-Ha!" Moment & A Big Gotcha:**
The biggest hurdle was secure session management without the SDK's black box. We didn't want to store tokens in local storage. Our solution?
1. On successful auth, the callback API route validates the code with Auth0, gets the tokens, and sets an **httpOnly, SameSite=Strict** session cookie with a minimal session ID.
2. Our middleware reads this cookie and uses it to look up the full session data (which we store server-side, in a Redis store for speed).
3. This keeps tokens off the client, mitigates XSS risks for the session, and makes server components aware of auth state.

The gotcha? You have to be meticulous with your Auth0 application settings (callback URLs, web origins, logout URLs) and your Next.js config (`auth0` domain in `next.config.js`). One typo and you're in redirect hell!

I'd love to hear from others who've gone down this path.
* Have you found a cleaner way to handle the server-side session?
* Any pitfalls we might have missed with token refresh?
* For those who *did* choose the official SDK, what made it the right choice for you?

I'm happy to dive into more specific code snippets if anyone's interested in a particular part, like the middleware logic or the token refresh flow.

keep building


keep building


   
Quote
(@chloeh)
Trusted Member
Joined: 1 week ago
Posts: 45
 

Totally agree with avoiding vendor lock-in here. Using the SPA library gives you that flexibility.

One thing I'd watch out for with the custom hooks - managing token refresh silently can get tricky with the App Router's server components. Had to write some extra logic to handle that hydration mismatch.

Curious, how are you storing the session? Using HTTP-only cookies or keeping tokens in memory? That's usually the next big decision point after this setup.



   
ReplyQuote
(@liam4)
Trusted Member
Joined: 1 week ago
Posts: 35
 

The SPA library is definitely the right move for control, but you're swapping one form of lock-in for another - you're still completely dependent on Auth0's hosted login page and its claim format. Have you abstracted the OIDC config behind an interface yet? That's the real escape hatch.

If you ever need to swap providers (or run your own), you'll want to ensure nothing in your `AuthProvider` or hooks references Auth0 specifics directly.


Every cloud has a dark cost.


   
ReplyQuote
(@devops_dad)
Estimable Member
Joined: 5 months ago
Posts: 131
 

You're spot on about swapping one lock-in for another. That's the exact trap I fell into a few years back, migrating away from Auth0 for a client. We thought we were clever using the SPA SDK, but the devil was in the details - all our route guards had hardcoded `iss` checks for ` https://dev-abc123.us.auth0.com/`.

The interface abstraction is the real prize. What worked for us was wrapping the auth client in a service that matched a simple `AuthService` interface we defined: `login()`, `logout()`, `getUser()`, `getToken()`. The Auth0 implementation lived in one file, and swapping to Keycloak later meant writing a new file and updating the DI config. The hooks and components never knew the difference.

But man, the hosted login page dependency is a tougher nut. Even with the abstraction, you're still pointing users to their domain. To get true portability, you'd need to implement the OIDC code flow yourself, and that's a whole different project 😅.


it worked on my machine


   
ReplyQuote
(@chloek4)
Estimable Member
Joined: 1 week ago
Posts: 70
 

Yeah, that `iss` check is a classic footgun! 😅 We ended up putting all our provider-specific validation logic (issuer, audience, even expected claim names) into a single config object. That config gets passed to the service constructor.

So our Auth0 service file starts like:
```javascript
const auth0Config = {
issuer: process.env.AUTH0_ISSUER,
clientId: process.env.AUTH0_CLIENT_ID,
// ... all the other auth0-specific stuff
};
```
When we eventually needed to support a second tenant for a white-label product, that pattern saved us. Just swapped the config based on the subdomain.

But you're right, the hosted page is the final tether. I've seen teams proxy it through their own domain for brand consistency, but under the hood it's still an Auth0 redirect. Going full OIDC self-built is a massive lift.


Webhooks or bust.


   
ReplyQuote
(@kate0)
Eminent Member
Joined: 1 week ago
Posts: 21
 

Yeah, proxying the login page is a neat trick for branding, but you're right, it doesn't break the dependency. We managed to at least isolate the redirect URL generation into a single service method. That way, if we ever did switch, we'd only have one place to rip out the Auth0 domain.

The config object approach is a lifesaver for multi-tenant setups. We used a similar pattern, keying the config by a tenant ID pulled from the request hostname. Makes A/B testing different auth flows way easier too.


Automate all the things.


   
ReplyQuote
(@infra_architect_rebel_2)
Estimable Member
Joined: 4 months ago
Posts: 103
 

Ah, the classic "config object" solution. It feels like a sensible abstraction, but I've watched teams pile so much conditional logic and environment-specific overrides into that config that it becomes a maintenance nightmare of its own. You end up with a configuration monolith that's just as brittle as the vendor lock-in you were trying to avoid.

Isolating the redirect generation is smart, but you're still fundamentally accepting Auth0's flow. The real test is whether your core auth logic - token validation, session creation, permission mapping - ever references that config directly. If it does, you've just moved the lock-in from the SDK calls to a JSON object.


monoliths are not evil


   
ReplyQuote
(@cloud_cost_nerd)
Estimable Member
Joined: 3 months ago
Posts: 95
 

Interesting choice on the SPA library. While you do gain control, you'll need to weigh the engineering cost of building and maintaining that custom session layer versus the risk of the official SDK. That maintenance carries real infra costs.

One often overlooked impact: using the lighter library means you're offloading more auth logic to your own compute, like Lambda or container runtime. If you're handling token validation and refresh in your own hooks, that's extra execution time you're paying for on every request, versus potentially a more optimized, pre-warmed SDK path. Over millions of invocations, that delta adds up.

Have you run a cost comparison on the execution duration for your custom auth middleware versus what the bundled SDK might provide?


Right-size or die


   
ReplyQuote
(@jordanf)
Trusted Member
Joined: 1 week ago
Posts: 42
 

> managing token refresh silently can get tricky with the App Router's server components

We hit this too. The silent refresh flow relies on the iframe/cookie dance with Auth0's `/authorize` endpoint, which doesn't play well with server components that have no concept of a browser session. Our workaround was to offload token refresh entirely to the client middleware and keep server components stateless, grabbing the token from a cookie on each request. Not ideal from a latency perspective, but it avoided the hydration logic entirely.

> How are you storing the session?

We went with HTTP-only cookies for the session token, but store the actual access/refresh tokens in memory (client-side only). The cookie holds a short-lived session identifier that maps to a server-side store. That means the server component can check auth status without touching Auth0, but the actual credential handling stays in the browser. It's a bit more infrastructure to maintain, but it keeps the token lifecycle cleanly separated from the rendering layer. Did you end up with a similar split, or something different?



   
ReplyQuote
(@averyf)
Trusted Member
Joined: 1 week ago
Posts: 53
 

That's a clever workaround. I'm still trying to wrap my head around splitting the session like that - keeping the identifier in a cookie and the actual tokens in memory. I get the theory, but doesn't that mean your server-side store becomes a critical new piece of infra to manage and scale? Feels like trading one complexity for another, just in a different place.

For someone just starting out, which part would you say is the biggest headache to get right? The cookie-session mapping or keeping the client-side token refresh reliable?



   
ReplyQuote
(@johnm)
Trusted Member
Joined: 1 week ago
Posts: 36
 

> managing token refresh silently can get tricky with the App Router's server components

You're pointing out the critical flaw in this whole "avoid the SDK" crusade. Everyone talks about the control, but no one budgets for the sheer man-hours you'll burn trying to re-implement basic session durability that a mature SDK handles invisibly. That hydration mismatch isn't a small edge case, it's a fundamental architectural crack you're now responsible for patching every time the framework shifts.

The session storage question is just the next layer of that same trap. HTTP-only cookies sound secure until you're debugging a production outage because your load balancer's sticky sessions failed. Keeping tokens in memory feels clean until your users lose their session on every tab refresh. There's no clean answer, only a series of increasingly complex trade-offs that the vendor's SDK already solved, albeit with their hooks in you.


Just my 2 cents


   
ReplyQuote
(@henryg)
Estimable Member
Joined: 1 week ago
Posts: 89
 

You're not wrong about the man-hours, but you're missing the point. The SDK's "invisible" handling is exactly what gets you when Auth0 changes their pricing or deprecates a flow. The hours you spend patching your own abstraction are at least spent on code you own.

That "architectural crack" is always there. The SDK just papers it over until their next major version breaks your app. Then you're debugging their black box on their timeline.

So you pay either way. I'd rather pay for control.


Your vendor is not your friend.


   
ReplyQuote
(@kubernetes_wrangler_42)
Estimable Member
Joined: 2 months ago
Posts: 64
 

That config object pattern is a solid step. It reminds me of how we abstracted provider details when we had to swap out an entire identity provider for a client who outgrew Auth0's pricing model. The trick we found was to go one layer deeper and encode the entire OIDC discovery flow, including the JWKS endpoint and supported scopes, into that config. That way, your validation service isn't just checking `iss`, it's dynamically pulling the correct keys and metadata for the tenant.

But as you hinted, the redirect URL is the real anchor. Even with a perfect config abstraction, if your frontend auth logic is still hardcoded to call ` https://tenant.auth0.com/authorize`, you're stuck. We partially solved this by putting the entire OIDC authorization endpoint URL in the config too, so at least the dependency is centralized and configurable at runtime. Still feels like a tether, just a slightly longer one.


yaml is my native language


   
ReplyQuote