Safe with a Single URL — Token Auth and Time-Window Design

Protecting without passwords: unguessable tokens, validity windows, and staged disclosure

token authenticationmagic linkHMACexpiryaccess control
6 min read

Introduction

"Anyone who knows the URL can see it" might sound wide open. But with the right design, you can get safety on par with a password-based system.

This article covers the measures we took to protect the guest-only pages without passwords. Since these pages handle sensitive information like door codes, we designed them carefully.

Making a URL Nobody Can Guess

Use a Random String

The string in the URL — the token — is a random value that can't be predicted. We never use anything with a pattern to it, like a booking number or a sequential ID.

With a random value of sufficient length, brute-forcing your way to a valid one isn't realistically possible. And nobody is ever going to stumble onto someone else's page by chance.

Store a Transformed Value in the Database

This is the important part. The token we issue is never stored in the database itself. What we store is the result of putting that token through a one-way transformation.

How to store the token
BEFORE
Store it as-is

If the database leaks, anyone can open every guest's page

AFTER
Store the transformed value

Even in a leak, the URL can't be reconstructed from the stored value

When someone visits, we run the token from the URL through the same transformation and check whether it matches the stored value. It's the same thinking behind how passwords are stored.

The secret key used for the transformation is different from the one used for admin logins. If one leaks, the other is unaffected.

Keep the Last 4 Characters for Admin Identification

So operators can see which URL they issued from the admin screen, we separately store just the last four characters of the token. That alone can't reconstruct anything, but it's plenty for telling multiple issued URLs apart.

The complete URL is shown on screen exactly once, right after it's issued. Close that screen and it can never be displayed again. If it's needed, a new one gets issued.

Giving It a Validity Window

Set a Clear Start and End

Every token has a window during which it can be used. It starts the moment it's issued and ends at the close of checkout day.

Once the stay is over, the token automatically stops working, so nobody is left with an open view of the door codes indefinitely.

What Moves and What Doesn't When Dates Change

When an operator changes the dates on a booking, the validity window has to adjust too. The rule we were careful about here: the end date moves, but the start date never does.

Have a Way to Revoke

For cases where you suspect a URL has fallen into someone else's hands, there's a revoke function. From the admin screen, an operator can invalidate the old URL and issue a new one.

On the other hand, when a booking is cancelled in the reservation management system, we deliberately don't revoke it. We simply set the booking's status to cancelled.

The reason is that a cancellation might itself be undone. If we revoked the URL, reversing the cancellation wouldn't bring the same URL back, and we'd have to send the guest a new one. Since the system checks booking status first, the page appears unusable to the guest while the booking is cancelled — and works again if it's reinstated.

Checking at Every Entrance

Screens and APIs Go Through the Same Checks

Besides the portal's top page, there's the entry instructions page, the house rules page, the AI assistant's connection point, and the document delivery endpoint.

Every one of them independently verifies the token and checks the validity window. We didn't build it so that "you got past the top page, so everything beyond is fair game". Even if someone types a URL in directly, each entrance stops them.

Checks run on every access
Check the token's format

Anything with an abnormal length is rejected before we even look at the database

Transform and compare

Look for a token whose transformed value matches what's stored

Check the booking status

If it's cancelled, reject at this point

Check revocation and the window

Evaluate revoked, too early, and too late separately

Make Every Failure Look Identical

An invalid token, a revoked token, a token that never existed — we don't distinguish between them. They all return the same "not found" result.

If someone can tell that "this URL exists but has been revoked", you've just told them there was a period when it worked. It's a small detail, but the habit of giving nothing away matters.

The only cases we distinguish for the guest's benefit are "too early" and "too late". Even then, that screen shows no booking details and no access codes — just contact information.

Keep It Away from Search Engines and Caches

Every portal page carries instructions not to be indexed by search engines and not to be cached.

That's to prevent a situation where someone opens the page on a shared computer and the next person hits the back button and sees the contents.

Actions That Require Verifying Identity

For irreversible actions like cancellation, knowing the URL alone isn't enough to go through with it. We ask for the email address used at booking and only proceed if it matches.

For that comparison, we use a method whose processing time doesn't vary with the input. That prevents anyone inferring information from differences in response time.

We also limit how many times someone can attempt the cancellation process. Even when managing that limit, we use the transformed value rather than the token itself, so the raw token never lingers in the server's memory.

Wrapping Up

Three things anchored our password-free access control.

  1. Store the transformed value — even if the database leaks, the URL can't be reconstructed
  2. Never move the start date — a URL that has started working must never revert to unusable
  3. Verify at every entrance — getting past one screen is not permission for what comes after