Server-Backed Next.js
Recommended App Router integration with SDK-managed standalone sessions, runtime auth-source resolution, and logout route support.
SDK snapshot 21344f2c
This is the recommended integration path for @crimsoncorp/oauth-react.
Requirements
- Next.js App Router
- A callback page on the same origin as the opener
- A server route that can keep
TCM_OAUTH_CLIENT_SECRETprivate
Environment
NEXT_PUBLIC_TCM_CLIENT_ID=tcm_xxx
TCM_OAUTH_CLIENT_SECRET=your-secret
TCM_OAUTH_API_URL=https://www.thecrimsonmarket.com/mana
NEXT_PUBLIC_TCM_OAUTH_WEB_URL=https://www.thecrimsonmarket.com1. Callback Page
import { TcmOAuthCallbackPage } from "@crimsoncorp/oauth-react";
export default function Page() {
return <TcmOAuthCallbackPage />;
}Mount this at /auth/tcm/callback.
2. Exchange Route and Session Adapter
import {
createTcmCookieSessionAdapter,
resolveTcmAuthSession,
} from "@crimsoncorp/oauth-react/server";
import {
createTcmLogoutRoute,
createTcmOAuthExchangeRoute,
} from "@crimsoncorp/oauth-react/nextjs";
const sessionAdapter = createTcmCookieSessionAdapter({
appId: "my-app",
maxAgeSeconds: 60 * 60 * 24,
});
const route = createTcmOAuthExchangeRoute({
oauth: {
apiBaseUrl: process.env.TCM_OAUTH_API_URL!,
clientId: process.env.NEXT_PUBLIC_TCM_CLIENT_ID!,
clientSecret: process.env.TCM_OAUTH_CLIENT_SECRET!,
callbackPath: "/auth/tcm/callback",
googleOnly: true,
},
async onResolvedUser({ userInfo, traceId }) {
if (!userInfo.googleId) {
return {
status: 400,
body: { message: "Missing googleId", traceId },
};
}
const user = await upsertUserFromTcm(userInfo);
return {
body: { userId: user.id, email: user.email },
session: { id: user.id },
};
},
applySession(response, session) {
sessionAdapter.apply(response, session.id);
},
});
export const { POST } = route;createTcmOAuthExchangeRoute owns:
- payload validation
- redirect URI resolution and retry
- token exchange
- userinfo fetch
- correlation header forwarding
- duplicate-request single-flight handling
Your app still owns:
- user creation or lookup
- session payload signing and verification
- response body shape
3. Runtime Auth Resolution
Mixed-mode apps should resolve auth per request instead of choosing a global build-time auth mode.
function readStandaloneSession(request: Request) {
const cookieValue = sessionAdapter.read(request);
return cookieValue ? { sub: cookieValue } : null;
}
function readParentAuthToken(request: Request) {
const hasParentCookie = request.headers.get("cookie")?.includes("authToken=");
return hasParentCookie ? { sub: "host-user-id" } : null;
}
export function getSessionFromRequest(request: Request) {
return resolveTcmAuthSession(request, {
sources: [
{ name: "sdk_session", resolve: readStandaloneSession },
{ name: "parent_auth_token", resolve: readParentAuthToken },
],
precedence: ["sdk_session", "parent_auth_token"],
});
}Recommended behavior:
- standalone apps use only
sdk_session - embedded shared-domain apps keep host-owned
authToken - mixed-mode apps like
socialriddleresolvesdk_sessionfirst and fall back toparent_auth_token
4. Logout Route
const logoutRoute = createTcmLogoutRoute({
resolveSession: getSessionFromRequest,
standaloneSessionAdapter: sessionAdapter,
onSharedCookieLogout() {
return Response.json({
success: true,
authSource: "parent_auth_token",
delegated: true,
});
},
});
export const { GET, POST } = logoutRoute;Behavior:
sdk_sessionclears the SDK-managed standalone cookieparent_auth_tokendelegates to host/platform logout logic and does not clear the standalone cookie
5. Browser Hook
import { useTcmOAuth } from "@crimsoncorp/oauth-react";
export function LoginButton() {
const oauth = useTcmOAuth<{ userId: string }>({
clientId: process.env.NEXT_PUBLIC_TCM_CLIENT_ID!,
tcmWebUrl: process.env.NEXT_PUBLIC_TCM_OAUTH_WEB_URL!,
exchangeEndpoint: "/api/auth/tcm/oauth-exchange",
callbackPath: "/auth/tcm/callback",
interactionMode: "auto",
googleOnly: true,
});
return (
<button onClick={() => void oauth.startLogin()} disabled={oauth.authenticating}>
{oauth.authenticating ? "Signing in..." : "Continue with Google"}
</button>
);
}Interaction Behavior
Recommended behavior:
- use
interactionMode: "auto" - let the SDK choose popup on desktop-like environments
- let the SDK choose redirect on mobile-like environments
- allow the default popup-to-redirect fallback when popup opening is blocked
This keeps the app integration simple while making mobile web and privacy-restricted environments more reliable.
Migrating to 1.3.0
startLogin(provider) now forces provider intent in the authorize request. Keep explicit provider calls for provider-gated apps, such as Twitch-only admin tools. Use startLogin() with no provider when the user should choose from the client's allowed providers.
Popup Contract
For popup flows, the SDK standardizes the authorize URL down to:
- OAuth protocol params
ui_mode=popupprompt=select_providerfor chooser mode, orprovider=<provider>&prompt=loginfor explicit provider buttonsrequired_provider=googleonly as the legacy compatibility alias for Google-only flows
UI-specific popup params such as auto_start_provider and popup_variant are not part of the standard SDK contract. service.core-ui is expected to:
- show its own minimal spinner shell for popup login
- silently preflight
/oauth/authorize - redirect the popup window itself to Google when the backend returns
401orprovider_link_required - show consent only when the backend returns consent payload
Notes
- Keep the callback page same-origin with the opener.
- Register the exact callback URL in the Developers UI.
- Do not expose
TCM_OAUTH_CLIENT_SECRETto the browser. - No separate server-only client ID is required; the server route can read
NEXT_PUBLIC_TCM_CLIENT_ID. - Use
createTcmCookieSessionAdapterso the SDK owns standalone session cookie naming and clearing. - Embedded shared-domain
authTokenremains host-owned and is not replaced by the SDK cookie adapter. - Popup-specific exports still exist for compatibility, but the neutral
useTcmOAuth+TcmOAuthCallbackPagepath is now the recommended production integration.
Roles RBAC
Services that need platform RBAC should request roles, persist userInfo.roles in the app-local session, and map role names to service-local permissions in code. See Roles RBAC Integration.