OAuth authentication between Next.js and Laravel Passport
This tutorial helps you implement a secure OAuth authentication system connecting a Next.js 15 frontend with a Laravel backend. You’ll create an Authorization Code Flow with PKCE (Proof Key for Code Exchange), providing robust security for your web applications.
Throughout this tutorial, CLI prompts and outputs may be shortened for clarity. When following along, you can select any option for the prompts shown that are not explicit in the tutorial - your choices won’t affect the final outcome.
As some code blocks have also been shortened, please always refer to the complete source files available in the public GitHub repository.
Pre-requisites
Before starting, make sure you have these tools installed:
- Git
- PHP 8.2+
- Composer
- Node.js 20+
Initialize a new local project
Start by creating a new project directory and setting up Git:
$ mkdir oauth-project && cd oauth-project
$ git init .
Add test hostnames
Configure test domains for local development by adding entries to your hosts file:
$ sudo vim /etc/hosts
Add the following entries to map test domains to your local machine:
127.0.0.1 api.oauth-project.test next.oauth-project.test
These entries allow your browser to resolve the test domains to your local machine, creating a necessary multi-domain setup for OAuth testing.
Set up a new Next.js 15 project
Create your Next.js frontend application with TypeScript, Tailwind CSS, and the App Router:
npx create-next-app@latest
Need to install the following packages:
create-next-app@15.3.1
Ok to proceed? (y)
✔ What is your project named? … next
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No
Creating a new Next.js app in [path]/oauth-project/next.
Go into that newly created folder:
cd next
You can enhance your UI with shadcn/ui, a collection of reusable components:
npx shadcn@latest init
Start the development server to test your setup:
npm run dev
> next@0.1.0 dev
> next dev --turbopack
▲ Next.js 15.3.1 (Turbopack)
- Local: http://localhost:3000
- Network: http://10.1.10.221:3000
✓ Starting...
✓ Ready in 743ms
You can leave the npm
process running and create a new terminal session for the next steps.
.cursorrules
file for Next.js.Bootstrap a new Laravel project
Create a new Laravel backend that will serve as your OAuth server at the root of the project:
$ composer global require laravel/installer
$ laravel new api
┌ Would you like to install a starter kit? ────────────────────┐
│ No starter kit │
└──────────────────────────────────────────────────────────────┘
┌ Which testing framework do you prefer? ──────────────────────┐
│ Pest │
└──────────────────────────────────────────────────────────────┘
┌ Would you like to initialize a Git repository? ──────────────┐
│ No │
└──────────────────────────────────────────────────────────────┘
INFO Application key set successfully.
┌ Which database will your application use? ───────────────────┐
│ ○ SQLite │
│ ○ MySQL │
│ › ● MariaDB │
│ ○ PostgreSQL │
│ ○ SQL Server (Missing PDO extension) │
└──────────────────────────────────────────────────────────────┘
┌ Default database updated. Would you like to run the default database migrations? ┐
│ No │
└──────────────────────────────────────────────────────────────────────────────────┘
Now cd
into the api
folder to install additional packages.
cd api
Install Sail for local development
Laravel Sail provides a Docker-based development environment, making it easy to run your application with isolated dependencies:
$ composer require laravel/sail --dev
$ php artisan sail:install
┌ Which services would you like to install? ───────────────────┐
│ › ◼ pgsql │ │
│ ◻ mariadb ┃ │
│ ◻ mongodb │ │
│ ◼ mailpit │ │
│ ◼ valkey │ │
└────────────────────────────────────────────────── 3 selected ┘
Use the space bar to select options.
Configure your Laravel environment by editing the .env
file:
APP_NAME=OAuth
APP_URL=http://api.oauth-project.test
DB_CONNECTION=pgsql
DB_HOST=pgsql
DB_PORT=5432
DB_DATABASE=oauth
DB_USERNAME=oauth
DB_PASSWORD=oauth
SESSION_DRIVER=redis
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=redis
QUEUE_CONNECTION=redis
CACHE_STORE=redis
Create an alias for the sail command to simplify usage. You can add this line in your shell profile (.bashrc
, .zshrc
, etc.). Don’t forget to source
that file afterwards or open a new terminal.
$ alias sail="./vendor/bin/sail"
Start the Laravel environment:
$ sail up
You should now see the Laravel welcome page at http://api.oauth-project.test/
.
For any troubleshooting needs, check the Sail documentation.
Install Telescope to debug requests
Laravel Telescope provides debugging tools for your application, including insights into requests, queries, and more:
$ sail composer require laravel/telescope
$ sail php artisan telescope:install
$ sail php artisan migrate
Please refer to the Telescope documentation to configure it properly for production usage if needed.
Install Passport and the API package
Install Laravel Passport to handle OAuth authentication:
$ sail php artisan install:api --passport
INFO Publishing [laravel-assets] assets.
Copying directory [vendor/laravel/telescope/public] to [public/vendor/telescope] DONE
No security vulnerability advisories found.
INFO Published API routes file.
Would you like to use UUIDs for all client IDs? (yes/no) [no]: yes
INFO API scaffolding installed. Please add the [Laravel\Passport\HasApiTokens] trait to your User model.
Add the HasApiTokens
trait to your User model:
use Laravel\Passport\HasApiTokens;
use HasFactory, Notifiable, HasApiTokens;
Configure the authentication guard in auth.php
:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
Install Fortify for auth boilerplate
Laravel Fortify provides authentication scaffolding without a specific frontend implementation:
$ sail composer require laravel/fortify
$ sail php artisan fortify:install
INFO Fortify scaffolding installed successfully.
$ sail php artisan migrate
.cursorrules
file for Laravel..Deploy both apps on Upsun
Commit your project before deploying to Upsun:
$ cd ../ # root of project
$ git add next
$ git commit -m "Bootstrap Next.js"
$ git add api
$ git commit -m "Bootstrap Laravel"
Upsun multi-apps configuration
Create a new Upsun project to host both applications:
$ upsun project:create
Creating a project under the organization)
* Project title (--title)
Default: Untitled Project
> OAuth Project
* Region (--region)
The region where the project will be hosted.
Get a 3% discount on resources for regions with a carbon intensity of less than 100 gCO2eq/kWh.
> ca-1.platform.sh
Default branch (--default-branch)
The default Git branch name for the project (the production environment)
Default: main
>
Git repository detected: [path]/oauth-project
Set the new project OAuth Project as the remote for this repository? [Y/n]
The estimated monthly cost of this project is: ---
Pricing information: https://upsun.com/pricing
Are you sure you want to continue? [Y/n]
Your Upsun project has been requested (subscription ID: [id])
The Upsun Bot is activating your project
▄ ▄
▄ ▄█▄▄▄█▄ ▄
███▄███▄███
▀█▀▀▀▀▀█▀
▀ ▀
Setting the remote project for this repository to: OAuth Project ([project_id])
Create a .upsun/config.yaml
file to configure your multi-applications project. You can copy/paste the configuration file in the repository.
This configuration:
- Routes requests to the appropriate application based on the hostname (
@
(root) andapi.
) L80-82. As we are using the{all}
value, all added hostnames and Upsun auto-generated hostnames will be directed to thenext
application while allapi.
prefixed hostnames will be routed to the Laravelapi
. - Sets up PostgreSQL and Valkey (Redis-compatible) services L85-89
- Configures both applications with proper build and deploy hooks
Now create a .environment
file for Laravel to set environment variables:
export DB_SCHEME="pgsql"
export DATABASE_URL="postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_PATH}"
export DB_CONNECTION="$DB_SCHEME"
export DB_DATABASE="$DB_PATH"
# Set Cache environment variables
export CACHE_STORE=redis
export CACHE_PREFIX=
export CACHE_URL="${CACHE_SCHEME}://${CACHE_HOST}:${CACHE_PORT}"
export REDIS_CLIENT=phpredis
export REDIS_HOST="$CACHE_HOST"
export REDIS_PORT="$CACHE_PORT"
export REDIS_PASSWORD=null
Commit the configuration and the new .environment
:
$ git add .upsun/config.yaml .environment
$ git commit -m "Add Upsun configuration and environment"
First deployment
Create an encryption key for Laravel as an environment variable. You can use your local key that has been generated in your local .env
or create a new one with sail php artisan key:generate
.
upsun variable:create --name env:APP_KEY
* Level (--level)
The level at which to set the variable
[project ] Project-wide
[environment] Environment-specific
> project
* Value (--value)
The variable's value
> base64:zVXg60t5ch[...]=
Deploy your project to Upsun:
$ upsun push -y
Selected project: OAuth Project ([project_id])
Pushing HEAD to the environment main (type: production).
Review of the OAuth flow
Let’s understand the OAuth flow you’ll implement.
Understanding OAuth grant types
OAuth 2.0 offers several grant types, each suited for different scenarios. For browser-based applications like yours, the Authorization Code Flow with Proof Key for Code Exchange (PKCE) provides the best security.
PKCE (pronounced “pixy”) adds security by ensuring that only the application that initiated the authentication flow can exchange the authorization code for tokens. This prevents authorization code interception attacks.
Auth0 provides a great introduction to different OAuth grant types. Here’s a helpful diagram by Alex Bilbie to choose the right grant:
Your OAuth flow
Here’s the detailed flow between your Next.js client and Laravel backend:
Step-by-step breakdown
1. User starts the flow
- User visits
/next/dashboard
in the browser. - Next.js server checks for an
access_token
:- ✅ If found: renders the dashboard.
- ❌ If not found: redirects to
/oauth/redirect
.
2. OAuth redirect setup (Next.js)
- Generates an OAuth
authorize
request with:client_id
redirect_uri
state
(for CSRF protection)code_challenge
(for PKCE)
- Sends redirect to Laravel API
/api/oauth/authorize
.
3. Laravel API authorization flow
- Laravel checks if the user is logged in:
- ✅ If logged in: shows authorization form.
- ❌ If not: displays login form.
- After login:
- Valid credentials → continues the flow.
- Invalid credentials → shows error.
- If user approves the app:
- Laravel handles the authorization.
- Creates and stores a client
access_token
in the database. - Redirects to
/next/oauth/callback
with the authorizationcode
.
4. Callback handling (Next.js)
- Validates the
state
to prevent CSRF attacks. - Exchanges the
authorization code
andcode_verifier
for anaccess_token
. - Stores the token as an HttpOnly cookie.
- Cleans up temporary cookies (
state
,challenge
). - Redirects to
/dashboard
if successful. - Shows an error page or redirects to login if something fails.
This flow ensures:
- Users authenticate directly with the authentication server
- Access tokens remain secure and inaccessible to browser JavaScript
- The application securely accesses protected resources
Laravel OAuth implementation with Passport
Laravel Passport simplifies OAuth implementation by handling token generation, storage, and validation. You need to create OAuth clients and implement login and authorization views.
Create a client and a test user
Create a test user and OAuth client through a database seeder:
<?php
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Laravel\Passport\Client;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]);
Client::create([
'id' => '9ebd8f82-e3fe-4205-87ad-bfe10e03cdd9',
'name' => 'next.oauth-project.test',
'secret' => null,
'redirect' => 'http://next.oauth-project.test:3000/oauth/callback',
'personal_access_client' => false,
'password_client' => false,
'revoked' => false,
]);
}
}
This creates:
- A test user with email
test@example.com
L19 with a default password set topassword
. - A public OAuth client (no secret, as required for PKCE) L29
- A redirect URI matching your Next.js callback route L28
Run the seeder to populate your database:
sail php artisan migrate:refresh --seed
OAuth and Fortify routes
Define API routes for user information and logout:
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::group(['middleware' => 'auth:api'], function () {
Route::get('/user', function (Request $request) {
return $request->user();
});
Route::post('/logout', function (Request $request) {
$request->user()->token()->revoke();
return response()->json(['message' => 'Successfully logged out']);
});
});
These routes are protected by the auth:api
middleware, ensuring they’re only accessible with a valid access token.
Add the login view to the boot
method of FortifyServiceProvider
L46:
Fortify::loginView(function () {
return view('auth.login');
});
Create the login view at api/resources/views/auth/login.blade.php
. You can find an example in the GitHub repository.
Customize the OAuth authorization form by publishing and modifying the Passport views:
$ sail php artisan vendor:publish --tag=passport-views
Edit the authorization form at api/resources/views/vendor/passport/authorize.blade.php
. Find an example in the GitHub repository.
Test your login page at http://api.oauth-project.test/login to ensure it works correctly.
Next.js client implementation
Now for the client-side of your OAuth flow in Next.js. You need to create three key components:
- A dashboard component that checks authentication and loads user data
- The
/oauth/redirect
endpoint that initiates the authorization flow - The
/oauth/callback
endpoint that processes the authorization response
Home and dashboard
Create a simple home page with a button linking to your dashboard. First, install the Button component:
$ cd next/
$ npx shadcn@latest add button
Need to install the following packages:
shadcn@2.5.0
Ok to proceed? (y)
✔ You need to create a components.json file to add components. Proceed? … yes
✔ Which color would you like to use as the base color? › Neutral
✔ Writing components.json.
✔ Checking registry.
Installing dependencies.
It looks like you are using React 19.
Some packages may fail to install due to peer dependency issues in npm (see https://ui.shadcn.com/react-19).
✔ How would you like to proceed? › Use --force
✔ Installing dependencies.
✔ Created 1 file:
- src/components/ui/button.tsx
Create a home page (next/src/app/page.tsx
) with a dashboard link:
import { Button } from "@/components/ui/button";
import Link from "next/link";
export default function Home() {
return (
<div className="h-full w-full flex items-center justify-center min-h-screen font-[family-name:var(--font-geist-sans)]">
<Button>
<Link href="/dashboard">Go to the Dashboard</Link>
</Button>
</div>
);
}
This creates a simple, centered button linking to your dashboard:
Next, create a dashboard page that handles authentication checks. Don’t forget to also create the components it relies on.
First, add the shadcn
dependencies:
npx shadcn@latest add avatar dropdown-menu
Create the dashboard page - View the full implementation.
export default async function DashboardPage () {
const cookieStore = await cookies()
const accessToken = await cookieStore.get('access_token')?.value
if (!accessToken) {
console.log('No access token cookie found, redirecting to /login')
redirect('/oauth/redirect')
}
const userData = await getUserData(accessToken)
if (!userData) {
console.log('Failed to fetch user data or token invalid, redirecting to /login')
redirect('/oauth/redirect')
}
return(...)
}
This code:
- Checks for an access token in cookies
- Redirects to the OAuth flow if no token exists
- Fetches user data to verify the token is valid
- Redirects to the OAuth flow if the token is invalid or expired
- Renders the dashboard for authenticated users
Create the two other components that the dashboard includes:
The /oauth/redirect
endpoint
Create a server action that initiates the OAuth flow at next/src/app/oauth/redirect/route.ts
. View the full implementation.
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import crypto from 'node:crypto'
export async function GET () {
try {
const state = crypto.randomBytes(20).toString('hex') // 40 chars
const codeVerifier = crypto.randomBytes(64).toString('hex') // 128 chars
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '') // Base64url encoding
const cookieStore = await cookies()
const secure = process.env.NODE_ENV === 'production'
const cookieOptions = {
httpOnly: true,
secure,
path: '/',
maxAge: 60 * 15 // 15 minutes
}
cookieStore.set('oauth_state', state, cookieOptions)
cookieStore.set('oauth_code_verifier', codeVerifier, cookieOptions)
const clientId = process.env.OAUTH_CLIENT_ID
const redirectUri = process.env.OAUTH_REDIRECT_URI
const authorizeUrl = process.env.OAUTH_AUTHORIZE_URL
const scope = process.env.OAUTH_SCOPE || '' // Default to empty scope if not set
if (!clientId || !redirectUri || !authorizeUrl) {
console.error('Missing OAuth environment variables (OAUTH_CLIENT_ID, OAUTH_REDIRECT_URI, OAUTH_AUTHORIZE_URL)')
return NextResponse.json({ error: 'Server configuration error.' }, { status: 500 })
}
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
response_type: 'code',
scope,
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
prompt: 'login'
// prompt: '', // Add if needed: "none", "consent", or "login"
})
const fullAuthorizeUrl = `${authorizeUrl}?${params.toString()}`
return NextResponse.redirect(fullAuthorizeUrl)
} catch (error) {
console.error('Error during OAuth redirect:', error)
// Return a generic error response to the client
return NextResponse.json({ error: 'An unexpected error occurred.' }, { status: 500 })
}
}
This endpoint:
- Generates a random state value for CSRF protection
- Creates a code verifier and code challenge for PKCE
- Stores these values in HTTP-only cookies
- Builds the authorization URL with required parameters
- Redirects the user to the Laravel authorization endpoint
The prompt
parameter controls the authorization server’s behavior:
login
: Always show the login formconsent
: Always show the authorization formnone
: Skip forms if the user is already logged in- Empty: Use default behavior (may skip forms if logged in)
For local development, create a .env.local
file with the necessary variables:
NEXT_PUBLIC_BASE_URL=http://next.oauth-project.test:3000
OAUTH_CLIENT_ID=9ebd8f82-e3fe-4205-87ad-bfe10e03cdd9
OAUTH_REDIRECT_URI=http://next.oauth-project.test:3000/oauth/callback
OAUTH_AUTHORIZE_URL=http://api.oauth-project.test/oauth/authorize
OAUTH_SCOPE=''
These values must match the OAuth client you created in the Laravel seeder.
The /oauth/callback
endpoint
Create a callback endpoint to handle the authorization response at next/src/app/oauth/callback/route.ts
.
View the full implementation.
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
export async function GET (request) {
const cookieStore = await cookies()
const storedState = cookieStore.get('oauth_state')?.value
const storedCodeVerifier = cookieStore.get('oauth_code_verifier')?.value
// Clear cookies immediately after retrieval
cookieStore.delete('oauth_state')
cookieStore.delete('oauth_code_verifier')
try {
const { searchParams } = new URL(request.url)
const state = searchParams.get('state')
const code = searchParams.get('code')
const error = searchParams.get('error')
// --- State Validation & Verifier ---
[...]
// --- Environment Variable Checks ---
[...]
// --- Token Exchange ---
const params = new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
redirect_uri: redirectUri,
code,
code_verifier: storedCodeVerifier
})
const tokenResponse = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
})
// --- Handle Token Response ---
const tokenData = await tokenResponse.json()
if (!tokenResponse.ok) {
console.error('Failed to fetch OAuth tokens:', tokenData)
[...]
}
// --- Success ---
// At this point, you have the tokens in tokenData (e.g., access_token, refresh_token, id_token)
cookieStore.set('access_token', tokenData.access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
maxAge: tokenData.expires_in // Use token's expiration time
});
return NextResponse.redirect(new URL('/dashboard', process.env.NEXT_PUBLIC_BASE_URL))
} catch (error) {
[...]
}
}
The callback endpoint:
- Retrieves and deletes the stored state and code verifier from cookies
- Validates the state parameter to prevent CSRF attacks
- Exchanges the authorization code for an access token using the code verifier
- Stores the access token in an HTTP-only cookie
- Redirects to the dashboard upon successful authentication
The HTTP-only cookie approach is crucial for security. Unlike localStorage, HTTP-only cookies can’t be accessed by JavaScript, protecting tokens from cross-site scripting (XSS) attacks.
Add these additional environment variables to .env.local
:
OAUTH_TOKEN_URI=http://api.oauth-project.test/oauth/token
#OAUTH_LOGOUT_URI=http://api.oauth-project.test/api
API_ENDPOINT=http://api.oauth-project.test/api
Local testing
Verify that all routes are available before testing the complete flow. Check Next.js routes:
$ cd next
$ npx next-list
Listing routes in src/app:
┌───────────────┬─────────────┬──────────┬──────────┬───────────────┬
│ Function │ Route │ Type │ Metadata │ Server Action │
├───────────────┼─────────────┼──────────┼──────────┼───────────────┼
│ DashboardPage │ /dashboard/ │ ⇠ server │ × │ × │
│ function │ / │ ⇠ server │ × │ × │
└───────────────┴─────────────┴──────────┴──────────┴───────────────┴
┌────────┬───────────────────────┐
│ Method │ Route │
├────────┼───────────────────────┤
│ GET │ /oauth/callback/route │
│ GET │ /oauth/redirect/route │
└────────┴───────────────────────┘
Then check Laravel routes:
$ cd api
$ sail php artisan route:list
GET|HEAD /
POST api/logout
GET|HEAD api/user
GET|HEAD forgot-password
POST forgot-password
GET|HEAD login
POST login
POST logout
GET|HEAD oauth/authorize
POST oauth/authorize
DELETE oauth/authorize
[...]
POST oauth/token
POST oauth/token/refresh
GET|HEAD oauth/tokens
DELETE oauth/tokens/{token_id}
Showing [87] routes
Now test the full OAuth flow. Go to http://next.oauth-project.test:3000/
and click “Go to the Dashboard.” This should trigger:
- Redirection to the Laravel login page (
test@example.com
/password
) - After login, presentation of the authorization form
- After approval, redirection back to your Next.js dashboard
Here’s what the flow looks like:
next.oauth-project.test:3000
and api.oauth-project.test
domains. Existing sessions might interfere with testing.Upsun environments & deployment
When deploying to Upsun, ensure your Next.js app can communicate with the Laravel OAuth server. Upsun provides environment variables with routes for all deployed apps.
Get the API URL with this command:
$ upsun ssh -A next
_ _
| | | |_ __ ____ _ _ _
| |_| | '_ (_-< || | ' \
\___/| .__/__/\_,_|_||_|
|_|
Welcome to Upsun.
Environment: main-bvxea6i
Branch: main
Project: rbdomchcjle6y
web@next.0:~$ echo $PLATFORM_ROUTES | base64 --decode | jq -r 'to_entries[] | select(.value.id=="api") | .key'
https://api.main-bvxea6i-rbdomchcjle6y.ca-1.platformsh.site/
Create a .environment
file in the next
directory to set environment variables during deployment:
export API_HOST=$(echo $PLATFORM_ROUTES | base64 --decode | jq -r 'to_entries[] | select(.value.id=="api") | .key')
export NEXT_PUBLIC_BASE_URL=$(echo $PLATFORM_ROUTES | base64 --decode | jq -r 'to_entries[] | select(.value.id=="next") | .key')
export API_ENDPOINT=${API_HOST}api
export OAUTH_TOKEN_URI=${API_HOST}oauth/token
export OAUTH_AUTHORIZE_URL=${API_HOST}oauth/authorize
export OAUTH_LOGOUT_URI=${API_HOST}api/logout
export OAUTH_REDIRECT_URI=${NEXT_PUBLIC_BASE_URL}oauth/callback
export OAUTH_SCOPE=''
This script extracts URLs for your applications from Upsun’s environment variables and sets OAuth endpoints accordingly.
You still need to set the OAUTH_CLIENT_ID
variable:
$ upsun variable:create --name env:OAUTH_CLIENT_ID
* Level (--level)
The level at which to set the variable
> project
* Value (--value)
The variable's value
> [your client ID in the Laravel Seeder]
Creating variable env:OAUTH_CLIENT_ID on the project OAuth Project (...)
Before deploying, let’s move the faker
dependency to the require
section in composer.json
instead of require-dev
:
$ composer require fakerphp/faker
fakerphp/faker is currently present in the require-dev key and you ran the command without the --dev flag, which will move it to the require key.
This allows your deployed environment to run the database seeder.
Commit everything and deploy:
$ cd [root of project]
$ git add .
$ git commit -am "Add environment configuration"
$ upsun push -y
After deployment, seed the database and generate Passport keys:
$ upsun ssh -A api
$ php artisan migrate:refresh --seed
$ php artisan passport:keys
Final test and review
Get your application URL with upsun url
and test the complete flow. You’ll see:
- The Next.js homepage:
- Click “Go to dashboard” to trigger the OAuth flow and see the login form:
- Enter your credentials (default password:
password
) to see the authorization form:
- Click “Authorize” to complete the flow and access your dashboard:
Implementing the logout feature
A complete authentication system needs a secure logout process. This requires:
- Clearing tokens stored as HTTP-only cookies in Next.js
- Revoking the token on the Laravel side
- Clearing the session on the Laravel side
Add a logout endpoint to your Laravel API routes. Note: You might already have that change if you copy pasted the file from the example repository
|
|
This endpoint revokes the access token and deletes the user’s session.
Create a /logout
endpoint in Next.js. View the full implementation.
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
// Handle POST requests for logging out
export async function POST () {
try {
const cookieStore = await cookies()
await fetch(process.env.OAUTH_LOGOUT_URI || '', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
// Delete the main access token cookie
cookieStore.delete('access_token')
// Attempt to delete OAuth state/verifier cookies as a cleanup measure
cookieStore.delete('oauth_state')
cookieStore.delete('oauth_code_verifier')
// Get the base URL from the request
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL
// Redirect to the homepage
return NextResponse.redirect(baseUrl + '/', {
status: 302 // Use 302 Found for temporary redirect after action
})
} catch (error) {
console.error('Error during logout:', error)
// Return a generic error if something goes wrong
return NextResponse.json({ error: 'Logout failed.' }, { status: 500 })
}
}
This endpoint calls the Laravel logout API, deletes authentication cookies, and redirects to the homepage.
To actually execute the logout, a handler is present in your user-dropdown
component. View the full implementation.
.
// Logout handler that calls the backend endpoint
const handleLogout = async () => {
const response = await fetch('/logout', { // Use the correct path to your logout endpoint
method: 'POST',
headers: {
'Content-Type': 'application/json' // Adjust if your endpoint expects different Content-Type
}
})
if (response.ok) {
// Check if the server performed a redirect
if (response.redirected) {
// The server handled the redirect (to '/' in this case)
// Use window.location to force a full page refresh and clear client state
window.location.href = response.url
}
}
}
Conclusion
You’ve built a secure OAuth authentication system between Next.js and Laravel using the Authorization Code Flow with PKCE. This approach provides strong security by:
- Keeping tokens in HTTP-only cookies, protected from JavaScript access
- Using state parameters to prevent CSRF attacks
- Adding PKCE protection against authorization code interception
- Properly revoking tokens during logout
You can extend this system with additional features like:
- Refresh token handling for longer sessions
- Scoped permissions for granular access control
- Role-based authorization
- Multi-factor authentication
For more information, check these resources:
Find the complete code for this tutorial in the GitHub repository.