Next.js course Β· Module 7: Authentication and Authorization
User sessions and token management
In this lesson14
In previous modules, we covered the basics of authentication in Next.js and the implementation of login with external providers. Now we will delve into a key aspect of every authentication system: managing user sessions and tokens. Understanding these mechanisms is essential for building secure and efficient web applications.
Session management basics
A user session is a mechanism that allows the application to "remember" a logged-in user during navigation between pages and requests. In modern web applications, sessions are implemented in two main ways:
- Server-side sessions - the session identifier is stored in a cookie, and the session data on the server
- Stateless sessions (e.g. JWT) - all necessary information stored in the token, often in a cookie
In Next.js, especially in combination with NextAuth.js, we have access to both approaches.
Sessions in NextAuth.js
NextAuth.js offers two main session management strategies:
1. JWT strategy (default)
In this strategy, all session information is stored in a JWT (JSON Web Token) that is saved in a cookie:
1// JWT strategy configuration in NextAuth.js
2export const authOptions = {
3 providers: [
4 // Provider configuration...
5 ],
6 session: {
7 strategy: "jwt",
8 maxAge: 30 * 24 * 60 * 60, // 30 days in seconds
9 updateAge: 24 * 60 * 60, // 24 hours in seconds
10 },
11 jwt: {
12 secret: process.env.NEXTAUTH_SECRET,
13 // You can also use private/public keys instead of a simple secret
14 // encryption: true, // Enables JWT encryption
15 // signingKey: process.env.JWT_SIGNING_PRIVATE_KEY,
16 // encryptionKey: process.env.JWT_ENCRYPTION_KEY,
17 }
18};Zalety:
- Better scalability - the server does not need to store session data
- Works well in a serverless environment
- Simpler to implement
Wady:
- Limited data size (tokens should not be too large)
- More difficult to invalidate a token before expiration
- Sensitive data is stored in the token (although encrypted)
2. Database strategy
In this strategy, only the session identifier is stored in the cookie, and the rest of the session data is stored in the database:
1// Database strategy configuration in NextAuth.js
2import { PrismaAdapter } from "@auth/prisma-adapter";
3import { PrismaClient } from "@prisma/client";
4
5const prisma = new PrismaClient();
6
7export const authOptions = {
8 providers: [
9 // Provider configuration...
10 ],
11 adapter: PrismaAdapter(prisma),
12 session: {
13 strategy: "database",
14 maxAge: 30 * 24 * 60 * 60, // 30 days in seconds
15 updateAge: 24 * 60 * 60, // 24 hours in seconds
16 }
17};Zalety:
- Easier session invalidation (can remove the entry from the database)
- No limits on data amount
- Safer for sensitive data
Wady:
- Requires database access on every request
- Slightly more difficult scalability
- A database adapter is needed
JWT token structure in NextAuth.js
When we use the JWT strategy, NextAuth.js creates a token containing basic user information:
1{
2 "name": "John Doe",
3 "email": "john@example.com",
4 "sub": "cl5ab1gxk0001t8kgxs4t8j1h", // user identifier
5 "iat": 1658309600, // token issued time
6 "exp": 1660901600, // token expiration time
7 "jti": "4b32c6a4-d2c5-4f1a-a2b3-5c84c1d94208" // unique token identifier
8}We can extend the token with additional data using the callback jwt:
1export const authOptions = {
2 //
3 callbacks: {
4 async jwt({ token, user, account }) {
5 // On first login we have access to the user object
6 if (user) {
7 token.role = user.role;
8 token.userId = user.id;
9 token.provider = account?.provider;
10 }
11
12 // Here we can add token refresh logic
13 // e.g. checking if the OAuth access token has not expired
14
15 return token;
16 },
17
18 async session({ session, token }) {
19 // Add data from the token to the session object (available on the client)
20 if (session.user) {
21 session.user.role = token.role;
22 session.user.id = token.userId;
23 session.user.provider = token.provider;
24 }
25
26 return session;
27 }
28 }
29};Configuring session cookies
By default, NextAuth.js configures session cookies according to best security practices, but we can customize these settings:
1export const authOptions = {
2 //
3 cookies: {
4 sessionToken: {
5 name: `__Secure-next-auth.session-token`,
6 options: {
7 httpOnly: true,
8 sameSite: "lax",
9 path: "/",
10 secure: true, // Set to false during local testing without HTTPS
11 maxAge: 30 * 24 * 60 * 60, // 30 days in seconds
12 },
13 },
14 // You can also configure other cookies used by NextAuth.js
15 callbackUrl: {
16 name: `__Secure-next-auth.callback-url`,
17 options: { /* ... */ },
18 },
19 csrfToken: {
20 name: `__Host-next-auth.csrf-token`,
21 options: { /* ... */ },
22 },
23 },
24};Client-side session management
In client components we can use the hook useSession to access session data and its status:
1'use client';
2
3import { useSession } from 'next-auth/react';
4
5export default function ProfileComponent() {
6 const { data: session, status } = useSession();
7
8 if (status === 'loading') {
9 return <p>Loading...</p>;
10 }
11
12 if (status === 'unauthenticated') {
13 return <p>Access only for logged-in users</p>;
14 }
15
16 return (
17 <div>
18 <h1>Profile</h1>
19 <p>Hello, {session.user.name}!</p>
20 <p>Email: {session.user.email}</p>
21 {session.user.role === 'admin' && (
22 <p>You have administrator permissions</p>
23 )}
24 </div>
25 );
26}We can also require a session using the option required:
1'use client';
2
3import { useSession } from 'next-auth/react';
4import { redirect } from 'next/navigation';
5
6export default function ProtectedComponent() {
7 const { data: session, status } = useSession({
8 required: true,
9 onUnauthenticated() {
10 redirect('/auth/login?callbackUrl=/profile');
11 },
12 });
13
14 // When required is set, we don't need to check 'unauthenticated' status
15 if (status === 'loading') {
16 return <p>Loading...</p>;
17 }
18
19 return (
20 <div>
21 <h1>Protected content</h1>
22 <p>This page is visible only to logged-in users.</p>
23 </div>
24 );
25}Server-side session management
In server components or Route Handlers we can use getServerSession to access session data:
1// Server Component
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
6export default async function ServerProfilePage() {
7 const session = await getServerSession(authOptions);
8
9 if (!session) {
10 redirect('/auth/login?callbackUrl=/profile');
11 }
12
13 return (
14 <div>
15 <h1>Profile (rendered on the server)</h1>
16 <p>Hello, {session.user.name}!</p>
17 <p>Email: {session.user.email}</p>
18 {/* Content visible only to logged-in users */}
19 </div>
20 );
21}
22
23// Route Handler (API Route)
24import { getServerSession } from "next-auth/next";
25import { NextResponse } from "next/server";
26import { authOptions } from "@/app/api/auth/[...nextauth]/route";
27
28export async function GET() {
29 const session = await getServerSession(authOptions);
30
31 if (!session) {
32 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
33 }
34
35 // Fetch data specific to the logged-in user
36 const userData = await fetchUserData(session.user.id);
37
38 return NextResponse.json(userData);
39}Refresh tokens and OAuth access management
If we use OAuth providers, we may want to store and refresh access tokens to use the providers' APIs:
1export const authOptions = {
2 //
3 callbacks: {
4 async jwt({ token, user, account }) {
5 // On first login, save access/refresh tokens
6 if (account && user) {
7 token.accessToken = account.access_token;
8 token.refreshToken = account.refresh_token;
9 token.accessTokenExpires = account.expires_at * 1000; // Convert to milliseconds
10 token.userId = user.id;
11 return token;
12 }
13
14 // If the access token has not expired, return it
15 if (Date.now() < token.accessTokenExpires) {
16 return token;
17 }
18
19 // If the access token has expired, refresh it
20 return refreshAccessToken(token);
21 },
22
23 async session({ session, token }) {
24 // Pass the access token to the client (but not the refresh token!)
25 session.accessToken = token.accessToken;
26 session.user.id = token.userId;
27
28 return session;
29 },
30 },
31};
32
33async function refreshAccessToken(token) {
34 try {
35 // Example of refreshing a Google OAuth token
36 const url = "https://oauth2.googleapis.com/token";
37 const response = await fetch(url, {
38 headers: { "Content-Type": "application/x-www-form-urlencoded" },
39 method: "POST",
40 body: new URLSearchParams({
41 client_id: process.env.GOOGLE_CLIENT_ID,
42 client_secret: process.env.GOOGLE_CLIENT_SECRET,
43 grant_type: "refresh_token",
44 refresh_token: token.refreshToken,
45 }),
46 });
47
48 const refreshedTokens = await response.json();
49
50 if (!response.ok) {
51 throw refreshedTokens;
52 }
53
54 return {
55 ...token,
56 accessToken: refreshedTokens.access_token,
57 accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
58 // Keep the refresh token unless we received a new one
59 refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
60 };
61 } catch (error) {
62 console.error("Error refreshing access token", error);
63 // The refresh token may have expired, the user will need to log in again
64 return {
65 ...token,
66 error: "RefreshAccessTokenError",
67 };
68 }
69}Logging out and session invalidation
NextAuth.js provides the function signOut() for logging out:
1'use client';
2
3import { signOut } from 'next-auth/react';
4
5export function LogoutButton() {
6 return (
7 <button
8 onClick={() => signOut({ callbackUrl: '/' })}
9 className="bg-red-500 text-white px-4 py-2 rounded"
10 >
11 Log out
12 </button>
13 );
14}In the case of the database-based strategy, we can manually invalidate a session by removing it from the database:
1// app/api/auth/invalidate-sessions/route.ts
2import { NextResponse } from "next/server";
3import { getServerSession } from "next-auth/next";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5import { prisma } from "@/lib/prisma";
6
7export async function POST(request: Request) {
8 const session = await getServerSession(authOptions);
9
10 if (!session || session.user.role !== 'admin') {
11 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
12 }
13
14 const { userId } = await request.json();
15
16 // Remove all sessions of a user with the given ID
17 await prisma.session.deleteMany({
18 where: {
19 userId,
20 },
21 });
22
23 return NextResponse.json({ success: true });
24}In the case of the JWT strategy, this is more difficult because tokens are self-contained. However, we can implement a token "blacklist" or a secret rotation mechanism:
1// Blacklist implementation for the JWT token
2const revokedTokens = new Set();
3
4export const authOptions = {
5 //
6 callbacks: {
7 async jwt({ token }) {
8 // Check if the token is on the blacklist
9 if (revokedTokens.has(token.jti)) {
10 return null; // Token invalidated
11 }
12 return token;
13 },
14 },
15 events: {
16 async signOut({ token }) {
17 // Add the token to the blacklist on logout
18 if (token?.jti) {
19 revokedTokens.add(token.jti);
20 }
21 },
22 },
23};This simple solution only works for a single server. For multiple servers or serverless solutions, we would need to use a shared data store (e.g. Redis) to store the list of invalidated tokens.
Inactivity detection and automatic logout
We can implement a mechanism to detect user inactivity and automatic logout:
1'use client';
2
3import { useSession, signOut } from 'next-auth/react';
4import { useEffect, useState } from 'react';
5
6export function InactivityDetector({ timeoutMinutes = 30 }) {
7 const { data: session } = useSession();
8
9 useEffect(() => {
10 if (!session) return;
11
12 let timeout;
13 const timeoutMs = timeoutMinutes * 60 * 1000;
14
15 const resetTimeout = () => {
16 if (timeout) clearTimeout(timeout);
17 timeout = setTimeout(() => {
18 signOut({ callbackUrl: '/auth/login?timeout=true' });
19 }, timeoutMs);
20 };
21
22 // Reset the timer on every user activity
23 const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
24
25 // Set the initial timer
26 resetTimeout();
27
28 // Add event listeners
29 events.forEach(event => {
30 window.addEventListener(event, resetTimeout);
31 });
32
33 return () => {
34 // Clear timer and listeners on unmount
35 if (timeout) clearTimeout(timeout);
36 events.forEach(event => {
37 window.removeEventListener(event, resetTimeout);
38 });
39 };
40 }, [session, timeoutMinutes]);
41
42 // The component does not render anything visible
43 return null;
44}Then you can add this component to the main application layout:
1// app/layout.tsx
2import { Providers } from "./providers";
3import { InactivityDetector } from "@/components/InactivityDetector";
4
5export default function RootLayout({
6 children,
7}: {
8 children: React.ReactNode;
9}) {
10 return (
11 <html lang="pl">
12 <body>
13 <Providers>
14 {children}
15 <InactivityDetector timeoutMinutes={30} />
16 </Providers>
17 </body>
18 </html>
19 );
20}Handling multiple devices and sessions
If we use the database-based strategy, we can implement management of multiple sessions for the same user, allowing them to review active sessions and log out from specific devices:
1// app/settings/sessions/page.tsx
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5import { prisma } from "@/lib/prisma";
6import { SessionList } from "./SessionList";
7
8export default async function SessionsPage() {
9 const session = await getServerSession(authOptions);
10
11 if (!session) {
12 redirect('/auth/login?callbackUrl=/settings/sessions');
13 }
14
15 // Fetch all active user sessions
16 const userSessions = await prisma.session.findMany({
17 where: {
18 userId: session.user.id,
19 expires: {
20 gt: new Date(), // Only active sessions
21 },
22 },
23 orderBy: {
24 expires: 'desc',
25 },
26 });
27
28 // Mark the current session
29 const currentSessionToken = session.user.sessionToken; // You must adjust to have access to the token
30 const enhancedSessions = userSessions.map(s => ({
31 ...s,
32 isCurrent: s.sessionToken === currentSessionToken,
33 }));
34
35 return (
36 <div className="max-w-4xl mx-auto p-6">
37 <h1 className="text-2xl font-bold mb-6">Active sessions</h1>
38 <SessionList sessions={enhancedSessions} currentSessionId={session.user.id} />
39 </div>
40 );
41}
42
43// app/settings/sessions/SessionList.tsx (client component)
44'use client';
45
46import { useState } from 'react';
47import { useRouter } from 'next/navigation';
48
49export function SessionList({ sessions, currentSessionId }) {
50 const [isLoading, setIsLoading] = useState(false);
51 const [error, setError] = useState('');
52 const router = useRouter();
53
54 const handleRevokeSession = async (sessionToken) => {
55 if (confirm('Are you sure you want to log out this device?')) {
56 setIsLoading(true);
57 setError('');
58
59 try {
60 const response = await fetch('/api/auth/revoke-session', {
61 method: 'POST',
62 headers: {
63 'Content-Type': 'application/json',
64 },
65 body: JSON.stringify({ sessionToken }),
66 });
67
68 if (!response.ok) {
69 throw new Error('Failed to invalidate session');
70 }
71
72 // Refresh the page
73 router.refresh();
74 } catch (e) {
75 setError(e.message);
76 } finally {
77 setIsLoading(false);
78 }
79 }
80 };
81
82 const formatDate = (dateString) => {
83 return new Date(dateString).toLocaleString();
84 };
85
86 return (
87 <div>
88 {error && (
89 <div className="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg">
90 {error}
91 </div>
92 )}
93
94 <div className="overflow-x-auto">
95 <table className="min-w-full divide-y divide-gray-200">
96 <thead className="bg-gray-50">
97 <tr>
98 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Device</th>
99 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last activity</th>
100 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expires</th>
101 <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
102 </tr>
103 </thead>
104 <tbody className="bg-white divide-y divide-gray-200">
105 {sessions.map((session) => (
106 <tr key={session.id}>
107 <td className="px-6 py-4 whitespace-nowrap">
108 <div className="flex items-center">
109 <div className="ml-4">
110 <div className="text-sm font-medium text-gray-900">
111 {session.isCurrent ? 'Current device' : 'Other device'}
112 </div>
113 </div>
114 </div>
115 </td>
116 <td className="px-6 py-4 whitespace-nowrap">
117 <div className="text-sm text-gray-900">{formatDate(session.updatedAt)}</div>
118 </td>
119 <td className="px-6 py-4 whitespace-nowrap">
120 <div className="text-sm text-gray-900">{formatDate(session.expires)}</div>
121 </td>
122 <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
123 <button
124 onClick={() => handleRevokeSession(session.sessionToken)}
125 disabled={isLoading || session.isCurrent}
126 className={`text-red-600 hover:text-red-900 ${(isLoading || session.isCurrent) ? 'opacity-50 cursor-not-allowed' : ''}`}
127 >
128 {session.isCurrent ? 'Current session' : 'Log out'}
129 </button>
130 </td>
131 </tr>
132 ))}
133 </tbody>
134 </table>
135 </div>
136 </div>
137 );
138}
139
140// app/api/auth/revoke-session/route.ts
141import { NextResponse } from "next/server";
142import { getServerSession } from "next-auth/next";
143import { authOptions } from "@/app/api/auth/[...nextauth]/route";
144import { prisma } from "@/lib/prisma";
145
146export async function POST(request: Request) {
147 const session = await getServerSession(authOptions);
148
149 if (!session) {
150 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
151 }
152
153 const { sessionToken } = await request.json();
154
155 // Find the session, making sure it belongs to the logged-in user
156 const sessionToRevoke = await prisma.session.findFirst({
157 where: {
158 sessionToken,
159 userId: session.user.id,
160 },
161 });
162
163 if (!sessionToRevoke) {
164 return NextResponse.json(
165 { error: "Session not found or not owned by current user" },
166 { status: 404 }
167 );
168 }
169
170 // Remove the session
171 await prisma.session.delete({
172 where: {
173 id: sessionToRevoke.id,
174 },
175 });
176
177 return NextResponse.json({ success: true });
178}Token and session security
When implementing session and token management, keep in mind several key security aspects:
Use strong secrets - ensure that
NEXTAUTH_SECRETis a long, random string of charactersSecure cookies - use flags:
HttpOnly: prevents JavaScript access to the cookieSecure: requires HTTPSSameSite: protects against CSRF attacks
Set appropriate lifetime - tokens/sessions should not be valid for too long a period
Limit data in the token - the JWT token should contain minimum necessary information
Implement session revocation - allow users to log out from all devices
Monitor suspicious activity - detect repeated login attempts and unusual access patterns
Secret rotation - periodically change secrets used to sign tokens
Session and token tests
Tests are a key element in ensuring the reliability of session and token mechanisms. Here is an example unit test for token refresh functionality:
1// __tests__/refreshToken.test.ts
2import { refreshAccessToken } from '@/lib/auth';
3import fetchMock from 'jest-fetch-mock';
4
5fetchMock.enableMocks();
6
7beforeEach(() => {
8 fetchMock.resetMocks();
9});
10
11describe('refreshAccessToken', () => {
12 it('should refresh the token successfully', async () => {
13 // Prepare a mock response from the OAuth server
14 fetchMock.mockResponseOnce(JSON.stringify({
15 access_token: 'new_access_token',
16 expires_in: 3600
17 }));
18
19 const originalToken = {
20 accessToken: 'old_access_token',
21 refreshToken: 'refresh_token',
22 accessTokenExpires: Date.now() - 1000, // Expired token
23 };
24
25 const refreshedToken = await refreshAccessToken(originalToken);
26
27 expect(refreshedToken.accessToken).toBe('new_access_token');
28 expect(refreshedToken.refreshToken).toBe('refresh_token'); // Should keep the old refresh token
29 expect(refreshedToken.accessTokenExpires).toBeGreaterThan(Date.now());
30 expect(fetchMock).toHaveBeenCalledTimes(1);
31 });
32
33 it('should handle refresh token errors', async () => {
34 // Simulate refresh error
35 fetchMock.mockRejectOnce(new Error('Failed to refresh token'));
36
37 const originalToken = {
38 accessToken: 'old_access_token',
39 refreshToken: 'refresh_token',
40 accessTokenExpires: Date.now() - 1000,
41 };
42
43 const result = await refreshAccessToken(originalToken);
44
45 expect(result.error).toBe('RefreshAccessTokenError');
46 expect(result.accessToken).toBe('old_access_token'); // Should keep the old token
47 });
48});Good practices in the context of Next.js App Router
In the Next.js App Router architecture, it is important to follow these principles when implementing authentication:
- Use middleware to protect entire directories or route groups:
1// middleware.ts
2import { NextResponse } from "next/server";
3import type { NextRequest } from "next/server";
4import { getToken } from "next-auth/jwt";
5
6export async function middleware(request: NextRequest) {
7 const token = await getToken({ req: request });
8
9 // Check if the user has access to the requested page
10 if (!token) {
11 const url = new URL('/auth/login', request.url);
12 url.searchParams.set('callbackUrl', request.nextUrl.pathname);
13 return NextResponse.redirect(url);
14 }
15
16 // Checking permissions based on path and role
17 if (request.nextUrl.pathname.startsWith('/admin') && token.role !== 'admin') {
18 return NextResponse.redirect(new URL('/dashboard', request.url));
19 }
20
21 return NextResponse.next();
22}
23
24export const config = {
25 matcher: [
26 '/dashboard/:path*',
27 '/settings/:path*',
28 '/admin/:path*',
29 '/api/protected/:path*'
30 ],
31};- Use Server Components where possible, to reduce the amount of client-side JavaScript code and improve the security of sensitive operations:
1// app/admin/dashboard/page.tsx (Server Component)
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
6export default async function AdminDashboard() {
7 const session = await getServerSession(authOptions);
8
9 if (!session || session.user.role !== 'admin') {
10 redirect('/');
11 }
12
13 // Fetch administrative data directly on the server
14 // without exposing API keys or other sensitive information to the client
15 const adminData = await fetchAdminData();
16
17 return (
18 <div>
19 <h1>Administrator panel</h1>
20 {/* Render administrator data */}
21 </div>
22 );
23}- Use Route Handlers to secure API:
1// app/api/protected/user-data/route.ts
2import { getServerSession } from "next-auth/next";
3import { NextResponse } from "next/server";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
6export async function GET() {
7 const session = await getServerSession(authOptions);
8
9 if (!session) {
10 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
11 }
12
13 // Perform protected operations
14 const userData = await fetchUserData(session.user.id);
15
16 // Return data without sensitive information
17 const sanitizedData = sanitizeUserData(userData);
18
19 return NextResponse.json(sanitizedData);
20}Summary
In this module, we discussed in detail how to manage user sessions and tokens in Next.js applications:
- We learned about different session strategies in NextAuth.js - JWT and database-based
- We learned how to extend tokens and sessions with our own data
- We understood how to secure cookies and configure session parameters
- We learned about token refresh mechanisms for OAuth providers
- We implemented multi-session management and logout from specific devices
- We discussed best security practices for tokens and sessions
Proper session and token management is a key aspect of web application security. A well-designed authentication system not only protects user data but also ensures a good user experience, allowing easy login, logout, and access management from different devices.
In the next module, we will deal with implementing a system of roles and permissions that will allow for more granular management of user access to different application features.
Code for this lesson: App.tsx
1import React, { useState, useEffect } from 'react';
2
3// Demo: User sessions and token management
4interface SessionInfo {
5 id: string;
6 createdAt: string;
7 ipAddress: string;
8 userAgent: string;
9}
10
11function SessionTokenDemo() {
12 const [activeTab, setActiveTab] = useState<'session' | 'jwt' | 'refresh'>('session');
13 const [sessions, setSessions] = useState<SessionInfo[]>([]);
14 const [jwtPayload, setJwtPayload] = useState<any>(null);
15 const [tokenExpiry, setTokenExpiry] = useState(100);
16 const [isRefreshing, setIsRefreshing] = useState(false);
17
18 useEffect(() => {
19 if (jwtPayload && tokenExpiry > 0) {
20 const timer = setInterval(() => {
21 setTokenExpiry(prev => prev <= 1 ? 0 : prev - 1);
22 }, 100);
23 return () => clearInterval(timer);
24 }
25 }, [jwtPayload, tokenExpiry]);
26
27 const createSession = () => {
28 const session: SessionInfo = {
29 id: 'ses_' + Math.random().toString(36).substr(2, 9),
30 createdAt: new Date().toLocaleTimeString(),
31 ipAddress: '192.168.1.' + Math.floor(Math.random() * 255),
32 userAgent: ['Chrome/120', 'Firefox/121', 'Safari/17'][Math.floor(Math.random() * 3)],
33 };
34 setSessions(prev => [...prev, session]);
35 };
36
37 const generateJWT = () => {
38 const now = Math.floor(Date.now() / 1000);
39 setJwtPayload({ sub: 'user_001', email: 'jan@quantum.city', role: 'admin', iat: now, exp: now + 3600 });
40 setTokenExpiry(100);
41 };
42
43 const refreshToken = async () => {
44 setIsRefreshing(true);
45 await new Promise(r => setTimeout(r, 800));
46 const now = Math.floor(Date.now() / 1000);
47 setJwtPayload((prev: any) => prev ? { ...prev, iat: now, exp: now + 3600 } : null);
48 setTokenExpiry(100);
49 setIsRefreshing(false);
50 };
51
52 return (
53 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
54 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Sessions and Tokens</h1>
55 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Managing user sessions and JWT tokens</p>
56
57 <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
58 {[{ id: 'session' as const, label: 'Sessions' }, { id: 'jwt' as const, label: 'JWT' }, { id: 'refresh' as const, label: 'Refresh' }].map(tab => (
59 <button key={tab.id} onClick={() => setActiveTab(tab.id)} style={{
60 padding: '10px 20px', borderRadius: '8px', border: 'none', cursor: 'pointer',
61 background: activeTab === tab.id ? '#64ffda' : '#1a1a2e', color: activeTab === tab.id ? '#0f0f23' : '#8892b0', fontWeight: 'bold',
62 }}>
63 {tab.label}
64 </button>
65 ))}
66 </div>
67
68 {activeTab === 'session' && (
69 <div>
70 <button onClick={createSession} style={{ padding: '10px 20px', borderRadius: '8px', background: '#7c3aed', color: '#fff', border: 'none', cursor: 'pointer', marginBottom: '16px' }}>+ Create session</button>
71 {sessions.map(s => (
72 <div key={s.id} style={{ background: '#1a1a2e', padding: '12px', borderRadius: '8px', border: '1px solid #333', marginBottom: '8px', display: 'flex', justifyContent: 'space-between' }}>
73 <div>
74 <code style={{ color: '#64ffda' }}>{s.id}</code>
75 <div style={{ fontSize: '12px', color: '#8892b0' }}>IP: {s.ipAddress} | UA: {s.userAgent} | {s.createdAt}</div>
76 </div>
77 <button onClick={() => setSessions(prev => prev.filter(x => x.id !== s.id))} style={{ background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', padding: '4px 8px', cursor: 'pointer', fontSize: '12px' }}>Revoke</button>
78 </div>
79 ))}
80 {sessions.length === 0 && <p style={{ color: '#555' }}>No sessions</p>}
81 </div>
82 )}
83
84 {activeTab === 'jwt' && (
85 <div>
86 <button onClick={generateJWT} style={{ padding: '10px 20px', borderRadius: '8px', background: '#f59e0b', color: '#0f0f23', border: 'none', cursor: 'pointer', marginBottom: '16px', fontWeight: 'bold' }}>Generate JWT</button>
87 {jwtPayload && (
88 <div style={{ background: '#1a1a2e', padding: '16px', borderRadius: '8px', border: '1px solid #333' }}>
89 <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
90 <span>Token:</span>
91 <div style={{ flex: 1, height: '8px', background: '#333', borderRadius: '4px', overflow: 'hidden' }}>
92 <div style={{ width: tokenExpiry + '%', height: '100%', background: tokenExpiry > 30 ? '#10b981' : tokenExpiry > 10 ? '#f59e0b' : '#ef4444', transition: 'width 0.1s' }} />
93 </div>
94 <span style={{ color: tokenExpiry > 30 ? '#10b981' : '#ef4444' }}>{tokenExpiry}%</span>
95 </div>
96 {tokenExpiry === 0 && <div style={{ color: '#ef4444', fontWeight: 'bold', marginBottom: '8px' }}>Token expired!</div>}
97 <pre style={{ background: '#0f0f23', padding: '12px', borderRadius: '6px', fontSize: '13px', overflow: 'auto' }}>{JSON.stringify(jwtPayload, null, 2)}</pre>
98 </div>
99 )}
100 </div>
101 )}
102
103 {activeTab === 'refresh' && (
104 <div style={{ background: '#1a1a2e', padding: '24px', borderRadius: '8px', border: '1px solid #333' }}>
105 <h3 style={{ color: '#64ffda', marginTop: 0 }}>Refresh Token Flow</h3>
106 {['Access token expires', 'Client sends refresh token', 'Server verifies refresh token', 'New access + refresh token'].map((step, i) => (
107 <div key={i} style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
108 <div style={{ width: '32px', height: '32px', borderRadius: '50%', background: '#7c3aed', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px', fontWeight: 'bold', flexShrink: 0 }}>{i + 1}</div>
109 <span>{step}</span>
110 </div>
111 ))}
112 <button onClick={refreshToken} disabled={isRefreshing} style={{ marginTop: '12px', padding: '10px 20px', borderRadius: '8px', background: isRefreshing ? '#333' : '#7c3aed', color: '#fff', border: 'none', cursor: isRefreshing ? 'default' : 'pointer' }}>
113 {isRefreshing ? 'Refreshing...' : 'Simulate Refresh'}
114 </button>
115 </div>
116 )}
117 </div>
118 );
119}
120
121export default SessionTokenDemo;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. OAuth 2.0 is:
2. PKCE (Proof Key for Code Exchange) is used in:
Hands-on tasks in the game
- Code editor
Create a ProtectedRoute component using useSession to guard against unauthorized access
- Code editor
Create a credentials provider in NextAuth with email/password authentication and bcrypt hashing