All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 12s
202 lines
5.7 KiB
JavaScript
202 lines
5.7 KiB
JavaScript
const MischLabsAuth = (() => {
|
|
const TOKEN_KEY = 'mischlabs.dashboard.tokens';
|
|
const PKCE_KEY = 'mischlabs.dashboard.pkce';
|
|
const CONFIG_KEY = 'mischlabs.dashboard.authConfig';
|
|
const SCOPES = 'openid profile email';
|
|
|
|
let configPromise = null;
|
|
let cachedConfig = null;
|
|
|
|
function base64UrlEncode(buffer) {
|
|
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_')
|
|
.replace(/=+$/g, '');
|
|
}
|
|
|
|
function randomString(length = 64) {
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
const bytes = new Uint8Array(length);
|
|
crypto.getRandomValues(bytes);
|
|
return [...bytes].map((byte) => chars[byte % chars.length]).join('');
|
|
}
|
|
|
|
async function createChallenge(verifier) {
|
|
const data = new TextEncoder().encode(verifier);
|
|
const digest = await crypto.subtle.digest('SHA-256', data);
|
|
return base64UrlEncode(digest);
|
|
}
|
|
|
|
async function getConfig() {
|
|
if (cachedConfig) return cachedConfig;
|
|
if (!configPromise) {
|
|
configPromise = fetch('/api/auth/config', { cache: 'no-store' })
|
|
.then((response) => {
|
|
if (!response.ok) throw new Error(`Auth config returned ${response.status}`);
|
|
return response.json();
|
|
})
|
|
.then((config) => {
|
|
cachedConfig = config;
|
|
sessionStorage.setItem(CONFIG_KEY, JSON.stringify(config));
|
|
return config;
|
|
})
|
|
.catch((error) => {
|
|
const fallback = JSON.parse(sessionStorage.getItem(CONFIG_KEY) || 'null');
|
|
if (fallback) return fallback;
|
|
throw error;
|
|
});
|
|
}
|
|
return configPromise;
|
|
}
|
|
|
|
function redirectUri() {
|
|
const path = window.location.pathname === '/index.html' ? '/' : window.location.pathname;
|
|
return `${window.location.origin}${path}`;
|
|
}
|
|
|
|
function decodeJwt(token) {
|
|
const payload = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
const json = decodeURIComponent(atob(payload).split('').map((char) => (
|
|
`%${(`00${char.charCodeAt(0).toString(16)}`).slice(-2)}`
|
|
)).join(''));
|
|
return JSON.parse(json);
|
|
}
|
|
|
|
function tokens() {
|
|
try {
|
|
const raw = sessionStorage.getItem(TOKEN_KEY);
|
|
return raw ? JSON.parse(raw) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function storeTokens(nextTokens) {
|
|
sessionStorage.setItem(TOKEN_KEY, JSON.stringify(nextTokens));
|
|
}
|
|
|
|
function clearTokens() {
|
|
sessionStorage.removeItem(TOKEN_KEY);
|
|
sessionStorage.removeItem(PKCE_KEY);
|
|
}
|
|
|
|
function isAuthenticated() {
|
|
const current = tokens();
|
|
if (!current?.id_token) return false;
|
|
try {
|
|
return Date.now() < decodeJwt(current.id_token).exp * 1000;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function claims() {
|
|
const current = tokens();
|
|
if (!current?.id_token) return null;
|
|
try {
|
|
return decodeJwt(current.id_token);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function authHeader() {
|
|
const current = tokens();
|
|
return current?.id_token ? { Authorization: `Bearer ${current.id_token}` } : {};
|
|
}
|
|
|
|
async function login() {
|
|
const config = await getConfig();
|
|
const verifier = randomString();
|
|
const challenge = await createChallenge(verifier);
|
|
const state = randomString(32);
|
|
const redirect = redirectUri();
|
|
sessionStorage.setItem(PKCE_KEY, JSON.stringify({ verifier, state, redirect }));
|
|
|
|
const params = new URLSearchParams({
|
|
client_id: config.clientId,
|
|
redirect_uri: redirect,
|
|
response_type: 'code',
|
|
scope: config.scopes || SCOPES,
|
|
state,
|
|
code_challenge: challenge,
|
|
code_challenge_method: 'S256'
|
|
});
|
|
|
|
window.location.href = `${config.authority}/protocol/openid-connect/auth?${params}`;
|
|
}
|
|
|
|
async function handleCallback() {
|
|
const url = new URL(window.location.href);
|
|
const code = url.searchParams.get('code');
|
|
const state = url.searchParams.get('state');
|
|
const error = url.searchParams.get('error');
|
|
|
|
if (error) {
|
|
window.history.replaceState({}, document.title, redirectUri());
|
|
throw new Error(`SSO Fehler: ${error}`);
|
|
}
|
|
|
|
if (!code) return false;
|
|
|
|
const config = await getConfig();
|
|
const stored = JSON.parse(sessionStorage.getItem(PKCE_KEY) || '{}');
|
|
if (!stored.verifier || stored.state !== state) {
|
|
throw new Error('SSO Antwort konnte nicht verifiziert werden.');
|
|
}
|
|
|
|
const body = new URLSearchParams({
|
|
grant_type: 'authorization_code',
|
|
client_id: config.clientId,
|
|
redirect_uri: stored.redirect || redirectUri(),
|
|
code,
|
|
code_verifier: stored.verifier
|
|
});
|
|
|
|
const response = await fetch(`${config.authority}/protocol/openid-connect/token`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Token endpoint returned ${response.status}`);
|
|
}
|
|
|
|
storeTokens(await response.json());
|
|
sessionStorage.removeItem(PKCE_KEY);
|
|
window.history.replaceState({}, document.title, redirectUri());
|
|
return true;
|
|
}
|
|
|
|
async function logout() {
|
|
const config = await getConfig();
|
|
const idToken = tokens()?.id_token;
|
|
clearTokens();
|
|
const params = new URLSearchParams({
|
|
client_id: config.clientId,
|
|
post_logout_redirect_uri: redirectUri()
|
|
});
|
|
if (idToken) params.set('id_token_hint', idToken);
|
|
window.location.href = `${config.authority}/protocol/openid-connect/logout?${params}`;
|
|
}
|
|
|
|
function displayName() {
|
|
const currentClaims = claims();
|
|
return currentClaims?.name || currentClaims?.preferred_username || currentClaims?.email || 'Angemeldet';
|
|
}
|
|
|
|
return {
|
|
authHeader,
|
|
claims,
|
|
clearTokens,
|
|
displayName,
|
|
getConfig,
|
|
handleCallback,
|
|
isAuthenticated,
|
|
login,
|
|
logout,
|
|
tokens
|
|
};
|
|
})();
|