A self-service portal for Windows LAPS β lets users securely retrieve the local administrator password for their own device, without a helpdesk ticket.
Built on Azure Static Web Apps + Azure Functions, secured with Entra ID. Zero stored secrets for API access.
- π₯οΈ Only-my-device rule β users can only see and retrieve passwords for devices registered to them (enforced server-side)
- π Mandatory justification β configurable minimum length (default: 10 characters)
- β±οΈ 60-second auto-hide β password disappears with a countdown bar; copy available during the window
- π Audit log β every access attempt written to Azure Table Storage with user, device, justification, and IP
- π Managed Identity β no stored secrets for Graph API access
- π‘οΈ Easy Auth β Entra ID JWT validated by Azure before your code runs; invalid or missing tokens rejected with 401 automatically
- π¨ CSS theming β full white-labeling via CSS custom properties
- π Custom domain β supported via Azure Static Web Apps (Standard tier)
- π€ User signs in with their Entra ID account
- π₯οΈ The portal shows only their own registered devices (enforced server-side)
- π User selects a device and enters a justification
- π LAPS password is shown for 60 seconds, then hidden automatically
- π Every access attempt is logged to Azure Table Storage
| Sign-in | Justification |
|---|---|
![]() |
![]() |
| Password (60 s countdown) | Auto-expired |
|---|---|
![]() |
![]() |
| Audit log (Azure Table Storage) |
|---|
![]() |
| Tool | Min. Version | Install |
|---|---|---|
| Azure CLI | 2.50 | aka.ms/installazurecli |
| Node.js | 24 LTS | nodejs.org β includes npm |
| Static Web Apps CLI | latest | npm install -g @azure/static-web-apps-cli |
| Scope | Role |
|---|---|
| Azure subscription | Contributor |
| Entra ID tenant | Application Administrator (or Global Administrator) |
| Entra ID tenant | Privileged Role Administrator |
π‘ A Global Administrator covers all of the above by default.
1. Install prerequisites (once):
# macOS (Homebrew)
brew install azure-cli node
npm install -g @azure/static-web-apps-cli2. Clone and deploy:
git clone https://github.com/daniel-fraubaum/laps-self-service-portal.git
cd laps-self-service-portal
chmod +x infra/deploy.sh
./infra/deploy.sh --project laps-prod1. Install prerequisites (once):
- π΅ Azure CLI β download and run the MSI installer
- π’ Node.js 24 LTS β download and run the installer (includes npm)
- π¦ Static Web Apps CLI:
npm install -g @azure/static-web-apps-cli
2. Clone and deploy:
git clone https://github.com/daniel-fraubaum/laps-self-service-portal.git
cd laps-self-service-portal
.\infra\deploy.ps1 -Project laps-prodThe script asks for confirmation, then handles everything end-to-end (~10 minutes):
| Step | Action |
|---|---|
| 1οΈβ£ | Creates (or reuses) the Entra ID App Registration, sets the identifier URI |
| 2οΈβ£ | Deploys all Azure infrastructure via Bicep (single pass) |
| 3οΈβ£ | Assigns Device.Read.All, DeviceLocalCredential.Read.All, Directory.Read.All to the Managed Identity |
| 4οΈβ£ | Deploys the backend (Azure Functions) |
| 5οΈβ£ | Generates frontend/authConfig.js from deployment outputs |
| 6οΈβ£ | Deploys the frontend (Azure Static Web App) |
| 7οΈβ£ | Registers the Static Web App URL as a redirect URI on the App Registration |
| 8οΈβ£ | Grants admin consent for User.Read |
β³ Allow a few minutes after deployment before the portal is fully functional. Graph API role assignments (Managed Identity permissions) can take 2β5 minutes to propagate in Entra ID. If you see
403/ permission errors right after deployment, simply wait and refresh.
| Resource | Details |
|---|---|
| π¦ Resource Group | rg-<projectName> |
| π Log Analytics + Application Insights | Telemetry and custom events |
| πΎ Storage Account | Runtime storage + LapsAuditLog audit table |
| βοΈ App Service Plan | Linux B1 |
| π Azure Static Web App | Standard tier (SPA frontend) |
| β‘ Azure Function App | Node.js 24, system-assigned Managed Identity |
| π Entra ID App Registration | MSAL user login + Easy Auth JWT validation |
All prices are approximate, based on West Europe, low/idle usage. Billed in USD by Azure.
| Resource | Tier | ~$/month |
|---|---|---|
| βοΈ App Service Plan | Linux B1 | ~$13 |
| π Azure Static Web App | Standard | ~$9 |
| πΎ Storage Account | LRS, minimal usage | ~$1 |
| π Log Analytics + App Insights | Pay-per-use, minimal ingestion | ~$1 |
| β‘ Function App | Runs on B1 plan (no extra charge) | β |
| π Entra ID App Registration | Free tier | β |
| Total | ~$24/month |
β οΈ Security requirement β do this before going live. By default, any user in your Entra ID tenant can log in to the portal. Enable assignment enforcement immediately after deployment.
- π’ Entra ID β Enterprise Applications β search for your app (e.g.
laps-prod-laps-portal) - βοΈ Properties β set "Assignment required?" to Yes β Save
- π₯ Users and groups β Add assignment β select your security group (e.g.
SG-LAPS-Self-Service)
Unassigned users receive AADSTS50105 from Entra ID at sign-in β the portal and backend never see the request.
π‘ Always assign a security group rather than individual users β group membership can then be managed independently in Entra ID.
See docs/CONFIGURATION.md for CLI commands and details.
β οΈ MFA cannot be enforced from the application code. MSAL can request a step-up challenge, but this is bypassable β a user with a valid token from another app could call the backend API directly. The only secure enforcement is via Entra ID Conditional Access.
Create a Conditional Access Policy targeting your app:
- Entra ID β Security β Conditional Access β + New policy
- Users: your LAPS security group (e.g.
SG-LAPS-Self-Service) - Target resources: select your App Registration (e.g.
laps-prod-laps-portal) - Grant: βοΈ Require multi-factor authentication
- Enable policy: On β Create
Entra ID then enforces MFA at token issuance β before the portal or backend sees anything.
# π macOS / Linux
./infra/deploy.sh --project laps-prod# πͺ Windows PowerShell
.\infra\deploy.ps1 -Project laps-prod# π macOS / Linux β code only (skip Bicep)
./infra/deploy.sh --project laps-prod --skip-infra
# π macOS / Linux β infrastructure only (skip backend + frontend)
./infra/deploy.sh --project laps-prod --skip-backend --skip-frontend# πͺ Windows PowerShell β code only (skip Bicep)
.\infra\deploy.ps1 -Project laps-prod -SkipInfra
# πͺ Windows PowerShell β infrastructure only
.\infra\deploy.ps1 -Project laps-prod -SkipBackend -SkipFrontend# π macOS / Linux
./infra/deploy.sh \
--project laps-prod \
--location westeurope \
--domain laps.company.com# πͺ Windows PowerShell
.\infra\deploy.ps1 `
-Project laps-prod `
-Location westeurope `
-CustomDomain laps.company.com
β οΈ After enabling a custom domain, add the new URL as a redirect URI on the App Registration. Open Entra ID β App registrations β your-app β Authentication β Redirect URIs and addhttps://laps.company.com. Logins will fail withAADSTS50011until this is done. See docs/deployment.md for CLI commands.
[User Browser]
β MSAL Login (Authorization Code + PKCE)
βΌ
[Azure Static Web App] β Single-file SPA (index.html + MSAL.js)
β Bearer Token (Entra ID JWT)
βΌ
[Azure Function App] β Node.js 24, Easy Auth validates JWT before code runs
β App-only token (Managed Identity, no stored secrets)
βΌ
[Microsoft Graph]
βββ /users/{id}/registeredDevices β ownership check (only-my-device rule)
βββ /deviceLocalCredentials/{id} β LAPS password retrieval
[Azure Table Storage] β LapsAuditLog (every access attempt, success or failure)
[Application Insights] β Telemetry and custom events
βββ infra/
β βββ deploy.sh β Full deployment (Bash β macOS/Linux/WSL)
β βββ deploy.ps1 β Full deployment (PowerShell β Windows)
β βββ main.bicep β Bicep template (subscription scope)
β βββ bicepconfig.json β Bicep linting rules
β βββ main.parameters.example.json β Parameters template
β βββ modules/
β βββ monitoring.bicep β Log Analytics + Application Insights
β βββ storage.bicep β Storage Account + audit table
β βββ appServicePlan.bicep β Linux App Service Plan (B1)
β βββ staticwebapp.bicep β Frontend Static Web App
β βββ functionapp.bicep β Backend Function App + Easy Auth
βββ frontend/
β βββ index.html β Single-page SPA (CSS + JS fully embedded)
β βββ authConfig.example.js β Template β copy to authConfig.js and fill in
β βββ staticwebapp.config.json β SWA routing + security headers
β βββ css/
β βββ theme.css β Branding overrides (colors, fonts, logo)
βββ backend/
β βββ host.json
β βββ package.json
β βββ eslint.config.js β ESLint 9 flat config
β βββ src/
β βββ index.js β Entry point (boots telemetry + registers functions)
β βββ functions/
β β βββ myDevices.js β GET /api/my-devices
β β βββ lapsPassword.js β POST /api/laps-password
β βββ lib/
β βββ auth.js β Easy Auth + JWKS JWT verification
β βββ graph.js β Microsoft Graph (DefaultAzureCredential)
β βββ telemetry.js β Application Insights custom events
β βββ audit.js β Azure Table Storage audit log
βββ docs/
βββ architecture.md β Architecture overview + auth flow
βββ deployment.md β Detailed deployment guide
βββ CONFIGURATION.md β All config values explained
Note on App Registration: Created and configured by the deploy scripts via
az ad appCLI β not via a Bicep module.
- π Deployment Guide β detailed steps, re-deploy, troubleshooting
- βοΈ Configuration Reference β all config values and where they come from
MIT β see LICENSE




