Fix safe login submission, mobile fullscreen, and OIDC discovery

This commit is contained in:
Technomancer702
2026-10-13 18:31:48 -07:00
parent b85b160013
commit ce122eb34b
16 changed files with 479 additions and 72 deletions

View File

@ -0,0 +1,74 @@
const MAX_DOCUMENT_BYTES = 128 * 1024;
function validateUrl(value, name, allowHttp) {
if (typeof value !== 'string' || !value || /[\x00-\x20\x7f]/.test(value)) throw new Error('Invalid OIDC ' + name);
let parsed;
try { parsed = new URL(value); } catch { throw new Error('Invalid OIDC ' + name); }
if (!(parsed.protocol === 'https:' || (allowHttp && parsed.protocol === 'http:')) ||
parsed.username || parsed.password || parsed.hash) throw new Error('Invalid OIDC ' + name);
return parsed;
}
async function readDocument(response) {
const length = Number(response.headers.get('content-length'));
if (length > MAX_DOCUMENT_BYTES) {
await response.body?.cancel();
throw new Error('OIDC discovery document is too large');
}
if (!response.body) throw new Error('OIDC discovery document is empty');
const reader = response.body.getReader();
const chunks = [];
let size = 0;
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
size += value.length;
if (size > MAX_DOCUMENT_BYTES) {
await reader.cancel();
throw new Error('OIDC discovery document is too large');
}
chunks.push(Buffer.from(value));
}
} finally { reader.releaseLock(); }
try { return JSON.parse(Buffer.concat(chunks, size).toString('utf8')); }
catch { throw new Error('OIDC discovery document is not valid JSON'); }
}
async function discoverOidcEndpoints(env, { fetchImpl = fetch, timeoutMs = 5000 } = {}) {
const issuer = env.OIDC_ISSUER_URL;
const issuerUrl = validateUrl(issuer, 'issuer URL', true);
if (issuerUrl.search) throw new Error('OIDC issuer URL must not contain a query');
const allowHttp = issuerUrl.protocol === 'http:';
const overrides = [env.OIDC_AUTH_URL, env.OIDC_TOKEN_URL, env.OIDC_USERINFO_URL];
let document = {};
if (!overrides.every(Boolean)) {
let response;
try {
response = await fetchImpl(issuer.replace(/\/+$/, '') + '/.well-known/openid-configuration', {
headers: { Accept: 'application/json' }, redirect: 'manual', signal: AbortSignal.timeout(timeoutMs)
});
} catch { throw new Error('OIDC discovery request failed or timed out'); }
if (response.status !== 200) {
await response.body?.cancel();
throw new Error('OIDC discovery returned HTTP ' + response.status);
}
try { document = await readDocument(response); }
catch (error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') throw new Error('OIDC discovery request timed out');
throw error;
}
if (!document || typeof document !== 'object' || Array.isArray(document) || document.issuer !== issuer) {
throw new Error('OIDC discovery issuer does not match OIDC_ISSUER_URL');
}
}
const authorizationURL = env.OIDC_AUTH_URL || document.authorization_endpoint;
const tokenURL = env.OIDC_TOKEN_URL || document.token_endpoint;
const userInfoURL = env.OIDC_USERINFO_URL || document.userinfo_endpoint;
validateUrl(authorizationURL, 'authorization endpoint', allowHttp);
validateUrl(tokenURL, 'token endpoint', allowHttp);
if (userInfoURL !== undefined) validateUrl(userInfoURL, 'userinfo endpoint', allowHttp);
return { issuer, authorizationURL, tokenURL, userInfoURL };
}
module.exports = { discoverOidcEndpoints };