Vanilla JavaScript Integration Example
SSO Version: 5.24.0
Overview
This guide demonstrates OAuth 2.0 Authorization Code Flow integration using pure JavaScript without any framework dependencies. The frontend handles authorization redirect while the backend manages token exchange securely.
⚠️ Security Note: Never expose
client_secret in your frontend code. All token operations must happen on your backend server.1. HTML Structure
Basic HTML setup with login and user profile sections:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSO Integration Example</title>
<style>
.hidden { display: none; }
.loading { opacity: 0.5; pointer-events: none; }
.error { color: red; margin: 10px 0; }
.success { color: green; margin: 10px 0; }
</style>
</head>
<body>
<div id="app">
<!-- Loading State -->
<div id="loading" class="hidden">
<p>Loading...</p>
</div>
<!-- Login View -->
<div id="login-view" class="hidden">
<h1>Sign In</h1>
<button id="login-btn">Sign in with SSO</button>
</div>
<!-- Access Pending View -->
<div id="pending-view" class="hidden">
<h1>Access Pending</h1>
<p>Your access request is pending approval.</p>
<p>An administrator will review your request shortly.</p>
<button id="logout-btn-pending">Sign Out</button>
</div>
<!-- Access Denied View -->
<div id="denied-view" class="hidden">
<h1>Access Denied</h1>
<p>Your access to this application has been revoked.</p>
<button id="logout-btn-denied">Sign Out</button>
</div>
<!-- Dashboard View -->
<div id="dashboard-view" class="hidden">
<h1>Dashboard</h1>
<div id="user-info"></div>
<button id="logout-btn">Sign Out</button>
</div>
<!-- Error Message -->
<div id="error-message" class="error hidden"></div>
</div>
<script src="/js/auth.js"></script>
<script src="/js/app.js"></script>
</body>
</html>2. Auth Module (auth.js)
Core authentication logic for OAuth 2.0 flow:
// js/auth.js
// WHY: Configuration for SSO OAuth 2.0
const SSO_CONFIG = {
clientId: 'YOUR_CLIENT_ID_HERE', // Safe to expose
baseUrl: 'https://sso.doneisbetter.com',
redirectUri: window.location.origin + '/callback', // Adjust to your callback URL
scope: 'openid profile email'
};
// WHY: Generate random string for CSRF protection
function generateState() {
return Math.random().toString(36).substring(2) +
Math.random().toString(36).substring(2);
}
// WHY: Initiate OAuth 2.0 authorization flow
function login() {
const state = generateState();
sessionStorage.setItem('oauth_state', state);
const authUrl = new URL(SSO_CONFIG.baseUrl + '/api/oauth/authorize');
authUrl.searchParams.append('client_id', SSO_CONFIG.clientId);
authUrl.searchParams.append('redirect_uri', SSO_CONFIG.redirectUri);
authUrl.searchParams.append('response_type', 'code');
authUrl.searchParams.append('scope', SSO_CONFIG.scope);
authUrl.searchParams.append('state', state);
window.location.href = authUrl.toString();
}
// WHY: Check current session status with backend
async function checkSession() {
try {
const response = await fetch('/api/auth/session', {
method: 'GET',
credentials: 'include' // Include cookies
});
if (response.ok) {
const data = await response.json();
return {
isAuthenticated: true,
user: data.user,
permissionStatus: data.permissionStatus
};
} else {
return { isAuthenticated: false };
}
} catch (error) {
console.error('Session check failed:', error);
return { isAuthenticated: false };
}
}
// WHY: Clear session and logout
async function logout() {
try {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
// Optional: Also logout from SSO
window.location.href = SSO_CONFIG.baseUrl + '/api/public/logout';
} catch (error) {
console.error('Logout failed:', error);
window.location.href = '/';
}
}3. App Logic (app.js)
Main application logic and UI state management:
// js/app.js
// WHY: View management helper functions
function hideAllViews() {
document.querySelectorAll('#app > div').forEach(el => {
el.classList.add('hidden');
});
}
function showView(viewId) {
hideAllViews();
document.getElementById(viewId).classList.remove('hidden');
}
function showError(message) {
const errorEl = document.getElementById('error-message');
errorEl.textContent = message;
errorEl.classList.remove('hidden');
setTimeout(() => errorEl.classList.add('hidden'), 5000);
}
// WHY: Render user information in dashboard
function renderUserInfo(user) {
const userInfoEl = document.getElementById('user-info');
userInfoEl.innerHTML = `
<div>
<p><strong>Name:</strong> ${user.name}</p>
<p><strong>Email:</strong> ${user.email}</p>
<p><strong>Role:</strong> ${user.role}</p>
<p><strong>User ID:</strong> ${user.userId}</p>
</div>
`;
}
// WHY: Initialize app and check session on page load
async function initApp() {
showView('loading');
const session = await checkSession();
if (!session.isAuthenticated) {
showView('login-view');
return;
}
// WHY: Handle different permission statuses
switch (session.permissionStatus) {
case 'approved':
renderUserInfo(session.user);
showView('dashboard-view');
break;
case 'pending':
showView('pending-view');
break;
case 'revoked':
showView('denied-view');
break;
default:
showView('login-view');
}
}
// WHY: Attach event listeners
document.addEventListener('DOMContentLoaded', () => {
// Login button
document.getElementById('login-btn')?.addEventListener('click', login);
// Logout buttons
document.getElementById('logout-btn')?.addEventListener('click', logout);
document.getElementById('logout-btn-pending')?.addEventListener('click', logout);
document.getElementById('logout-btn-denied')?.addEventListener('click', logout);
// Initialize app
initApp();
});4. OAuth Callback Handler (Backend)
Server-side callback handler (Node.js/Express example):
// server.js or routes/auth.js
const express = require('express');
const jwt = require('jsonwebtoken');
const fetch = require('node-fetch');
const router = express.Router();
const SSO_CONFIG = {
clientId: process.env.SSO_CLIENT_ID,
clientSecret: process.env.SSO_CLIENT_SECRET,
redirectUri: process.env.SSO_REDIRECT_URI,
baseUrl: process.env.SSO_BASE_URL || 'https://sso.doneisbetter.com'
};
// WHY: OAuth callback endpoint
router.get('/api/auth/callback', async (req, res) => {
const { code, state } = req.query;
// WHY: Validate state parameter for CSRF protection
const expectedState = req.cookies.oauth_state;
if (state !== expectedState) {
return res.status(400).json({ error: 'Invalid state parameter' });
}
try {
// WHY: Exchange authorization code for tokens
const tokenResponse = await fetch(
`${SSO_CONFIG.baseUrl}/api/oauth/token`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
redirect_uri: SSO_CONFIG.redirectUri,
client_id: SSO_CONFIG.clientId,
client_secret: SSO_CONFIG.clientSecret
})
}
);
if (!tokenResponse.ok) {
const error = await tokenResponse.json();
return res.status(tokenResponse.status).json(error);
}
const tokens = await tokenResponse.json();
const { access_token, id_token, refresh_token } = tokens;
// WHY: Decode ID token to get user info
const decoded = jwt.decode(id_token);
const { sub, email, name, role, permissionStatus } = decoded;
// WHY: Store tokens in HTTP-only cookies
res.cookie('access_token', access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600000 // 1 hour
});
res.cookie('refresh_token', refresh_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 2592000000 // 30 days
});
res.cookie('id_token', id_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600000
});
// WHY: Redirect based on permission status
if (permissionStatus === 'pending') {
return res.redirect('/?status=pending');
} else if (permissionStatus === 'revoked') {
return res.redirect('/?status=denied');
}
res.redirect('/');
} catch (error) {
console.error('OAuth callback error:', error);
res.status(500).json({ error: 'Authentication failed' });
}
});
// WHY: Session validation endpoint
router.get('/api/auth/session', (req, res) => {
const { id_token, access_token } = req.cookies;
if (!id_token || !access_token) {
return res.status(401).json({ error: 'Not authenticated' });
}
try {
const decoded = jwt.decode(id_token);
const { sub, email, name, role, permissionStatus } = decoded;
if (decoded.exp * 1000 < Date.now()) {
return res.status(401).json({ error: 'Token expired' });
}
res.json({
user: { userId: sub, email, name, role },
permissionStatus
});
} catch (error) {
console.error('Session validation error:', error);
res.status(500).json({ error: 'Session validation failed' });
}
});
// WHY: Logout endpoint
router.post('/api/auth/logout', (req, res) => {
res.clearCookie('access_token');
res.clearCookie('refresh_token');
res.clearCookie('id_token');
res.json({ success: true });
});
module.exports = router;5. Environment Configuration
Backend environment variables:
# .env (Backend - NEVER commit this file) SSO_CLIENT_ID=your_client_id_here SSO_CLIENT_SECRET=your_client_secret_here SSO_REDIRECT_URI=https://yourapp.com/api/auth/callback SSO_BASE_URL=https://sso.doneisbetter.com NODE_ENV=production SESSION_SECRET=your_random_secret_here
6. Token Refresh (Optional)
Implement automatic token refresh:
// Add to auth.js
// WHY: Refresh access token before expiry
async function refreshToken() {
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
return { success: true };
} else {
// Token refresh failed, redirect to login
window.location.href = '/';
return { success: false };
}
} catch (error) {
console.error('Token refresh failed:', error);
return { success: false };
}
}
// Add to server
router.post('/api/auth/refresh', async (req, res) => {
const { refresh_token } = req.cookies;
if (!refresh_token) {
return res.status(401).json({ error: 'No refresh token' });
}
try {
const tokenResponse = await fetch(
`${SSO_CONFIG.baseUrl}/api/oauth/token`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token,
client_id: SSO_CONFIG.clientId,
client_secret: SSO_CONFIG.clientSecret
})
}
);
if (!tokenResponse.ok) {
return res.status(401).json({ error: 'Token refresh failed' });
}
const tokens = await tokenResponse.json();
const { access_token, id_token } = tokens;
res.cookie('access_token', access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600000
});
res.cookie('id_token', id_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 3600000
});
res.json({ success: true });
} catch (error) {
console.error('Token refresh error:', error);
res.status(500).json({ error: 'Token refresh failed' });
}
});Summary
- ✅ Pure JavaScript OAuth 2.0 implementation (no frameworks)
- ✅ Secure token handling with HTTP-only cookies
- ✅ CSRF protection with state parameter
- ✅ App permission status handling (pending/approved/revoked)
- ✅ Clean separation of frontend and backend concerns
- ✅ Token refresh capability
🔗 Next Steps:
- Review Authentication Flow for detailed OAuth 2.0 explanation
- Check App Permissions to understand permission lifecycle
- See API Reference for complete endpoint documentation