Authenticate browser media with durable HttpOnly login cookies

Adapted cookie, origin, URL validation and secret handling from MikaelKW/nodecast-tv-plus. Protect API roles, revoke logins across restart, preserve existing accounts, and isolate transcode sessions by account.

Co-authored-by: Mikael <mikaelkwessel@gmail.com>
This commit is contained in:
Technomancer702
2026-10-12 16:53:15 -07:00
parent 2999d6542f
commit 1f665dcb11
24 changed files with 553 additions and 111 deletions

View File

@ -1096,7 +1096,7 @@
<!-- JavaScript -->
<script src="/js/icons.js"></script>
<script src="/js/api.js?v=2"></script>
<script src="/js/api.js?v=auth-2"></script>
<script src="/js/components/VodDuration.js?v=1"></script>
<script src="/js/components/VideoPlayer.js?v=2"></script>
<script src="/js/components/ChannelList.js?v=3"></script>
@ -1109,21 +1109,8 @@
<script src="/js/pages/SeriesPage.js?v=1"></script>
<script src="/js/pages/Settings.js?v=2"></script>
<script src="/js/pages/WatchPage.js?v=2"></script>
<script src="/js/app.js?v=4"></script>
<script>
// Check for SSO token in URL
(function () {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
if (token) {
console.log('SSO Login successful, saving token...');
localStorage.setItem('authToken', token);
// Clean URL without reloading
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
}
})();
</script>
<script src="/js/app.js?v=5"></script>
</body>
</html>

View File

@ -9,17 +9,12 @@ const API = {
async request(method, endpoint, data = null) {
const options = {
method,
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
}
};
// Add authentication token if available
const token = localStorage.getItem('authToken');
if (token) {
options.headers['Authorization'] = `Bearer ${token}`;
}
if (data) {
options.body = JSON.stringify(data);
}
@ -40,7 +35,7 @@ const API = {
if (response.status === 401) {
localStorage.removeItem('authToken');
window.location.href = '/login.html';
return;
throw new Error('Authentication required');
}
throw new Error(result.error || `Server responded with ${response.status}`);
}

View File

@ -28,7 +28,7 @@ class App {
async init() {
// Check authentication first
await this.checkAuth();
if (!await this.checkAuth()) return;
// Mobile menu toggle
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
@ -167,19 +167,14 @@ class App {
async checkAuth() {
const token = localStorage.getItem('authToken');
if (!token) {
// No token, redirect to login (replace to avoid back button issues)
window.location.replace('/login.html');
return;
}
try {
// Verify token with server
const response = await fetch('/api/auth/me', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (token) {
const migrated = await fetch('/api/auth/session', { method: 'POST',
headers: { Authorization: `Bearer ${token}` } });
localStorage.removeItem('authToken');
if (!migrated.ok) throw new Error('Please sign in again');
}
const response = await fetch('/api/auth/me', { credentials: 'same-origin' });
if (!response.ok) {
throw new Error('Invalid token');
@ -197,11 +192,13 @@ class App {
// Add logout button to navbar
this.addLogoutButton();
return true;
} catch (err) {
console.error('Authentication error:', err);
localStorage.removeItem('authToken');
window.location.replace('/login.html');
return false;
}
}
@ -223,15 +220,7 @@ class App {
logoutLink.addEventListener('click', async (e) => {
e.preventDefault();
const token = localStorage.getItem('authToken');
if (token) {
await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
}
await fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' });
localStorage.removeItem('authToken');
window.location.replace('/login.html');

View File

@ -299,7 +299,7 @@
}
// Store token
localStorage.setItem('authToken', data.token);
localStorage.removeItem('authToken');
// Redirect to main app (replace history to prevent back button issues)
window.location.replace('/');