Handling password-protected PDFs in Javascript

PDF is one of the simplest formats for sharing documents. They are portable and can provide basic access control through password protection. This post shows one of many ways to unlock and open password-protected PDF documents in JavaScript.
Specifically, this post uses PDF.js and client side JavaScript tools built into modern browsers to:
Read a PDF file from a user’s device.
Prompt for passwords only when the PDF is password-protected.
Display feedback for failed attempts to unlock a PDF file.
Render pages of the decrypted PDF using the browser’s Canvas API.
TL;DR: Handling password-protected PDFs in JavaScript isn’t as hard as it seems. You just need the right tools. This guide shows how to use PDF.js and other libraries to open and work with secured PDFs in the browser. It explains how to detect if a PDF has a password, ask the user for it, and unlock the file so you can read or display it. Whether you’re building a custom viewer or need to extract data, the key takeaway is that you can manage password-protected PDFs easily with a few smart code tricks and the right setup.
Start a New JavaScript Project
As a first step, let’s set up scaffolding for a vanilla JavaScript application using Vite. Run the below command in your terminal to create a new vanilla JavaScript web app named pdf-password and install its dependencies.
npm create vite@latest -- --template vanilla pdf-password && cd pdf-password && npm install
Next, install PDF.js as a project dependency:
npm install pdfjs-dist
Then, open the newly created pdf-password folder in your preferred code editor to begin building the PDF viewer.
Create HTML Elements to Handle User Input and PDF Rendering
Replace the contents of the project’s index.html file with the following.
<!-- pdf-password/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Locked PDF Viewer</title>
</head>
<body>
<form class="pdf-form">
<label for="pdf-form__input" class="pdf-form__label"
>View a PDF file in your browser</label
>
<input type="file" name="pdf-file" id="" class="pdf-form__input" />
</form>
<div class="password-form-backdrop">
<form class="password-form">
<label for="" class="password-form__label"
>The PDF is password-protected. Please enter its password.</label
>
<p class="password-incorrect">Incorrect password. Please try again.</p>
<input
type="password"
name="password"
class="password-input"
placeholder="PDF Password"
autocomplete=""
autofocus
/>
<button type="submit" class="password-submit">Unlock</button>
</form>
</div>
<canvas class="pdf-canvas"></canvas>
<script type="module" src="/src/main.js"></script>
</body>
</html>
The first form in the above markup handles file uploads from the user’s device, the second form collects a PDF’s password, while the canvas element will be used to render the PDF’s pages.
Since we’re aiming for a visually pleasing PDF upload form (even though it’s just a demo 🙂), let’s replace the contents of the project’s src/style.css file with the following style rules:
/* pdf-password/src/style.css */
*,
::before,
::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body,
input,
input::placeholder,
button,
.pdf-form__input::file-selector-button {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
background-color: rgb(239, 246, 253);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1em;
max-width: 100%;
}
form {
background-color: white;
border-radius: 1em;
padding: 4em 2em;
flex-basis: 400px;
max-width: 100%;
}
label {
font-size: 1.5rem;
display: inline-block;
margin-bottom: 12px;
}
.pdf-form__input::file-selector-button {
border: 2px solid rgba(0, 0, 0, 0.1);
border-radius: 0.5em;
padding: 0.75em;
font-size: 1rem;
background-color: transparent;
cursor: pointer;
}
.password-form-backdrop {
display: none;
position: fixed;
background-color: rgba(0, 0, 0, 0.5);
top: 0;
right: 0;
bottom: 0;
left: 0;
justify-content: center;
align-items: center;
}
.password-incorrect {
color: rgb(220, 20, 60);
display: none;
}
.password-input,
.password-submit {
border: 2px solid rgba(0, 0, 0, 0.1);
border-radius: 0.5em;
padding: 0.75em;
font-size: 1rem;
width: 100%;
margin-bottom: 20px;
}
.password-submit {
border: none;
background-color: dodgerblue;
color: white;
cursor: pointer;
}
.pdf-canvas {
max-width: 100%;
margin: 0 auto;
display: none;
justify-content: center;
align-items: center;
flex-direction: column;
}
Import the Project’s Dependencies
Delete the contents of the project’s src/main.js file. Then, import and configure PDF.js and the project’s stylesheet into the src/main.js script as shown below:
// pdf-password/src/main.js
import './style.css';
// Import pdfJs
import {
GlobalWorkerOptions,
getDocument,
PasswordResponses,
} from 'pdfjs-dist';
// Setup pdfJs' worker from the package's node_modules folder
GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.mjs',
import.meta.url
).toString();
At this point, the project’s index page should look similar to the following image if you run npm run dev -- --open from the root folder of the project.
Display the PDF’s Contents
Copy the following code into the src/main.js file. It attaches an event listener to the file input field of the pdf-form element defined in the index.html file. The event listener detects when a user selects a PDF and its handler attempts to display the document’s contents on the screen.
// Display an uploaded PDF file
document.querySelector('.pdf-form__input')
.addEventListener('change', viewPDF);
Now, let’s define the viewPDF event handler, which is passed as the second argument of the addEventListener method. Copy the following code into the src/main.js file. Check out the comments in the function definition for some insight into what each statement and expression does.
function viewPDF(e) {
// Get the File object of the uploaded PDF.
const file = e.target.files[0];
// Continue only if the user has uploaded a PDF document
if (file.type !== 'application/pdf') {
alert(`Error: ${file.name} is not a PDF file`);
return;
}
// Read the contents of the PDF file from the user's device
const reader = new FileReader();
reader.readAsArrayBuffer(file);
// Handle error(s) encountered while reading the contents of the PDF file
reader.onerror = () => {
alert(`Unable to read ${file.name} to an ArrayBuffer`);
console.error(reader.error);
};
// Wait till FileReader has read all contents of the PDF file before proceeding
reader.onload = async () => {
// Transform the contents of the PDF file to a generic byte array
const bytes = new Uint8Array(reader.result);
// Using PDF.js, start loading the PDF contents from the above byte array
const loadingTask = getDocument(bytes);
// Prompt for a password only if PDF.js detects password protection while loading the document
loadingTask.onPassword = handlePDFPassword;
// Complete the process of loading the PDF document
const pdfDocument = await loadingTask.promise;
// Hide the PDF upload form since we don't need it anymore
document.querySelector('.pdf-form').style.display = 'none';
renderPage(pdfDocument);
};
}
Prompt for a Password and unlock password-protected PDFs
The handlePDFPassword function set as the loadingTask’s onPassword event handler is undefined at this point. Let’s define it by adding the below function to the src/main.js file.
function handlePDFPassword(setPassword, reason) {
const passwordForm = document.querySelector('.password-form-backdrop').style;
const passwordIncorrect = document.querySelector('.password-incorrect').style;
// Prompt for a password if PDF.js needs the file’s password to proceed
if (reason === PasswordResponses.NEED_PASSWORD) {
passwordForm.display = 'flex';
passwordIncorrect.display = 'none';
document.querySelector('.password-form').addEventListener('submit', (e) => {
e.preventDefault();
setPassword(document.querySelector('.password-input').value);
// Hide password prompt after the correct password is submitted
passwordForm.display = 'none';
passwordIncorrect.display = 'none';
});
}
// Display incorrect password error message if the entered password doesn’t unlock the PDF file
if (reason === PasswordResponses.INCORRECT_PASSWORD) {
passwordForm.display = 'flex';
passwordIncorrect.display = 'block';
}
}
handlePDFPassword gets called only if PDF.js detects password protection when loading a PDF document. Users trying to view non-password-protected PDFs won’t be prompted for a password.
Render the Unlocked PDF’s pages
Finally, copy the following function definition to the src/main.js file. Like its name suggests, the renderPage function renders a page of the loaded PDF onto the web page.
async function renderPage(pdfDocument) {
// Load the first page of the document.
const page = await pdfDocument.getPage(1);
// Use the page's dimensions (in pixels) to set the
// dimensions of the canvas on which the page will be rendered
const viewport = page.getViewport({ scale: 1 });
const canvas = document.querySelector('.pdf-canvas');
const canvasContext = canvas.getContext('2d');
canvas.style.display = 'block';
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render the PDF page on the site
const renderTask = page.render({
canvasContext,
viewport,
});
await renderTask.promise;
}
Try It Out!
Open the app in your browser (by running npm run dev -- --open), click the Choose file button, and select a password-protected PDF file.
The app will prompt for the PDF’s password using the popup form shown below before displaying the PDF’s contents.
Go Further
The renderPage function defined above renders only the first page of the PDF document. See https://mozilla.github.io/pdf.js/examples for guidance on adding pagination and/or better error handling to the app.
Need to build PDF capabilities inside your SaaS application? Joyfill makes it easy for developers to natively build and embed form and PDF experiences inside their own SaaS applications.
Subscribe to my newsletter
Read articles from John Pagley directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by