Basic authentication for Storybook
For xmCloud and many other projects we are using Storybook, in this article I discuss how to secure the storybook deployed on vercel using basic auth and other available options.
The same architecture can be used with any other host provider and also any other Identity provider like okta/auth0.
Disclaimer: I am not a frontend developer, please check the code with your frontend expert.
Unfortunately, the storybook does not have support for basic auth: https://github.com/storybookjs/storybook/issues/5193
Hosting on Vercel
If you are using Vercel you have two easy options:
- Vercel Authentication: It is now activated by default, which means you need to have a Vercel account and be a team member to access the deployed app and it is not an option for the hobby accounts.
- Password protection: Anyone with a password can access the app, which will cost $ 150$ per month.
So, let's step back and think about the Storybook. Storybook build result is a static HTML website. so if we have a server that can authenticate the incoming requests, we can secure the storybook.
Basic Authentication and express
The idea is to use the Express server as a wrapper around the storybook, so it can take care of the basic authentication (or any kind of authentication with any external provider like okta or auth0) and after the request is authenticated return the static assets.
there are two approaches based on how you want to deploy your application to Vercel:
- Build the storybook on an external build agent like Azure DevOps and then deploy the build result to the vercel.
- Build on the vercel, which can be more complicated.
Preparation for Deployment
I will use Azure DevOps for this post because recently Vercel has created two Tasks for Azure DevOps integration and it makes it much easier. If you use an unsupported system you can always use Vercel CLI for deployment.
I assume you already have an Azure DevOps project and you have also created a Vercel project for a storybook (vercel project add MyStorybook
) and you have vercel access token.
Under the Library create a new variable group and name it storybook-vercel
and add the following variables to it:
- VERCEL_ORG_ID: Vercel Organization ID
- VERCEL_PROJECT_STORYBOOK_ID: Project ID for Storybook
- VERCEL_TOKEN: Vercel Access token for deployment.
On the Vercel Project, make sure your Project type is set to Other and the root directory is empty:
Under the environment variables of your project, add the following variables:
- PREVIEW_USER: username for basic auth.
- PREVIEW_PASS: Password for basic auth
Study the following documentation for Vercels Azure tasks:
https://github.com/vercel/vercel-azure-devops-extension
https://vercel.com/docs/deployments/git/vercel-for-azure-pipelines
documentation
The provided storybook example is created using a sandbox template:npx storybook@latest sandbox
Building using an external Build System
In this approach we build the storybook on an external build system like Azure DevOps, then we will add the express server to the storybook build result and deploy it to the Vercel.
In the root of the solution, I have added the azure
folder which contains the deployment pipelines, for this approach I use the build-storybook-azure.yaml
In the root of the frontend application, I added the storybookvercel
which contains the express server and build configuration needed for the Vercel deployment.
Express App Logic is quite simple:
import * as path from 'path';
import express from 'express';
const app = express();
// Basic auth logic
app.use(function (req, res, next) {
var auth;
// check whether an autorization header was send
if (req.headers.authorization) {
// only accepting basic auth, so:
// * cut the starting "Basic " from the header
// * decode the base64 encoded username:password
// * split the string at the colon
// -> should result in an array
auth = new Buffer.from(req.headers.authorization.substring(6), 'base64').toString().split(':');
// use Buffer.from in with node v5.10.0+
// auth = Buffer.from(req.headers.authorization.substring(6), 'base64').toString().split(':');
}
const username = process.env.PREVIEW_USER;
const password = process.env.PREVIEW_PASS;
// checks if:
// * auth array exists
// * first value matches the expected user
// * second value the expected password
if (!auth || auth[0] !== username || auth[1] !== password) {
// any of the tests failed
// send an Basic Auth request (HTTP Code: 401 Unauthorized)
res.statusCode = 401;
// MyRealmName can be changed to anything, will be prompted to the user
res.setHeader('WWW-Authenticate', 'Basic realm="RsnFeSbpwvlexp"');
// this will displayed in the browser when authorization is cancelled
res.end('Unauthorized');
} else {
// continue with processing, user was authenticated
next();
}
});
app.use(express.static(path.join(process.cwd(), 'public')));
app.get('/', async (req, res) => {
res.sendFile(path.join(process.cwd(), 'public', 'home.html'));
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
We are checking the headers for basic authentication using a simple middleware, if the user is authenticated we will server the storybook from the public folder.
In the vercel.json we notify the Vercel about the nodejs application and configure all roots to server the requests from the index.js:
{
"version": 2,
"builds": [
{
"src": "index.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "index.js"
}
]
}
And in the packages.json
we need to configure the type to module to prevent vercel from converting the storybook code to commonjs during the deployment.
{
"name": "build",
"version": "1.0.0",
"description": "",
"type": "module",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2"
}
}
The folder structure will look like this:
Now for the pipelines:
Now for the pipeline:
trigger:
branches:
include:
- "*"
exclude:
- feature/*
paths:
include:
- fe-app/*
variables:
- group: storybook-vercel
stages:
- stage: Build
displayName: Build Storybook
jobs:
- template: /azure/templates/jobs-build-storybook-azure.yml
And the template:
jobs:
- job: BuildStorybook
displayName: Build Storybook
variables:
rootDirectory: src/fe-app
verbose: false
buildDirectoryStorybook: $(rootDirectory)/storybook-static
vercelDirectoryStorybook: $(rootDirectory)/storybookvercel
isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
steps:
# Install NPM Packages
- task: Npm@1
displayName: "Run npm install"
inputs:
command: "ci"
workingDir: "$(rootDirectory)"
verbose: $(verbose)
# Build Storybook
- task: Npm@1
displayName: "Build Storybook"
inputs:
command: "custom"
customCommand: "run storybook:build"
workingDir: "$(rootDirectory)"
verbose: $(verbose)
# Copy Build Result to vercel directory for deployment
- task: CopyFiles@2
displayName: 'Prepare assets for vercel deployment. Copying to: $(vercelDirectoryStorybook)'
condition: or(eq(variables.isMain, true), eq(variables['Build.Reason'], 'PullRequest'))
inputs:
SourceFolder: '$(buildDirectoryStorybook)'
Contents: '**/*'
TargetFolder: '$(vercelDirectoryStorybook)/public'
# Storybook Vercel
- task: vercel-deployment-task@1
name: 'DeployStorybook'
displayName: "Deploy Storybook"
inputs:
vercelProjectId: $(VERCEL_PROJECT_STORYBOOK_ID)
vercelOrgId: $(VERCEL_ORG_ID)
vercelToken: $(VERCEL_TOKEN)
production: $(isMain)
vercelCWD: $(vercelDirectoryStorybook)
debug: $(verbose)
condition: or(eq(variables.isMain, true), eq(variables['Build.Reason'], 'PullRequest'))
# Add the vercel preview url to the pull request
- task: vercel-azdo-pr-comment-task@1
displayName: "Update Pull Request"
inputs:
azureToken: $(AZURE_TOKEN)
deploymentTaskMessage: $(DeployStorybook.deploymentTaskMessage)
It is quite simple, we are running the following steps:
- Installing npm packages insde the fe root
- Building the storybook
- Copying build result to the
storybookvercel/public
folder, this folder is the source of deployment to vercel - Deploying the express app inside the
storybookvercel
folder to vercel - Adding a comment to the pull-request containing the deployed app url
And that is it, now when you open the deployed url you will get the basic auth pop-up.
Second Approach: Building using Vercel Build
This one is much more complicated and needs a deeper understanding of how Vercel build and deployment work.
This time we are using the official serverless function for the vercel. To prevent any effect on the main repository on the deployment time in azure devops, I will copy the index.js
to api
folder inside the root of FE-App. Then I need to register that as a function in the vercel.json
{
"functions": {
"api/index.js": {
"maxDuration": 30,
"includeFiles": "storybook-static/public/**/*"
}
},
"routes": [
{
"src": "/(.*)",
"dest": "/api"
}
]
}
Keep in mind the api will be run on the aws, so it needs to have access to the storybook build result. That is the reason for the includeFiles
entry.
In this approach the storybook build will be run on the Vercel, which means the only way to change something is through the main package.json
, so I added two new scripts to it:
After storybook:build
:
"storybook:build-preview": "npm-run-all --serial build-storybook storybook-move",
"storybook-move": "cd storybook-static && mkdir public && ls | grep -v public | xargs mv -t public && cd .. && rm ./package.json && cp ./storybookvercelsecondapproach/package.json ./",
When the storybook is finished, everything inside the storybook-static
will be moved to storybook-static/public
folder and the package.json
in the root will be replaced by package.json
from storybookvercelsecondapproach
folder. The reason behind is to prevent storybook scripts to be converted to commonjs and also preventing the installation of unnecessary npm packages on the AWS.
When the vercel is finished building the storybook, it deploy the api on AWS based on the package.json
Now the pipeline:
Now the pipeline (build-storybook-vercel.yaml):
trigger:
branches:
include:
- "*"
exclude:
- feature/*
paths:
include:
- fe-app/*
variables:
- group: storybook-vercel
stages:
- stage: Build
displayName: Build Storybook
jobs:
- template: /azure/templates/jobs-build-storybook-vercel.yml
And the Jobs (jobs-build-storybook-vercel.yml):
jobs:
- job: BuildStorybook
displayName: Build Storybook
variables:
rootDirectory: src/fe-app
verbose: false
isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
steps:
# Copying vercel build config to the root
- task: CopyFiles@2
displayName: "Copying vercel configuration to root"
inputs:
SourceFolder: "$(rootDirectory)/storybookvercelsecondapproach"
Contents: 'vercel.json'
TargetFolder: "$(rootDirectory)"
# Copying vercel api
- task: CopyFiles@2
displayName: "Copying express api"
inputs:
SourceFolder: "$(rootDirectory)/storybookvercelsecondapproach"
Contents: '*.js'
TargetFolder: "$(rootDirectory)/api"
# Storybook Vercel
- task: vercel-deployment-task@1
name: 'DeployStorybook'
displayName: "Deploy Storybook"
inputs:
vercelProjectId: $(VERCEL_PROJECT_STORYBOOK_ID)
vercelOrgId: $(VERCEL_ORG_ID)
vercelToken: $(VERCEL_TOKEN)
vercelCWD: "$(rootDirectory)"
production: $(isMain)
debug: $(verbose)
condition: or(eq(variables.isMain, true), eq(variables['Build.Reason'], 'PullRequest'))
# Add the vercel preview url to the pull request
- task: vercel-azdo-pr-comment-task@1
displayName: "Update Pull Request"
inputs:
azureToken: $(AZURE_TOKEN)
deploymentTaskMessage: $(DeployStorybook.deploymentTaskMessage)
It is simple:
- Copying vercel configuration to the root of FE folder
- Copying the express api to the api folder in the root
- Deploying to the vercel
- Adding a comment to the pull-request containing the deployed app url
On the Vercel Project we need to configure the correct build command:
With that, on the deployment time the whole source code of your FE App will be uploaded to the vercel, then vercel will run the configured npm run storybook:build-preview
command.
Fingers crossed when it is done, you will be able to see the basic auth pop-up.
The GitHub repository is here.
References:
- https://github.com/vercel/next.js/discussions/14807
- https://github.com/vercel/next.js/issues/8251
- https://vercel.com/docs/file-system-api/v2
- https://vercel.com/docs/build-output-api/v3
- https://vercel.com/docs/projects/project-configuration#functions
- https://vercel.com/guides/using-express-with-vercel
- https://github.com/storybookjs/storybook/issues/5193