A simple Node.js application that serves static files with magic link authentication.
This implements the ideas described in my blog post A Login Security Architecture Without Passwords. See my new blog post Static Website Authentication with Magic Link Made Easy for more details about this implementation.
I wanted to create a simple example that shows how to implement this security architecture.
The goal is to add a layer of security on top of an existing static website without changing the website code.
To keep things simple, I accept some security compromises:
- We use symmetric encryption for the session cookie and the magic link to avoid having to store secrets in a database.
- We don't bind the token to the browser to avoid having to store the token on the server.
- We don't use a refresh token to avoid having to store the refresh token on the server.
- The magic link expires after 15 minutes, but in that time it can be used multiple times on different devices, e.g. if somebody forwarded it to another user.
All that can be mitigated by using a database to store information about the users, their devices, and the tokens.
Please send me PRs to make this code better!
- π Magic link authentication (no passwords needed)
- π§ Email-based login
- π‘οΈ Secure session management
- π Configurable email templates
- π Automatic session refresh
- π¨ Customizable error pages
- π Debug and trace modes
- π Support for both plaintext and hashed email addresses
Live demo: youtu.be/iyMaHxJ73_k
- Clone the repository
- Install dependencies:
npm install - Create a
.envfile (see Configuration section) - Create an
allowed_emails.txtfile with authorized email addresses - Start the server:
npm start
Create a .env file in the root directory with the following variables:
# Required
SESSION_SECRET=your-32-byte-hex-secret
MAGIC_LINK_SECRET=your-32-byte-hex-secret
# Optional
PORT=3000
NODE_ENV=development
APP_NAME="Your App Name"
APP_BASE_URL=http://localhost:3000
SESSION_HOURS=24
ALLOWED_EMAILS_FILE=allowed_emails.txt
# Email Configuration (optional)
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
EMAIL_USER=your-email@example.com
EMAIL_PASS=your-password
# Or use API key
EMAIL_API_KEY=your-api-key
EMAIL_FROM="Your App <noreply@example.com>"You can configure email sending in two ways:
- SMTP Server:
EMAIL_HOST=smtp.example.com
EMAIL_PORT=587
EMAIL_USER=your-email@example.com
EMAIL_PASS=your-password- API Key (e.g., for SendGrid):
EMAIL_API_KEY=your-api-key
EMAIL_USER=apikeyThe application uses templates for magic link emails:
EMAIL_TEXT_TEMPLATE: Plain text versionEMAIL_HTML_TEMPLATE: HTML version
Templates support the following variables:
{url}: The magic link URL{appName}: The application name{expiryMinutes}: Number of minutes until the link expires
You can customize the login page by changing the login.html file in the auth directory or by mounting another version of login page and assets at /app/auth.
Create an allowed_emails.txt file (or specify a different name in ALLOWED_EMAILS_FILE) with one email address per line. You can use either plaintext emails or SHA-256 hashes.
Example allowed_emails.txt:
# This is a comment
user@example.com # This is an inline comment
8f4e2f1b3c7d6e5a9b8c7d6e5a9b8c7d6e5a9b8c7d6e5a9b8c7d6e5a9b8c7d6e5 # This is a hashed email
To generate a hash for an email, you can use:
echo -n "user@example.com" | shasum -a 256Run npm run build to build the docker image.
Run npm run run-example to start the server with the example environment variables. Look at package.json for more options.
You can use this docker image in production to replace an existing static website hosting service. Simply set the environment variables in your production environment and mount your static files in the public directory. Also don't forget to set the NODE_ENV to production and to add a allowed_emails.txt file with your production email addresses.
-
Install dependencies:
npm install
-
Create environment files:
.env- Default environment fileproduction.env- Production environmentstaging.env- Staging environment etc.
-
Start the server with a specific environment file:
# Using NODE_ENV_FILE environment variable NODE_ENV_FILE=production.env node server.js -
Create an
allowed_emails.txtfile with test email addresses (make sure to use the same file as defined in your configuration) -
Start the server in dev mode with
allowed_emails_dev.txt:npm run dev
In development mode:
- Emails are logged to console
- Sends out emails unless EMAIL_HOST is left empty or set to a non-reachable host like
127.0.0.1
- Session secrets must be 32-byte hex strings
- Magic links expire after 15 minutes
- Sessions expire based on SESSION_HOURS, extended after less than half the time remaining
- Non-root user in Docker
- Proper signal handling for graceful shutdown
- Support for hashed email addresses
- No password storage
- Configurable session duration
- Automatic session refresh
server.js: Main application fileauth/: Authentication-related static filespublic/: Protected static files (sample static website)allowed_emails.txt: List of allowed email addressesexample.env: Example environment variablesallowed_emails_dev.txt: List of allowed email addresses (dev)
The Docker image is automatically built and published to GitHub Container Registry (ghcr.io) on every push to the main branch.
To use the image:
docker pull ghcr.io/schlomo/static-website-with-magic-link-auth:latestOr in a docker-compose.yml:
services:
app:
image: ghcr.io/schlomo/static-website-with-magic-link-auth:latest
# ... rest of your configurationJust make sure to provide the configuration via environment variables and to add also the file with the allowed emails. You can either mount your own path with the static website onto /public or set a new path via the PUBLIC_DIR variable. Mount the content for the authentication page onto /auth or set a new path via the AUTH_DIR variable. Use the content of the auth dir here for inspiration.
To build your own Docker image based on this you can create a Docker file like this one:
FROM ghcr.io/schlomo/static-website-with-magic-link-auth:latest
COPY my-website /my-website
ENV PUBLIC_DIR /my-website
COPY my-auth /my-auth
ENV AUTH_DIR /my-auth
COPY my-allowed-emails.txt /my-allowed-emails.txt
ENV ALLOWED_EMAILS_FILE /my-allowed-emails.txt
COPY my-config.env /my-config.env
ENV NODE_ENV_FILE /my-config.envAnd of course add the remaining configuration to my-config.env.
The application supports two modes for debugging and tracing:
Enable debug mode by setting the DEBUG environment variable to any value:
DEBUG=1 node server.jsDebug mode will output detailed information about:
- Token generation and verification
- Session management
- Authentication state changes
- Error conditions
Example debug output:
[DEBUG] Token generated {"expires":1234567890}
[DEBUG] Session updated {"authenticated":true,"expires":1234567890}
Enable trace mode by setting the TRACE environment variable to any value:
TRACE=1 node server.jsTrace mode will output detailed HTTP request and response information, including:
- Request method, URL, and status
- Response time
- Complete request and response headers (sorted alphabetically)
Example trace output:
GET /auth/login 200 15.123 ms
Request Headers:
{
"accept": "text/html...",
"cookie": "session=...",
"host": "localhost:3000",
"user-agent": "Mozilla/5.0..."
}
Response Headers:
{
"content-length": "1234",
"content-type": "text/html",
"set-cookie": ["session=..."]
}
Both modes can be enabled simultaneously:
DEBUG=1 TRACE=1 node server.jsThis project is licensed under the GNU General Public License v3.0 (GPLv3). See the LICENSE file for details.
