We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
How to Generate a PDF on a Page Behind Auth in Elixir
The request is inevitable: “The client wants to be able to download a PDF of this page”.
PDF generation woes aside, this particular request is extra problematic because the page the customer wants to download is behind authentication.
What are our options?
- Create an anonymous viewing URL that the PDF renderer can view?
- Somehow render the page server-side and feed the HTML to the PDF generator?
The first poses a security risk - Try explaining to the SOC2 auditors that technically auth can be bypassed with a magic URL parameter.
I have yet to find a way to accomplish the second option with Phoenix & LiveView. Where does that leave us?
Generate a cookie for the logged-in user that the PDF generator can use!
Any HTML to PDF converter can take advantage of this approach. The HTML can be fetched with any HTTP client and then fed to the PDF converter.
Some PDF generation engines (e.g. ChromicPDF) can even accept a cookie and use it to access webpages that require a logged-in user.
We’ll look at two examples: ChromicPDF and WeasyPrint
The best part is, it only takes about 10 lines of code:
alias Plug.Crypto.KeyGenerator
alias Plug.Crypto.MessageVerifier
token = Accounts.generate_user_session_token(current_user)
key_base = Application.get_env(:agility, AgilityWeb.Endpoint)[:secret_key_base]
salt = Application.get_env(:agility, :cookie_signing_salt)
key = KeyGenerator.generate(key_base, salt)
signed_token =
%{"user_token" => token}
|> :erlang.term_to_binary()
|> MessageVerifier.sign(term, key)
Now your cookie can be passed to any HTTP request with the name of _your_application_key
and the value being signed_token
!
Move the Signing Salt
In order for the snippet above to work, the signing salt needs to be moved into config so that the salt can be accessed outside of the Endpoint
module.
Replace @session_options
with:
# lib/your_app_web/endpoint.ex
@session_options [
store: :cookie,
key: "_your_application_key",
signing_salt: Application.compile_env!(:your_application, :cookie_signing_salt),
same_site: "Lax"
]
And inside of config/config.exs
:
# config/config.exs
config :your_application, :cookie_signing_salt, "YourSigningSalt"
Example - ChromicPDF
In order to use with ChromicPDF, create a map of the cookie and pass it to print_to_pdf
with the set_cookie
option, like so:
cookie = %{
name: "_your_app_key",
value: signed_token,
domain: "your_domain" # or "localhost" to test on dev
}
{:ok, base64blob} = ChromicPDF.print_to_pdf({:url, "localhost:4000/auth/url"}, set_cookie: cookie)
Example - WeasyPrint
WeasyPrint converts HTML to PDF without the need for a full browser engine. Since it doesn’t have a browser engine, it cannot accept a cookie. What needs to happen instead is writing a custom URL resolver that passes the HTML back to WeasyPrint for processing.
Normally, WeasyPrint documents are fetched like so:
html = HTML("url")
To get around the auth portion, we pass a custom url resolver:
html = HTML("url", url_fetcher=authenticated_fetcher)
Where authenticated_fetcher
uses the python lib requests
with our cookie:
def authenticated_fetcher(url):
try:
# Pass the cookie in from argparse or similar
cookies = {}
cookies['_your_app_key'] = cookie
# Send a request with cookies
response = requests.get(url, cookies=cookies)
response.raise_for_status() # Raises a HTTPError for bad responses
return dict(string=response.content, mime_type=response.headers['Content-Type'])
except requests.RequestException as e:
raise URLFetchingError('Could not fetch URL: {}'.format(e))
Now, WeasyPrint can turn the authenticated HTML into the PDF for the client!
Build an AI Powered Instagram Clone with LiveView is on sale now!