Universal OAuth 2.1 client SDK for Evup Auth with automatic token refresh, PKCE, and multi-framework support
npm install @evup/auth-clientProduction-ready OAuth 2.1 authentication for Next.js with automatic token refresh, PKCE, and backchannel logout support.


- 2 Files to Auth - Create config + one route file, you're done
- Automatic Token Refresh - Tokens refresh 5 minutes before expiry
- Backchannel Logout - Logout from auth server logs out all client apps
- OAuth 2.1 Compliant - PKCE, state validation, secure token handling
- TypeScript Native - Full type safety out of the box
- Zero UI Lock-in - Bring your own components
``bash`
npm install @evup/auth-client
Create a .env.local file in your project root:
`bashOAuth Client Credentials (get from auth server admin panel)
EVUP_CLIENT_ID=your_client_id
EVUP_CLIENT_SECRET=your_client_secret
Getting Client Credentials:
1. Go to your Evup Auth admin panel:
https://auth.yourapp.com/admin/oauth-clients
2. Click "Create OAuth Client"
3. Configure redirect URIs:
- Redirect URI: http://localhost:3000/api/auth/callback
- Post-logout Redirect URI: http://localhost:3000 (optional)
- Backchannel Logout URI: http://localhost:3000/api/auth/backchannel-logout (optional, for automatic logout)
4. Save and copy your client_id and client_secret (shown only once)$3
`typescript
// evup-auth.config.ts
import { initializeAuth } from '@evup/auth-client/nextjs';export const authConfig = {
clientId: process.env.EVUP_CLIENT_ID!,
clientSecret: process.env.EVUP_CLIENT_SECRET!,
issuer: process.env.EVUP_ISSUER!,
appUrl: process.env.NEXT_PUBLIC_APP_URL!,
scopes: ['openid', 'profile', 'email', 'offline_access'],
};
initializeAuth(authConfig);
`$3
`typescript
// app/api/auth/[...all]/route.ts
import { createCatchAllHandler } from '@evup/auth-client/nextjs';
import { authConfig } from '@/evup-auth.config';const handler = createCatchAllHandler(authConfig);
export { handler as GET, handler as POST };
`This single file handles:
-
GET /api/auth/login - Initiate login
- GET /api/auth/callback - Handle OAuth callback
- POST /api/auth/logout - User logout
- POST /api/auth/backchannel-logout - Receive logout notifications from auth server$3
Server Component:
`tsx
// app/page.tsx
import { getUser } from '@evup/auth-client/nextjs';export default async function HomePage() {
const user = await getUser();
if (!user) {
return Sign in;
}
return (
Hello, {user.name}
Email: {user.email}
);
}
`Protected Route:
`tsx
// app/dashboard/page.tsx
import { requireAuth } from '@evup/auth-client/nextjs';export default async function DashboardPage() {
const user = await requireAuth(); // Redirects to login if not authenticated
return
Welcome, {user.name}!;
}
`Client Component:
`tsx
// components/auth-buttons.tsx
'use client';import { useAuth } from '@evup/auth-client/nextjs';
export function AuthButtons() {
const { login, logout } = useAuth();
return (
<>
>
);
}
`API Reference
$3
`typescript
import { getUser, requireAuth, getSession } from '@evup/auth-client/nextjs';
`####
getUser()Get current user, returns
null if not authenticated.`typescript
const user = await getUser();
// user: { id, email, name, image?, roles?, permissions? } | null
`####
requireAuth()Require authentication, redirects to login if not authenticated.
`typescript
const user = await requireAuth(); // Never returns null
`####
getSession()Get full session including tokens.
`typescript
const session = await getSession();
// session: { accessToken, refreshToken?, idToken, expiresAt, user } | null
`####
getUserFromToken(token, config)Verify a Bearer token and get user info. Useful for API routes.
`typescript
import { getUserFromToken } from '@evup/auth-client/nextjs';
import { authConfig } from '@/evup-auth.config';const token = request.headers.get('Authorization')?.replace('Bearer ', '');
const user = await getUserFromToken(token!, authConfig);
`$3
`typescript
import { authClient, useAuth } from '@evup/auth-client/nextjs';
`####
authClient.login(options?)Navigate to login page.
`typescript
authClient.login(); // Simple login
authClient.login({ redirectTo: '/dashboard' }); // With redirect
authClient.login({ prompt: 'login' }); // Force re-auth (account switching)
`####
authClient.logout(options?)Log out and redirect.
`typescript
authClient.logout(); // Simple logout
authClient.logout({ redirectTo: '/' }); // With redirect
`####
useAuth()React hook for authentication actions.
`typescript
const { login, logout } = useAuth();
`$3
`typescript
import { createCatchAllHandler, createAuthHandlers } from '@evup/auth-client/nextjs';
`####
createCatchAllHandler(config) - RecommendedCreates a single handler for all auth routes (requires Next.js 15+).
`typescript
// app/api/auth/[...all]/route.ts
const handler = createCatchAllHandler(authConfig);
export { handler as GET, handler as POST };
`Handles:
-
GET /api/auth/login
- GET /api/auth/callback
- POST /api/auth/logout
- POST /api/auth/backchannel-logout####
createAuthHandlers(config) - AdvancedFor individual route handlers (Next.js 14+):
`typescript
const { loginHandler, callbackHandler, logoutHandler, backchannelLogoutHandler } =
createAuthHandlers(authConfig);
`Configuration Options
`typescript
interface EvupAuthConfig {
clientId: string; // OAuth client ID
clientSecret: string; // OAuth client secret
issuer: string; // Auth server URL
appUrl: string; // Your app URL
scopes?: string[]; // Default: ['openid', 'profile', 'email'] redirects?: {
afterLogin?: string; // Default: '/'
afterLogout?: string; // Default: '/'
onError?: string; // Default: '/auth/error'
};
defaultPrompt?: 'login' | 'select_account' | 'consent';
// Control login behavior:
// - undefined: Use SSO session if available (default)
// - 'login': Always force re-authentication
// - 'select_account': Show account picker
// - 'consent': Always show consent screen
}
`Features
$3
Tokens are automatically refreshed 5 minutes before expiration when you call
getUser() or getSession().Requirements:
- Include
offline_access in scopes
- User must have a valid refresh token`typescript
scopes: ['openid', 'profile', 'email', 'offline_access'] // ← Required
`If refresh fails, the session is cleared and user must re-login.
$3
When a user logs out from the auth server, ALL client applications are automatically logged out via backchannel logout notifications.
How it works:
1. User clicks "Sign out" in any app (or from auth server dashboard)
2. Auth server sends signed JWT to each client's
backchannel_logout_uri
3. Client SDK validates JWT and revokes all user sessions
4. Next time user makes a request, they're automatically logged outSetup:
1. Configure Backchannel Logout URI when creating OAuth client:
`2. Use catch-all handler:
`typescript
// app/api/auth/[...all]/route.ts
const handler = createCatchAllHandler(authConfig);
export { handler as GET, handler as POST };
`3. That's it! The SDK handles everything automatically.
$3
The SDK validates the session on each request through middleware, ensuring:
- User profile data is always fresh
- Sessions are automatically invalidated when tokens are revoked
$3
Allow users to switch between accounts using the
prompt parameter:`tsx
'use client';import { useAuth } from '@evup/auth-client/nextjs';
export function SwitchAccountButton() {
const { login } = useAuth();
return (
);
}
`Prompt options:
-
prompt: 'login' - Force re-authentication (ignore SSO)
- prompt: 'select_account' - Show account picker
- prompt: 'consent' - Force consent screen$3
- PKCE (S256) - Authorization code interception protection
- State Parameter - CSRF protection during OAuth flow
- HttpOnly Cookies - Session tokens not accessible via JavaScript
- Secure Flag - Cookies only sent over HTTPS in production
- SameSite=Lax - Additional CSRF protection
- ID Token Verification - JWKS-based signature validation
- Token Revocation - Tokens revoked on logout (RFC 7009)
- Automatic Session Validation - Sessions validated on each request
$3
- Without
prompt: User automatically logged in if they have an active SSO session
- With prompt: 'login': User sees login page even if they have an SSO session
- On logout: Local session cleared, but SSO session remains (other apps stay logged in)
- Sessions are validated on each request - revoked tokens will be detected automaticallyCommon Use Cases
$3
`typescript
// app/api/protected/route.ts
import { getUserFromToken } from '@evup/auth-client/nextjs';
import { authConfig } from '@/evup-auth.config';export async function GET(request: Request) {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const user = await getUserFromToken(token, authConfig);
if (!user) {
return Response.json({ error: 'Invalid token' }, { status: 401 });
}
return Response.json({ user });
}
`$3
`tsx
`$3
`tsx
`$3
`tsx
// app/admin/page.tsx
import { requireAuth } from '@evup/auth-client/nextjs';export default async function AdminPage() {
const user = await requireAuth();
if (!user.roles?.includes('admin')) {
return
Access denied;
} return
Admin panel;
}
`Requirements
- Node.js 18+
- Next.js 14+ (individual handlers) or 15+ (catch-all handler)
Troubleshooting
$3
Add
initializeAuth(authConfig) to your config file:`typescript
// evup-auth.config.ts
import { initializeAuth } from '@evup/auth-client/nextjs';export const authConfig = { / ... / };
initializeAuth(authConfig); // ← Add this
`$3
Add
offline_access to scopes:`typescript
scopes: ['openid', 'profile', 'email', 'offline_access']
`$3
1. Check backchannel logout URI is configured in auth server
2. Verify URI is publicly accessible from auth server
3. Ensure client secret matches (used for JWT verification)
4. Check logs for JWT verification errors
$3
Restart TypeScript server:
Cmd+Shift+P → "TypeScript: Restart TS Server"$3
This is expected behavior in Next.js Server Components (cannot modify cookies). The session is marked as invalid and will be cleared on next user action. To force immediate clearing, implement Next.js middleware.
Advanced
$3
For Next.js 14 or custom routing:
`typescript
// app/api/auth/login/route.ts
import { createAuthHandlers } from '@evup/auth-client/nextjs';
import { authConfig } from '@/evup-auth.config';const { loginHandler } = createAuthHandlers(authConfig);
export async function GET(request: Request) {
return loginHandler(request);
}
``typescript
// app/api/auth/callback/route.ts
const { callbackHandler } = createAuthHandlers(authConfig);export async function GET(request: Request) {
return callbackHandler(request);
}
``typescript
// app/api/auth/logout/route.ts
const { logoutHandler } = createAuthHandlers(authConfig);export async function POST() {
return logoutHandler();
}
``typescript
// app/api/auth/backchannel-logout/route.ts
const { backchannelLogoutHandler } = createAuthHandlers(authConfig);export async function POST(request: Request) {
return backchannelLogoutHandler(request);
}
`$3
`typescript
import { OAuthError, TokenRefreshError } from '@evup/auth-client/nextjs';try {
const session = await getSession();
} catch (error) {
if (error instanceof TokenRefreshError) {
// Handle token refresh failure
console.error('Token expired, user needs to re-login');
} else if (error instanceof OAuthError) {
// Handle OAuth errors
console.error('OAuth error:', error.message);
}
}
`$3
For framework-agnostic use cases:
`typescript
import {
exchangeCodeForTokens,
getUserInfo,
verifyIdToken,
refreshAccessToken,
revokeToken,
} from '@evup/auth-client/core';
`Architecture
`
@evup/auth-client/
├── core/ # Framework-agnostic OAuth logic
│ └── oauth.ts # PKCE, token exchange, refresh, revoke
└── nextjs/ # Next.js App Router adapter
├── server.ts # getUser, requireAuth, getSession, refreshUser
├── client.ts # useAuth, authClient
├── routes.ts # Route handlers (login, callback, logout, refresh-user, validate-session)
└── middleware.ts # Automatic session validation
``Design Philosophy:
- Core logic is framework-agnostic
- Adapters wrap core logic with framework conventions
- Future-proof: Adding new frameworks only requires a new adapter
MIT - Copyright (c) 2026 Evup Team
- GitHub Issues
- Documentation
- Email: support@evup.vn