Skip to content
Docs

Quickstart / Fullstack

MojoAuth Hosted Login Page with Next.js

Next.js is a popular React framework that enables server-side rendering, static site generation, and API routes, making it an excellent choice for building modern web applications. Its robust ecosystem and built-in features provide a solid foundation for implementing secure authentication flows.

This guide will walk you through the process of connecting your Next.js application to MojoAuth’s Hosted Login Page using OpenID Connect (OIDC) with next-auth, the most widely adopted and well-maintained authentication library for Next.js.

Links:

Before you begin, make sure you have:

  1. A MojoAuth account and an OIDC application set up in the MojoAuth dashboard
  2. Your Client ID and Client Secret from the MojoAuth dashboard
  3. Node.js (v16.0.0 or higher) installed on your development machine
  4. Basic knowledge of Next.js and React

If you don’t have an existing Next.js project, you can create a new one with the following command:

terminal
# Create a new Next.js app
npx create-next-app@latest mojoauth-nextjs-example
cd mojoauth-nextjs-example

If prompted, you can select the following options:

  • Use TypeScript: Yes
  • Use ESLint: Yes
  • Use Tailwind CSS: Yes (recommended for styling)
  • Use src/ directory: Optional
  • Use App Router: Yes (for modern Next.js features)

Install NextAuth.js and the necessary dependencies:

terminal
npm install next-auth

Create a .env.local file in your project root to store your MojoAuth credentials:

code
# Authentication
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-nextauth-secret-generate-a-secure-random-string
# MojoAuth OIDC
MOJOAUTH_CLIENT_ID=your-client-id
MOJOAUTH_CLIENT_SECRET=your-client-secret
MOJOAUTH_ISSUER=https://your-project.auth.mojoauth.com

For the NEXTAUTH_SECRET, you can generate a secure random string using:

terminal
openssl rand -base64 32

Create a new file at app/api/auth/[...nextauth]/route.ts (for App Router) or pages/api/auth/[...nextauth].ts (for Pages Router):

For App Router (recommended for new projects):

typescript
import NextAuth from "next-auth";
import { NextAuthOptions } from "next-auth";
import OIDCProvider from "next-auth/providers/openid-connect";
export const authOptions: NextAuthOptions = {
providers: [
OIDCProvider({
id: "mojoauth",
name: "MojoAuth",
clientId: process.env.MOJOAUTH_CLIENT_ID as string,
clientSecret: process.env.MOJOAUTH_CLIENT_SECRET as string,
issuer: process.env.MOJOAUTH_ISSUER as string,
wellKnown: `${process.env.MOJOAUTH_ISSUER}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } },
checks: ["pkce", "state"],
client: {
token_endpoint_auth_method: "client_secret_post",
},
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
};
},
}),
],
callbacks: {
async jwt({ token, user, account }) {
// Initial sign in
if (account && user) {
return {
...token,
accessToken: account.access_token,
accessTokenExpires: Date.now() + (account.expires_in as number) * 1000,
refreshToken: account.refresh_token,
user,
};
}
// Return previous token if the access token has not expired yet
if (token.accessTokenExpires && Date.now() < token.accessTokenExpires) {
return token;
}
// Access token has expired, try to update it
return token;
},
async session({ session, token }) {
session.user = token.user as any;
session.accessToken = token.accessToken as string;
session.error = token.error as string;
return session;
},
},
pages: {
signIn: "/auth/signin",
error: "/auth/error",
},
session: {
strategy: "jwt",
},
debug: process.env.NODE_ENV === "development",
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

For Pages Router:

typescript
import NextAuth, { NextAuthOptions } from "next-auth";
import OIDCProvider from "next-auth/providers/openid-connect";
export const authOptions: NextAuthOptions = {
providers: [
OIDCProvider({
id: "mojoauth",
name: "MojoAuth",
clientId: process.env.MOJOAUTH_CLIENT_ID as string,
clientSecret: process.env.MOJOAUTH_CLIENT_SECRET as string,
issuer: process.env.MOJOAUTH_ISSUER as string,
wellKnown: `${process.env.MOJOAUTH_ISSUER}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } },
checks: ["pkce", "state"],
client: {
token_endpoint_auth_method: "client_secret_post",
},
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
};
},
}),
],
callbacks: {
async jwt({ token, user, account }) {
// Initial sign in
if (account && user) {
return {
...token,
accessToken: account.access_token,
accessTokenExpires: Date.now() + (account.expires_in as number) * 1000,
refreshToken: account.refresh_token,
user,
};
}
// Return previous token if the access token has not expired yet
if (token.accessTokenExpires && Date.now() < token.accessTokenExpires) {
return token;
}
// Access token has expired, try to update it
return token;
},
async session({ session, token }) {
session.user = token.user as any;
session.accessToken = token.accessToken as string;
session.error = token.error as string;
return session;
},
},
pages: {
signIn: "/auth/signin",
error: "/auth/error",
},
session: {
strategy: "jwt",
},
debug: process.env.NODE_ENV === "development",
};
export default NextAuth(authOptions);

If you’re using TypeScript, create a types file to extend the NextAuth types with next-auth.d.ts in your project root:

typescript
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
accessToken?: string;
error?: string;
user: {
id: string;
name?: string | null;
email?: string | null;
image?: string | null;
} & DefaultSession["user"];
}
interface JWT {
accessToken?: string;
accessTokenExpires?: number;
refreshToken?: string;
error?: string;
user?: {
id: string;
name?: string | null;
email?: string | null;
image?: string | null;
};
}
}

Create a file at app/context/AuthProvider.tsx:

tsx
'use client';
import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";
export default function AuthProvider({ children }: { children: ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}

Then update your root layout at app/layout.tsx:

tsx
import AuthProvider from "./context/AuthProvider";
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<AuthProvider>{children}</AuthProvider>
</body>
</html>
);
}

Update your pages/_app.tsx:

tsx
import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";
import "../styles/globals.css";
export default function App({
Component,
pageProps: { session, ...pageProps },
}: AppProps) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
);
}

Create a sign-in page at app/auth/signin/page.tsx:

tsx
'use client';
import { signIn } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
export default function SignIn() {
const searchParams = useSearchParams();
const callbackUrl = searchParams?.get("callbackUrl") || "/dashboard";
const error = searchParams?.get("error");
useEffect(() => {
// Auto-redirect to MojoAuth login
signIn("mojoauth", { callbackUrl });
}, [callbackUrl]);
return (
<div className="flex min-h-screen flex-col items-center justify-center p-4">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h1 className="mb-6 text-center text-2xl font-bold">Sign In</h1>
{error && (
<div className="mb-4 rounded-md bg-red-50 p-4 text-red-700">
<p>Authentication error: {error}</p>
</div>
)}
<div className="flex flex-col items-center">
<p className="mb-4 text-center">Redirecting to MojoAuth login...</p>
<button
onClick={() => signIn("mojoauth", { callbackUrl })}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Sign in with MojoAuth
</button>
</div>
</div>
</div>
);
}

Create a sign-in page at pages/auth/signin.tsx:

tsx
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
export default function SignIn() {
const router = useRouter();
const { callbackUrl = "/dashboard", error } = router.query;
useEffect(() => {
// Auto-redirect to MojoAuth login
signIn("mojoauth", { callbackUrl: callbackUrl as string });
}, [callbackUrl]);
return (
<div className="flex min-h-screen flex-col items-center justify-center p-4">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h1 className="mb-6 text-center text-2xl font-bold">Sign In</h1>
{error && (
<div className="mb-4 rounded-md bg-red-50 p-4 text-red-700">
<p>Authentication error: {error}</p>
</div>
)}
<div className="flex flex-col items-center">
<p className="mb-4 text-center">Redirecting to MojoAuth login...</p>
<button
onClick={() => signIn("mojoauth", { callbackUrl: callbackUrl as string })}
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Sign in with MojoAuth
</button>
</div>
</div>
</div>
);
}

Create an error page at app/auth/error/page.tsx:

tsx
'use client';
import Link from "next/link";
import { useSearchParams } from "next/navigation";
export default function ErrorPage() {
const searchParams = useSearchParams();
const error = searchParams?.get("error");
return (
<div className="flex min-h-screen flex-col items-center justify-center p-4">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h1 className="mb-6 text-center text-2xl font-bold text-red-600">
Authentication Error
</h1>
<div className="mb-6 rounded-md bg-red-50 p-4 text-red-700">
<p className="text-center">{error || "An unknown authentication error occurred."}</p>
</div>
<div className="flex justify-center">
<Link
href="/"
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Return to Home
</Link>
</div>
</div>
</div>
);
}

Create an error page at pages/auth/error.tsx:

tsx
import Link from "next/link";
import { useRouter } from "next/router";
export default function ErrorPage() {
const router = useRouter();
const { error } = router.query;
return (
<div className="flex min-h-screen flex-col items-center justify-center p-4">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h1 className="mb-6 text-center text-2xl font-bold text-red-600">
Authentication Error
</h1>
<div className="mb-6 rounded-md bg-red-50 p-4 text-red-700">
<p className="text-center">{error || "An unknown authentication error occurred."}</p>
</div>
<div className="flex justify-center">
<Link
href="/"
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Return to Home
</Link>
</div>
</div>
</div>
);
}

Create a middleware file at middleware.ts in the root of your project:

typescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";
export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
// Define which paths require authentication
const protectedPaths = ["/dashboard", "/profile"];
const isPathProtected = protectedPaths.some((prefix) =>
path === prefix || path.startsWith(`${prefix}/`)
);
if (isPathProtected) {
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET,
});
// Redirect unauthenticated users to the login page
if (!token) {
const url = new URL(`/auth/signin`, request.url);
url.searchParams.set("callbackUrl", encodeURI(request.url));
return NextResponse.redirect(url);
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/profile/:path*"],
};

Create a dashboard page at app/dashboard/page.tsx:

tsx
'use client';
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Dashboard() {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === "unauthenticated") {
router.push("/auth/signin");
}
}, [status, router]);
if (status === "loading") {
return (
<div className="flex min-h-screen items-center justify-center">
<p>Loading...</p>
</div>
);
}
return (
<div className="container mx-auto max-w-4xl p-6">
<h1 className="mb-6 text-3xl font-bold">Dashboard</h1>
<div className="mb-8 rounded-lg bg-white p-6 shadow-md">
<h2 className="mb-4 text-xl font-semibold">
Welcome, {session?.user?.name || "User"}!
</h2>
<p className="mb-2">You are signed in with MojoAuth.</p>
<div className="mt-4">
<Link
href="/profile"
className="mr-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
View Profile
</Link>
<Link
href="/api/auth/signout"
className="rounded-md bg-red-600 px-4 py-2 text-white hover:bg-red-700"
>
Sign Out
</Link>
</div>
</div>
</div>
);
}

Create a profile page at app/profile/page.tsx:

tsx
'use client';
import { useSession, signOut } from "next-auth/react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Profile() {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === "unauthenticated") {
router.push("/auth/signin");
}
}, [status, router]);
if (status === "loading") {
return (
<div className="flex min-h-screen items-center justify-center">
<p>Loading...</p>
</div>
);
}
return (
<div className="container mx-auto max-w-4xl p-6">
<h1 className="mb-6 text-3xl font-bold">Your Profile</h1>
<div className="mb-8 overflow-hidden rounded-lg bg-white shadow-md">
<div className="p-6">
<div className="mb-6 flex items-center">
{session?.user?.image ? (
<div className="mr-4 h-16 w-16 overflow-hidden rounded-full">
<Image
src={session.user.image}
alt="Profile"
width={64}
height={64}
className="h-full w-full object-cover"
/>
</div>
) : (
<div className="mr-4 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100 text-2xl font-bold text-blue-600">
{session?.user?.name?.[0] || "U"}
</div>
)}
<div>
<h2 className="text-xl font-semibold">{session?.user?.name || "User"}</h2>
<p className="text-gray-600">{session?.user?.email}</p>
</div>
</div>
<div className="mt-6">
<h3 className="mb-3 text-lg font-semibold">User Information</h3>
<pre className="overflow-auto rounded-md bg-gray-100 p-4 text-sm">
{JSON.stringify(session?.user, null, 2)}
</pre>
</div>
<div className="mt-6 flex">
<Link
href="/dashboard"
className="mr-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
Back to Dashboard
</Link>
<button
onClick={() => signOut({ callbackUrl: "/" })}
className="rounded-md bg-red-600 px-4 py-2 text-white hover:bg-red-700"
>
Sign Out
</button>
</div>
</div>
</div>
</div>
);
}

Create a dashboard page at pages/dashboard.tsx:

tsx
import { useSession, signOut } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
export default function Dashboard() {
const { data: session, status } = useSession();
const router = useRouter();
useEffect(() => {
if (status === "unauthenticated") {
router.push("/auth/signin");
}
}, [status, router]);
if (status === "loading") {
return (
<div className="flex min-h-screen items-center justify-center">
<p>Loading...</p>
</div>
);
}
return (
<div className="container mx-auto max-w-4xl p-6">
<h1 className="mb-6 text-3xl font-bold">Dashboard</h1>
<div className="mb-8 rounded-lg bg-white p-6 shadow-md">
<h2 className="mb-4 text-xl font-semibold">
Welcome, {session?.user?.name || "User"}!
</h2>
<p className="mb-2">You are signed in with MojoAuth.</p>
<div className="mt-4">
<Link
href="/profile"
className="mr-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
View Profile
</Link>
<button
onClick={() => signOut({ callbackUrl: "/" })}
className="rounded-md bg-red-600 px-4 py-2 text-white hover:bg-red-700"
>
Sign Out
</button>
</div>
</div>
</div>
);
}
// Server-side protection
export async function getServerSideProps(context) {
const session = await getServerSession(context.req, context.res, authOptions);
if (!session) {
return {
redirect: {
destination: '/auth/signin',
permanent: false,
},
};
}
return {
props: { session }
};
}

Create a profile page at pages/profile.tsx (similar to the App Router version, but add the getServerSideProps protection).

Update your homepage at app/page.tsx:

tsx
'use client';
import { useSession } from "next-auth/react";
import Link from "next/link";
export default function Home() {
const { data: session, status } = useSession();
return (
<div className="flex min-h-screen flex-col items-center justify-center p-4">
<div className="w-full max-w-4xl rounded-lg bg-white p-8 text-center shadow-md">
<h1 className="mb-6 text-4xl font-bold">MojoAuth Next.js Example</h1>
<p className="mb-8 text-xl text-gray-700">
This is an example application demonstrating MojoAuth integration with Next.js
</p>
<div className="flex flex-col items-center justify-center space-y-4">
{status === "authenticated" ? (
<>
<p className="text-green-600">
Signed in as {session.user?.name || session.user?.email}
</p>
<div className="flex space-x-4">
<Link
href="/dashboard"
className="rounded-md bg-blue-600 px-6 py-3 text-lg font-medium text-white hover:bg-blue-700"
>
Go to Dashboard
</Link>
<Link
href="/api/auth/signout"
className="rounded-md bg-red-600 px-6 py-3 text-lg font-medium text-white hover:bg-red-700"
>
Sign Out
</Link>
</div>
</>
) : (
<Link
href="/auth/signin"
className="rounded-md bg-blue-600 px-6 py-3 text-lg font-medium text-white hover:bg-blue-700"
>
Sign In with MojoAuth
</Link>
)}
</div>
</div>
</div>
);
}

Update your homepage at pages/index.tsx (similar to the App Router version).

  1. Start the Next.js development server:

    terminal
    npm run dev
  2. Open your browser and navigate to http://localhost:3000

  3. Click on “Sign In with MojoAuth”

  4. You will be redirected to MojoAuth’s Hosted Login Page

  5. After successful authentication, you will be redirected back to your application’s callback URL

  6. The application will process the authentication response and redirect you to the dashboard

  7. You can navigate to the profile page to see your user information

You can enhance the JWT callback to handle token refreshing:

typescript
async jwt({ token, user, account }) {
// Initial sign in
if (account && user) {
return {
...token,
accessToken: account.access_token,
accessTokenExpires: Date.now() + (account.expires_in as number) * 1000,
refreshToken: account.refresh_token,
user,
};
}
// Return previous token if the access token has not expired yet
if (token.accessTokenExpires && Date.now() < token.accessTokenExpires) {
return token;
}
// Access token has expired, try to update it
try {
const response = await fetch(`${process.env.MOJOAUTH_ISSUER}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: process.env.MOJOAUTH_CLIENT_ID as string,
client_secret: process.env.MOJOAUTH_CLIENT_SECRET as string,
grant_type: 'refresh_token',
refresh_token: token.refreshToken as string,
}),
});
const refreshedTokens = await response.json();
if (!response.ok) {
throw refreshedTokens;
}
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires:
Date.now() + (refreshedTokens.expires_in as number) * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
};
} catch (error) {
console.error('Error refreshing access token', error);
return {
...token,
error: 'RefreshAccessTokenError',
};
}
}

Create a reusable navigation component at app/components/Navigation.tsx (for App Router) or components/Navigation.tsx (for Pages Router):

tsx
'use client'; // Only needed for App Router
import { useSession, signOut } from "next-auth/react";
import Link from "next/link";
export default function Navigation() {
const { data: session, status } = useSession();
const isAuthenticated = status === "authenticated";
return (
<nav className="bg-white shadow-md">
<div className="container mx-auto px-6 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center">
<Link href="/" className="text-xl font-bold text-gray-800">
MojoAuth Demo
</Link>
<div className="ml-10 hidden space-x-8 md:flex">
<Link href="/" className="hover:text-blue-600">
Home
</Link>
{isAuthenticated && (
<>
<Link href="/dashboard" className="hover:text-blue-600">
Dashboard
</Link>
<Link href="/profile" className="hover:text-blue-600">
Profile
</Link>
</>
)}
</div>
</div>
<div className="flex items-center">
{isAuthenticated ? (
<button
onClick={() => signOut({ callbackUrl: "/" })}
className="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
>
Sign Out
</button>
) : (
<Link
href="/auth/signin"
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
>
Sign In
</Link>
)}
</div>
</div>
</div>
</nav>
);
}

When deploying this application to production, consider the following:

  1. HTTPS: Always use HTTPS in production
  2. Environment Variables: Use proper environment variables management with a service like Vercel or Netlify
  3. Session Security: Ensure your NEXTAUTH_SECRET is secure and not committed to version control
  4. Deployment Platform: Deploy on platforms like Vercel or Netlify that work well with Next.js
  5. Error Monitoring: Add error monitoring services like Sentry
  1. Role-Based Access Control: Implement user roles and permissions
  2. Custom Claims: Extract and use additional claims from the ID token
  3. Styling Enhancements: Improve the UI with more advanced Tailwind CSS components
  4. API Protection: Add protected API routes
  5. Server Actions: For App Router, implement secure server actions