No description
Find a file
Nathan Bland 4dd6e9cf16
All checks were successful
CI/CD Pipeline / Lint, Type-check, Test (push) Successful in 32s
CI/CD Pipeline / Build and Push Image (push) Successful in 48s
CI/CD Pipeline / Deploy Staging (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Successful in 13s
feat: add server-side request logging with user identity and permissions tracking
2025-08-15 10:38:34 -06:00
.forgejo/workflows chore: migrate test runner from Vitest to Bun with compatibility shims and preload setup 2025-08-08 12:56:20 -06:00
.vscode Initial commit 2025-08-07 22:35:29 -06:00
public Initial commit 2025-08-07 22:35:29 -06:00
scripts fix: add base URL support for QRL resolution in Qwik tests under Bun 2025-08-15 10:13:00 -06:00
src feat: add server-side request logging with user identity and permissions tracking 2025-08-15 10:38:34 -06:00
tests fix: add base URL support for QRL resolution in Qwik tests under Bun 2025-08-15 10:13:00 -06:00
.env.example feat: add server-side request logging with user identity and permissions tracking 2025-08-15 10:38:34 -06:00
.gitignore Initial commit 2025-08-07 22:35:29 -06:00
.prettierignore Initial commit 2025-08-07 22:35:29 -06:00
bun.lock feat: implement request/approve access flow with Keycloak group management 2025-08-15 00:15:26 -06:00
bunfig.toml chore: migrate test runner from Vitest to Bun with compatibility shims and preload setup 2025-08-08 12:56:20 -06:00
deploy-portainer.sh chore: add default webhook URL for Portainer deployment 2025-08-08 15:56:40 -06:00
docker-compose.yml feat: add configurable port mapping for Docker container via HOST_PORT and PORT env vars 2025-08-08 10:53:26 -06:00
Dockerfile feat: implement Keycloak SSO with PKCE auth flow and admin dashboard 2025-08-08 03:20:58 -06:00
eslint.config.js Initial commit 2025-08-07 22:35:29 -06:00
package.json feat: implement request/approve access flow with Keycloak group management 2025-08-15 00:15:26 -06:00
postcss.config.cjs feat: implement Keycloak SSO with PKCE auth flow and admin dashboard 2025-08-08 03:20:58 -06:00
qwik.env.d.ts Initial commit 2025-08-07 22:35:29 -06:00
README.md feat: implement request/approve access flow with Keycloak group management 2025-08-15 00:15:26 -06:00
rules.md feat: implement request/approve access flow with Keycloak group management 2025-08-15 00:15:26 -06:00
tailwind.config.cjs feat: implement Keycloak SSO with PKCE auth flow and admin dashboard 2025-08-08 03:20:58 -06:00
tsconfig.json chore: migrate test runner from Vitest to Bun with compatibility shims and preload setup 2025-08-08 12:56:20 -06:00
vite.config.ts chore: migrate test runner from Vitest to Bun with compatibility shims and preload setup 2025-08-08 12:56:20 -06:00
vitest.setup.ts fix: add base URL support for QRL resolution in Qwik tests under Bun 2025-08-15 10:13:00 -06:00

Auth Orbit Keycloak SSO Portal (Qwik)

Auth Orbit is a lightweight, SSR-enabled portal that helps users discover and launch applications protected by Keycloak. It supports a requestaccess workflow for discoverable apps and includes an Admin UI to manage app visibility and approvals.

Built with Qwik + Qwik City. Runtime uses Bun; tests run Vitest via Bun.

Getting Started

  1. Prerequisites
  • Bun 1.x (Node 18+/20+ compatible)
  • A Keycloak realm with a public OIDC client (no secret) for the portal
  • Optional: a confidential admin client if you want Admin UI features (serverside only)
  1. Install
bun install
  1. Configure environment

Copy .env.example to .env and set the minimal required client-side variables:

VITE_KEYCLOAK_BASE_URL=https://keycloak.example.com
VITE_KEYCLOAK_REALM=main
VITE_KEYCLOAK_CLIENT_ID=portal
VITE_KEYCLOAK_REDIRECT_URI=http://localhost:5173/auth/callback
VITE_KEYCLOAK_LOGOUT_REDIRECT_URI=http://localhost:5173/
VITE_KEYCLOAK_SCOPE="openid profile email"

Optional server-side variables (enable Admin UI and server calls):

KEYCLOAK_ADMIN_CLIENT_ID=portal-admin
KEYCLOAK_ADMIN_CLIENT_SECRET=... # keep server-side only

# Optional: portal admin group name
AUTH_ORBIT_ADMIN_GROUP=auth-orbit-admin
  1. Run the app (dev)
bun start

Visit http://localhost:5173.

  1. Keycloak quick setup (public client)
  • Create/select realm → note VITE_KEYCLOAK_REALM
  • Create client (OIDC) → VITE_KEYCLOAK_CLIENT_ID
  • Client authentication: OFF (public)
  • Standard flow: ON; PKCE: S256
  • Valid redirect URIs: http://localhost:5173/auth/callback
  • Post-logout redirect URIs: http://localhost:5173/
  • Web origins: your dev origin (or +)

Thats it for basic login/logout and the user dashboard.

Features

  • User dashboard that lists visible apps with safe defaults
  • Request access action for discoverable apps without access
  • Admin UI to adjust app attributes (visibility/access/requests) and approve/deny requests
  • Group-based access model using per-app groups and attributes
  • SSR-first Qwik app with streaming loaders and good UX patterns

Configuration

Create a .env from .env.example and fill values:

  • VITE_KEYCLOAK_BASE_URL: e.g. https://keycloak.example.com
  • VITE_KEYCLOAK_REALM: realm name, e.g. main
  • VITE_KEYCLOAK_CLIENT_ID: public client id
  • VITE_KEYCLOAK_REDIRECT_URI: e.g. http://localhost:5173/auth/callback
  • VITE_KEYCLOAK_LOGOUT_REDIRECT_URI: e.g. http://localhost:5173/
  • VITE_KEYCLOAK_SCOPE: openid profile email (default)

Optional server-only (not exposed to client):

  • KEYCLOAK_ADMIN_CLIENT_ID
  • KEYCLOAK_ADMIN_CLIENT_SECRET

Optional access-control env:

  • AUTH_ORBIT_ADMIN_GROUP — group name that grants admin privileges in addition to roles (default: auth-orbit-admin).

These enable admin features like listing clients on the dashboard.

Keycloak setup (where to get values)

Follow these steps in the Keycloak Admin Console to configure a client for this app and to find values for your .env:

  1. Create or select a Realm
  • Use your existing realm or create a new one. The value is your VITE_KEYCLOAK_REALM.
  1. Note the Base URL
  • VITE_KEYCLOAK_BASE_URL is the Keycloak server base, e.g. https://keycloak.example.com.
  • You can verify realm endpoints at: {BASE_URL}/realms/{REALM}/.well-known/openid-configuration.
  1. Create a Client (OIDC)
  • Clients → Create client
  • Client type: OpenID Connect
  • Client ID: e.g. portal (this becomes VITE_KEYCLOAK_CLIENT_ID).
  1. Client Capability/Settings
  • Standard flow: ON (Authorization Code)
  • Direct access grants: OFF (optional)
  • Service accounts: OFF (not needed for SPA)
  • Client authentication: OFF (this makes it a public client)
  • Proof Key for Code Exchange (PKCE): S256 (recommended)
  • Valid redirect URIs: include your callback, e.g. http://localhost:5173/auth/callback
  • Post-logout redirect URIs: include, e.g. http://localhost:5173/
  • Web origins: include your origin, e.g. http://localhost:5173 (or + to match redirect origins)
  1. Scopes
  • Ensure the client has openid and optionally profile, email in default scopes. This maps to VITE_KEYCLOAK_SCOPE.
  1. Map values to .env
  • VITE_KEYCLOAK_BASE_URL → Keycloak base URL
  • VITE_KEYCLOAK_REALM → Realm name
  • VITE_KEYCLOAK_CLIENT_ID → Client ID you created
  • VITE_KEYCLOAK_REDIRECT_URI → Your app callback URL
  • VITE_KEYCLOAK_LOGOUT_REDIRECT_URI → Where to land after logout
  • VITE_KEYCLOAK_SCOPEopenid profile email (default)

Do we need a client secret?

  • For this applications browser-based login (SPA with PKCE), no client secret is required and must not be used in the browser. Configure the client as a public client (Client authentication OFF) with Standard flow ON and PKCE S256.
  • A client secret is only required for confidential clients used from trusted server-side code. In this repo, optional admin features (listing clients, etc.) can use a separate admin client configured as confidential; those credentials must be provided server-side only via:
    • KEYCLOAK_ADMIN_CLIENT_ID
    • KEYCLOAK_ADMIN_CLIENT_SECRET

How to obtain KEYCLOAK_ADMIN_CLIENT_ID and KEYCLOAK_ADMIN_CLIENT_SECRET

Create a separate confidential client for server-side admin access:

  1. Create client (OIDC)
  • Clients → Create client
  • Client type: OpenID Connect
  • Client ID: e.g. portal-admin (this will be KEYCLOAK_ADMIN_CLIENT_ID).
  1. Configure as confidential
  • Client authentication: ON (confidential)
  • Standard flow: OFF (not needed for client credentials)
  • Service accounts: ON (enables client credentials grant)
  • Direct access grants: OFF
  1. Obtain client secret
  • Open the client → Credentials tab → copy the Client Secret and set KEYCLOAK_ADMIN_CLIENT_SECRET.
  1. Grant admin roles to the service account
  • Open the client → Service account roles
  • Assign realm roles from the realm-management client as needed, for example:
    • view-clients (read clients)
    • manage-clients (create/update clients, only if required)
    • view-users (read users, if required)
    • query-groups, query-users as needed

Notes:

  • Scope roles to the minimum necessary for your dashboard features.
  • These credentials are used only server-side and must never be exposed to the browser.

Required permissions (service account client and/or admin user)

This app calls specific Keycloak Admin REST endpoints. Grant the following minimal roles from the realm-management client depending on which features you enable. You can assign these to:

  • The confidential service account client (for server-to-server calls), and/or
  • A human admin user (if you prefer user-based admin access).

Dashboard: list visible apps

  • Endpoints: GET /admin/realms/{realm}/clients
  • Roles: view-clients

Admin Apps: list apps + show counts + group existence

  • Endpoints:
    • GET /admin/realms/{realm}/clients
    • GET /admin/realms/{realm}/groups?search=...
    • GET /admin/realms/{realm}/groups/{groupId}
    • GET /admin/realms/{realm}/groups/{groupId}/members
  • Roles: view-clients, query-groups, view-users (or query-users)

Admin Apps: update app attributes (visibility/access/request)

  • Endpoints: PUT /admin/realms/{realm}/clients/{clientUuid}
  • Roles: manage-clients

Approve/Deny requests (move users between groups)

  • Endpoints:
    • PUT /admin/realms/{realm}/users/{userId}/groups/{groupId} (add)
    • DELETE /admin/realms/{realm}/users/{userId}/groups/{groupId} (remove)
    • GET /admin/realms/{realm}/groups?search=... (lookup)
    • POST /admin/realms/{realm}/groups (create missing groups)
  • Roles: manage-users (required to change group membership), query-groups (lookup), and manage-realm (only if you want the app to auto-create missing groups)

Optional: broad admin

  • As an alternative to the granular roles above, you can assign composite realm-admin to a dedicated admin user. This is broader than necessary; use it only if your orgs policy permits.

Notes:

  • Exact role names come from the realm-management client. Names may vary slightly across Keycloak versions; prefer the least-privileged combination that allows the endpoints above.
  • If you do not grant manage-realm, the UI will still work but the “ensure groups” action will fail when groups are missing; create groups out-of-band or grant the role temporarily.

How to make a user an "admin"

There are multiple approaches; choose one that fits your org model. Two common options:

  1. Create a realm role portal-admin (recommended)
  • Realm roles → Create role → Name: portal-admin.
  • Make it a composite role and add minimal roles from realm-management (e.g. view-clients, optionally manage-clients, view-users).
  • Assign portal-admin to users who should administer the portal.
  1. Use a group portal-admins
  • Create group portal-admins.
  • In Group → Role mappings, add the same realm-management roles as above.
  • Add users to this group to grant admin privileges.

Application-side usage: The portal includes an Admin UI at /admin, gated by realm role/group. Users with the portal-admin role or membership in portal-admins (and/or the configured admin group) will see an “Admin” link on the dashboard. Admin pages call server-side endpoints that use the Admin client credentials.

Access control and approvals

This portal implements a group-based access control model using Keycloak client attributes and per-app groups.

  • Per-app groups created/used:
    • Users: app:{clientId}:users
    • Requests: app:{clientId}:requests
  • Client attributes (set per client):
    • ao_visibility: public | discoverable | hidden
    • ao_access_mode: all | group | user
    • ao_request_enabled: true | false
    • ao_group_id: optional group id override

User flow:

  • The dashboard hides hidden apps for non-admins and shows public/discoverable ones.
  • If a user lacks access and ao_request_enabled is true, they can request access from the app card.

Admin flow (via /admin):

  • View all apps and see counts of pending requests and users.
  • Update each apps visibility, accessMode, and whether requests are enabled.
  • Approve/Deny requests, which moves users from app:{clientId}:requests to app:{clientId}:users (approve) or removes from requests (deny).

Development

bun install
bun start           # dev server (SSR)

Visit http://localhost:5173.

Production build and local preview

bun run build       # full client+server build
bun run preview     # serve production preview on http://localhost:4173

Testing and linting

bun run lint        # ESLint
bun run build.types # TypeScript type-check
bun run test:vitest # Vitest (executed via Bun)

Testing notes

  • Use Qwik's testing utilities from @builder.io/qwik/testing (createDOM, userEvent) for component tests. This avoids SSR manifest coupling.
  • Use Vitest mocks (vi.mock, vi.fn) and Qwik City's <QwikCityMockProvider> to mock routes/loaders/navigation when needed.
  • Only use @builder.io/qwik/server renderToString when asserting SSR HTML snapshots. In that case, you may need a prebuilt client manifest; otherwise prefer createDOM.

Authentication flows

  • Login: GET /auth/login starts OIDC auth with PKCE.
  • Callback: GET /auth/callback exchanges code, sets session.
  • Logout: GET /auth/logout clears session and redirects via Keycloak.
  • Dashboard: GET /dashboard protected page shows visible applications for the user. It sources data from GET /api/apps (user-scoped) and no longer requires admin credentials client-side.

Admin UI

  • Entry point: /admin (visible when the user has the portal-admin role, is in portal-admins, or matches the configured admin group).
  • Manage app: /admin/apps/:clientId to adjust visibility/access and approve/deny requests.

Admin API endpoints

  • GET /api/admin/apps — list apps with attributes and counts.
  • POST /api/admin/apps/:clientId/attributes — update ao_visibility, ao_access_mode, ao_request_enabled, ao_group_id.
  • POST /api/admin/apps/:clientId/approve — body: { userId }.
  • POST /api/admin/apps/:clientId/deny — body: { userId }.
  • GET /api/admin/apps/:clientId/requests — list pending request members.

User-facing API

  • GET /api/apps — returns visible applications for the current user (includes visible, hasAccess, and requestable).

Request Access

/request-access is wired to Keycloak:

  • Validates the user session.
  • Checks that the target app has ao_request_enabled.
  • Adds the user to app:{clientId}:requests for admin review.

Docker

Build a production-like preview image:

docker compose build \
  --build-arg VITE_KEYCLOAK_BASE_URL=$VITE_KEYCLOAK_BASE_URL \
  --build-arg VITE_KEYCLOAK_REALM=$VITE_KEYCLOAK_REALM \
  --build-arg VITE_KEYCLOAK_CLIENT_ID=$VITE_KEYCLOAK_CLIENT_ID \
  --build-arg VITE_KEYCLOAK_REDIRECT_URI=$VITE_KEYCLOAK_REDIRECT_URI \
  --build-arg VITE_KEYCLOAK_LOGOUT_REDIRECT_URI=$VITE_KEYCLOAK_LOGOUT_REDIRECT_URI \
  --build-arg VITE_KEYCLOAK_SCOPE="openid profile email"

docker compose up

The app will be available at http://localhost:4173.

Notes:

  • The image uses Qwik preview build served by vite preview. For real SSR in production, add an adapter (e.g. bun qwik add adapter-node) and adjust Dockerfile accordingly.
  • Server-only admin credentials are runtime environment variables on the container: KEYCLOAK_ADMIN_CLIENT_ID, KEYCLOAK_ADMIN_CLIENT_SECRET.
  • Ports are configurable via env: set HOST_PORT and PORT (defaults to 4173:4173). Example:
    HOST_PORT=8080 PORT=4173 docker compose up
    # Now available at http://localhost:8080
    

CI/CD and Deployment (Forgejo + Portainer)

This repo includes a Forgejo CI workflow at .forgejo/workflows/ci.yml and a Portainer deploy helper script at deploy-portainer.sh.

Secrets to add in Forgejo repo settings

  • PORTAINER_API_KEY (for Portainer API calls used by the script)
  • PORTAINER_WEBHOOK_URL (your Portainer stack webhook URL)
  • Optional: PORTAINER_ENDPOINT_ID (defaults to 1)
  • Optional: REGISTRY_USERNAME, REGISTRY_PASSWORD (only if your registry requires auth)

Note: The CI workflow currently skips docker/login-action because the registry does not require authentication. If you later enable registry auth, add the above credentials as secrets and re-enable the login step in .forgejo/workflows/ci.yml.

Update env in .forgejo/workflows/ci.yml as needed

  • REGISTRY: e.g. registry.aqueous.network
  • IMAGE_NAME: e.g. auth-orbit
  • PORTAINER_URL: e.g. https://port.aqueous.network
  • PORTAINER_STACK_ID_STAGING / PORTAINER_STACK_ID_PRODUCTION: your stack IDs

Branch behavior

  • develop → deploy-staging
  • main → deploy-production
  • Build pushes tags: ${sha}, ${ref_name}, and latest

CI test mode

  • CI runs Vitest via Bun. The script scripts/run-bun-tests-ci.sh is used by the workflow.
  • To replicate locally: NODE_ENV=production bun run test:ci.

Deploy script (deploy-portainer.sh)

  • Reads PORTAINER_URL, PORTAINER_API_KEY, and WEBHOOK_URL from env/secrets
  • Usage example:
    WEBHOOK_URL="https://port.example/api/stacks/webhooks/xxxxxxxx" \
    PORTAINER_API_KEY="<redacted>" \
    ./deploy-portainer.sh <STACK_ID> staging <IMAGE_TAG>
    
  • Requirements: curl, jq (installed in CI jobs)

Build args / app config

  • The Dockerfile defines ARG for the VITE_KEYCLOAK_* variables. If you need these at build time in CI, add build-args to the docker/build-push-action step or configure them via your registry/runtime.

Optional improvements

  • Add a .dockerignore to speed up Docker builds
  • Tune Buildx cache settings if builds are frequent

Advanced: Testing and environment

  • Tests run Vitest via Bun. Preload and aliases are set in bunfig.toml where applicable.
  • Prefer @builder.io/qwik/testing for component rendering. If you intentionally test SSR HTML via renderToString, you may need to prebuild the client manifest and import it from tests/ssr-manifest.ts.
  • For CI, see the workflow in .forgejo/workflows/ci.yml.

Security

  • Session and PKCE state/verifier are stored in HTTP-only cookies.
  • Admin credentials never ship to the client; only used server-side.

Tech stack

  • Qwik + Qwik City
  • Tailwind CSS
  • Vitest + ESLint + TypeScript