To implement an authentication feature in a Next.js application by integrating WordPress user information, you need to configure WordPress as an Authentication Provider and set up the Next.js application to handle user authentication through it. For this, standard authentication methods like JSON Web Tokens (JWT) or OAuth 2.0 can be used. Here, we will focus on JWT-based authentication.
Overall Approach
- 1. Configure JWT authentication in WordPress
- 2. Communicate with WordPress from the Next.js application to handle authentication
- 3. Apply authentication middleware to Next.js API routes
- 4. Manage authentication tokens on the client side
Let’s examine each step in detail.
1. Configure JWT Authentication in WordPress
First, you need to set up JWT authentication in WordPress. To do this, you can use a plugin like JWT Authentication for WP REST API.
1.1. Install the JWT Authentication Plugin
- 1. Log in to the WordPress admin dashboard.
- 2. Navigate to Plugins > Add New.
- 3. Search for “JWT Authentication for WP REST API”, install it, and then activate it.
1.2. Configure the Plugin
After activating the plugin, follow these steps to configure it.
1. Modify the `wp-config.php` File:
To make the plugin work, you need to add a secret key to the `wp-config.php` file. Add the following code to your `wp-config.php` file. The secret key should be a secure random string.
define('JWT_AUTH_SECRET_KEY', 'your_super_secret_key_here');
define('JWT_AUTH_CORS_ENABLE', true);
Caution:
- `JWT_AUTH_SECRET_KEY` must never be exposed and should be a complex, hard-to-guess string.
- Managing it as an environment variable is more secure. For example, you can use `dotenv` to manage the secret key.
2. Modify the `.htaccess` File (If Using Apache):
Add the following content to the `.htaccess` file to allow access to the JWT authentication endpoint.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]
</IfModule>
3. Refer to the Plugin Documentation:
If additional configuration is required, refer to the official plugin documentation.
1.3. Test JWT Authentication
Use Postman or cURL to test if JWT authentication is working correctly.
Login Request:
curl -X POST http://your-wordpress-site.com/wp-json/jwt-auth/v1/token \
-d "username=your_username&password=your_password"
Example Response:
{
"token": "your_jwt_token_here",
"user_email": "user@example.com",
"user_nicename": "username",
"user_display_name": "User Name"
}
Test Authenticated Request:
curl -X GET http://your-wordpress-site.com/wp-json/wp/v2/posts \
-H "Authorization: Bearer your_jwt_token_here"
If the request is successfully processed as an authenticated user, the post data will be returned.
2. Communicate with WordPress from the Next.js Application to Handle Authentication
Now, configure the Next.js application to utilize WordPress’s JWT authentication. Send login requests to WordPress from the client side, store the received JWT token, and use it for API requests.
2.1. Install Necessary Packages
Install a few packages to handle HTTP requests and manage JWT tokens in Next.js.
npm install axios
npm install --save-dev @types/axios
- axios: A library for handling HTTP requests easily.
- @types/axios: Type definitions for TypeScript.
2.2. Create a Client-Side Login Component
Create a component that allows users to log in using WordPress’s JWT authentication.
`components/Login.tsx`
import { useState } from "react";
import axios from "axios";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
try {
const response = await axios.post("http://your-wordpress-site.com/wp-json/jwt-auth/v1/token", {
username: email, // WordPress typically uses 'username'
password: password,
});
const { token } = response.data;
// Store the token on the client side (e.g., localStorage)
localStorage.setItem("token", token);
alert("Login successful!");
} catch (error: any) {
console.error("Login error:", error.response?.data?.message || error.message);
alert("Login failed: " + (error.response?.data?.message || "Unknown error"));
}
};
return (
<div>
<h2>Login</h2>
<input
type="text"
placeholder="Email or Username"
value={email}
onChange={(e) => setEmail(e.target.value)}
/><br/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/><br/>
<button onClick={handleLogin}>Login</button>
</div>
);
};
export default Login;
Description:
- When a user enters their email (or username) and password, a login request is sent to WordPress’s JWT endpoint.
- If authentication is successful, the received JWT token is stored in `localStorage`.
2.3. Add Logout Functionality and Token Deletion
Add a feature that allows users to log out by deleting the stored token.
`components/Logout.tsx`
import { useEffect } from "react";
const Logout = () => {
useEffect(() => {
// Delete the token upon logout
localStorage.removeItem("token");
alert("You have been logged out.");
}, []);
return null;
};
export default Logout;
2.4. Include JWT Token in API Requests
Include the stored JWT token in the `Authorization` header for API requests to ensure they are authenticated.
Example: Data Fetching Component
import { useEffect, useState } from "react";
import axios from "axios";
const FetchData = () => {
const [data, setData] = useState<any[]>([]);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
const token = localStorage.getItem("token");
if (!token) {
setError("Login is required.");
return;
}
try {
const response = await axios.get("/api/data", {
headers: {
Authorization: `Bearer ${token}`,
},
});
setData(response.data);
} catch (err: any) {
console.error("Data fetching error:", err.response?.data?.error || err.message);
setError(err.response?.data?.error || "An error occurred while fetching data.");
}
};
useEffect(() => {
fetchData();
}, []);
if (error) {
return <div>{error}</div>;
}
return (
<div>
<h2>Data List</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default FetchData;
Description:
- Retrieve the JWT token from `localStorage` and include it in the `Authorization` header.
- The server’s API route (`/api/data`) verifies this token to allow access only to authenticated users.
3. Apply Authentication Middleware to Next.js API Routes
Verify the JWT token in Next.js API routes to ensure that only authenticated users can access the data.
3.1. Create a JWT Verification Utility
Write a utility function to verify JWT tokens.
`utils/jwt.ts`
import jwt from "jsonwebtoken";
interface JwtPayload {
userId: string;
email: string;
exp: number;
}
export const verifyJWT = (token: string): JwtPayload | null => {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as JwtPayload;
return decoded;
} catch (error) {
console.error("JWT verification error:", error);
return null;
}
};
Description:
- The `verifyJWT` function verifies the given token and returns the payload if valid. Otherwise, it returns `null`.
- Caution: WordPress and Next.js must use the same secret key to correctly verify the JWT token. However, for security reasons, this is not recommended. Therefore, consider using a separate verification method.
3.2. Set Environment Variables
To verify WordPress’s JWT tokens in the Next.js application, set the same secret key used by WordPress (`JWT_AUTH_SECRET_KEY`) as the `JWT_SECRET` environment variable.
`.env.local`
NEXT_PUBLIC_JWT_SECRET=your_super_secret_key_here
- Caution: The `NEXT_PUBLIC_` prefix allows client-side access. For security reasons, secret keys should only be used server-side. Therefore, it’s better to use separate environment variables.
Modified `utils/jwt.ts`
Manage environment variables to ensure they are only accessible server-side for security. Use `JWT_SECRET` instead of `NEXT_PUBLIC_JWT_SECRET`.
import jwt from "jsonwebtoken";
interface JwtPayload {
userId: string;
email: string;
exp: number;
}
export const verifyJWT = (token: string): JwtPayload | null => {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as JwtPayload;
return decoded;
} catch (error) {
console.error("JWT verification error:", error);
return null;
}
};
`.env.local`
MONGODB_URI=mongodb://localhost:27017/mongo
JWT_SECRET=your_super_secret_key_here
- Caution: `JWT_SECRET` is used only server-side and cannot be accessed from the client side.
3.3. Create Authentication Middleware
Write middleware to verify JWT tokens in API routes.
`middlewares/auth.ts`
import { NextRequest, NextResponse } from "next/server";
import { verifyJWT } from "@/utils/jwt";
export const authenticate = (request: NextRequest): { userId: string; email: string } | null => {
const authHeader = request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return null;
}
const token = authHeader.split(" ")[1];
const decoded = verifyJWT(token);
return decoded;
};
Description:
- The `authenticate` function extracts the Bearer token from the `Authorization` header and verifies it.
- If the token is valid, it returns the payload; otherwise, it returns `null`.
3.4. Modify Existing API Routes
Apply the authentication middleware to existing API routes.
Modified `app/api/data/route.ts`
import { NextRequest, NextResponse } from "next/server";
import mongooseConnect from "@/lib/mongoose";
import Data from "@/models/annotation";
import { authenticate } from "@/middlewares/auth";
export async function GET(request: NextRequest) {
const user = authenticate(request);
if (!user) {
return NextResponse.json({ error: "Unauthorized." }, { status: 401 });
}
try {
await mongooseConnect();
const data = await Data.find({});
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("GET Error:", error);
return NextResponse.json({ error: "Error fetching data." }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
const user = authenticate(request);
if (!user) {
return NextResponse.json({ error: "Unauthorized." }, { status: 401 });
}
try {
const body = await request.json();
await mongooseConnect();
const newData = new Data(body);
await newData.save();
return NextResponse.json(newData, { status: 201 });
} catch (error) {
console.error("POST Error:", error);
return NextResponse.json({ error: "Error saving data." }, { status: 500 });
}
}
Description:
- Each API handler (`GET`, `POST`) calls the `authenticate` function to verify authentication.
- Unauthenticated users receive a 401 status code with an error message.
- Authenticated users proceed with the existing logic.
4. Manage Authentication Tokens on the Client Side
Securely manage JWT tokens on the client side and include them in API requests when necessary. Although this example uses `localStorage` for simplicity, using `HttpOnly` cookies is more secure. However, since Next.js API routes cannot directly set `HttpOnly` cookies, this example uses `localStorage`.
4.1. Example Login Component
The previously created `Login` component uses WordPress’s JWT endpoint to log in and stores the received token in `localStorage`.
`components/Login.tsx`
import { useState } from "react";
import axios from "axios";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
try {
const response = await axios.post("http://your-wordpress-site.com/wp-json/jwt-auth/v1/token", {
username: email, // WordPress uses 'username'
password: password,
});
const { token } = response.data;
// Store the token on the client side (e.g., localStorage)
localStorage.setItem("token", token);
alert("Login successful!");
} catch (error: any) {
console.error("Login error:", error.response?.data?.message || error.message);
alert("Login failed: " + (error.response?.data?.message || "Unknown error"));
}
};
return (
<div>
<h2>Login</h2>
<input
type="text"
placeholder="Email or Username"
value={email}
onChange={(e) => setEmail(e.target.value)}
/><br/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/><br/>
<button onClick={handleLogin}>Login</button>
</div>
);
};
export default Login;
4.2. Include Token in Data Requests
Include the JWT token when fetching or saving data to make authenticated requests.
`components/FetchData.tsx`
import { useEffect, useState } from "react";
import axios from "axios";
const FetchData = () => {
const [data, setData] = useState<any[]>([]);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
const token = localStorage.getItem("token");
if (!token) {
setError("Login is required.");
return;
}
try {
const response = await axios.get("/api/data", {
headers: {
Authorization: `Bearer ${token}`,
},
});
setData(response.data);
} catch (err: any) {
console.error("Data fetching error:", err.response?.data?.error || err.message);
setError(err.response?.data?.error || "An error occurred while fetching data.");
}
};
useEffect(() => {
fetchData();
}, []);
if (error) {
return <div>{error}</div>;
}
return (
<div>
<h2>Data List</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default FetchData;
Description:
- Retrieve the JWT token from `localStorage` and include it in the `Authorization` header.
- The API route (`/api/data`) on the server verifies this token to allow access only to authenticated users.
5. Security Considerations
5.1. Use HTTPS
Since JWT tokens contain sensitive information, it is essential to use HTTPS to encrypt data during transmission. Always use HTTPS in production environments.
5.2. Token Storage Methods
- localStorage: Convenient but vulnerable to XSS attacks.
- HttpOnly Cookies: Resistant to XSS attacks but vulnerable to CSRF attacks. Implement additional defense mechanisms for CSRF protection.
- Recommendation: Use `HttpOnly` cookies if possible and implement CSRF tokens for protection. However, in Next.js, `HttpOnly` cookies can be set via API routes.
5.3. Token Expiration and Renewal
Set an expiration time for JWT tokens to enhance security. In this example, tokens are set to expire in one hour. Implement functionality to allow users to re-login or obtain new access tokens via Refresh Tokens when tokens expire.
5.4. Secret Key Management
`JWT_SECRET` is highly sensitive and must never be exposed. Manage it as an environment variable and ensure that the `.env.local` file is excluded from version control systems (e.g., Git) by adding it to `.gitignore`.
5.5. Password Security
- Always hash passwords before storing them. In this example, WordPress’s JWT authentication hashes passwords before storage.
- Implement strong password policies to enhance security.
5.6. Input Validation
Thoroughly validate all user inputs and use regular expressions to ensure data integrity when necessary.
5.7. Manage Error Messages
Ensure that error messages do not contain sensitive information that could help attackers understand the system’s internal structure.
5.8. Rate Limiting
Implement rate limiting on API endpoints to defend against DDoS attacks. Middleware like `express-rate-limit` can be used for this purpose.
Final Directory Structure
Below is an example of the project’s directory structure:
my-app/
app/
api/
auth/
login/
route.ts
signup/
route.ts
data/
route.ts
components/
Login.tsx
Logout.tsx
FetchData.tsx
lib/
mongoose.ts
middlewares/
auth.ts
models/
annotation.ts
types/
annotation.ts
global.d.ts
utils/
jwt.ts
.eslintrc.json
.env.local
tsconfig.json
package.json
...
Full Code Examples
1. `models/annotation.ts`
import mongoose, { Schema, Document, Model, model } from "mongoose";
import { Annotation, Maskings } from "../types/annotation";
export interface List {
id: number;
src: string;
annotations: Annotation[];
maskings: Maskings[][];
}
export interface DataDocument extends Document {
id: string;
list: List[];
}
const AnnotationSchema: Schema<Annotation> = new Schema({
x: { type: Number, required: true },
y: { type: Number, required: true },
width: { type: Number, required: true },
height: { type: Number, required: true },
classType: { type: String, required: true },
label: { type: String, required: true },
color: { type: String, required: true },
});
const MaskingsSchema: Schema<Maskings> = new Schema({
x: { type: Number, required: true },
y: { type: Number, required: true },
}, { _id: false });
const ListSchema: Schema<List> = new Schema({
id: { type: Number, required: true },
src: { type: String, required: true },
annotations: { type: [AnnotationSchema], required: true },
maskings: { type: [[MaskingsSchema]], required: true },
});
const DataSchema: Schema<DataDocument> = new Schema({
id: { type: String, required: true, unique: true },
list: { type: [ListSchema], required: true },
});
export default mongoose.models.Data || model<DataDocument>("Data", DataSchema);
2. `models/User.ts`
import mongoose, { Schema, Document, Model, model } from "mongoose";
export interface IUser extends Document {
email: string;
password: string;
}
const UserSchema: Schema<IUser> = new Schema({
email: { type: String, required: true, unique: true, lowercase: true },
password: { type: String, required: true },
});
export default mongoose.models.User || model<IUser>("User", UserSchema);
3. `app/api/auth/signup/route.ts`
import { NextRequest, NextResponse } from "next/server";
import axios from "axios";
import mongooseConnect from "@/lib/mongoose";
import User from "@/models/User";
import bcrypt from "bcryptjs";
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json();
if (!email || !password) {
return NextResponse.json({ error: "Email and password are required." }, { status: 400 });
}
await mongooseConnect();
const existingUser = await User.findOne({ email });
if (existingUser) {
return NextResponse.json({ error: "This email is already registered." }, { status: 400 });
}
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = new User({
email,
password: hashedPassword,
});
await newUser.save();
return NextResponse.json({ message: "User registered successfully." }, { status: 201 });
} catch (error) {
console.error("Signup Error:", error);
return NextResponse.json({ error: "An error occurred during user registration." }, { status: 500 });
}
}
4. `app/api/auth/login/route.ts`
import { NextRequest, NextResponse } from "next/server";
import axios from "axios";
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json();
if (!email || !password) {
return NextResponse.json({ error: "Email and password are required." }, { status: 400 });
}
// Send login request to WordPress JWT Authentication endpoint
const response = await axios.post("http://your-wordpress-site.com/wp-json/jwt-auth/v1/token", {
username: email, // WordPress uses 'username'
password: password,
});
const { token } = response.data;
return NextResponse.json({ token }, { status: 200 });
} catch (error: any) {
console.error("Login Error:", error.response?.data?.message || error.message);
return NextResponse.json({ error: "An error occurred during login." }, { status: 500 });
}
}
Description:
- The `login` API sends a login request to WordPress’s JWT endpoint and returns the received token to the client.
- This approach simplifies the authentication integration between the Next.js application and WordPress.
5. `utils/jwt.ts`
import jwt from "jsonwebtoken";
interface JwtPayload {
userId: string;
email: string;
exp: number;
}
export const verifyJWT = (token: string): JwtPayload | null => {
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as JwtPayload;
return decoded;
} catch (error) {
console.error("JWT verification error:", error);
return null;
}
};
6. `middlewares/auth.ts`
import { NextRequest, NextResponse } from "next/server";
import { verifyJWT } from "@/utils/jwt";
export const authenticate = (request: NextRequest): { userId: string; email: string } | null => {
const authHeader = request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return null;
}
const token = authHeader.split(" ")[1];
const decoded = verifyJWT(token);
return decoded;
};
7. `app/api/data/route.ts`
import { NextRequest, NextResponse } from "next/server";
import mongooseConnect from "@/lib/mongoose";
import Data from "@/models/annotation";
import { authenticate } from "@/middlewares/auth";
export async function GET(request: NextRequest) {
const user = authenticate(request);
if (!user) {
return NextResponse.json({ error: "Unauthorized." }, { status: 401 });
}
try {
await mongooseConnect();
const data = await Data.find({});
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("GET Error:", error);
return NextResponse.json({ error: "Error fetching data." }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
const user = authenticate(request);
if (!user) {
return NextResponse.json({ error: "Unauthorized." }, { status: 401 });
}
try {
const body = await request.json();
await mongooseConnect();
const newData = new Data(body);
await newData.save();
return NextResponse.json(newData, { status: 201 });
} catch (error) {
console.error("POST Error:", error);
return NextResponse.json({ error: "Error saving data." }, { status: 500 });
}
}
8. `types/global.d.ts`
import mongoose from "mongoose";
declare global {
var mongoose: {
conn: mongoose.Mongoose | null;
promise: Promise<mongoose.Mongoose> | null;
};
}
export {};
9. `lib/mongoose.ts`
import mongoose from 'mongoose';
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/mindcrash';
if (!MONGODB_URI) {
throw new Error('Please define the MONGODB_URI environment variable');
}
let cached = global.mongoose;
if (!cached) {
cached = global.mongoose = { conn: null, promise: null };
}
async function mongooseConnect() {
if (cached.conn) {
return cached.conn;
}
if (!cached.promise) {
const opts = {
bufferCommands: false,
};
cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongooseInstance) => {
return mongooseInstance;
});
}
cached.conn = await cached.promise;
return cached.conn;
}
export default mongooseConnect;
10. `.eslintrc.json`
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off"
},
"overrides": [
{
"files": ["**/*.d.ts"],
"rules": {
"no-var": "off"
}
}
]
}
11. `tsconfig.json`
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"types": []
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types/**/*.d.ts"],
"exclude": ["node_modules"]
}
6. Client Components Example
6.1. `components/Login.tsx`
import { useState } from "react";
import axios from "axios";
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async () => {
try {
const response = await axios.post("/api/auth/login", {
email,
password,
});
const { token } = response.data;
// Store the token on the client side (e.g., localStorage)
localStorage.setItem("token", token);
alert("Login successful!");
} catch (error: any) {
console.error("Login error:", error.response?.data?.error || error.message);
alert("Login failed: " + (error.response?.data?.error || "Unknown error"));
}
};
return (
<div>
<h2>Login</h2>
<input
type="text"
placeholder="Email or Username"
value={email}
onChange={(e) => setEmail(e.target.value)}
/><br/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/><br/>
<button onClick={handleLogin}>Login</button>
</div>
);
};
export default Login;
6.2. `components/FetchData.tsx`
import { useEffect, useState } from "react";
import axios from "axios";
const FetchData = () => {
const [data, setData] = useState<any[]>([]);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
const token = localStorage.getItem("token");
if (!token) {
setError("Login is required.");
return;
}
try {
const response = await axios.get("/api/data", {
headers: {
Authorization: `Bearer ${token}`,
},
});
setData(response.data);
} catch (err: any) {
console.error("Data fetching error:", err.response?.data?.error || err.message);
setError(err.response?.data?.error || "An error occurred while fetching data.");
}
};
useEffect(() => {
fetchData();
}, []);
if (error) {
return <div>{error}</div>;
}
return (
<div>
<h2>Data List</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
};
export default FetchData;
Description:
- Retrieve the JWT token from `localStorage` and include it in the `Authorization` header.
- Use this token to authenticate API requests.
Conclusion
By following the steps above, you can successfully implement authentication in a Next.js application by integrating WordPress user information. In summary:
- 1. Configure JWT Authentication in WordPress: Install and set up the `JWT Authentication for WP REST API` plugin to enable JWT-based authentication.
- 2. Communicate with WordPress in Next.js: Send login requests to WordPress’s JWT endpoint and store the received token on the client side.
- 3. Apply Authentication to Next.js API Routes: Verify JWT tokens in API routes to restrict access to authenticated users only.
- 4. Manage Tokens on the Client Side: Securely store tokens on the client side and include them in API requests to make authenticated requests.
Additional Considerations:
- Enhance Security: Use HTTPS, choose secure token storage methods, implement CSRF protection, and manage token expiration and renewal diligently.
- Manage Environment Variables: Handle sensitive information like secret keys as environment variables and ensure they are excluded from version control by adding them to `.gitignore`.
- Improve User Experience: Implement features like automatic logout upon token expiration and the use of Refresh Tokens to enhance user experience.