The superadmin portal for Socrate: applications, users, security and access policy in one place, with no token in the browser.
The admin console is the web portal Socrate administrators use to run the identity provider
itself: register OAuth applications, manage global users and superadmins, watch security
events, and edit the central access policy. It is a Vue 3 single-page application with a Go
Backend-for-Frontend (BFF) that holds the OAuth tokens server-side. It is built for Socrate
superadmins, and it needs a Socrate deployment to talk to: Socrate, the suite's OAuth 2.1 /
OpenID Connect server (ovander/go-oauth2, not public yet), provides the admin API and the
sign-in.
- Why the admin console
- Features
- Tech stack
- Architecture
- Project structure
- Getting started
- Environment variables
- Testing
- Deployment
- Security
- Status
An identity provider's admin surface is the most valuable target in the platform: whoever controls it can mint clients, reset accounts and change who may do what. The console is built around that:
- No token in the browser. The BFF is the confidential OAuth client. The browser holds an
opaque
__Host-session cookie, so an XSS or a poisoned dependency cannot steal a replayable admin token. - A narrow door. The admin API listens on loopback only. The BFF is its only client, and it forwards an explicit list of paths, never anything else.
- Step-up for sensitive actions. When the admin API asks for a recent re-authentication, the console prompts for it and the BFF absorbs the short-lived elevated token server-side.
- Counters for users, active users, sign-ins, sign-ups, applications, failed logins and locked accounts
- Login-trend chart, system health (database, server version, uptime, Go version)
- Recent activity, application usage, and shortcuts to common tasks
- List, create, edit and delete OAuth applications: name, URL, redirect URIs
- Public clients (PKCE only, no secret) or confidential clients, chosen at creation
- Client-secret rotation, with the new secret displayed for copying
- Per-application users: invite or add, change role, remove, resend verification, force a password reset
- Per-application activity log
- Global user list with role and verification status
- User detail: application memberships, active sessions, revoke tokens, unlock, block, delete
- Invite a user to an application
- Superadmin accounts: create, edit, delete
- Admin activity log
- Security overview: threat metrics, token statistics, sign-in geography and a live event stream (Server-Sent Events)
- Security events with filters and a detail view
- Active sessions across all applications (read-only)
- Blocked IPs: block, unblock, and per-IP reputation lookup
- Alert rules (create, edit, delete) and alert history with acknowledgement
- Security reports: generate, follow, download
- Admin audit log with filters and export
- Editor for Socrate's central access policy (
/api/admin/policy/*): view and edit rules, validate them, and save a new version (the server asks for step-up); a save based on a stale version is refused as a conflict - Version history with restore
- Decision simulator, against the saved rules or the draft
- Recent denials, and the requests the policy would have denied while it is not enforced
- Server configuration overview, database and cache connection tests, and which server features are on (MFA, password policy, audit logging)
- Own profile (name, email), password-reset email and own active sessions
- Forced password change when the server requires it
- Idle warning and sign-out after 15 minutes of inactivity
- Version badge, and a notice when the server is upgraded under an open tab
- Light and dark theme
The admin API is the authority: it accepts only global administrators (Socrate roles
superadmin and admin) and applies its own checks to every route. The console adds a router
guard on top, so pages the server would refuse are not offered:
| Page | superadmin |
admin |
|---|---|---|
| Dashboard, Applications, User detail, Security overview, Events, Sessions, Settings, Profile | ✅ | ✅ |
| Users, Blocked IPs, Alerts, Reports, Access policy, Audit logs | ✅ | Forbidden page |
Any other account cannot load the admin profile and is sent back to the sign-in page. The
guard is in src/router/router.ts; role names are normalised in src/types/auth.ts and
src/utils/roles.ts.
| Concern | Library / version |
|---|---|
| Framework | Vue 3 (v3.5), Composition API |
| Language | TypeScript (v5.6) |
| Build tool | Vite (v6) |
| UI components | PrimeVue (v4, Aura theme) |
| Styling | Tailwind CSS (v4, CSS-first theme in src/assets/tailwind.css) |
| State management | Pinia (v2) |
| Routing | Vue Router (v4) |
| HTTP client | Axios |
| Charts | Chart.js through PrimeVue |
| Dates | date-fns |
| Unit and integration tests | Vitest (v4), MSW, happy-dom |
| End-to-end tests | Playwright |
| BFF | Go (toolchain pinned in bff/go.mod), built on the bff and socrate packages of backendkit |
| Edge | Caddy (TLS, security headers, routing) |
The browser only ever talks to its own origin. Caddy serves the built SPA and sends an explicit list of paths to the BFF on loopback; the BFF resolves the session, checks CSRF, refreshes the token when needed, injects the bearer and forwards to Socrate.
admin.example.com (Caddy, the only public listener)
┌──────────┐ HTTPS ┌────────────────────────────────────────────────────────┐
│ Browser │──────▶│ / → built SPA (file_server) │
│ • cookie │ │ /bff/* /api/admin/* /api/apps/* │
│ • CSRF │ │ /api/profile GET /api/version │
└──────────┘ │ POST /api/auth/{request-password-reset,reset-password} │
│ → admin BFF │
└───────────────────────────┬────────────────────────────┘
│ loopback
▼
┌────────────────────────────────────────────────────────┐
│ admin BFF (Go, backendkit/bff) 127.0.0.1:8091 │
│ • confidential OAuth client (Authorization Code + PKCE)│
│ • server-side sessions, CSRF, token refresh │
│ • injects the bearer, strips the cookie │
└──────────────┬─────────────────────────┬───────────────┘
│ 127.0.0.1:8081 │ 127.0.0.1:8080
▼ ▼
┌──────────────────────────┐ ┌───────────────────────────┐
│ Socrate admin API │ │ Socrate issuer │
│ /api/admin/*, /api/apps/*│ │ /oauth/*, /api/profile, │
│ (loopback only) │ │ /api/version, /api/auth/* │
└──────────────────────────┘ └───────────────────────────┘
Sign-in is a full-page navigation to /bff/login; the BFF runs Authorization Code + PKCE
against Socrate's hosted login (password and MFA), stores the tokens in its session and sets
the cookie. The SPA then reads GET /bff/session for the user and the CSRF token. Sequence
diagrams, the endpoint table and the security properties are in
docs/architecture.md; the BFF's routes and configuration are in
bff/README.md.
The SPA's calls live in src/services/, one file per area (applicationService.ts,
userService.ts, securityService.ts, monitoringService.ts, policyService.ts,
dashboardService.ts, settingsService.ts, authService.ts, and session.ts for /bff/*).
Read them for the exact admin API paths the console uses.
.
├── .github/ # CI workflow, issue forms, PR template, CODEOWNERS
├── bff/ # Go Backend-for-Frontend (own module, built on backendkit)
│ ├── main.go # startup, graceful shutdown
│ ├── config.go # BFF_* environment variables and their validation
│ ├── app.go # route allowlist, login/callback/session/logout handlers
│ ├── server.go # canonical-path filter, health check
│ ├── oauth.go # token exchange, refresh, revocation, user derivation
│ ├── elevate.go # /bff/elevate step-up
│ ├── ratelimit.go # per-IP budgets
│ ├── *_test.go # tests against httptest upstreams
│ ├── .env.example # BFF configuration template
│ └── Dockerfile # distroless, non-root image
├── deploy/ # production kit: Caddyfile, systemd unit, env template, scripts
├── docs/
│ ├── architecture.md # BFF cookie-session model, flows, security properties
│ └── security-headers.md # CSP and hardening headers at the edge
├── e2e/ # Playwright tests (auth flows, guards, security headers)
├── public/ # favicon and logo
├── scripts/
│ └── npm-audit-gate.sh # dependency-advisory gate used by CI
├── src/ # the Vue SPA (below)
├── .env.example # SPA configuration template
├── eslint.config.js # ESLint, used as a security gate (XSS sinks, eval)
├── playwright.config.ts
├── tailwind.config.js # not loaded by Tailwind 4; the theme is in src/assets/tailwind.css
└── vite.config.ts # dev proxy, dev/preview headers, Vitest and coverage settings
src/
├── __tests__/ # Vitest: unit, integration (MSW), components, router, stores
├── assets/
│ ├── fonts.css # self-hosted fonts
│ └── tailwind.css # Tailwind 4 theme and custom styles
├── components/
│ ├── VersionBadge.vue # client and server versions
│ ├── dashboard/ # StatCard, ActivityFeed, QuickActions, SystemHealth
│ ├── security/
│ │ ├── ElevationDialog.vue # step-up re-authentication prompt
│ │ └── SessionTimeoutWarning.vue
│ └── ui/ # EmptyState, LoadingState, PageHeader, StatusBadge
├── composables/
│ ├── useClipboard.ts
│ ├── useConfirm.ts
│ ├── usePolicyEditor.ts # state and actions behind the access-policy editor
│ ├── useSessionTimeout.ts # idle warning and sign-out
│ ├── useToast.ts
│ ├── useVersionCheck.ts # polls /api/version, flags a server upgrade
│ └── useVersionInfo.ts
├── dev/
│ └── devProxy.ts # Vite dev-server proxy table (mirrors the Caddy routes)
├── layouts/
│ ├── AdminLayout.vue # sidebar, top bar, theme toggle
│ └── AuthLayout.vue # sign-in and password pages
├── router/
│ └── router.ts # routes and guards (auth, forced password change, superadmin)
├── security/
│ ├── csp.ts # canonical CSP, Trusted Types policy and hardening headers
│ └── trustedTypes.ts # the app's rejecting Trusted Types `default` policy
├── services/
│ ├── api.ts # same-origin Axios instance: cookie, CSRF, 401 handling
│ ├── session.ts # /bff/* client and CSRF store
│ ├── adminGuards.ts # step-up and forced-password-change state
│ ├── applicationService.ts # /api/admin/apps, /api/apps/{id}/users and logs
│ ├── authService.ts # admin profile, password change and reset
│ ├── dashboardService.ts
│ ├── monitoringService.ts # sessions, blocked IPs, alerts, reports, SSE event stream
│ ├── policyService.ts # /api/admin/policy/*
│ ├── securityService.ts # security events, admin audit log
│ ├── settingsService.ts
│ └── userService.ts # global users, superadmins, admin activity
├── stores/
│ ├── authStore.ts # session user and role checks
│ ├── themeStore.ts
│ └── version.ts # server version from /api/version
├── types/ # application, auth, dashboard, index, monitoring, policy,
│ # security, user
├── utils/
│ ├── devlog.ts # logging in development builds only
│ ├── formatDate.ts
│ ├── policy.ts # policy editor helpers
│ ├── roles.ts # role checks
│ └── secureConfig.ts # VITE_* parsing; refuses a non-https origin in production
├── views/
│ ├── applications/ # list, create, detail (settings, users, activity)
│ ├── auth/ # login, change, forgot and reset password
│ ├── dashboard/
│ ├── errors/ # 403, 404
│ ├── logs/ # admin audit log
│ ├── security/ # overview, events, sessions, blocked IPs, alerts, reports, policy
│ ├── settings/ # settings, profile
│ └── users/ # list, detail
├── App.vue
├── env.d.ts
└── main.ts
- Node.js 24 (the version in
.nvmrc, which CI uses) and npm. - Go for the BFF:
bff/go.moddeclaresgo 1.25.0and pinstoolchain go1.27.1, which the Go command downloads for you. - A reachable Socrate server with its issuer (by default
:8080) and its loopback admin API (:8081). - In Socrate, a confidential OAuth client for the BFF, with the redirect URI
http://localhost:5173/bff/callbackfor local development (in production,https://admin.example.com/bff/callback) and the scopesopenid profile email. - A Socrate account with the global role
superadmin(oradmin, see Who can see what).
git clone https://github.com/ovander/oauth2-admin && cd oauth2-admin
npm ciThe SPA needs no configuration for local development: it calls its own origin, and the Vite dev server forwards the API paths. Create a local file only if you need to override something:
cp .env.example .env.local # optional; Vite loads .env.local, and git ignores itThe BFF reads its settings from the environment only. Start from bff/.env.example:
cp bff/.env.example bff/.envand set, for local development:
BFF_CLIENT_ID=<the client id registered in Socrate>
BFF_CLIENT_SECRET=<its secret>
BFF_OAUTH_UPSTREAM=http://127.0.0.1:8080 # issuer, back channel
BFF_OAUTH_PUBLIC_URL=http://localhost:8080 # issuer as the browser sees it
BFF_PUBLIC_ORIGIN=http://localhost:5173 # the Vite dev server; the callback lands here
BFF_COOKIE_SECURE=false # allowed only with an http:// public originbff/.env is ignored by git. Keep BFF_SCOPES quoted ("openid profile email") or delete
the line (that is the default), so that the shell can source the file. All variables and
defaults are listed in bff/README.md.
In one terminal, the BFF:
cd bff
set -a && . ./.env && set +a
go run .In another, the SPA:
npm run dev # http://localhost:5173The Vite dev server (src/dev/devProxy.ts, used by vite.config.ts) forwards the same paths to
the BFF on localhost:8091 as Caddy does in production: /bff/*, /api/admin/*, /api/apps/*,
/api/profile, /api/version and the two password-reset posts. Only /oauth/* goes straight to
the issuer on localhost:8080; any other path is served by the SPA. A unit test keeps this table
in step with deploy/Caddyfile. Open http://localhost:5173, choose Sign in with Socrate,
and complete Socrate's hosted login.
npm run build # vue-tsc type check, then the production bundle in dist/
npm run preview # serve dist/ with the production CSP and the Trusted Types report-only policynpm run coverage # Vitest with the coverage gate
npx playwright test # end-to-end; starts its own dev server with mocked APIsThe full list of checks is in Testing.
Both API variables default to the same origin and are normally left unset, in development and in
production. .env.example documents them.
| Variable | Description | Default / example |
|---|---|---|
VITE_ADMIN_API_URL |
Base URL of the admin API calls and the security event stream. Set only for a split-origin setup; must be https:// in a production build. |
unset (same origin) |
VITE_OIDC_ISSUER |
Public issuer origin. Parsed and checked (https:// in production) by src/utils/secureConfig.ts, but no request uses it today: the password-reset calls go through the BFF. |
unset |
VITE_BASE |
Base path the built assets are served from (vite.config.ts). |
/ |
The BFF variables (client credentials, upstreams, public origins, session lifetimes, cookie and
rate limits) are listed in bff/README.md. In production they live
in /etc/socrate/admin-bff.env; see deploy/README.md.
These are the checks CI runs; all of them are required:
npm run lint:check # ESLint security gate: no v-html, innerHTML, eval, javascript: URLs
npx vue-tsc -b && npm run build # type check and production build
npm run coverage # Vitest unit and integration tests, coverage thresholds at 80 %
npx playwright test # end-to-end, Chromium (npx playwright install chromium first)
./scripts/npm-audit-gate.sh # fails on a high or critical advisory, or on missing audit data
cd bff && go vet ./... && go test -race ./... && golangci-lint run ./...- Vitest (
src/__tests__/) covers the security-critical modules: the API client and its interceptors, the BFF session client, step-up and forced password change, the CSP module, the auth store, role checks, the router guards and the access-policy editor. MSW mocks the backend. The coverage scope and thresholds are invite.config.ts. - Playwright (
e2e/) runs sign-in, guards, forced password change and password reset against the dev server with mocked APIs, and checks that the served app carries the CSP and hardening headers. - BFF tests (
bff/*_test.go) run the BFF againsthttptestupstreams: login binding, CSRF, refresh, step-up, the route allowlist, non-canonical paths and rate limits. No Socrate instance is needed. golangci-lint is v2.14.0, built with the Go ofbff/go.mod.
Production is not a plain static host: the SPA needs its BFF and the Caddy routing in front of it. The kit in deploy/ builds the SPA and the BFF on your workstation, ships them to the server and installs them:
deploy/Caddyfile: servesdist/, sends the allowlisted paths to the BFF on127.0.0.1:8091, and sets the security headers.deploy/systemd/socrate-admin-bff.service: the BFF under its own user, with systemd sandboxing.deploy/env/admin-bff.env.exampleanddeploy/scripts/: the environment template, and the build, push, install and bootstrap scripts.
A container image is also available: docker build -t socrate-admin-bff bff/.
The browser never holds an OAuth token; the BFF is the confidential client and the only client
of the loopback admin API; unsafe requests need a double-submit CSRF token; the Content Security
Policy is defined in src/security/csp.ts and served by Caddy. SECURITY.md lists
the controls enforced in the SPA, the BFF and the deployment, and
docs/architecture.md explains the session model.
The admin console is in active use as part of the Socrate suite. Current focus:
- Keeping the console in step with the admin API, including the access-policy editor
- Test coverage beyond the security-critical modules (views such as applications and users)
See CONTRIBUTING.md for setup, the checks CI runs, and the pull-request workflow; changes are listed in CHANGELOG.md. Report vulnerabilities privately as described in SECURITY.md.
Copyright © 2026 Olivier Vandermoten. Licensed under the Apache License, Version 2.0; see LICENSE. SPDX-License-Identifier: Apache-2.0