After some research, I have discovered a method that appears to be effective in certain scenarios. If the service being protected also has a way to transmit a valid image (such as protecting a "login complete" page with images or securing the Swagger docs page), then you can conduct an img
tag test like so:
// Utilize a hidden `<img>` tag to assess if the provided (protected) resource URL
// can be retrieved. Returns `true` upon successful loading of the image, otherwise returns `false`.
// Rejects if the specified timeout period is exceeded before resolution.
function testAuthWithImage(imgUrl: string, timeoutMS: number): Promise<boolean> {
return new Promise((resolve, reject) => {
const canary = document.createElement("img");
function cleanup() {
window.clearTimeout(timeout);
// Event listeners must be removed for proper garbage collection of canary
canary.removeEventListener("load", loaded);
canary.removeEventListener("error", failed);
}
async function loaded() {
cleanup();
resolve(true);
}
async function failed() {
cleanup();
resolve(false);
}
const timeout = window.setTimeout(() => {
cleanup();
reject("Connection timed out");
}, timeoutMS);
canary.addEventListener("load", loaded);
canary.addEventListener("error", failed);
// Setting this will trigger loading or failure of the image
canary.src = imgUrl;
});
}
Based on my observations, it seems that modern browsers tend to ignore the 401
response without displaying a login prompt if it pertains to a "subresource" from a different domain, likely as a security measure against phishing attempts. Once I grasped this concept, managing customized login processes became straightforward:
protected async checkLogin(promptForPass: boolean = false): Promise<UserIdentity | undefined> {
if (await testAuthWithImage(this.TEST_IMG_ENDPOINT.url, this.timeoutMS)) {
// The image-test was successful, indicating an existing session; proceed to check the user profile.
try { return await this.fetchUserInfo(); }
catch (err) {
// In case of HttpErrorResponse, throw the `message`
throw err.message || err;
}
} else if (promptForPass) {
// If the test failed but prompting is enabled, present the user/password dialog immediately
return await this.doLogin();
}
// Indicate no prompting required by returning undefined
}
I suspect this method should handle legacy browser scenarios gracefully, as the subresource load via the img
tag would likely trigger a native login prompt, subsequently succeeding or failing based on user interaction. However, this approach depends on the server already providing a suitable protected resource and involves at least one additional request for the specific image, leaving room for improvement.